Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Night/Dark mode support for Android and iOS #25

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,27 @@ Recommended for multicolor images.
> - For iOS generate images from @1x to @3x (where @1x is 1:1 in pixels to specified size)
> - For JVM and JS a single image of specified size is generated.

> **night**
>
> Night/Dark Mode images are supported for Android and iOS by adding the `(night)` modifier. The filename and type of
> the image must match the corresponding day/light version without the `(night)` modifier.
>
> - For Android this creates night images in `drawable-night-nodpi`.
> - For iOS this creates a `"appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ]` entry in the `imageset`.

Filename examples:
```
some_hd_image_(100).jpg
app_logo_(orig).svg
my_colorful_bitmap_(orig)_(150).png
image_with_night_support_(night).png
```
Kotlin:
```kotlin
MainRes.image.some_hd_image
MainRes.image.app_logo
MainRes.image.my_colorful_bitmap
MainRes.image.image_with_night_support
```
Swift:
```swift
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package io.github.skeptick.libres.plugin

import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import io.github.skeptick.libres.plugin.common.declarations.saveToDirectory
import io.github.skeptick.libres.plugin.common.extensions.deleteFilesInDirectory
import io.github.skeptick.libres.plugin.images.ImagesTypeSpecsBuilder
import io.github.skeptick.libres.plugin.images.declarations.EmptyImagesObject
import io.github.skeptick.libres.plugin.images.declarations.ImagesObjectFile
import io.github.skeptick.libres.plugin.images.models.ImageProps
import io.github.skeptick.libres.plugin.images.models.ImageSet
import io.github.skeptick.libres.plugin.images.processing.removeImage
import io.github.skeptick.libres.plugin.images.processing.saveImage
import io.github.skeptick.libres.plugin.images.processing.saveImageSet
import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.io.File

@CacheableTask
Expand All @@ -35,18 +37,33 @@ abstract class LibresImagesGenerationTask : DefaultTask() {

@TaskAction
fun apply(inputChanges: InputChanges) {
inputChanges.getFileChanges(inputDirectory).forEach { change ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (change.changeType) {
ChangeType.REMOVED -> ImageProps(change.file).removeImage(outputResourcesDirectories)
ChangeType.MODIFIED, ChangeType.ADDED -> ImageProps(change.file).saveImage(outputResourcesDirectories)
// Update images for changed files
inputChanges.getFileChanges(inputDirectory)
.forEach { change ->
val image = ImageProps(change.file)

@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (change.changeType) {
ChangeType.MODIFIED, ChangeType.ADDED -> image.saveImage(outputResourcesDirectories)
ChangeType.REMOVED -> image.removeImage(outputResourcesDirectories)
}
}
}

// Generate image sets
inputDirectory.files
.map(::ImageProps)
.groupBy(ImageProps::name)
.map { (name, files) -> ImageSet(name, files) }
.forEach { catalog ->
catalog.saveImageSet(outputResourcesDirectories)
}

// Generate code
inputDirectory.files
.takeIf { files -> files.isNotEmpty() }
?.map { file -> ImageProps(file) }
?.let { imageProps -> buildImages(imageProps) }
?.map(::ImageProps)
?.distinctBy(ImageProps::name)
?.let(::buildImages)
?: buildEmptyImages()
}

Expand All @@ -66,5 +83,4 @@ abstract class LibresImagesGenerationTask : DefaultTask() {
imagesObjectFileSpec.saveToDirectory(directory)
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@ internal class ImageProps(val file: File) {
val extension: String
val targetSize: Int?
val isTintable: Boolean
val isNightMode: Boolean

init {
val nameWithoutExtension = file.nameWithoutExtension
val parameters = ParametersRegex.findAll(nameWithoutExtension).toList()

this.name = nameWithoutExtension.substringBefore("_(").lowercase()
this.extension = file.extension.lowercase()
this.targetSize = if (!isVector) parameters.firstNotNullOfOrNull { it.groupValues[1].toIntOrNull() } else null
this.isTintable = parameters.none { it.groupValues[1].startsWith("orig") }
this.isNightMode = parameters.any { it.groupValues[1] == "night" }
}

companion object {
private val ParametersRegex = Regex("_\\((.*?)\\)")
}

}

internal val ImageProps.isVector: Boolean get() = extension == "svg"
internal val ImageProps.isVector: Boolean get() = extension == "svg"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.skeptick.libres.plugin.images.models

internal class ImageSet(
val name: String,
val images: Iterable<ImageProps>,
) {
val isVector: Boolean
get() = images.all { it.isVector }

val isTintable: Boolean
get() = images.all { it.isTintable }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,24 @@ internal data class ImageSetContents(
data class Image(
val filename: String,
val scale: ImageScale? = null,
val idiom: String = "universal"
val idiom: String = "universal",
val appearances: List<Appearance>? = null,
)

sealed interface Appearance {
val appearance: String
val value: String

sealed interface Luminosity : Appearance {
override val appearance: String
get() = "luminosity"

object Dark : Luminosity {
override val value: String = "dark"
}
}
}

data class Info(
val author: String = "xcode",
val version: Int = 1
Expand All @@ -35,24 +50,4 @@ internal data class ImageSetContents(
@JsonProperty("preserves-vector-representation") val preserveVectorRepresentation: Boolean?,
@JsonProperty("template-rendering-intent") val templateRenderingIntent: VectorRenderingType
)

}

internal fun ImageProps.toImageSetContents() =
ImageSetContents(
images = when (targetSize) {
null -> listOf(
ImageSetContents.Image(filename = "$name.$extension")
)
else -> ImageSetContents.ImageScale.values().map {
ImageSetContents.Image(filename = "${this.name}_${it.name}.$extension", scale = it)
}
},
properties = ImageSetContents.Properties(
preserveVectorRepresentation = if (isVector) true else null,
templateRenderingIntent = when (isTintable) {
true -> ImageSetContents.VectorRenderingType.Template
false -> ImageSetContents.VectorRenderingType.Original
}
)
)
Loading