From 1df0dd8b623df0893aa4996824495aa4a906300d Mon Sep 17 00:00:00 2001 From: Lammert Westerhoff Date: Wed, 17 Jan 2024 10:16:07 -0600 Subject: [PATCH 1/5] Add support for dark mode for images --- README.md | 9 ++++++- .../image/AndroidImageResourceGenerator.kt | 10 +++++-- .../image/AppleImageResourceGenerator.kt | 10 ++++++- .../resources/image/ImageResourceGenerator.kt | 7 +++-- .../image/JsImageResourceGenerator.kt | 2 +- .../image/JvmImageResourceGenerator.kt | 2 +- .../gradle/generator/resources/image/Utils.kt | 4 +-- .../metadata/resource/ResourceMetadata.kt | 11 ++++++-- .../dev/icerock/gradle/utils/FileExt.kt | 5 ++++ .../dev/icerock/gradle/utils/StringExt.kt | 14 ++++++++++ .../moko-resources/images/car_white.svg | 26 ------------------- .../shared/src/commonMain/kotlin/App.kt | 1 - 12 files changed, 62 insertions(+), 39 deletions(-) delete mode 100644 samples/compose-resources-gallery/shared/src/androidMain/moko-resources/images/car_white.svg diff --git a/README.md b/README.md index 232c4b4cf..81fd59809 100755 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ implement all you UI in Kotlin with Jetpack Compose and MOKO resources. - **Strings, Plurals** to access the corresponding resources from common code; - **Colors** with light/dark mode support; - **Compose Multiplatform** support; -- **Images** support (`svg`, `png`, `jpg`); +- **Images** support (`svg`, `png`, `jpg`) with light/dark mode support; - **Fonts** support (`ttf`, `otf`); - **Files** support (as `raw` or `assets` for android); - **StringDesc** for lifecycle-aware access to resources and unified localization on both platforms; @@ -547,6 +547,13 @@ Then we get an autogenerated `MR.images.home_black_18` `ImageResource` in code. - Android: `imageView.setImageResource(image.drawableResId)` - iOS: `imageView.image = image.toUIImage()` +#### dark mode + +To support Dark Mode images, you can add _dark and optionally _light to the name of an image. Make sure the rest of the name matches the corresponding light mode image: + +- `car.svg` +- `car_dark.svg` + #### svg The Image generator also supports `svg` files. diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt index 0a91f4f94..ce6ebbf48 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt @@ -8,6 +8,7 @@ import com.android.ide.common.vectordrawable.Svg2Vector import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import dev.icerock.gradle.generator.PlatformResourceGenerator +import dev.icerock.gradle.metadata.resource.Appearance import dev.icerock.gradle.metadata.resource.ImageMetadata import org.slf4j.Logger import java.io.File @@ -33,8 +34,8 @@ internal class AndroidImageResourceGenerator( override fun generateResourceFiles(data: List) { data.flatMap { imageMetadata -> imageMetadata.values.map { imageMetadata.key to it } - }.forEach { (key: String, item: ImageMetadata.ImageQualityItem) -> - val drawableDirName: String = "drawable" + when (item.quality) { + }.forEach { (key: String, item: ImageMetadata.ImageItem) -> + val densityRes = when (item.quality) { "0.75" -> "-ldpi" "1" -> "-mdpi" "1.5" -> "-hdpi" @@ -47,6 +48,11 @@ internal class AndroidImageResourceGenerator( return@forEach } } + val themeRes = when(item.appearance) { + Appearance.LIGHT, null -> "" + Appearance.DARK -> "-night" + } + val drawableDirName = "drawable$themeRes$densityRes" val drawableDir = File(resourcesGenerationDir, drawableDirName) val processedKey: String = processKey(key) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt index c0cf07310..ec2a6cd6a 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt @@ -39,7 +39,7 @@ internal class AppleImageResourceGenerator( assetDir.mkdirs() val contentsFile = File(assetDir, "Contents.json") - val validItems: List = + val validItems: List = imageMetadata.values.filter { item -> item.quality == null || VALID_SIZES.any { item.quality == it.toString() } } @@ -67,6 +67,14 @@ internal class AppleImageResourceGenerator( item.quality?.let { quality -> put("scale", JsonPrimitive(quality + "x")) } + item.appearance?.let { appearance -> + put("appearances", buildJsonArray { + add(buildJsonObject { + put("appearance", JsonPrimitive("luminosity")) + put("value", JsonPrimitive(appearance.name.lowercase())) + }) + }) + } } }.forEach { add(it) } } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt index f06c8eba4..8f9e5c637 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt @@ -9,9 +9,11 @@ import dev.icerock.gradle.generator.Constants import dev.icerock.gradle.generator.ResourceGenerator import dev.icerock.gradle.generator.generateKey import dev.icerock.gradle.metadata.resource.ImageMetadata +import dev.icerock.gradle.utils.appearance import dev.icerock.gradle.utils.nameWithoutScale import dev.icerock.gradle.utils.scale import dev.icerock.gradle.utils.svg +import dev.icerock.gradle.utils.withoutAppearance import java.io.File internal class ImageResourceGenerator : ResourceGenerator { @@ -21,8 +23,9 @@ internal class ImageResourceGenerator : ResourceGenerator { ImageMetadata( key = generateKey(key), values = files.map { file -> - ImageMetadata.ImageQualityItem( + ImageMetadata.ImageItem( quality = if (file.svg) null else file.scale, + appearance = file.appearance, filePath = file ) } @@ -39,6 +42,6 @@ internal class ImageResourceGenerator : ResourceGenerator { file.nameWithoutExtension } else { file.nameWithoutScale - } + }.withoutAppearance } } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt index f9a2fb985..965a1b49a 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt @@ -22,7 +22,7 @@ internal class JsImageResourceGenerator( override fun imports(): List = emptyList() override fun generateInitializer(metadata: ImageMetadata): CodeBlock { - val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem() + val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() val fileName = "${metadata.key}.${item.filePath.extension}" val requireDeclaration = """require("$IMAGES_DIR/$fileName")""" return CodeBlock.of( diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt index ef9bd2f27..2dd22c772 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt @@ -20,7 +20,7 @@ internal class JvmImageResourceGenerator( override fun imports(): List = emptyList() override fun generateInitializer(metadata: ImageMetadata): CodeBlock { - val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem() + val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() val fileName = "${metadata.key}.${item.filePath.extension}" return CodeBlock.of( "ImageResource(resourcesClassLoader = %L, filePath = %S)", diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt index 69b894187..99ce10544 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt @@ -16,7 +16,7 @@ internal fun generateHighestQualityImageResources( imagesDir.mkdirs() data.forEach { metadata -> - val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem() + val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() val file: File = item.filePath val key: String = metadata.key @@ -24,7 +24,7 @@ internal fun generateHighestQualityImageResources( } } -internal fun ImageMetadata.getHighestQualityItem(): ImageMetadata.ImageQualityItem { +internal fun ImageMetadata.getHighestQualityItem(): ImageMetadata.ImageItem { return values.singleOrNull { it.quality == null } ?: values.maxBy { it.quality!!.toDouble() } } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt index 6733d7359..12508d138 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt @@ -64,11 +64,12 @@ internal data class PluralMetadata( @Serializable internal data class ImageMetadata( override val key: String, - val values: List + val values: List ) : ResourceMetadata { @Serializable - data class ImageQualityItem( + data class ImageItem( val quality: String?, + val appearance: Appearance?, val filePath: File ) @@ -143,3 +144,9 @@ internal data class AssetMetadata( override fun contentHash(): String = filePath.calculateResourcesHash() } + +@Serializable +internal enum class Appearance { + LIGHT, + DARK +} diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt index 5ddc2ec22..30ff4f025 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt @@ -4,6 +4,7 @@ package dev.icerock.gradle.utils +import dev.icerock.gradle.metadata.resource.Appearance import java.io.File import org.jetbrains.kotlin.konan.file.File as KonanFile @@ -19,4 +20,8 @@ internal val File.nameWithoutScale: String get() = nameWithoutExtension.withoutScale +internal val File.appearance: Appearance? + get() = + nameWithoutExtension.appearance + internal fun File.toKonanFile(): KonanFile = KonanFile(this.path) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt index 434cdc4d6..a1b65ebb5 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt @@ -4,6 +4,7 @@ package dev.icerock.gradle.utils +import dev.icerock.gradle.metadata.resource.Appearance import java.util.Locale /** @@ -37,3 +38,16 @@ internal fun String.remove(char: String): String { internal val String.flatName: String get() = this.remove('.') + +internal val String.appearance: Appearance? + get() = Appearance.values().firstOrNull { this.contains("_${it.name}", true) } + +internal val String.withoutAppearance: String + get() { + var result = this + Appearance.values() + .forEach { + result = result.replace("_${it.name}", "", true) + } + return result + } diff --git a/samples/compose-resources-gallery/shared/src/androidMain/moko-resources/images/car_white.svg b/samples/compose-resources-gallery/shared/src/androidMain/moko-resources/images/car_white.svg deleted file mode 100644 index eb41b6115..000000000 --- a/samples/compose-resources-gallery/shared/src/androidMain/moko-resources/images/car_white.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/compose-resources-gallery/shared/src/commonMain/kotlin/App.kt b/samples/compose-resources-gallery/shared/src/commonMain/kotlin/App.kt index f2a9e7191..d6e869d68 100644 --- a/samples/compose-resources-gallery/shared/src/commonMain/kotlin/App.kt +++ b/samples/compose-resources-gallery/shared/src/commonMain/kotlin/App.kt @@ -49,7 +49,6 @@ internal fun App() { modifier = Modifier.size(30.dp).padding(top = 16.dp), painter = painterResource(MR.images.car_black), contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colors.onBackground) ) var text: String by remember { mutableStateOf("") } From a2bb1572f6ae365257b572f8ccc5c9a36173c414 Mon Sep 17 00:00:00 2001 From: Konstantin Kolchurin Date: Fri, 19 Apr 2024 09:48:26 +0700 Subject: [PATCH 2/5] #624 rework theme suffix, logic based on Image.Appearance, fix changes in readme --- README.md | 4 +- .../image/AndroidImageResourceGenerator.kt | 8 +--- .../image/AppleImageResourceGenerator.kt | 12 +++--- .../resources/image/ImageResourceGenerator.kt | 4 +- .../gradle/generator/resources/image/Utils.kt | 2 - .../metadata/resource/ResourceMetadata.kt | 40 ++++++++++++------ .../dev/icerock/gradle/utils/FileExt.kt | 5 --- .../dev/icerock/gradle/utils/StringExt.kt | 32 +++++++++----- .../gradle.properties | 1 - ...{car_black_dark.svg => car_black-dark.svg} | 0 ...logo_dark@1x.png => moko_logo-dark@1x.png} | Bin 11 files changed, 60 insertions(+), 48 deletions(-) rename samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/{car_black_dark.svg => car_black-dark.svg} (100%) rename samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/{moko_logo_dark@1x.png => moko_logo-dark@1x.png} (100%) diff --git a/README.md b/README.md index 12e320875..18df753c8 100755 --- a/README.md +++ b/README.md @@ -549,10 +549,10 @@ Then we get an autogenerated `MR.images.home_black_18` `ImageResource` in code. #### dark mode -To support Dark Mode images, you can add _dark and optionally _light to the name of an image. Make sure the rest of the name matches the corresponding light mode image: +To support Dark Mode images, you can add -dark and optionally -light to the name of an image. Make sure the rest of the name matches the corresponding light mode image: - `car.svg` -- `car_dark.svg` +- `car-dark.svg` #### svg diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt index d68e39c0a..b5418a9fd 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AndroidImageResourceGenerator.kt @@ -13,7 +13,6 @@ import dev.icerock.gradle.generator.Constants import dev.icerock.gradle.generator.PlatformResourceGenerator import dev.icerock.gradle.generator.addEmptyPlatformResourceProperty import dev.icerock.gradle.generator.addValuesFunction -import dev.icerock.gradle.metadata.resource.Appearance import dev.icerock.gradle.metadata.resource.ImageMetadata import org.slf4j.Logger import java.io.File @@ -73,11 +72,8 @@ internal class AndroidImageResourceGenerator( return@forEach } } - val themeRes = when(item.appearance) { - Appearance.LIGHT, null -> "" - Appearance.DARK -> "-night" - } - val drawableDirName = "drawable$themeRes$densityRes" + val themeSuffix = item.appearance.resourceSuffix + val drawableDirName = "drawable$themeSuffix$densityRes" val drawableDir = File(resourcesGenerationDir, drawableDirName) val processedKey: String = processKey(key) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt index c5e79d6ee..82a8993ed 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt @@ -69,14 +69,12 @@ internal class AppleImageResourceGenerator( item.quality?.let { quality -> put("scale", JsonPrimitive(quality + "x")) } - item.appearance?.let { appearance -> - put("appearances", buildJsonArray { - add(buildJsonObject { - put("appearance", JsonPrimitive("luminosity")) - put("value", JsonPrimitive(appearance.name.lowercase())) - }) + put("appearances", buildJsonArray { + add(buildJsonObject { + put("appearance", JsonPrimitive("luminosity")) + put("value", JsonPrimitive(item.appearance.name.lowercase())) }) - } + }) } }.forEach { add(it) } } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt index 8f9e5c637..5167a0e8c 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/ImageResourceGenerator.kt @@ -9,7 +9,7 @@ import dev.icerock.gradle.generator.Constants import dev.icerock.gradle.generator.ResourceGenerator import dev.icerock.gradle.generator.generateKey import dev.icerock.gradle.metadata.resource.ImageMetadata -import dev.icerock.gradle.utils.appearance +import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance import dev.icerock.gradle.utils.nameWithoutScale import dev.icerock.gradle.utils.scale import dev.icerock.gradle.utils.svg @@ -25,7 +25,7 @@ internal class ImageResourceGenerator : ResourceGenerator { values = files.map { file -> ImageMetadata.ImageItem( quality = if (file.svg) null else file.scale, - appearance = file.appearance, + appearance = Appearance.getFromFile(file), filePath = file ) } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt index bb867f557..51ea700c0 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt @@ -16,8 +16,6 @@ internal fun generateHighestQualityImageResources( imagesDir.mkdirs() data.forEach { metadata -> - println("GENHIGHT: $metadata") - println("GENHIGHT: ---") val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() val file: File = item.filePath val key: String = metadata.key diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt index 3a3f7ed94..53bb851ca 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt @@ -13,6 +13,7 @@ import dev.icerock.gradle.serialization.FileSerializer import dev.icerock.gradle.serialization.ResourceMetadataSerializer import dev.icerock.gradle.utils.calculateHash import dev.icerock.gradle.utils.calculateResourcesHash +import dev.icerock.gradle.utils.nameWithoutScale import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName @@ -100,7 +101,7 @@ internal data class ImageMetadata( @EncodeDefault override val resourceType: String = ImageMetadata::class.java.name, override val key: String, - val values: List + val values: List, ) : ResourceMetadata { init { @@ -111,11 +112,27 @@ internal data class ImageMetadata( data class ImageItem( val quality: String?, val appearance: Appearance, - val filePath: File + val filePath: File, ) - override fun contentHash(): String = values.map { it.filePath.calculateResourcesHash() } - .calculateHash() + @Serializable + enum class Appearance( + val suffix: String, + val resourceSuffix: String, + ) { + LIGHT(suffix = "-light", resourceSuffix = ""), + DARK(suffix = "-dark", resourceSuffix = "-night"); + + companion object { + fun getFromFile(file: File): Appearance { + return if (file.nameWithoutScale.endsWith(Appearance.DARK.suffix)) DARK else LIGHT + } + } + } + + override fun contentHash(): String = values.map { + it.filePath.calculateResourcesHash() + }.calculateHash() } @Serializable @@ -190,14 +207,16 @@ data class ColorMetadata( ) { @Suppress("MagicNumber") fun toArgbHex(): String { - return listOf(alpha, red, green, blue) - .joinToString(separator = "") { it.toString(16).padStart(2, '0') } + return listOf(alpha, red, green, blue).joinToString(separator = "") { + it.toString(16).padStart(2, '0') + } } @Suppress("MagicNumber") fun toRgbaHex(): String { - return listOf(red, green, blue, alpha) - .joinToString(separator = "") { it.toString(16).padStart(2, '0') } + return listOf(red, green, blue, alpha).joinToString(separator = "") { + it.toString(16).padStart(2, '0') + } } } @@ -256,8 +275,3 @@ private fun getFilePath(filePath: File, relativePath: File): List { .dropLast(1) .map(::normalizePathName) } - -internal enum class Appearance { - LIGHT, - DARK -} diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt index 1fc071711..5ddc2ec22 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/FileExt.kt @@ -4,7 +4,6 @@ package dev.icerock.gradle.utils -import dev.icerock.gradle.metadata.resource.Appearance import java.io.File import org.jetbrains.kotlin.konan.file.File as KonanFile @@ -20,8 +19,4 @@ internal val File.nameWithoutScale: String get() = nameWithoutExtension.withoutScale -internal val File.appearance: Appearance - get() = - nameWithoutExtension.appearance - internal fun File.toKonanFile(): KonanFile = KonanFile(this.path) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt index ad779e39b..f1ab67134 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt @@ -4,7 +4,7 @@ package dev.icerock.gradle.utils -import dev.icerock.gradle.metadata.resource.Appearance +import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.translate.UnicodeUnescaper import java.util.Locale @@ -54,17 +54,29 @@ internal fun String.convertXmlStringToAndroidLocalization(): String { StringEscapeUtils.escapeXml11(it) } } + internal val String.appearance: Appearance - get() = Appearance.values().firstOrNull { - this.contains("_${it.name}", true) - } ?: Appearance.LIGHT + get() { + return if (withoutScale.endsWith(suffix = Appearance.DARK.suffix, ignoreCase = true)) { + Appearance.DARK + } else { + Appearance.LIGHT + } + } internal val String.withoutAppearance: String get() { - var result = this - Appearance.values() - .forEach { - result = result.replace("_${it.name}", "", true) - } - return result + Appearance.values().forEach { type -> + val typeSuffix: String = type.suffix + val latestIncludeIndex: Int = lastIndexOf(string = typeSuffix, ignoreCase = true) + val nameWithAppearanceLength: Int = latestIncludeIndex + typeSuffix.length + val latestSuffixIsTheme: Boolean = length == nameWithAppearanceLength + + if (latestSuffixIsTheme) return removeRange( + startIndex = latestIncludeIndex, + endIndex = nameWithAppearanceLength + ) + } + + return this } diff --git a/samples/compose-resources-gallery/gradle.properties b/samples/compose-resources-gallery/gradle.properties index b99d20441..dc3ec06d2 100644 --- a/samples/compose-resources-gallery/gradle.properties +++ b/samples/compose-resources-gallery/gradle.properties @@ -9,7 +9,6 @@ xcodeproj=./iosApp kotlin.native.cocoapods.generate.wrapper=true kotlin.native.useEmbeddableCompilerJar=true -kotlin.native.binary.memoryModel=experimental android.useAndroidX=true diff --git a/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_black_dark.svg b/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_black-dark.svg similarity index 100% rename from samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_black_dark.svg rename to samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_black-dark.svg diff --git a/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/moko_logo_dark@1x.png b/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/moko_logo-dark@1x.png similarity index 100% rename from samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/moko_logo_dark@1x.png rename to samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/moko_logo-dark@1x.png From 380f5c4db49490210d01c6ef151c3f7bb4c7bbc2 Mon Sep 17 00:00:00 2001 From: Konstantin Kolchurin Date: Fri, 19 Apr 2024 11:50:59 +0700 Subject: [PATCH 3/5] #624 reformat --- .../image/AppleImageResourceGenerator.kt | 58 +++++++++++++------ .../dev/icerock/gradle/utils/StringExt.kt | 10 ++-- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt index 82a8993ed..94f062fd4 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/AppleImageResourceGenerator.kt @@ -33,6 +33,7 @@ internal class AppleImageResourceGenerator( ) } + @Suppress("LongMethod") override fun generateResourceFiles(data: List) { val assetsDirectory = File(assetsGenerationDir, Constants.Apple.assetsDirectoryName) @@ -64,36 +65,59 @@ internal class AppleImageResourceGenerator( val imagesContent: JsonArray = buildJsonArray { validItems.map { item -> buildJsonObject { - put("idiom", JsonPrimitive("universal")) - put("filename", JsonPrimitive(item.filePath.name)) + put( + key = "idiom", + element = JsonPrimitive("universal") + ) + put( + key = "filename", + element = JsonPrimitive(item.filePath.name) + ) item.quality?.let { quality -> - put("scale", JsonPrimitive(quality + "x")) + put( + key = "scale", + element = JsonPrimitive(quality + "x") + ) } - put("appearances", buildJsonArray { - add(buildJsonObject { - put("appearance", JsonPrimitive("luminosity")) - put("value", JsonPrimitive(item.appearance.name.lowercase())) - }) - }) + put( + key = "appearances", + element = buildJsonArray { + add( + buildJsonObject { + put( + key = "appearance", + element = JsonPrimitive("luminosity") + ) + put( + key = "value", + element = JsonPrimitive(item.appearance.name.lowercase()) + ) + } + ) + } + ) } }.forEach { add(it) } } val content: String = buildJsonObject { - put("images", imagesContent) + put(key = "images", element = imagesContent) put( - "info", - buildJsonObject { - put("version", JsonPrimitive(1)) - put("author", JsonPrimitive("xcode")) + key = "info", + element = buildJsonObject { + put(key = "version", element = JsonPrimitive(1)) + put(key = "author", element = JsonPrimitive("xcode")) } ) if (validItems.any { it.quality == null }) { put( - "properties", - buildJsonObject { - put("preserves-vector-representation", JsonPrimitive(true)) + key = "properties", + element = buildJsonObject { + put( + key = "preserves-vector-representation", + element = JsonPrimitive(true) + ) } ) } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt index f1ab67134..ab0d70eb3 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/utils/StringExt.kt @@ -72,10 +72,12 @@ internal val String.withoutAppearance: String val nameWithAppearanceLength: Int = latestIncludeIndex + typeSuffix.length val latestSuffixIsTheme: Boolean = length == nameWithAppearanceLength - if (latestSuffixIsTheme) return removeRange( - startIndex = latestIncludeIndex, - endIndex = nameWithAppearanceLength - ) + if (latestSuffixIsTheme) { + return removeRange( + startIndex = latestIncludeIndex, + endIndex = nameWithAppearanceLength + ) + } } return this From 304eb119a49943818239a7b7b833e550a35c0681 Mon Sep 17 00:00:00 2001 From: Konstantin Kolchurin Date: Fri, 19 Apr 2024 15:15:06 +0700 Subject: [PATCH 4/5] #624 add support of dark resources for jvm and js targets --- .../moko/resources/compose/ImageResource.kt | 16 ++++++-- .../moko/resources/compose/ImageResource.kt | 15 ++++++- .../image/JsImageResourceGenerator.kt | 41 +++++++++++++++---- .../image/JvmImageResourceGenerator.kt | 24 +++++++++-- .../gradle/generator/resources/image/Utils.kt | 32 +++++++++++---- .../metadata/resource/ResourceMetadata.kt | 5 ++- .../icerock/moko/resources/ImageResource.kt | 15 ++++++- .../icerock/moko/resources/ImageResource.kt | 22 ++++++++-- .../moko-resources/images/car_red.svg | 30 ++++++++++++++ 9 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_red.svg diff --git a/resources-compose/src/jsMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt b/resources-compose/src/jsMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt index cd37aa0bb..f7d7658d3 100644 --- a/resources-compose/src/jsMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt +++ b/resources-compose/src/jsMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt @@ -5,8 +5,11 @@ package dev.icerock.moko.resources.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.LocalSystemTheme +import androidx.compose.ui.SystemTheme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter @@ -22,18 +25,25 @@ import org.jetbrains.skia.Data import org.jetbrains.skia.Image import org.jetbrains.skia.svg.SVGDOM +@OptIn(InternalComposeApi::class) @Composable actual fun painterResource(imageResource: ImageResource): Painter { - val bytes: ByteArray? by produceByteArray(url = imageResource.fileUrl) + val fileUrl: String = if (LocalSystemTheme.current == SystemTheme.Dark) { + imageResource.darkFileUrl ?: imageResource.fileUrl + } else { + imageResource.fileUrl + } + val bytes: ByteArray? by produceByteArray(url = fileUrl) val localBytes: ByteArray? = bytes val density: Density = LocalDensity.current - return remember(localBytes) { + + return remember(localBytes, density, fileUrl) { if (localBytes == null) { return@remember ColorPainter(color = Color.Transparent) } - if (imageResource.fileUrl.endsWith(".svg", ignoreCase = true)) { + if (fileUrl.endsWith(".svg", ignoreCase = true)) { SVGPainter(SVGDOM(Data.makeFromBytes(localBytes)), density) } else { val skiaImage: Image = Image.makeFromEncoded(bytes = localBytes) diff --git a/resources-compose/src/jvmMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt b/resources-compose/src/jvmMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt index d24503fef..d087ebf42 100644 --- a/resources-compose/src/jvmMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt +++ b/resources-compose/src/jvmMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt @@ -5,10 +5,21 @@ package dev.icerock.moko.resources.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.ui.LocalSystemTheme +import androidx.compose.ui.SystemTheme import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import dev.icerock.moko.resources.ImageResource +@OptIn(InternalComposeApi::class) @Composable -actual fun painterResource(imageResource: ImageResource): Painter = - painterResource(resourcePath = imageResource.filePath) +actual fun painterResource(imageResource: ImageResource): Painter { + val filePath: String = if (LocalSystemTheme.current == SystemTheme.Dark) { + imageResource.darkFilePath ?: imageResource.filePath + } else { + imageResource.filePath + } + + return painterResource(resourcePath = filePath) +} diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt index 40a300ba1..702bf9794 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JsImageResourceGenerator.kt @@ -14,6 +14,7 @@ import dev.icerock.gradle.generator.Constants import dev.icerock.gradle.generator.PlatformResourceGenerator import dev.icerock.gradle.generator.addEmptyPlatformResourceProperty import dev.icerock.gradle.metadata.resource.ImageMetadata +import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance import java.io.File internal class JsImageResourceGenerator( @@ -23,14 +24,36 @@ internal class JsImageResourceGenerator( override fun imports(): List = emptyList() override fun generateInitializer(metadata: ImageMetadata): CodeBlock { - val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() - val fileName = "${metadata.key}.${item.filePath.extension}" - val requireDeclaration = """require("$IMAGES_DIR/$fileName")""" - return CodeBlock.of( - "ImageResource(fileUrl = js(%S) as String, fileName = %S)", - requireDeclaration, - fileName - ) + var fileName: String = "" + var darkFileName: String? = null + + metadata.values.groupBy { it.appearance }.forEach { (theme, resources) -> + val item: ImageMetadata.ImageItem = resources.getHighestQualityItem(theme) + + if (theme == Appearance.DARK) { + darkFileName = "${metadata.key}${theme.themeSuffix}.${item.filePath.extension}" + } else { + fileName = "${metadata.key}.${item.filePath.extension}" + } + } + + val requireDeclaration: String = """require("$IMAGES_DIR/$fileName")""" + val darkRequireDeclaration: String = """require("$IMAGES_DIR/$darkFileName")""" + + return if (darkFileName != null) { + CodeBlock.of( + "ImageResource(fileUrl = js(%S) as String, darkFileUrl = js(%S) as String, fileName = %S)", + requireDeclaration, + darkRequireDeclaration, + fileName + ) + } else { + CodeBlock.of( + "ImageResource(fileUrl = js(%S) as String, fileName = %S)", + requireDeclaration, + fileName + ) + } } override fun generateBeforeProperties( @@ -52,7 +75,7 @@ internal class JsImageResourceGenerator( override fun generateAfterProperties( builder: Builder, metadata: List, - modifier: KModifier? + modifier: KModifier?, ) { val languageKeysList: String = metadata.joinToString { it.key } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt index 530d43aa8..bbe56565c 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt @@ -15,6 +15,7 @@ import dev.icerock.gradle.generator.PlatformResourceGenerator import dev.icerock.gradle.generator.addJvmPlatformResourceClassLoaderProperty import dev.icerock.gradle.generator.addValuesFunction import dev.icerock.gradle.metadata.resource.ImageMetadata +import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance import java.io.File internal class JvmImageResourceGenerator( @@ -23,10 +24,27 @@ internal class JvmImageResourceGenerator( override fun imports(): List = emptyList() override fun generateInitializer(metadata: ImageMetadata): CodeBlock { - val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() - val fileName = "${metadata.key}.${item.filePath.extension}" + var fileName: String = "" + var darkFileName: String? = null + + metadata.values.groupBy { it.appearance }.forEach { (theme, resources) -> + val item: ImageMetadata.ImageItem = resources.getHighestQualityItem(theme) + + if (theme == Appearance.DARK) { + darkFileName = "${metadata.key}${theme.suffix}.${item.filePath.extension}" + } else { + fileName = "${metadata.key}.${item.filePath.extension}" + } + } + + val darkFilePath: String = if (darkFileName != null) { + "$IMAGES_DIR/$darkFileName" + } else { + "null" + } + return CodeBlock.of( - "ImageResource(resourcesClassLoader = %L, filePath = %S)", + "ImageResource(resourcesClassLoader = %L, filePath = %S, darkFilePath = $darkFilePath)", "${PlatformDetails.platformDetailsPropertyName}.${Jvm.resourcesClassLoaderPropertyName}", "$IMAGES_DIR/$fileName" ) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt index 51ea700c0..79c04628c 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/Utils.kt @@ -5,26 +5,42 @@ package dev.icerock.gradle.generator.resources.image import dev.icerock.gradle.metadata.resource.ImageMetadata +import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance.DARK +import dev.icerock.gradle.metadata.resource.ImageMetadata.ImageItem import java.io.File internal fun generateHighestQualityImageResources( resourcesGenerationDir: File, data: List, - imagesDirName: String + imagesDirName: String, ) { val imagesDir = File(resourcesGenerationDir, imagesDirName) imagesDir.mkdirs() data.forEach { metadata -> - val item: ImageMetadata.ImageItem = metadata.getHighestQualityItem() - val file: File = item.filePath - val key: String = metadata.key + metadata.values + .groupBy { it.appearance } + .forEach { (theme, list: List) -> + val item: ImageMetadata.ImageItem = list.getHighestQualityItem(theme) + val file: File = item.filePath + val key: String = metadata.key - file.copyTo(File(imagesDir, "$key.${file.extension}")) + val fileName: String = if (theme == DARK) { + "$key${theme.themeSuffix}.${file.extension}" + } else { + "$key.${file.extension}" + } + + file.copyTo(File(imagesDir, fileName)) + } } } -internal fun ImageMetadata.getHighestQualityItem(): ImageMetadata.ImageItem { - return values.firstOrNull { it.quality == null } - ?: values.maxBy { it.quality!!.toDouble() } +internal fun List.getHighestQualityItem( + appearance: ImageMetadata.Appearance, +): ImageMetadata.ImageItem { + val filteredList = filter { it.appearance == appearance } + + return filteredList.singleOrNull { it.quality == null } + ?: filteredList.maxBy { it.quality!!.toDouble() } } diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt index 53bb851ca..f0ad09d2a 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/metadata/resource/ResourceMetadata.kt @@ -118,10 +118,11 @@ internal data class ImageMetadata( @Serializable enum class Appearance( val suffix: String, + val themeSuffix: String, val resourceSuffix: String, ) { - LIGHT(suffix = "-light", resourceSuffix = ""), - DARK(suffix = "-dark", resourceSuffix = "-night"); + LIGHT(suffix = "-light", themeSuffix = "", resourceSuffix = ""), + DARK(suffix = "-dark", themeSuffix = "_dark", resourceSuffix = "-night"); companion object { fun getFromFile(file: File): Appearance { diff --git a/resources/src/jsMain/kotlin/dev/icerock/moko/resources/ImageResource.kt b/resources/src/jsMain/kotlin/dev/icerock/moko/resources/ImageResource.kt index 018e3ac48..98d093fe6 100644 --- a/resources/src/jsMain/kotlin/dev/icerock/moko/resources/ImageResource.kt +++ b/resources/src/jsMain/kotlin/dev/icerock/moko/resources/ImageResource.kt @@ -4,4 +4,17 @@ package dev.icerock.moko.resources -actual data class ImageResource(val fileName: String, val fileUrl: String) +actual data class ImageResource( + val fileName: String, + val fileUrl: String, + val darkFileUrl: String?, +) { + constructor( + fileName: String, + fileUrl: String, + ) : this( + fileName = fileName, + fileUrl = fileUrl, + darkFileUrl = null + ) +} diff --git a/resources/src/jvmMain/kotlin/dev/icerock/moko/resources/ImageResource.kt b/resources/src/jvmMain/kotlin/dev/icerock/moko/resources/ImageResource.kt index de5add921..94384a13b 100644 --- a/resources/src/jvmMain/kotlin/dev/icerock/moko/resources/ImageResource.kt +++ b/resources/src/jvmMain/kotlin/dev/icerock/moko/resources/ImageResource.kt @@ -12,18 +12,32 @@ import java.awt.image.BufferedImage import java.io.File import java.io.FileNotFoundException import java.io.InputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream import javax.imageio.ImageIO actual data class ImageResource( val resourcesClassLoader: ClassLoader, - val filePath: String + val filePath: String, + val darkFilePath: String? ) { + constructor( + resourcesClassLoader: ClassLoader, + filePath: String + ) : this(resourcesClassLoader, filePath, null) + + fun getThemedImage(isDark: Boolean): BufferedImage { + return readImage( + filePath = if (isDark) (darkFilePath ?: filePath) else filePath + ) + } + val image: BufferedImage by lazy { + readImage(filePath) + } + + private fun readImage(filePath: String): BufferedImage { val stream = resourcesClassLoader.getResourceAsStream(filePath) ?: throw FileNotFoundException("Couldn't open resource as stream at: $filePath") - stream.use { + return stream.use { if (filePath.endsWith(".svg", ignoreCase = true)) { readSvg(it) } else { diff --git a/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_red.svg b/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_red.svg new file mode 100644 index 000000000..29b0a6751 --- /dev/null +++ b/samples/compose-resources-gallery/shared/src/commonMain/moko-resources/images/car_red.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + From 532a2f60ecaa36774f8305523e37df4e03047402 Mon Sep 17 00:00:00 2001 From: Konstantin Kolchurin Date: Fri, 19 Apr 2024 17:34:50 +0700 Subject: [PATCH 5/5] #624 fix jvm image code generation --- .../generator/resources/image/JvmImageResourceGenerator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt index bbe56565c..967dddd2b 100644 --- a/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt +++ b/resources-generator/src/main/kotlin/dev/icerock/gradle/generator/resources/image/JvmImageResourceGenerator.kt @@ -31,14 +31,14 @@ internal class JvmImageResourceGenerator( val item: ImageMetadata.ImageItem = resources.getHighestQualityItem(theme) if (theme == Appearance.DARK) { - darkFileName = "${metadata.key}${theme.suffix}.${item.filePath.extension}" + darkFileName = "${metadata.key}${theme.themeSuffix}.${item.filePath.extension}" } else { fileName = "${metadata.key}.${item.filePath.extension}" } } val darkFilePath: String = if (darkFileName != null) { - "$IMAGES_DIR/$darkFileName" + "\"$IMAGES_DIR/$darkFileName\"" } else { "null" }