Feature - Export (#3)

Add exporting for web
This commit is contained in:
Jordon de Hoog 2024-10-01 16:21:36 -04:00 committed by GitHub
parent 8173e13967
commit 9337330735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 3402 additions and 414 deletions

View file

@ -1,14 +1,13 @@
name: Deploy Web App
name: Deploy Web App to Staging
on:
workflow_dispatch:
# TODO: Disabled until release
# push:
# branches:
# - main
# paths-ignore:
# - "**.md"
# - "ios/**"
push:
branches:
- main
paths-ignore:
- "**.md"
- "ios/**"
concurrency:
group: deploy-${{ github.ref }}
@ -46,7 +45,7 @@ jobs:
path: app/build/dist/wasmJs/productionExecutable
deploy:
name: "Deploy"
name: "Deploy to Staging"
runs-on: ubuntu-latest
needs:
- build

View file

@ -163,3 +163,26 @@ jobs:
}
console.log('All assets uploaded successfully.');
deploy:
name: "Deploy to Production"
runs-on: ubuntu-latest
needs: build
if: success() && contains(needs.build.outputs.matrix-target, 'web')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download web build artifacts
uses: actions/download-artifact@v4
with:
name: web
path: app/build/dist/wasmJs/productionExecutable
- name: Deploy to production
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_MATERIALKOLOR }}
channelId: live
projectId: materialkolor

View file

@ -1,5 +1,5 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.INT
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
@ -70,6 +70,7 @@ kotlin {
optIn("androidx.compose.material3.ExperimentalMaterial3Api")
optIn("androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi")
optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi")
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
@ -101,11 +102,13 @@ kotlin {
implementation(libs.stateHolder)
implementation(libs.stateHolder.compose)
implementation(libs.materialKolor)
implementation(libs.materialKolor.utilities)
implementation(libs.compose.colorpicker)
implementation(libs.calf.filePicker)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.highlights)
}
commonTest.dependencies {
@ -135,6 +138,7 @@ kotlin {
wasmJsMain.dependencies {
implementation(libs.kstore.storage)
implementation(npm("jszip", "3.10.1"))
}
val nonBrowserMain by creating {

View file

@ -1,5 +1,7 @@
package com.materialkolor.builder.core
import com.materialkolor.builder.export.DefaultExportRepo
import com.materialkolor.builder.export.ExportRepo
import com.materialkolor.builder.settings.DarkModeProvider
import com.materialkolor.builder.settings.DefaultDarkModeProvider
import com.materialkolor.builder.settings.DefaultSettingsRepo
@ -25,6 +27,8 @@ object DI {
val settingsRepo: SettingsRepo by lazy {
DefaultSettingsRepo(darkModeProvider, settingsStore, defaultScope)
}
val exportRepo: ExportRepo = DefaultExportRepo()
}
expect fun DI.provideSettingsStore(): SettingsStore
expect fun DI.provideSettingsStore(): SettingsStore

View file

@ -9,8 +9,6 @@ expect val baseUrl: String
/**
* Whether the platform supports exporting the current theme to code.
*
* TODO: Maybe on non-supported platforms we can provide a URL to the web version.
*/
expect val exportSupported: Boolean

View file

@ -0,0 +1,5 @@
package com.materialkolor.builder.export
import com.materialkolor.builder.export.model.ExportFile
expect suspend fun exportFiles(list: List<ExportFile>)

View file

@ -0,0 +1,35 @@
package com.materialkolor.builder.export
import co.touchlab.kermit.Logger
import com.materialkolor.builder.core.exportSupported
import com.materialkolor.builder.export.model.ExportOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.cancellation.CancellationException
interface ExportRepo {
suspend fun export(options: ExportOptions): Boolean
}
class DefaultExportRepo : ExportRepo {
override suspend fun export(options: ExportOptions): Boolean {
if (!exportSupported) {
Logger.e { "Export is not supported" }
return false
}
try {
Logger.d { "Exporting ${options.type.displayName} theme" }
Logger.d { "Files to export: ${options.files.joinToString { it.name }}" }
withContext(Dispatchers.Default) {
exportFiles(options.files)
}
return true
} catch (cause: Exception) {
if (cause is CancellationException) throw cause
Logger.e(cause) { "Export failed" }
return false
}
}
}

View file

@ -0,0 +1,16 @@
package com.materialkolor.builder.export
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import com.materialkolor.ktx.toHex
fun Color?.variable(name: String): String? {
if (this == null) return null
return "val ${name.capitalize(Locale("EN"))} = ${string()}"
}
private fun Color.string(): String {
val hex = toHex(alwaysIncludeAlpha = true, includePrefix = false)
return "Color(0x$hex)"
}

View file

@ -0,0 +1,11 @@
package com.materialkolor.builder.export.model
import androidx.compose.runtime.Stable
import dev.snipme.highlights.model.SyntaxLanguage
@Stable
data class ExportFile(
val name: String,
val content: String,
val language: SyntaxLanguage = SyntaxLanguage.KOTLIN,
)

View file

@ -0,0 +1,46 @@
package com.materialkolor.builder.export.model
import com.materialkolor.builder.export.model.library.createMaterialKolorFiles
import com.materialkolor.builder.export.model.standard.createStandardFiles
import com.materialkolor.builder.settings.model.Settings
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
enum class ExportType(val displayName: String) {
MaterialKolor("Material Kolor"),
Standard("Standard"),
}
data class ExportOptions(
val type: ExportType,
val settings: Settings,
val multiplatform: Boolean = DEFAULT_MULTIPLATFORM,
val themeName: String = DEFAULT_THEME_NAME,
val packageName: String = DEFAULT_PACKAGE_NAME,
val useVersionCatalog: Boolean = DEFAULT_USE_VERSION_CATALOG,
val animate: Boolean = DEFAULT_ANIMATE,
) {
val files: PersistentList<ExportFile> = when (type) {
ExportType.MaterialKolor -> createMaterialKolorFiles()
ExportType.Standard -> createStandardFiles()
}.toPersistentList()
fun toggleType(): ExportOptions = copy(
type = when (type) {
ExportType.MaterialKolor -> ExportType.Standard
ExportType.Standard -> ExportType.MaterialKolor
},
)
companion object {
const val DEFAULT_THEME_NAME = "AppTheme"
const val DEFAULT_PACKAGE_NAME = "com.example"
const val DEFAULT_MULTIPLATFORM = true
const val DEFAULT_USE_VERSION_CATALOG = true
const val DEFAULT_ANIMATE = true
fun default(settings: Settings) = ExportOptions(ExportType.MaterialKolor, settings)
}
}

View file

@ -0,0 +1,17 @@
package com.materialkolor.builder.export.model
import com.materialkolor.builder.BuildKonfig
import com.materialkolor.builder.core.baseUrl
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.settings.store.entity.toEntity
import com.materialkolor.builder.settings.store.entity.toQueryParams
fun header(settings: Settings) = """
// Generated using MaterialKolor Builder version ${BuildKonfig.VERSION_NAME} (${BuildKonfig.VERSION_CODE})
// ${settings.url()}
""".trimIndent()
private fun Settings.url(): String {
return "$baseUrl/${toEntity().toQueryParams()}"
}

View file

@ -0,0 +1,27 @@
package com.materialkolor.builder.export.model.library
import com.materialkolor.builder.export.variable
import com.materialkolor.builder.settings.model.ColorSettings
fun mkColorsKt(
packageName: String,
colors: ColorSettings,
): String {
val colorList = listOfNotNull(
if (colors.primary == null) colors.seed.variable("Seed")
else colors.primary.variable("Primary"),
colors.secondary.variable("Secondary"),
colors.tertiary.variable("Tertiary"),
colors.error.variable("Error"),
colors.neutral.variable("Neutral"),
colors.neutralVariant.variable("NeutralVariant"),
)
return """
package $packageName
import androidx.compose.ui.graphics.Color
${colorList.joinToString("\n ")}
""".trimIndent()
}

View file

@ -0,0 +1,30 @@
package com.materialkolor.builder.export.model.library
import com.materialkolor.builder.export.model.ExportFile
import com.materialkolor.builder.export.model.ExportOptions
import dev.snipme.highlights.model.SyntaxLanguage
fun ExportOptions.createMaterialKolorFiles(): List<ExportFile> {
val libs = if (useVersionCatalog) libsVersionsToml() else null
val gradle = gradleKts(isMultiplatform = multiplatform, useVersionCatalog = useVersionCatalog)
val colors = mkColorsKt(packageName = packageName, colors = settings.colors)
val theme = mkThemeKt(
packageName = packageName,
themeName = themeName,
animate = animate,
settings = settings,
)
return listOfNotNull(
libs?.let { content ->
ExportFile(
name = "libs.versions.toml",
content = content,
language = SyntaxLanguage.DEFAULT,
)
},
ExportFile(name = "build.gradle.kts", content = gradle),
ExportFile(name = "Color.kt", content = colors),
ExportFile(name = "Theme.kt", content = theme),
)
}

View file

@ -0,0 +1,44 @@
package com.materialkolor.builder.export.model.library
import com.materialkolor.builder.BuildKonfig
private val mkVersion = BuildKonfig.MATERIAL_KOLOR_VERSION
private val mkLib = "com.materialkolor:material-kolor:$mkVersion"
fun libsVersionsToml(): String = """
[versions]
materialKolor = "$mkVersion"
[libraries]
materialKolor = "$mkLib"
""".trimIndent()
fun buildImplementation(useVersionCatalog: Boolean): String = if (useVersionCatalog) {
"implementation(libs.materialKolor)"
} else {
"implementation(\"$mkLib\")"
}
fun gradleKts(
isMultiplatform: Boolean,
useVersionCatalog: Boolean,
): String {
val lib = buildImplementation(useVersionCatalog)
return if (isMultiplatform) {
"""
kotlin {
sourceSets {
commonMain.dependencies {
$lib
}
}
}
""".trimIndent()
} else {
"""
dependencies {
$lib
}
""".trimIndent()
}
}

View file

@ -0,0 +1,67 @@
package com.materialkolor.builder.export.model.library
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.decapitalize
import androidx.compose.ui.text.intl.Locale
import com.materialkolor.Contrast
import com.materialkolor.builder.export.model.header
import com.materialkolor.builder.settings.model.Settings
fun mkThemeKt(
packageName: String,
themeName: String,
settings: Settings,
animate: Boolean,
): String {
val contrast = if (settings.contrast == Contrast.Default) null
else "contrastLevel = ${settings.contrast.value}"
val params = listOfNotNull(
contrast,
settings.isAmoled.parameter("isAmoled"),
settings.isExtendedFidelity.parameter("extendedFidelity"),
if (settings.colors.primary == null) settings.colors.seed.parameter("Seed")
else settings.colors.primary.parameter("Primary"),
settings.colors.secondary.parameter("Secondary"),
settings.colors.tertiary.parameter("Tertiary"),
settings.colors.error.parameter("Error"),
settings.colors.neutral.parameter("Neutral"),
settings.colors.neutralVariant.parameter("NeutralVariant"),
).joinToString(",\n ")
return """
${header(settings)}
package $packageName
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import com.materialkolor.rememberDynamicMaterialThemeState
@Composable
fun $themeName(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val dynamicThemeState = rememberDynamicMaterialThemeState(
isDark = darkTheme,
style = PaletteStyle.${settings.style},
$params,
)
DynamicMaterialTheme(
state = dynamicThemeState,
animate = $animate,
content = content,
)
}
""".trimIndent()
}
private fun Boolean.parameter(name: String) = if (this) "$name = true" else null
private fun Color?.parameter(name: String): String? {
if (this == null) return null
return "${name.decapitalize(Locale("EN"))} = ${name.replaceFirstChar { it.uppercase() }}"
}

View file

@ -0,0 +1,141 @@
package com.materialkolor.builder.export.model.standard
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.text.decapitalize
import androidx.compose.ui.text.intl.Locale
import com.materialkolor.Contrast
import com.materialkolor.builder.export.variable
import com.materialkolor.builder.ktx.snakeToCamelCase
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.dynamiccolor.DynamicColor
import com.materialkolor.dynamiccolor.MaterialDynamicColors
import com.materialkolor.ktx.DynamicScheme
import com.materialkolor.ktx.getColor
import com.materialkolor.scheme.DynamicScheme
fun standardColorsKt(
packageName: String,
settings: Settings,
): String {
val map = MaterialDynamicColors(settings.isExtendedFidelity).colorList()
val light = createScheme(isDark = false, settings = settings)
val dark = createScheme(isDark = true, settings = settings)
return """
package $packageName
import androidx.compose.ui.graphics.Color
${settings.colors.seed.variable("Seed")}
${map.toColorVariables(scheme = light)}
${map.toColorVariables(scheme = dark)}
""".trimIndent().dropLastWhile { it == '\n' }
}
/**
* Return a list of color names:
*
* Example:
* ```
* "primary" to "PrimaryLight"
* ```
*/
fun lightVariableNamePairs(settings: Settings): Map<String, String> {
return variableNamePairs(settings, isDark = false)
}
fun darkVariableNamePairs(settings: Settings): Map<String, String> {
return variableNamePairs(settings, isDark = true)
}
private fun variableNamePairs(settings: Settings, isDark: Boolean): Map<String, String> {
val list = MaterialDynamicColors(settings.isExtendedFidelity).colorList()
val scheme = createScheme(isDark = isDark, settings = settings)
return list.variableNamePair(scheme).map { entry ->
entry.value.name.snakeToCamelCase().decapitalize(Locale("EN")) to entry.key
}.toMap()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun createScheme(
isDark: Boolean,
settings: Settings,
) = DynamicScheme(
isDark = isDark,
seedColor = settings.colors.seed,
primary = settings.colors.primary ?: settings.colors.seed,
secondary = settings.colors.secondary,
tertiary = settings.colors.tertiary,
error = settings.colors.error,
neutral = settings.colors.neutral,
neutralVariant = settings.colors.neutralVariant,
style = settings.style,
contrastLevel = settings.contrast.value,
)
private fun MaterialDynamicColors.colorList(): List<DynamicColor> = listOf(
primary(),
onPrimary(),
primaryContainer(),
onPrimaryContainer(),
secondary(),
onSecondary(),
secondaryContainer(),
onSecondaryContainer(),
tertiary(),
onTertiary(),
tertiaryContainer(),
onTertiaryContainer(),
error(),
onError(),
errorContainer(),
onErrorContainer(),
background(),
onBackground(),
surface(),
onSurface(),
surfaceVariant(),
onSurfaceVariant(),
outline(),
outlineVariant(),
scrim(),
inverseSurface(),
inverseOnSurface(),
inversePrimary(),
surfaceDim(),
surfaceBright(),
surfaceContainerLowest(),
surfaceContainerLow(),
surfaceContainer(),
surfaceContainerHigh(),
surfaceContainerHighest(),
)
private fun List<DynamicColor>.variableNamePair(
scheme: DynamicScheme,
): Map<String, DynamicColor> {
val contrast = scheme.contrastSuffix()
val mode = if (scheme.isDark) "Dark" else "Light"
val suffix = "$mode$contrast"
return associateBy { color -> "${color.name.snakeToCamelCase()}$suffix" }
}
private fun List<DynamicColor>.toColorVariables(
scheme: DynamicScheme,
): String {
val values = variableNamePair(scheme)
return buildString {
values.forEach { (name, color) ->
appendLine(color.getColor(scheme).variable(name))
}
}
}
private fun DynamicScheme.contrastSuffix(): String = when (contrastLevel) {
Contrast.Reduced.value -> "ReducedContrast"
Contrast.Default.value -> ""
Contrast.Medium.value -> "MediumContrast"
Contrast.High.value -> "HighContrast"
else -> ""
}

View file

@ -0,0 +1,19 @@
package com.materialkolor.builder.export.model.standard
import com.materialkolor.builder.export.model.ExportFile
import com.materialkolor.builder.export.model.ExportOptions
fun ExportOptions.createStandardFiles(): List<ExportFile> {
val colors = standardColorsKt(packageName = packageName, settings = settings)
val theme = standardThemeKt(
packageName = packageName,
themeName = themeName,
multiplatform = multiplatform,
settings = settings,
)
return listOf(
ExportFile("Color.kt", colors),
ExportFile("Theme.kt", theme),
)
}

View file

@ -0,0 +1,115 @@
package com.materialkolor.builder.export.model.standard
import com.materialkolor.Contrast
import com.materialkolor.builder.export.model.header
import com.materialkolor.builder.settings.model.Settings
private val androidImports = """
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
""".trimIndent()
private val multiplatformImports = """
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
""".trimIndent()
fun standardThemeKt(
packageName: String,
themeName: String,
multiplatform: Boolean,
settings: Settings,
): String {
val lightColors = lightVariableNamePairs(settings)
val lightSchemeName = settings.contrast.schemeName(isDark = false)
val darkColors = darkVariableNamePairs(settings)
val darkSchemeName = settings.contrast.schemeName(isDark = true)
val themeComposable =
if (multiplatform) multiplatformTheme(themeName, lightSchemeName, darkSchemeName)
else androidTheme(themeName, lightSchemeName, darkSchemeName)
return """
${header(settings)}
package $packageName
${if (multiplatform) multiplatformImports else androidImports}
private val $lightSchemeName = lightColorScheme(
${lightColors.toParamList()},
)
private val $darkSchemeName = darkColorScheme(
${darkColors.toParamList()},
)
$themeComposable
""".trimIndent()
}
private fun multiplatformTheme(themeName: String, lightSchemeName: String, darkSchemeName: String) = """
@Composable
fun $themeName(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit,
) {
val colorScheme = when {
darkTheme -> $darkSchemeName
else -> $lightSchemeName
}
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}
""".trimIndent()
private fun androidTheme(themeName: String, lightSchemeName: String, darkSchemeName: String) = """
@Composable
fun $themeName(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable() () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> $darkSchemeName
else -> $lightSchemeName
}
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}
""".trimIndent()
private fun Contrast.schemeName(isDark: Boolean): String {
val mode = if (isDark) "Dark" else "Light"
return when (this) {
Contrast.Reduced -> "reducedContrast${mode}ColorScheme"
Contrast.Default -> "${mode.lowercase()}ColorScheme"
Contrast.Medium -> "mediumContrast${mode}ColorScheme"
Contrast.High -> "highContrast${mode}ColorScheme"
}
}
private fun Map<String, String>.toParamList(): String {
return toList().joinToString(",\n") { (name, variable) ->
" $name = $variable"
}
}

View file

@ -0,0 +1,10 @@
package com.materialkolor.builder.ktx
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
fun String.snakeToCamelCase(): String {
return this.split("_").joinToString("") { string ->
string.capitalize(Locale("EN"))
}
}

View file

@ -1,5 +1,6 @@
package com.materialkolor.builder.settings
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -18,6 +19,7 @@ class DefaultDarkModeProvider : DarkModeProvider {
override var isDarkMode = _isDarkMode.asStateFlow()
override fun initialize(isDarkMode: Boolean) {
Logger.i { "Initializing dark mode to $isDarkMode" }
_isDarkMode.update { isDarkMode }
}
}
}

View file

@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
val DESTINATION_QUERY_PARAM = "destination"
const val DESTINATION_QUERY_PARAM = "destination"
interface SettingsRepo {
val settings: StateFlow<Settings>

View file

@ -17,7 +17,7 @@ data class ColorSettings(
fun update(keyColor: KeyColor, color: Color): ColorSettings = when (keyColor) {
KeyColor.Seed -> copy(seed = color)
KeyColor.Primary -> copy(primary = color)
KeyColor.Primary -> copy(primary = color, seed = color)
KeyColor.Secondary -> copy(secondary = color)
KeyColor.Tertiary -> copy(tertiary = color)
KeyColor.Error -> copy(error = color)
@ -57,4 +57,4 @@ private val _colors = persistentListOf(
Color(0xFF00FF7F), // Spring Green
Color(0xFFFF4500), // Orange Red
Color(0xFF1E90FF), // Dodger Blue
)
)

View file

@ -9,8 +9,16 @@ data class Settings(
val colors: ColorSettings,
val isDarkMode: Boolean,
val selectedImage: SeedImage?,
val contrast: Contrast = Contrast.Default,
val style: PaletteStyle = PaletteStyle.TonalSpot,
val isExtendedFidelity: Boolean = false,
val contrast: Contrast = SettingsDefaults.contrast,
val style: PaletteStyle = SettingsDefaults.style,
val isAmoled: Boolean = SettingsDefaults.isAmoled,
val isExtendedFidelity: Boolean = SettingsDefaults.isExtendedFidelity,
val isModified: Boolean = false,
)
object SettingsDefaults {
val contrast = Contrast.Default
val style = PaletteStyle.TonalSpot
val isAmoled = false
val isExtendedFidelity = false
}

View file

@ -6,11 +6,13 @@ import com.materialkolor.Contrast
import com.materialkolor.PaletteStyle
import com.materialkolor.builder.ktx.parseHexToColor
import com.materialkolor.builder.settings.model.KeyColor
import com.materialkolor.builder.settings.model.SettingsDefaults
import com.materialkolor.ktx.toHex
import io.ktor.http.decodeURLQueryComponent
import io.ktor.http.encodeURLQueryComponent
private const val KEY_DARK_MODE = "dark_mode"
private const val KEY_IS_AMOLED = "is_amoled"
private const val KEY_CONTRAST = "contrast"
private const val KEY_SELECTED_PRESET_ID = "selected_preset_id"
private const val KEY_STYLE = "style"
@ -31,16 +33,17 @@ fun SettingsEntity.toQueryParams(): String {
)
.param(key.KEY)
}
.joinToString(SEPARATOR)
val params = listOf(
colors,
isDarkMode.param(KEY_DARK_MODE),
contrast.param(KEY_CONTRAST),
val colorParams = if (colors.isEmpty()) null else colors.joinToString(SEPARATOR)
val params = listOfNotNull(
colorParams,
"${KEY_DARK_MODE}=${isDarkMode ?: false}",
style.param(KEY_STYLE, SettingsDefaults.style),
selectedPresetId.param(KEY_SELECTED_PRESET_ID),
style.param(KEY_STYLE),
isExtendedFidelity.param(KEY_EXTENDED_FIDELITY),
).filter { it.isNotEmpty() }.joinToString(SEPARATOR)
contrast.param(KEY_CONTRAST, SettingsDefaults.contrast.value),
isExtendedFidelity.param(KEY_EXTENDED_FIDELITY, SettingsDefaults.isExtendedFidelity),
isAmoled.param(KEY_IS_AMOLED, SettingsDefaults.isAmoled),
).joinToString(SEPARATOR)
return "?$params"
}
@ -69,8 +72,8 @@ fun String.splitQueryParams(): Map<String, String> {
}
}
private fun Any?.param(key: String): String {
if (this == null) return ""
private inline fun <reified T> T?.param(key: String, default: T? = null): String? {
if (this == null || this == default) return null
return "$key=${this.toString().encodeURLQueryComponent()}"
}

View file

@ -9,25 +9,28 @@ import com.materialkolor.builder.settings.model.ImagePresets
import com.materialkolor.builder.settings.model.KeyColor
import com.materialkolor.builder.settings.model.SeedImage
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.settings.model.SettingsDefaults
data class SettingsEntity(
val colors: Map<KeyColor, Int?>,
val isDarkMode: Boolean? = null,
val contrast: Double = Contrast.Default.value,
val contrast: Double = SettingsDefaults.contrast.value,
val selectedPresetId: String? = null,
val style: PaletteStyle = PaletteStyle.TonalSpot,
val isExtendedFidelity: Boolean = false,
val style: PaletteStyle = SettingsDefaults.style,
val isExtendedFidelity: Boolean = SettingsDefaults.isExtendedFidelity,
val isAmoled: Boolean = SettingsDefaults.isAmoled,
)
fun Settings.toEntity(): SettingsEntity {
val presetId = (selectedImage as? SeedImage.Resource)?.id
return SettingsEntity(
colors = colors.toEntity(),
isDarkMode = isDarkMode,
contrast = contrast.value,
isDarkMode = isDarkMode,
selectedPresetId = presetId,
style = style,
isExtendedFidelity = isExtendedFidelity,
isAmoled = isAmoled,
)
}
@ -43,6 +46,7 @@ fun SettingsEntity.toModel(isDarkModeFallback: Boolean): Settings {
selectedImage = preset,
style = style,
isExtendedFidelity = isExtendedFidelity,
isAmoled = isAmoled,
)
}

View file

@ -1,17 +1,23 @@
package com.materialkolor.builder.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.materialkolor.builder.core.DI
import com.materialkolor.builder.ui.home.HomeScreen
import com.materialkolor.builder.ui.ktx.windowSizeClass
import com.materialkolor.builder.ui.theme.AppTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("Not initialized") }
@Composable
@Preview
fun App(destination: String? = null) {
@ -28,6 +34,10 @@ fun App(destination: String? = null) {
settings = state.settings,
urlLauncher = model.urlLauncher,
) {
HomeScreen(destination)
CompositionLocalProvider(
LocalWindowSizeClass provides windowSizeClass(),
) {
HomeScreen(destination)
}
}
}

View file

@ -21,13 +21,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.materialkolor.builder.ui.LocalWindowSizeClass
@Composable
fun AboutInfo(
visible: Boolean,
onDismiss: () -> Unit,
windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier,
windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
) {
if (!visible) return

View file

@ -0,0 +1,78 @@
package com.materialkolor.builder.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.LocalWindowSizeClass
import com.materialkolor.builder.ui.ktx.widthIsCompact
@Composable
fun AppTopBar(
settings: Settings,
toggleDarkMode: () -> Unit,
onReset: () -> Unit,
toggleAboutDialog: (Boolean) -> Unit,
modifier: Modifier = Modifier,
showBackButton: Boolean = false,
onBack: () -> Unit = {},
windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
AnimatedVisibility(visible = showBackButton) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Back",
)
}
}
},
title = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Palette,
contentDescription = "MaterialKolor Builder",
tint = MaterialTheme.colorScheme.primary,
)
val text = if (windowSizeClass.widthIsCompact()) "MKB"
else "MaterialKolor Builder"
Text(
text = text,
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.SemiBold,
),
)
}
},
actions = {
TopBarActions(
settings = settings,
onToggleDarkMode = toggleDarkMode,
onReset = onReset,
onAboutClicked = { toggleAboutDialog(true) },
)
},
)
}

View file

@ -0,0 +1,28 @@
package com.materialkolor.builder.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun CopyIcon(
visble: Boolean,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = visble,
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier,
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy color code",
)
}
}

View file

@ -0,0 +1,271 @@
package com.materialkolor.builder.ui.components
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons.Default
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.min
import com.materialkolor.builder.ui.ktx.conditional
import com.materialkolor.builder.ui.ktx.debugBorder
import com.materialkolor.builder.ui.ktx.whenNotNull
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
enum class SideSheetPosition {
Start,
End,
}
// TODO: Replace when Material3 includes the SideSheet component
@Composable
fun SideSheet(
modifier: Modifier = Modifier,
position: SideSheetPosition = SideSheetPosition.Start,
initialExpanded: Boolean = false,
maxWidthFraction: Float = 1f / 2.5f,
minWidth: Dp = 200.dp,
visibleWidth: Dp = 30.dp,
sheetCornerRadius: Dp = 25.dp,
isFloating: Boolean = false,
displayOverContent: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface,
contentContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
sheetContent: @Composable () -> Unit,
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
val density = LocalDensity.current
var lastDragState by remember {
mutableStateOf(if (initialExpanded) DragValue.Expanded else DragValue.Collapsed)
}
BoxWithConstraints {
val maxSheetWidth = maxWidth * maxWidthFraction
val sheetWidth = min(max(minWidth, maxSheetWidth), maxWidth)
val maxWidthPx = with(density) { maxWidth.toPx() }
val sheetWidthPx = with(density) { sheetWidth.toPx() }
val anchors = remember(sheetWidth, visibleWidth, position) {
DraggableAnchors {
DragValue.Collapsed at with(density) { -sheetWidth.toPx() + visibleWidth.toPx() }
DragValue.Expanded at 0f
}
}
val velocityThreshold = AnchoredDraggableDefaults.VelocityThreshold()
val state = remember(position, sheetWidth, maxWidth) {
AnchoredDraggableState(
initialValue = lastDragState,
anchors = anchors,
positionalThreshold = AnchoredDraggableDefaults.PositionalThreshold,
velocityThreshold = velocityThreshold,
snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec,
decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec,
confirmValueChange = { newValue ->
lastDragState = newValue
true
},
)
}
val contentWidth = remember(maxWidthPx, sheetWidthPx, state.offset) {
if (displayOverContent) null
else {
val visibleSheetWidth = sheetWidthPx + state.offset
val contentWidthPx = maxWidthPx - visibleSheetWidth
(contentWidthPx / density.density).dp
}
}
Box(
modifier = modifier
.fillMaxSize()
.background(contentContainerColor),
) {
val contentAlignment = when (position) {
SideSheetPosition.Start -> Alignment.CenterEnd
SideSheetPosition.End -> Alignment.CenterStart
}
Box(
contentAlignment = contentAlignment,
modifier = Modifier
.fillMaxSize()
.background(contentContainerColor),
) {
Surface(
color = contentContainerColor,
modifier = Modifier.whenNotNull(contentWidth) { Modifier.width(it) },
) {
content()
}
}
Surface(
color = containerColor,
shape = position.sheetShape(sheetCornerRadius),
modifier = Modifier
.align(
when (position) {
SideSheetPosition.Start -> Alignment.CenterStart
SideSheetPosition.End -> Alignment.CenterEnd
}
)
.width(sheetWidth)
.clipToBounds()
.conditional(isFloating) {
Modifier.padding(vertical = 32.dp)
}
.offset {
IntOffset(
y = 0,
x = when (position) {
SideSheetPosition.Start -> state.offset.roundToInt()
SideSheetPosition.End -> -state.offset.roundToInt()
},
)
}
.anchoredDraggable(
state = state,
orientation = Orientation.Horizontal,
reverseDirection = position == SideSheetPosition.End,
enabled = true,
)
.clip(position.sheetShape(sheetCornerRadius)),
) {
val panel = @Composable {
ExpandCollapsePanel(
value = state.currentValue,
visibleWidth = visibleWidth,
sheetPosition = position,
onClick = {
scope.launch {
state.animateTo(state.currentValue.opposite)
}
},
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize(),
) {
if (position == SideSheetPosition.End) panel()
Box(modifier = Modifier.weight(1f)) {
sheetContent()
}
if (position == SideSheetPosition.Start) panel()
}
}
}
}
}
private fun SideSheetPosition.sheetShape(radius: Dp): RoundedCornerShape {
return when (this) {
SideSheetPosition.Start -> RoundedCornerShape(topEnd = radius, bottomEnd = radius)
SideSheetPosition.End -> RoundedCornerShape(topStart = radius, bottomStart = radius)
}
}
private enum class DragValue {
Expanded,
Collapsed
}
private val DragValue.opposite: DragValue
get() = when (this) {
DragValue.Expanded -> DragValue.Collapsed
DragValue.Collapsed -> DragValue.Expanded
}
@Composable
private fun ExpandCollapsePanel(
value: DragValue,
visibleWidth: Dp,
sheetPosition: SideSheetPosition,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val isExpanded = value == DragValue.Expanded
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.width(visibleWidth)
.clickable(onClick = onClick)
.fillMaxHeight(),
) {
val icon = when (sheetPosition) {
SideSheetPosition.Start -> if (isExpanded) Default.ChevronLeft else Default.ChevronRight
SideSheetPosition.End -> if (isExpanded) Default.ChevronRight else Default.ChevronLeft
}
Icon(
imageVector = icon,
contentDescription = if (isExpanded) "Collapse" else "Expand",
modifier = Modifier.size(32.dp),
)
}
}
object AnchoredDraggableDefaults {
/** The default spec for snapping, a tween spec */
val SnapAnimationSpec: AnimationSpec<Float> = tween()
/** The default positional threshold, 50% of the distance */
val PositionalThreshold: (Float) -> Float = { distance -> distance * 0.5f }
/** The default velocity threshold, 125 dp per second */
@Composable
fun VelocityThreshold(): () -> Float {
val density = LocalDensity.current
return { with(density) { 125.dp.toPx() } }
}
/** The default spec for decaying, an exponential decay */
val DecayAnimationSpec: DecayAnimationSpec<Float> = exponentialDecay()
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.components
package com.materialkolor.builder.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.RowScope

View file

@ -0,0 +1,58 @@
package com.materialkolor.builder.ui.components.code
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import dev.snipme.highlights.Highlights
import dev.snipme.highlights.model.BoldHighlight
import dev.snipme.highlights.model.ColorHighlight
/**
* A composable that displays code with highlights.
*
* Copied from https://raw.githubusercontent.com/SnipMeDev/KodeView/refs/heads/main/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt
*/
@Composable
fun CodeTextView(
highlights: Highlights,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
) {
val text = remember(highlights) {
buildAnnotatedString {
append(highlights.getCode())
highlights.getHighlights()
.filterIsInstance<ColorHighlight>()
.forEach { highlight ->
addStyle(
style = SpanStyle(color = Color(highlight.rgb).copy(alpha = 1f)),
start = highlight.location.start,
end = highlight.location.end,
)
}
highlights.getHighlights()
.filterIsInstance<BoldHighlight>()
.forEach { highlight ->
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = highlight.location.start,
end = highlight.location.end,
)
}
}
}
Text(
modifier = modifier,
style = style,
text = text,
)
}

View file

@ -3,9 +3,10 @@ package com.materialkolor.builder.ui.home
import androidx.compose.ui.graphics.Color
import com.materialkolor.Contrast
import com.materialkolor.PaletteStyle
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.settings.model.KeyColor
import com.materialkolor.builder.settings.model.SeedImage
import com.materialkolor.builder.ui.home.page.HomeSection
import com.materialkolor.builder.ui.home.preview.PreviewSection
sealed interface HomeAction {
sealed interface ColorPicker : HomeAction
@ -17,8 +18,12 @@ sealed interface HomeAction {
data class SelectImage(val image: SeedImage.Resource?) : HomeAction
data class CopyColor(val name: String, val color: Color) : HomeAction
data object RandomColor : HomeAction
data class Nav(val screen: HomeScreens) : HomeAction
data class Share(val section: PreviewSection) : HomeAction
data object ToggleExportMode : HomeAction
data class UpdateExportOptions(val options: ExportOptions) : HomeAction
data object Export : HomeAction
data class Share(val section: HomeSection) : HomeAction
data object CancelExport : HomeAction
data class OpenColorPicker(val key: KeyColor, val initial: Color) : ColorPicker
data class UpdateColor(val color: Color) : ColorPicker

View file

@ -0,0 +1,124 @@
package com.materialkolor.builder.ui.home
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.core.Dispatcher
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.ui.components.SideSheet
import com.materialkolor.builder.ui.components.SideSheetPosition
import com.materialkolor.builder.ui.home.HomeAction.OpenColorPicker
import com.materialkolor.builder.ui.home.HomeAction.RandomColor
import com.materialkolor.builder.ui.home.HomeAction.SelectImage
import com.materialkolor.builder.ui.home.HomeAction.UpdateContrast
import com.materialkolor.builder.ui.home.HomeAction.UpdatePaletteStyle
import com.materialkolor.builder.ui.home.components.ContrastSelector
import com.materialkolor.builder.ui.home.export.ExportScreenContent
import com.materialkolor.builder.ui.home.preview.PreviewScreenContent
import com.materialkolor.builder.ui.home.preview.PreviewSection
import com.materialkolor.builder.ui.home.preview.customize.CustomizeSection
import com.materialkolor.builder.ui.ktx.widthIsExpanded
import com.materialkolor.builder.ui.ktx.windowSizeClass
@Composable
fun HomeContent(
screen: HomeScreens,
options: ExportOptions,
selectedSection: PreviewSection,
updateSelectedSection: (PreviewSection) -> Unit,
processingImage: Boolean,
dispatcher: Dispatcher<HomeAction>,
windowSizeClass: WindowSizeClass = windowSizeClass(),
) {
if (windowSizeClass.widthIsExpanded()) {
SideSheet(
position = SideSheetPosition.Start,
initialExpanded = true,
isFloating = true,
displayOverContent = false,
maxWidthFraction = 0.3f,
sheetContent = {
CustomizeSection(
settings = options.settings,
onSelectImage = dispatcher.rememberRelayOf(::SelectImage),
onRandomColor = dispatcher.rememberRelay(RandomColor),
openColorPicker = dispatcher.rememberRelayOf(::OpenColorPicker),
onUpdatePaletteStyle = dispatcher.rememberRelayOf(::UpdatePaletteStyle),
onUpdateContrast = dispatcher.rememberRelayOf(::UpdateContrast),
processingImage = processingImage,
windowSizeClass = windowSizeClass,
)
},
) {
Box(
modifier = Modifier.fillMaxSize(),
) {
Crossfade(targetState = screen) { targetState ->
Content(
screen = targetState,
options = options,
selectedSection = selectedSection,
updateSelectedSection = updateSelectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
ContrastSelector(
selected = options.settings.contrast,
onUpdate = dispatcher.rememberRelayOf(::UpdateContrast),
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, bottom = 16.dp),
)
}
}
} else {
Content(
screen = screen,
options = options,
selectedSection = selectedSection,
updateSelectedSection = updateSelectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
}
@Composable
fun Content(
screen: HomeScreens,
options: ExportOptions,
selectedSection: PreviewSection,
updateSelectedSection: (PreviewSection) -> Unit,
processingImage: Boolean,
dispatcher: Dispatcher<HomeAction>,
windowSizeClass: WindowSizeClass = windowSizeClass(),
) {
if (screen == HomeScreens.Preview) {
PreviewScreenContent(
settings = options.settings,
selectedSection = selectedSection,
updateSelectedSection = updateSelectedSection,
dispatcher = dispatcher,
processingImage = processingImage,
windowSizeClass = windowSizeClass,
)
} else {
ExportScreenContent(
options = options,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
}

View file

@ -10,13 +10,15 @@ import com.materialkolor.builder.core.DI
import com.materialkolor.builder.core.readBytes
import com.materialkolor.builder.core.shareToClipboard
import com.materialkolor.builder.core.shareUrl
import com.materialkolor.builder.export.ExportRepo
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.settings.SettingsRepo
import com.materialkolor.builder.settings.model.ColorSettings
import com.materialkolor.builder.settings.model.ImagePresets
import com.materialkolor.builder.settings.model.SeedImage
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.components.ColorPickerState
import com.materialkolor.builder.ui.home.page.HomeSection
import com.materialkolor.builder.ui.home.preview.PreviewSection
import com.materialkolor.builder.ui.ktx.UiStateViewModel
import com.materialkolor.ktx.themeColorOrNull
import com.materialkolor.ktx.toHex
@ -24,6 +26,7 @@ import com.mohamedrejeb.calf.io.KmpFile
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.decodeToImageBitmap
@ -31,13 +34,18 @@ import kotlin.random.Random
class HomeModel(
private val settingsRepo: SettingsRepo = DI.settingsRepo,
private val exportRepo: ExportRepo = DI.exportRepo,
private val clipboard: Clipboard = DI.clipboard,
private val random: Random = Random.Default,
) : UiStateViewModel<HomeModel.State, HomeModel.Event>(State(settingsRepo.settings.value)) {
) : UiStateViewModel<HomeModel.State, HomeModel.Event>(
State(ExportOptions.default(settingsRepo.settings.value)),
) {
private var exportJob: Job? = null
init {
settingsRepo.settings.collectToState { state, value ->
state.copy(settings = value)
state.copy(exportOptions = state.exportOptions.copy(settings = value))
}
}
@ -142,7 +150,7 @@ class HomeModel(
}
}
fun share(destination: HomeSection) {
fun share(destination: PreviewSection) {
val url = settingsRepo.getUrl(destination.name)
Logger.d { "Share URL: $url" }
shareUrl(url)
@ -152,6 +160,37 @@ class HomeModel(
}
}
fun toggleExportMode() {
updateState { state ->
state.copy(exportOptions = state.exportOptions.toggleType())
}
}
fun updateExportOptions(options: ExportOptions) {
updateState { it.copy(exportOptions = options) }
}
fun export() {
if (state.value.exporting) return
updateState { it.copy(exporting = true) }
exportJob = viewModelScope.launch {
val result = exportRepo.export(state.value.exportOptions)
updateState { it.copy(exporting = false) }
if (!result) {
emit(Event.ShowSnackbar("Failed to export theme..."))
}
}
}
fun cancelExport() {
if (exportJob == null) return
exportJob?.cancel()
updateState { it.copy(exporting = false) }
}
private fun updateSettings(block: (Settings) -> Settings) {
viewModelScope.launch {
settingsRepo.update(block)
@ -166,10 +205,11 @@ class HomeModel(
}
data class State(
val settings: Settings,
val exportOptions: ExportOptions,
val imagePresets: PersistentList<SeedImage> = ImagePresets.all.toPersistentList(),
val processingImage: Boolean = false,
val colorPickerState: ColorPickerState? = null,
val exporting: Boolean = false,
)
sealed interface Event {

View file

@ -2,145 +2,78 @@ package com.materialkolor.builder.ui.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowRight
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.core.Dispatcher
import com.materialkolor.builder.core.exportSupported
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.ui.about.AboutInfo
import com.materialkolor.builder.ui.components.AppSnackbarHost
import com.materialkolor.builder.ui.components.AppTopBar
import com.materialkolor.builder.ui.components.ColorPickerDialog
import com.materialkolor.builder.ui.components.ColorPickerState
import com.materialkolor.builder.ui.home.HomeAction.Export
import com.materialkolor.builder.ui.home.HomeAction.Share
import com.materialkolor.builder.ui.home.HomeAction.ToggleDarkMode
import com.materialkolor.builder.ui.home.HomeAction.UpdateColor
import com.materialkolor.builder.ui.home.components.ExportDialog
import com.materialkolor.builder.ui.home.components.HomeBottomBar
import com.materialkolor.builder.ui.home.components.HomeNavRail
import com.materialkolor.builder.ui.home.components.TopBarActions
import com.materialkolor.builder.ui.home.page.CompactContent
import com.materialkolor.builder.ui.home.page.ExpandedContent
import com.materialkolor.builder.ui.home.page.HomeSection
import com.materialkolor.builder.ui.home.preview.PreviewSection
import com.materialkolor.builder.ui.ktx.widthIsCompact
import com.materialkolor.builder.ui.ktx.widthIsExpanded
import com.materialkolor.builder.ui.ktx.windowSizeClass
val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("Not initialized") }
@Composable
fun HomeScreenScaffold(
settings: Settings,
options: ExportOptions,
colorPickerState: ColorPickerState?,
dispatcher: Dispatcher<HomeAction>,
modifier: Modifier = Modifier,
initialSection: HomeSection? = null,
exporting: Boolean = false,
screen: HomeScreens = HomeScreens.Preview,
initialSection: PreviewSection? = null,
snackbarState: SnackbarHostState = remember { SnackbarHostState() },
processingImage: Boolean = false,
windowSizeClass: WindowSizeClass = windowSizeClass(),
) {
var aboutDialogVisible by remember { mutableStateOf(false) }
var selectedSection by remember { mutableStateOf(initialSection ?: HomeSection.Customize) }
var selectedSection by remember { mutableStateOf(initialSection ?: PreviewSection.Customize) }
CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) {
HomeScreenScaffold(
settings = settings,
colorPickerState = colorPickerState,
dispatcher = dispatcher,
snackbarState = snackbarState,
processingImage = processingImage,
aboutDialogVisible = aboutDialogVisible,
toggleAboutDialog = { aboutDialogVisible = it },
selectedSection = selectedSection,
onSelectedSection = { selectedSection = it },
windowSizeClass = windowSizeClass,
modifier = modifier,
)
}
}
@Composable
private fun HomeScreenScaffold(
colorPickerState: ColorPickerState?,
settings: Settings,
dispatcher: Dispatcher<HomeAction>,
snackbarState: SnackbarHostState,
processingImage: Boolean,
aboutDialogVisible: Boolean,
toggleAboutDialog: (Boolean) -> Unit,
selectedSection: HomeSection,
onSelectedSection: (HomeSection) -> Unit,
windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
snackbarHost = { AppSnackbarHost(snackbarState) },
topBar = {
TopAppBar(
title = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Palette,
contentDescription = "MaterialKolor Builder",
tint = MaterialTheme.colorScheme.primary,
)
val text = if (windowSizeClass.widthIsCompact()) "MKB"
else "MaterialKolor Builder"
Text(
text = text,
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.SemiBold,
),
)
}
},
actions = {
TopBarActions(
settings = settings,
onToggleDarkMode = dispatcher.relay(ToggleDarkMode),
onReset = dispatcher.relay(HomeAction.Reset),
onAboutClicked = { toggleAboutDialog(true) },
)
},
AppTopBar(
settings = options.settings,
showBackButton = screen == HomeScreens.Export,
onBack = dispatcher.relay(HomeAction.Nav(HomeScreens.Preview)),
toggleDarkMode = dispatcher.relay(ToggleDarkMode),
onReset = dispatcher.relay(HomeAction.Reset),
toggleAboutDialog = { aboutDialogVisible = true },
)
},
bottomBar = {
AnimatedVisibility(windowSizeClass.widthIsCompact()) {
AnimatedVisibility(screen == HomeScreens.Preview && windowSizeClass.widthIsCompact()) {
HomeBottomBar(
selected = selectedSection,
onSelected = { onSelectedSection(it) },
onSelected = { selectedSection = it },
)
}
},
@ -148,11 +81,25 @@ private fun HomeScreenScaffold(
if (exportSupported) {
Crossfade(windowSizeClass.widthIsExpanded()) { isExpanded ->
if (isExpanded) {
val action =
if (screen == HomeScreens.Export) HomeAction.Export
else HomeAction.Nav(HomeScreens.Export)
ExtendedFloatingActionButton(
onClick = dispatcher.rememberRelay(Export),
icon = { Icon(Icons.Default.Download, contentDescription = "Export") },
onClick = dispatcher.rememberRelay(action),
icon = {
if (screen == HomeScreens.Export) {
Icon(Icons.Default.Download, contentDescription = "Export")
} else {
Icon(
Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = "Next",
)
}
},
text = {
Text(text = "Export")
val text = if (screen == HomeScreens.Export) "Export" else "Next"
Text(text = text)
},
)
} else {
@ -173,45 +120,20 @@ private fun HomeScreenScaffold(
Box(
modifier = Modifier.padding(innerPadding),
) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> {
ExpandedContent(
settings = settings,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
WindowWidthSizeClass.Medium -> {
Row {
HomeNavRail(
selected = selectedSection,
onSelected = { onSelectedSection(it) },
)
CompactContent(
settings = settings,
selectedSection = selectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
}
WindowWidthSizeClass.Compact -> {
CompactContent(
settings = settings,
selectedSection = selectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
)
}
}
HomeContent(
screen = screen,
options = options,
selectedSection = selectedSection,
updateSelectedSection = { selectedSection = it },
dispatcher = dispatcher,
processingImage = processingImage,
windowSizeClass = windowSizeClass,
)
}
AboutInfo(
visible = aboutDialogVisible,
onDismiss = { toggleAboutDialog(false) },
onDismiss = { aboutDialogVisible = false },
windowSizeClass = windowSizeClass,
)
@ -222,5 +144,11 @@ private fun HomeScreenScaffold(
toggleMode = dispatcher.rememberRelay(HomeAction.TogglePickerMode),
selectImage = dispatcher.rememberRelay(HomeAction.PickImageForColor),
)
if (exporting) {
ExportDialog(
onDismiss = dispatcher.relay(HomeAction.CancelExport),
)
}
}
}

View file

@ -14,22 +14,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.materialkolor.builder.core.rememberDebounceDispatcher
import com.materialkolor.builder.ui.home.HomeAction.CancelExport
import com.materialkolor.builder.ui.home.HomeAction.ColorPicker
import com.materialkolor.builder.ui.home.HomeAction.CopyColor
import com.materialkolor.builder.ui.home.HomeAction.Export
import com.materialkolor.builder.ui.home.HomeAction.Nav
import com.materialkolor.builder.ui.home.HomeAction.RandomColor
import com.materialkolor.builder.ui.home.HomeAction.Reset
import com.materialkolor.builder.ui.home.HomeAction.SelectImage
import com.materialkolor.builder.ui.home.HomeAction.Share
import com.materialkolor.builder.ui.home.HomeAction.ToggleDarkMode
import com.materialkolor.builder.ui.home.HomeAction.ToggleExportMode
import com.materialkolor.builder.ui.home.HomeAction.UpdateContrast
import com.materialkolor.builder.ui.home.HomeAction.UpdateExportOptions
import com.materialkolor.builder.ui.home.HomeAction.UpdatePaletteStyle
import com.materialkolor.builder.ui.home.page.HomeSection
import com.materialkolor.builder.ui.home.page.gallery.NavigationDrawerContent
import com.materialkolor.builder.ui.home.preview.PreviewSection
import com.materialkolor.builder.ui.home.preview.gallery.NavigationDrawerContent
import com.materialkolor.builder.ui.ktx.HandleEvents
import com.materialkolor.builder.ui.ktx.launch
import com.mohamedrejeb.calf.picker.FilePickerFileType
@ -70,8 +77,10 @@ fun HomeScreen(destination: String? = null) {
}
}
var screen by remember { mutableStateOf(HomeScreens.Preview) }
val initialSection = remember {
destination?.let { runCatching { HomeSection.valueOf(it) }.getOrNull() }
destination?.let { runCatching { PreviewSection.valueOf(it) }.getOrNull() }
}
CompositionLocalProvider(
@ -87,11 +96,13 @@ fun HomeScreen(destination: String? = null) {
},
) {
HomeScreenScaffold(
settings = state.settings,
options = state.exportOptions,
colorPickerState = state.colorPickerState,
snackbarState = snackbar,
initialSection = initialSection,
processingImage = state.processingImage,
exporting = state.exporting,
screen = screen,
dispatcher = rememberDebounceDispatcher { action ->
when (action) {
is UpdateContrast -> model.updateContrast(action.contrast)
@ -104,9 +115,13 @@ fun HomeScreen(destination: String? = null) {
is RandomColor -> model.randomColor()
is Reset -> model.reset()
is CopyColor -> model.copyColorToClipboard(action.name, action.color)
is HomeAction.ColorPicker -> model.handleColorPickerAction(action)
is Export -> {} // TODO: Implement export
is ColorPicker -> model.handleColorPickerAction(action)
is Nav -> screen = action.screen
is Share -> model.share(action.section)
is ToggleExportMode -> model.toggleExportMode()
is UpdateExportOptions -> model.updateExportOptions(action.options)
is Export -> model.export()
is CancelExport -> model.cancelExport()
}
},
)

View file

@ -0,0 +1,6 @@
package com.materialkolor.builder.ui.home
enum class HomeScreens {
Preview,
Export,
}

View file

@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
@ -61,6 +62,8 @@ fun ColorCard(
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = 0.8f),
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.outlined.BrightnessHigh
import androidx.compose.material.icons.outlined.BrightnessLow
import androidx.compose.material.icons.outlined.BrightnessMedium
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -23,6 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.materialkolor.Contrast
@ -35,8 +37,10 @@ fun ContrastSelector(
onUpdate: (Contrast) -> Unit,
modifier: Modifier = Modifier,
options: PersistentList<Contrast> = Contrast.entries.sortedBy { it.value }.toPersistentList(),
containerColor: Color = CardDefaults.elevatedCardColors().containerColor,
) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = containerColor),
shape = CircleShape,
modifier = modifier,
) {

View file

@ -0,0 +1,54 @@
package com.materialkolor.builder.ui.home.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@Composable
fun ExportDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
Dialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
content = {
Card(
modifier = modifier
.size(300.dp)
.padding(16.dp),
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Exporting...", fontStyle = FontStyle.Italic)
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = onDismiss) {
Text(text = "Cancel")
}
}
}
},
)
}

View file

@ -2,7 +2,6 @@ package com.materialkolor.builder.ui.home.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Contrast
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Preview
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.filled.Tune
@ -13,28 +12,30 @@ import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.materialkolor.builder.ui.home.page.HomeSection
import com.materialkolor.builder.ui.home.preview.PreviewSection
@Composable
fun HomeBottomBar(
selected: HomeSection,
onSelected: (HomeSection) -> Unit,
selected: PreviewSection,
onSelected: (PreviewSection) -> Unit,
modifier: Modifier = Modifier,
) {
NavigationBar(
modifier = modifier,
) {
HomeSection.All.forEach { section ->
PreviewSection.All.forEach { section ->
val name = section.name()
NavigationBarItem(
icon = {
Icon(
imageVector = section.icon(),
contentDescription = section.name,
contentDescription = name,
)
},
label = { Text(section.name) },
label = { Text(name) },
selected = section == selected,
onClick = { onSelected(section) },
)
@ -44,22 +45,23 @@ fun HomeBottomBar(
@Composable
fun HomeNavRail(
selected: HomeSection,
onSelected: (HomeSection) -> Unit,
selected: PreviewSection,
onSelected: (PreviewSection) -> Unit,
modifier: Modifier = Modifier,
) {
NavigationRail(
modifier = modifier,
) {
HomeSection.All.forEach { section ->
PreviewSection.All.forEach { section ->
val name = section.name()
NavigationRailItem(
icon = {
Icon(
imageVector = section.icon(),
contentDescription = section.name,
contentDescription = name,
)
},
label = { Text(section.name) },
label = { Text(name) },
selected = section == selected,
onClick = { onSelected(section) },
)
@ -67,10 +69,22 @@ fun HomeNavRail(
}
}
private fun HomeSection.icon(): ImageVector = when (this) {
HomeSection.Customize -> Icons.Default.Tune
HomeSection.Themes -> Icons.Default.Contrast
HomeSection.Palettes -> Icons.Default.Palette
HomeSection.Preview -> Icons.Default.Smartphone
HomeSection.Components -> Icons.Default.Preview
@Composable
private fun PreviewSection.icon(): ImageVector = remember(this) {
when (this) {
PreviewSection.Customize -> Icons.Default.Tune
PreviewSection.Themes -> Icons.Default.Contrast
PreviewSection.Preview -> Icons.Default.Smartphone
PreviewSection.Components -> Icons.Default.Preview
}
}
@Composable
private fun PreviewSection.name(): String = remember(this) {
when (this) {
PreviewSection.Customize,
PreviewSection.Themes,
PreviewSection.Preview -> this.name
PreviewSection.Components -> "Gallery"
}
}

View file

@ -0,0 +1,132 @@
package com.materialkolor.builder.ui.home.export
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.export.model.ExportType
import com.materialkolor.builder.ui.ktx.clickableWithoutRipple
@Composable
fun ExportOptionsCard(
options: ExportOptions,
toggleMode: () -> Unit,
updateOptions: (ExportOptions) -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier
.width(IntrinsicSize.Min)
.animateContentSize(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) {
Text(
text = "Options",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 8.dp),
)
SingleChoiceSegmentedButtonRow(
modifier = Modifier.width(250.dp),
) {
ExportType.entries.forEachIndexed { index, mode ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = ExportType.entries.size,
),
onClick = toggleMode,
selected = mode == options.type,
icon = {},
label = { Text(mode.displayName) },
)
}
}
OptionSwitch(
text = "Multiplatform",
value = options.multiplatform,
onValueChange = { updateOptions(options.copy(multiplatform = it)) },
)
AnimatedVisibility(visible = options.type == ExportType.MaterialKolor) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
OptionSwitch(
text = "Animate",
value = options.animate,
onValueChange = { updateOptions(options.copy(animate = it)) },
)
OptionSwitch(
text = "Use version catalog",
value = options.useVersionCatalog,
onValueChange = { updateOptions(options.copy(useVersionCatalog = it)) },
)
}
}
}
}
}
@Composable
private fun OptionSwitch(
text: String,
value: Boolean,
onValueChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.semantics { contentDescription = "Toggle $text" }
.fillMaxWidth()
.clickableWithoutRipple {
onValueChange(!value)
},
) {
Text(text = text)
Switch(
checked = value,
onCheckedChange = { onValueChange(it) },
thumbContent = {
if (value) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.fillMaxSize(0.8f)
)
}
},
)
}
}

View file

@ -0,0 +1,110 @@
package com.materialkolor.builder.ui.home.export
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.core.Dispatcher
import com.materialkolor.builder.export.model.ExportOptions
import com.materialkolor.builder.ui.LocalWindowSizeClass
import com.materialkolor.builder.ui.home.HomeAction
import com.materialkolor.builder.ui.home.HomeAction.UpdateExportOptions
import com.materialkolor.builder.ui.home.LocalSnackbarHostState
import com.materialkolor.builder.ui.ktx.launch
@Composable
fun ExportScreenContent(
options: ExportOptions,
dispatcher: Dispatcher<HomeAction>,
modifier: Modifier = Modifier,
windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> {
ExportExpandedContent(
options = options,
modifier = modifier,
dispatcher = dispatcher,
)
}
else -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize(),
) {
Text("Not supported", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Please expand the window to view the export settings, or view on a larger screen.",
textAlign = TextAlign.Center,
modifier = Modifier.widthIn(max = 300.dp),
)
}
}
}
}
@Composable
fun ExportExpandedContent(
options: ExportOptions,
dispatcher: Dispatcher<HomeAction>,
modifier: Modifier = Modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxSize()
.padding(horizontal = 32.dp, vertical = 16.dp),
) {
ExportOptionsCard(
options = options,
toggleMode = dispatcher.rememberRelay(HomeAction.ToggleExportMode),
updateOptions = dispatcher.rememberRelayOf(::UpdateExportOptions),
modifier = Modifier.widthIn(max = 300.dp),
)
var selected by remember { mutableStateOf(options.files.first()) }
LaunchedEffect(options.files) {
selected = options.files.firstOrNull { it.name == selected.name } ?: options.files.first()
}
val clipboard = LocalClipboardManager.current
val snackbar = LocalSnackbarHostState.current
val scope = rememberCoroutineScope()
FileListContainer(
selected = selected,
files = options.files,
onSelected = { selected = it },
onClick = {
clipboard.setText(AnnotatedString(selected.content))
snackbar.launch(scope, "Copied the contents of ${selected.name} to clipboard")
},
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
)
}
}

View file

@ -0,0 +1,183 @@
package com.materialkolor.builder.ui.home.export
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import com.materialkolor.builder.export.model.ExportFile
import com.materialkolor.builder.ui.components.CopyIcon
import com.materialkolor.builder.ui.components.code.CodeTextView
import com.materialkolor.builder.ui.theme.JetBrainsMono
import com.materialkolor.builder.ui.theme.LocalThemeIsDark
import dev.snipme.highlights.Highlights
import dev.snipme.highlights.model.SyntaxThemes
import kotlinx.collections.immutable.PersistentList
@Composable
fun FileListContainer(
selected: ExportFile,
files: PersistentList<ExportFile>,
onSelected: (ExportFile) -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(selected, files) {
if (selected.name !in files.map { it.name }) {
Logger.d { "Selected file ${selected.name} not in list, selecting first file" }
onSelected(files.first())
}
}
OutlinedCard(
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
files.forEach { file ->
Tab(
file = file,
isSelected = selected.name == file.name,
onClick = { onSelected(file) },
)
}
}
Box(modifier = Modifier) {
val isDark by LocalThemeIsDark.current
val highlights by remember(files, selected) {
mutableStateOf(
Highlights
.Builder()
.code(selected.content)
.language(selected.language)
.theme(SyntaxThemes.atom(isDark))
.build(),
)
}
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
modifier = Modifier.padding(16.dp),
) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp),
) {
SelectionContainer {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
CodeTextView(
highlights = highlights,
style = LocalTextStyle.current.copy(
fontFamily = JetBrainsMono,
fontWeight = FontWeight.Light,
),
modifier = Modifier.fillMaxSize()
)
Spacer(modifier = Modifier.height(64.dp))
}
}
Box(
modifier = Modifier.align(Alignment.TopEnd),
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text("Copy whole file to clipboard") } },
state = rememberTooltipState(),
) {
FilledTonalIconButton(
onClick = onClick,
) {
CopyIcon(visble = true)
}
}
}
}
}
}
}
}
@Composable
private fun Tab(
file: ExportFile,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val backgroundColor =
if (isSelected) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceVariant
val shape = RoundedCornerShape(
topStart = 8.dp,
topEnd = 8.dp,
)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.background(backgroundColor, shape)
.clip(shape)
.clickable(enabled = !isSelected, onClick = onClick)
.padding(vertical = 12.dp, horizontal = 16.dp)
.widthIn(max = 200.dp),
) {
Text(
text = file.name,
color = contentColorFor(backgroundColor).copy(if (isSelected) 1f else 0.8f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = LocalTextStyle.current.copy(
fontFamily = JetBrainsMono,
fontWeight = if (isSelected) FontWeight.Normal else FontWeight.Thin,
),
)
}
}

View file

@ -1,24 +0,0 @@
package com.materialkolor.builder.ui.home.page.export
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.materialkolor.builder.settings.model.Settings
@Composable
fun ExportPage(
settings: Settings,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier.fillMaxSize(),
) {
Text("Export Section")
}
}

View file

@ -1,10 +1,10 @@
package com.materialkolor.builder.ui.home.page
package com.materialkolor.builder.ui.home.preview
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -22,51 +22,17 @@ import com.materialkolor.builder.ui.home.HomeAction.RandomColor
import com.materialkolor.builder.ui.home.HomeAction.SelectImage
import com.materialkolor.builder.ui.home.HomeAction.UpdateContrast
import com.materialkolor.builder.ui.home.HomeAction.UpdatePaletteStyle
import com.materialkolor.builder.ui.home.page.customize.CustomizePage
import com.materialkolor.builder.ui.home.page.device.DeviceSection
import com.materialkolor.builder.ui.home.page.gallery.GallerySection
import com.materialkolor.builder.ui.home.page.palette.PaletteSection
import com.materialkolor.builder.ui.home.page.preview.PreviewSection
import com.materialkolor.builder.ui.home.page.theme.ThemeSection
import com.materialkolor.builder.ui.home.preview.customize.CustomizeSection
import com.materialkolor.builder.ui.home.preview.device.DeviceSection
import com.materialkolor.builder.ui.home.preview.gallery.GallerySection
import com.materialkolor.builder.ui.home.preview.palette.PaletteSection
import com.materialkolor.builder.ui.home.preview.theme.ThemeSection
import com.materialkolor.builder.ui.ktx.widthIsCompact
@Composable
fun ExpandedContent(
fun PreviewCompactContent(
settings: Settings,
processingImage: Boolean,
dispatcher: Dispatcher<HomeAction>,
windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxSize(),
) {
CustomizePage(
settings = settings,
onSelectImage = dispatcher.rememberRelayOf(::SelectImage),
onRandomColor = dispatcher.rememberRelay(RandomColor),
openColorPicker = dispatcher.rememberRelayOf(::OpenColorPicker),
onUpdatePaletteStyle = dispatcher.rememberRelayOf(::UpdatePaletteStyle),
onUpdateContrast = dispatcher.rememberRelayOf(::UpdateContrast),
processingImage = processingImage,
windowSizeClass = windowSizeClass,
modifier = Modifier.weight(0.5f),
)
PreviewSection(
settings = settings,
onUpdateContrast = dispatcher.rememberRelayOf(::UpdateContrast),
onCopyColor = dispatcher.rememberRelayOf(::CopyColor),
modifier = Modifier.weight(1f),
windowSizeClass = windowSizeClass,
)
}
}
@Composable
fun CompactContent(
settings: Settings,
selectedSection: HomeSection,
selectedSection: PreviewSection,
processingImage: Boolean,
dispatcher: Dispatcher<HomeAction>,
windowSizeClass: WindowSizeClass,
@ -74,8 +40,8 @@ fun CompactContent(
) {
Crossfade(selectedSection) { section ->
when (section) {
HomeSection.Customize -> {
CustomizePage(
PreviewSection.Customize -> {
CustomizeSection(
settings = settings,
onSelectImage = dispatcher.rememberRelayOf(::SelectImage),
onRandomColor = dispatcher.rememberRelay(RandomColor),
@ -87,12 +53,12 @@ fun CompactContent(
modifier = modifier,
)
}
HomeSection.Preview -> {
PreviewSection.Preview -> {
WrappedContent {
DeviceSection()
}
}
HomeSection.Components -> {
PreviewSection.Components -> {
val isCompact = windowSizeClass.widthIsCompact()
WrappedContent {
GallerySection(
@ -102,18 +68,16 @@ fun CompactContent(
)
}
}
HomeSection.Themes -> {
PreviewSection.Themes -> {
WrappedContent {
ThemeSection(
settings = settings,
onCopyColor = dispatcher.rememberRelayOf(::CopyColor),
modifier = Modifier.padding(vertical = 16.dp),
modifier = Modifier.padding(top = 16.dp),
)
}
}
HomeSection.Palettes -> {
WrappedContent {
PaletteSection(modifier = Modifier.padding(vertical = 16.dp))
PaletteSection(modifier = Modifier.padding(bottom = 16.dp))
}
}
}
@ -132,5 +96,7 @@ private fun WrappedContent(
.verticalScroll(rememberScrollState()),
) {
content()
Spacer(modifier = Modifier.height(100.dp))
}
}

View file

@ -0,0 +1,31 @@
package com.materialkolor.builder.ui.home.preview
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.materialkolor.builder.core.Dispatcher
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.home.HomeAction
import com.materialkolor.builder.ui.home.HomeAction.CopyColor
import com.materialkolor.builder.ui.home.preview.preview.PreviewSection
@Composable
fun PreviewExpandedContent(
settings: Settings,
dispatcher: Dispatcher<HomeAction>,
windowSizeClass: WindowSizeClass,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxSize(),
) {
PreviewSection(
settings = settings,
onCopyColor = dispatcher.rememberRelayOf(::CopyColor),
modifier = Modifier.weight(1f),
windowSizeClass = windowSizeClass,
)
}
}

View file

@ -0,0 +1,61 @@
package com.materialkolor.builder.ui.home.preview
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.materialkolor.builder.core.Dispatcher
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.LocalWindowSizeClass
import com.materialkolor.builder.ui.home.HomeAction
import com.materialkolor.builder.ui.home.components.HomeNavRail
@Composable
fun PreviewScreenContent(
settings: Settings,
processingImage: Boolean,
selectedSection: PreviewSection,
updateSelectedSection: (PreviewSection) -> Unit,
dispatcher: Dispatcher<HomeAction>,
modifier: Modifier = Modifier,
windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> {
PreviewExpandedContent(
settings = settings,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
modifier = modifier,
)
}
WindowWidthSizeClass.Medium -> {
Row(modifier = Modifier.fillMaxSize()) {
HomeNavRail(
selected = selectedSection,
onSelected = { updateSelectedSection(it) },
)
PreviewCompactContent(
settings = settings,
selectedSection = selectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
modifier = modifier,
)
}
}
WindowWidthSizeClass.Compact -> {
PreviewCompactContent(
settings = settings,
selectedSection = selectedSection,
processingImage = processingImage,
dispatcher = dispatcher,
windowSizeClass = windowSizeClass,
modifier = modifier,
)
}
}
}

View file

@ -1,17 +1,16 @@
package com.materialkolor.builder.ui.home.page
package com.materialkolor.builder.ui.home.preview
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
enum class HomeSection {
enum class PreviewSection {
Customize,
Preview,
Components,
Themes,
Palettes;
Themes;
companion object {
val All: PersistentList<HomeSection> = entries.toPersistentList()
val All: PersistentList<PreviewSection> = entries.toPersistentList()
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize
package com.materialkolor.builder.ui.home.preview.customize
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
@ -21,16 +21,16 @@ import com.materialkolor.builder.settings.model.ImagePresets
import com.materialkolor.builder.settings.model.KeyColor
import com.materialkolor.builder.settings.model.SeedImage
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.home.page.customize.colors.CoreColorsSection
import com.materialkolor.builder.ui.home.page.customize.contrast.ContrastSection
import com.materialkolor.builder.ui.home.page.customize.seed.SeedColorSection
import com.materialkolor.builder.ui.home.page.customize.style.PaletteStyleSection
import com.materialkolor.builder.ui.LocalWindowSizeClass
import com.materialkolor.builder.ui.home.preview.customize.colors.CoreColorsSection
import com.materialkolor.builder.ui.home.preview.customize.contrast.ContrastSection
import com.materialkolor.builder.ui.home.preview.customize.seed.SeedColorSection
import com.materialkolor.builder.ui.home.preview.customize.style.PaletteStyleSection
import com.materialkolor.builder.ui.ktx.widthIsExpanded
import com.materialkolor.builder.ui.ktx.windowSizeClass
import kotlinx.collections.immutable.PersistentList
@Composable
fun CustomizePage(
fun CustomizeSection(
settings: Settings,
modifier: Modifier = Modifier,
onSelectImage: (SeedImage.Resource?) -> Unit,
@ -40,7 +40,7 @@ fun CustomizePage(
onUpdateContrast: (Contrast) -> Unit,
scrollState: ScrollState = rememberScrollState(),
imagePresets: PersistentList<SeedImage.Resource> = ImagePresets.all,
windowSizeClass: WindowSizeClass = windowSizeClass(),
windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
processingImage: Boolean = false,
) {
Column(
@ -50,6 +50,8 @@ fun CustomizePage(
.padding(horizontal = 16.dp)
.verticalScroll(scrollState),
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Generate your own Material 3 color scheme, to use with Jetpack Compose, or Compose Multiplatform.",
)

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.colors
package com.materialkolor.builder.ui.home.preview.customize.colors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.colors
package com.materialkolor.builder.ui.home.preview.customize.colors
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -48,4 +48,4 @@ fun CoreColorsSection(
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.contrast
package com.materialkolor.builder.ui.home.preview.customize.contrast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.seed
package com.materialkolor.builder.ui.home.preview.customize.seed
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.seed
package com.materialkolor.builder.ui.home.preview.customize.seed
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.customize.style
package com.materialkolor.builder.ui.home.preview.customize.style
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -78,4 +78,4 @@ private fun PaletteStyle.description() = remember(this) {
PaletteStyle.Fidelity -> "A scheme that places the source color in Scheme.primaryContainer."
PaletteStyle.Content -> "Primary Container is the source color, adjusted for color relativity"
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device
package com.materialkolor.builder.ui.home.preview.device
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.page.device.components.content.PhoneContent
import com.materialkolor.builder.ui.home.page.device.components.content.PhoneContentScreen
import com.materialkolor.builder.ui.home.page.device.components.frame.AndroidPhoneFrame
import com.materialkolor.builder.ui.home.page.device.components.frame.IosPhoneFrame
import com.materialkolor.builder.ui.home.preview.device.components.content.PhoneContent
import com.materialkolor.builder.ui.home.preview.device.components.content.PhoneContentScreen
import com.materialkolor.builder.ui.home.preview.device.components.frame.AndroidPhoneFrame
import com.materialkolor.builder.ui.home.preview.device.components.frame.IosPhoneFrame
@Composable
fun DeviceSection(
@ -32,4 +32,4 @@ fun DeviceSection(
)
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.content
package com.materialkolor.builder.ui.home.preview.device.components.content
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.content
package com.materialkolor.builder.ui.home.preview.device.components.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.border

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.content
package com.materialkolor.builder.ui.home.preview.device.components.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.background

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.content
package com.materialkolor.builder.ui.home.preview.device.components.content
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.frame
package com.materialkolor.builder.ui.home.preview.device.components.frame
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -21,9 +21,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.materialkolor.builder.ui.home.page.device.components.frame.PhoneFrameDefaults.androidAspectRatio
import com.materialkolor.builder.ui.home.page.device.components.frame.status.AndroidStatusBar
import com.materialkolor.builder.ui.home.page.device.components.frame.status.IPhoneStatusBar
import com.materialkolor.builder.ui.home.preview.device.components.frame.PhoneFrameDefaults.androidAspectRatio
import com.materialkolor.builder.ui.home.preview.device.components.frame.status.AndroidStatusBar
import com.materialkolor.builder.ui.home.preview.device.components.frame.status.IPhoneStatusBar
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.datetime.Clock

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.frame
package com.materialkolor.builder.ui.home.preview.device.components.frame
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -43,4 +43,4 @@ object PhoneFrameDefaults {
inner: Dp = androidInnerPadding,
thickness: Dp = androidThickness,
) = PhoneFramePadding(outer, inner, thickness)
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.frame.status
package com.materialkolor.builder.ui.home.preview.device.components.frame.status
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.materialkolor.builder.ui.home.page.device.components.frame.PhotoFrameScope
import com.materialkolor.builder.ui.home.preview.device.components.frame.PhotoFrameScope
import kotlinx.datetime.Clock
@Suppress("UnusedReceiverParameter")

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.frame.status
package com.materialkolor.builder.ui.home.preview.device.components.frame.status
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
@ -30,7 +30,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.materialkolor.builder.ui.home.page.device.components.frame.PhotoFrameScope
import com.materialkolor.builder.ui.home.preview.device.components.frame.PhotoFrameScope
import com.materialkolor.builder.ui.ktx.clickableWithoutRipple
import com.materialkolor.builder.ui.theme.icons.BatteryFullAlt
import kotlinx.datetime.Clock

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.device.components.frame.status
package com.materialkolor.builder.ui.home.preview.device.components.frame.status
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -14,4 +14,4 @@ fun rememberFormattedTime(time: Long): String {
"${date.hour}:${date.minute.toString().padStart(2, '0')}"
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery
package com.materialkolor.builder.ui.home.preview.gallery
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
@ -24,7 +24,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.LocalWindowSizeClass
import com.materialkolor.builder.ui.LocalWindowSizeClass
import com.materialkolor.builder.ui.ktx.clickableWithoutRipple
import com.materialkolor.builder.ui.ktx.widthIsCompact
import com.materialkolor.builder.ui.theme.LocalUrlLauncher

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery
package com.materialkolor.builder.ui.home.preview.gallery
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -23,12 +23,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.page.gallery.sections.ActionGallery
import com.materialkolor.builder.ui.home.page.gallery.sections.CommunicationGallery
import com.materialkolor.builder.ui.home.page.gallery.sections.ContainmentGallery
import com.materialkolor.builder.ui.home.page.gallery.sections.NavigationGallery
import com.materialkolor.builder.ui.home.page.gallery.sections.SelectionGallery
import com.materialkolor.builder.ui.home.page.gallery.sections.TextGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.ActionGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.CommunicationGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.ContainmentGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.NavigationGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.SelectionGallery
import com.materialkolor.builder.ui.home.preview.gallery.sections.TextGallery
@Composable
fun GallerySection(

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery
package com.materialkolor.builder.ui.home.preview.gallery
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -42,10 +42,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.page.gallery.itemPadding
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.itemPadding
import kotlinx.collections.immutable.persistentListOf
private const val buttonUrl = "https://developer.android.com/jetpack/compose/components/button"

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.LocalSnackbarHostState
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
import kotlinx.coroutines.launch
@Composable

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
@ -47,9 +47,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
private const val modalBottomSheetInfoUrl =
"https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary?hl=en#ModalBottomSheet(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.material3.SheetState,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Color,kotlin.Function0,androidx.compose.foundation.layout.WindowInsets,androidx.compose.ui.window.SecureFlagPolicy,kotlin.Function1)"

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
@ -72,10 +72,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.LocalDrawerState
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.page.gallery.NavigationDrawerContent
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.NavigationDrawerContent
import kotlinx.coroutines.launch
@Composable

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -81,9 +81,9 @@ import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
private const val checkboxesInfoUrl =
"https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#Checkbox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.material3.CheckboxColors,androidx.compose.foundation.interaction.MutableInteractionSource)"

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.gallery.sections
package com.materialkolor.builder.ui.home.preview.gallery.sections
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -24,9 +24,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.page.gallery.GalleryContainerDefaults
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainer
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerChild
import com.materialkolor.builder.ui.home.preview.gallery.GalleryContainerDefaults
@Composable
fun TextGallery(

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.palette
package com.materialkolor.builder.ui.home.preview.palette
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -78,7 +78,6 @@ fun PaletteSection(
@Composable
private fun DynamicMaterialThemeState.tone(palette: KeyColor, tone: Int): Color {
// TODO: Once MaterialKolor is released, replace this with referencing the m3Colors itself.
val colors = m3Colors
val scheme = dynamicScheme
return remember(this, palette, tone) {

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.preview
package com.materialkolor.builder.ui.home.preview.preview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -17,19 +17,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.materialkolor.Contrast
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.home.components.ContrastSelector
import com.materialkolor.builder.ui.home.page.device.DeviceSection
import com.materialkolor.builder.ui.home.page.gallery.GallerySection
import com.materialkolor.builder.ui.home.page.palette.PaletteSection
import com.materialkolor.builder.ui.home.page.theme.ThemeSection
import com.materialkolor.builder.ui.home.preview.device.DeviceSection
import com.materialkolor.builder.ui.home.preview.gallery.GallerySection
import com.materialkolor.builder.ui.home.preview.palette.PaletteSection
import com.materialkolor.builder.ui.home.preview.theme.ThemeSection
import com.materialkolor.builder.ui.ktx.sidePadding
import com.materialkolor.builder.ui.ktx.widthIsExpanded
import com.materialkolor.builder.ui.ktx.windowSizeClass
@Composable
fun PreviewSection(
settings: Settings,
onUpdateContrast: (Contrast) -> Unit,
modifier: Modifier = Modifier,
onCopyColor: (String, Color) -> Unit = { _, _ -> },
windowSizeClass: WindowSizeClass = windowSizeClass(),
@ -65,15 +62,5 @@ fun PreviewSection(
Spacer(modifier = Modifier.height(200.dp))
}
if (windowSizeClass.widthIsExpanded()) {
ContrastSelector(
selected = settings.contrast,
onUpdate = onUpdateContrast,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 16.dp),
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.preview
package com.materialkolor.builder.ui.home.preview.preview
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable

View file

@ -1,9 +1,6 @@
package com.materialkolor.builder.ui.home.page.theme
package com.materialkolor.builder.ui.home.preview.theme
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
@ -16,9 +13,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
@ -30,11 +24,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.materialkolor.builder.ui.components.CopyIcon
import com.materialkolor.builder.ui.home.model.ThemeColor
import com.materialkolor.builder.ui.home.model.ThemeGroup
import com.materialkolor.builder.ui.home.model.ThemePair
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.BoxPadding
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.InnerDivider
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.BoxPadding
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.InnerDivider
@Composable
fun ColorGroupContainer(
@ -113,17 +108,7 @@ fun ColorBox(
Text(text = themeColor.swatchNumber, modifier = Modifier.align(Alignment.End))
}
AnimatedVisibility(
visible = isHovered,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.TopEnd),
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy color code",
)
}
CopyIcon(visble = isHovered, modifier = Modifier.align(Alignment.TopEnd))
}
}
}

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.theme
package com.materialkolor.builder.ui.home.preview.theme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -19,14 +19,14 @@ import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.DynamicMaterialThemeState
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.builder.ui.home.model.Theme
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.InnerDivider
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.InverseSurfacePair
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.MainColors
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.MiscColors
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.SectionDivider
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.Surface
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.SurfaceContainer
import com.materialkolor.builder.ui.home.page.theme.ThemeSectionDefaults.inverse
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.InnerDivider
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.InverseSurfacePair
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.MainColors
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.MiscColors
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.SectionDivider
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.Surface
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.SurfaceContainer
import com.materialkolor.builder.ui.home.preview.theme.ThemeSectionDefaults.inverse
import com.materialkolor.builder.ui.theme.AppTypography
import com.materialkolor.builder.ui.theme.createThemeState

View file

@ -1,4 +1,4 @@
package com.materialkolor.builder.ui.home.page.theme
package com.materialkolor.builder.ui.home.preview.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -51,4 +51,4 @@ object ThemeSectionDefaults {
fun Color.inverse(): Color {
return if (isLight()) Color.Black else Color.White
}
}
}

View file

@ -2,7 +2,27 @@ package com.materialkolor.builder.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_Bold
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_ExtraLight
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_Light
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_Medium
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_Regular
import materialkolorbuilder.app.generated.resources.JetBrainsMonoNL_SemiBold
import materialkolorbuilder.app.generated.resources.Res
import org.jetbrains.compose.resources.Font
val JetBrainsMono: FontFamily
@Composable
get() = FontFamily(
Font(Res.font.JetBrainsMonoNL_Bold, weight = FontWeight.Bold),
Font(Res.font.JetBrainsMonoNL_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.JetBrainsMonoNL_Medium, weight = FontWeight.Medium),
Font(Res.font.JetBrainsMonoNL_Regular, weight = FontWeight.Normal),
Font(Res.font.JetBrainsMonoNL_Light, weight = FontWeight.Light),
Font(Res.font.JetBrainsMonoNL_ExtraLight, weight = FontWeight.ExtraLight),
)
val AppTypography
@Composable
@ -15,4 +35,4 @@ val AppTypography
labelMedium = type.labelMedium.copy(fontWeight = FontWeight.Light),
labelSmall = type.labelSmall.copy(fontWeight = FontWeight.Light),
)
}
}

View file

@ -0,0 +1,172 @@
package com.materialkolor.builder.export.library
import androidx.compose.ui.graphics.Color
import com.materialkolor.builder.export.model.library.mkColorsKt
import com.materialkolor.builder.settings.model.ColorSettings
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MKColorsTest {
@Test
fun testMkColorsKtWithAllColors() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = Color(0xFF222222),
tertiary = Color(0xFF333333),
error = Color(0xFF444444),
neutral = Color(0xFF555555),
neutralVariant = Color(0xFF666666),
)
val result = mkColorsKt("com.example", colorSettings)
assertTrue(result.contains("package com.example"))
assertTrue(result.contains("import androidx.compose.ui.graphics.Color"))
assertTrue(result.contains("val Primary = Color(0xFF111111)"))
assertTrue(result.contains("val Secondary = Color(0xFF222222)"))
assertTrue(result.contains("val Tertiary = Color(0xFF333333)"))
assertTrue(result.contains("val Error = Color(0xFF444444)"))
assertTrue(result.contains("val Neutral = Color(0xFF555555)"))
assertTrue(result.contains("val NeutralVariant = Color(0xFF666666)"))
assertFalse(result.contains("val Seed = Color(0xFF000000)"))
}
@Test
fun testMkColorsKtWithSeedColor() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = null,
secondary = Color(0xFF222222),
tertiary = Color(0xFF333333),
error = Color(0xFF444444),
neutral = Color(0xFF555555),
neutralVariant = Color(0xFF666666),
)
val result = mkColorsKt("com.example", colorSettings)
assertTrue(result.contains("val Seed = Color(0xFF000000)"))
assertFalse(result.contains("val Primary = "))
}
@Test
fun testMkColorsKtWithMissingColors() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = null,
tertiary = null,
error = Color(0xFF444444),
neutral = null,
neutralVariant = Color(0xFF666666),
)
val result = mkColorsKt("com.example", colorSettings)
assertTrue(result.contains("val Primary = Color(0xFF111111)"))
assertTrue(result.contains("val Error = Color(0xFF444444)"))
assertTrue(result.contains("val NeutralVariant = Color(0xFF666666)"))
assertFalse(result.contains("val Secondary = "))
assertFalse(result.contains("val Tertiary = "))
assertFalse(result.contains("val Neutral = "))
assertFalse(result.contains("val Seed = "))
}
@Test
fun testMkColorsKtWithDifferentPackageName() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = Color(0xFF222222),
tertiary = Color(0xFF333333),
error = Color(0xFF444444),
neutral = Color(0xFF555555),
neutralVariant = Color(0xFF666666),
)
val result = mkColorsKt("com.differentpackage", colorSettings)
assertTrue(result.contains("package com.differentpackage"))
}
@Test
fun testMkColorsKtOutputFormat() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = Color(0xFF222222),
tertiary = null,
error = null,
neutral = Color(0xFF555555),
neutralVariant = null,
)
val result = mkColorsKt("com.example", colorSettings)
val expectedOutput = """
package com.example
import androidx.compose.ui.graphics.Color
val Primary = Color(0xFF111111)
val Secondary = Color(0xFF222222)
val Neutral = Color(0xFF555555)
""".trimIndent()
assertEquals(expectedOutput, result)
}
@Test
fun testMkColorsKtWithOnlyPrimaryColor() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = null,
tertiary = null,
error = null,
neutral = null,
neutralVariant = null,
)
val result = mkColorsKt("com.example", colorSettings)
val expectedOutput = """
package com.example
import androidx.compose.ui.graphics.Color
val Primary = Color(0xFF111111)
""".trimIndent()
assertEquals(expectedOutput, result)
}
@Test
fun testMkColorsKtWithNoColors() {
val colorSettings = ColorSettings(
seed = Color(0xFF000000),
primary = null,
secondary = null,
tertiary = null,
error = null,
neutral = null,
neutralVariant = null,
)
val result = mkColorsKt("com.example", colorSettings)
val expectedOutput = """
package com.example
import androidx.compose.ui.graphics.Color
val Seed = Color(0xFF000000)
""".trimIndent()
assertEquals(expectedOutput, result)
}
}

View file

@ -0,0 +1,91 @@
package com.materialkolor.builder.export.library
import com.materialkolor.builder.BuildKonfig
import com.materialkolor.builder.export.model.library.buildImplementation
import com.materialkolor.builder.export.model.library.gradleKts
import com.materialkolor.builder.export.model.library.libsVersionsToml
import kotlin.test.Test
import kotlin.test.assertEquals
class MKGradleTest {
private val mkVersion = BuildKonfig.MATERIAL_KOLOR_VERSION
private val mkLib = "com.materialkolor:material-kolor:$mkVersion"
@Test
fun testLibsVersionsToml() {
val expected = """
[versions]
materialKolor = "$mkVersion"
[libraries]
materialKolor = "$mkLib"
""".trimIndent()
assertEquals(expected, libsVersionsToml())
}
@Test
fun testBuildImplementationWithVersionCatalog() {
val expected = "implementation(libs.materialKolor)"
assertEquals(expected, buildImplementation(useVersionCatalog = true))
}
@Test
fun testBuildImplementationWithoutVersionCatalog() {
val expected = "implementation(\"$mkLib\")"
assertEquals(expected, buildImplementation(useVersionCatalog = false))
}
@Test
fun testGradleKtsMultiplatformWithVersionCatalog() {
val expected = """
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.materialKolor)
}
}
}
""".trimIndent()
assertEquals(expected, gradleKts(isMultiplatform = true, useVersionCatalog = true))
}
@Test
fun testGradleKtsAndroidOnlyWithVersionCatalog() {
val expected = """
dependencies {
implementation(libs.materialKolor)
}
""".trimIndent()
assertEquals(expected, gradleKts(isMultiplatform = false, useVersionCatalog = true))
}
@Test
fun testGradleKtsMultiplatformWithoutVersionCatalog() {
val expected = """
kotlin {
sourceSets {
commonMain.dependencies {
implementation("$mkLib")
}
}
}
""".trimIndent()
assertEquals(expected, gradleKts(isMultiplatform = true, useVersionCatalog = false))
}
@Test
fun testGradleKtsAndroidOnlyWithoutVersionCatalog() {
val expected = """
dependencies {
implementation("$mkLib")
}
""".trimIndent()
assertEquals(expected, gradleKts(isMultiplatform = false, useVersionCatalog = false))
}
}

View file

@ -0,0 +1,187 @@
package com.materialkolor.builder.export.library
import androidx.compose.ui.graphics.Color
import com.materialkolor.Contrast
import com.materialkolor.PaletteStyle
import com.materialkolor.builder.export.model.header
import com.materialkolor.builder.export.model.library.mkThemeKt
import com.materialkolor.builder.settings.model.ColorSettings
import com.materialkolor.builder.settings.model.Settings
import kotlin.test.Test
import kotlin.test.assertEquals
class MKThemeTest {
@Test
fun testMkThemeKtWithAllColors() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = Color(0xFF222222),
tertiary = Color(0xFF333333),
error = Color(0xFF444444),
neutral = Color(0xFF555555),
neutralVariant = Color(0xFF666666),
),
isDarkMode = false,
contrast = Contrast.Default,
style = PaletteStyle.TonalSpot,
isExtendedFidelity = false,
selectedImage = null,
isAmoled = false,
)
val result = mkThemeKt("com.example", "MyTheme", settings, animate = true)
val expected = """
${header(settings)}
package com.example
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import com.materialkolor.rememberDynamicMaterialThemeState
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val dynamicThemeState = rememberDynamicMaterialThemeState(
isDark = darkTheme,
style = PaletteStyle.TonalSpot,
primary = Primary,
secondary = Secondary,
tertiary = Tertiary,
error = Error,
neutral = Neutral,
neutralVariant = NeutralVariant,
)
DynamicMaterialTheme(
state = dynamicThemeState,
animate = true,
content = content,
)
}
""".trimIndent()
assertEquals(expected, result)
}
@Test
fun testMkThemeKtWithSeedColor() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF000000),
primary = null,
secondary = null,
tertiary = null,
error = null,
neutral = null,
neutralVariant = null,
),
isDarkMode = true,
contrast = Contrast.High,
style = PaletteStyle.Vibrant,
isExtendedFidelity = true,
selectedImage = null,
isAmoled = true,
)
val result = mkThemeKt("com.example", "MyTheme", settings, animate = false)
val expected = """
${header(settings)}
package com.example
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import com.materialkolor.rememberDynamicMaterialThemeState
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val dynamicThemeState = rememberDynamicMaterialThemeState(
isDark = darkTheme,
style = PaletteStyle.Vibrant,
contrastLevel = 1.0,
isAmoled = true,
extendedFidelity = true,
seed = Seed,
)
DynamicMaterialTheme(
state = dynamicThemeState,
animate = false,
content = content,
)
}
""".trimIndent()
assertEquals(expected, result)
}
@Test
fun testMkThemeKtWithMixedColors() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF000000),
primary = Color(0xFF111111),
secondary = null,
tertiary = Color(0xFF333333),
error = null,
neutral = Color(0xFF555555),
neutralVariant = null,
),
isDarkMode = false,
contrast = Contrast.Medium,
style = PaletteStyle.Expressive,
isExtendedFidelity = false,
selectedImage = null,
isAmoled = false,
)
val result = mkThemeKt("com.example", "MyTheme", settings, animate = true)
val expected = """
${header(settings)}
package com.example
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import com.materialkolor.rememberDynamicMaterialThemeState
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val dynamicThemeState = rememberDynamicMaterialThemeState(
isDark = darkTheme,
style = PaletteStyle.Expressive,
contrastLevel = 0.5,
primary = Primary,
tertiary = Tertiary,
neutral = Neutral,
)
DynamicMaterialTheme(
state = dynamicThemeState,
animate = true,
content = content,
)
}
""".trimIndent()
assertEquals(expected, result)
}
}

View file

@ -0,0 +1,156 @@
package com.materialkolor.builder.export.standard
import androidx.compose.ui.graphics.Color
import com.materialkolor.builder.export.model.standard.createScheme
import com.materialkolor.builder.export.model.standard.standardColorsKt
import com.materialkolor.builder.settings.model.ColorSettings
import com.materialkolor.builder.settings.model.Settings
import com.materialkolor.dynamiccolor.DynamicColor
import com.materialkolor.dynamiccolor.MaterialDynamicColors
import com.materialkolor.ktx.getColor
import com.materialkolor.ktx.toHex
import kotlin.test.Test
import kotlin.test.assertEquals
class StandardColorsTest {
@Test
fun testStandardColors() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
selectedImage = null,
)
compare(settings)
}
@Test
fun testStandardExtendedFidelityColors() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
selectedImage = null,
isExtendedFidelity = true,
)
compare(settings)
}
@Test
fun testStandardFullColors() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
primary = Color(0xFF111111),
secondary = Color(0xFF222222),
tertiary = Color(0xFF333333),
error = Color(0xFF444444),
neutral = Color(0xFF555555),
neutralVariant = Color(0xFF666666),
),
isDarkMode = false,
selectedImage = null,
)
compare(settings)
}
private fun compare(settings: Settings) {
val map = MaterialDynamicColors(settings.isExtendedFidelity)
val lightScheme = createScheme(isDark = false, settings = settings)
val light = { s: MaterialDynamicColors.() -> DynamicColor -> map.s().getColor(lightScheme).h() }
val darkScheme = createScheme(isDark = true, settings = settings)
val dark = { s: MaterialDynamicColors.() -> DynamicColor -> map.s().getColor(darkScheme).h() }
val actual = standardColorsKt("com.example", settings)
val expected = """
package com.example
import androidx.compose.ui.graphics.Color
val Seed = ${settings.colors.seed.h()}
val PrimaryLight = ${light { primary() }}
val OnPrimaryLight = ${light { onPrimary() }}
val PrimaryContainerLight = ${light { primaryContainer() }}
val OnPrimaryContainerLight = ${light { onPrimaryContainer() }}
val SecondaryLight = ${light { secondary() }}
val OnSecondaryLight = ${light { onSecondary() }}
val SecondaryContainerLight = ${light { secondaryContainer() }}
val OnSecondaryContainerLight = ${light { onSecondaryContainer() }}
val TertiaryLight = ${light { tertiary() }}
val OnTertiaryLight = ${light { onTertiary() }}
val TertiaryContainerLight = ${light { tertiaryContainer() }}
val OnTertiaryContainerLight = ${light { onTertiaryContainer() }}
val ErrorLight = ${light { error() }}
val OnErrorLight = ${light { onError() }}
val ErrorContainerLight = ${light { errorContainer() }}
val OnErrorContainerLight = ${light { onErrorContainer() }}
val BackgroundLight = ${light { background() }}
val OnBackgroundLight = ${light { onBackground() }}
val SurfaceLight = ${light { surface() }}
val OnSurfaceLight = ${light { onSurface() }}
val SurfaceVariantLight = ${light { surfaceVariant() }}
val OnSurfaceVariantLight = ${light { onSurfaceVariant() }}
val OutlineLight = ${light { outline() }}
val OutlineVariantLight = ${light { outlineVariant() }}
val ScrimLight = ${light { scrim() }}
val InverseSurfaceLight = ${light { inverseSurface() }}
val InverseOnSurfaceLight = ${light { inverseOnSurface() }}
val InversePrimaryLight = ${light { inversePrimary() }}
val SurfaceDimLight = ${light { surfaceDim() }}
val SurfaceBrightLight = ${light { surfaceBright() }}
val SurfaceContainerLowestLight = ${light { surfaceContainerLowest() }}
val SurfaceContainerLowLight = ${light { surfaceContainerLow() }}
val SurfaceContainerLight = ${light { surfaceContainer() }}
val SurfaceContainerHighLight = ${light { surfaceContainerHigh() }}
val SurfaceContainerHighestLight = ${light { surfaceContainerHighest() }}
val PrimaryDark = ${dark { primary() }}
val OnPrimaryDark = ${dark { onPrimary() }}
val PrimaryContainerDark = ${dark { primaryContainer() }}
val OnPrimaryContainerDark = ${dark { onPrimaryContainer() }}
val SecondaryDark = ${dark { secondary() }}
val OnSecondaryDark = ${dark { onSecondary() }}
val SecondaryContainerDark = ${dark { secondaryContainer() }}
val OnSecondaryContainerDark = ${dark { onSecondaryContainer() }}
val TertiaryDark = ${dark { tertiary() }}
val OnTertiaryDark = ${dark { onTertiary() }}
val TertiaryContainerDark = ${dark { tertiaryContainer() }}
val OnTertiaryContainerDark = ${dark { onTertiaryContainer() }}
val ErrorDark = ${dark { error() }}
val OnErrorDark = ${dark { onError() }}
val ErrorContainerDark = ${dark { errorContainer() }}
val OnErrorContainerDark = ${dark { onErrorContainer() }}
val BackgroundDark = ${dark { background() }}
val OnBackgroundDark = ${dark { onBackground() }}
val SurfaceDark = ${dark { surface() }}
val OnSurfaceDark = ${dark { onSurface() }}
val SurfaceVariantDark = ${dark { surfaceVariant() }}
val OnSurfaceVariantDark = ${dark { onSurfaceVariant() }}
val OutlineDark = ${dark { outline() }}
val OutlineVariantDark = ${dark { outlineVariant() }}
val ScrimDark = ${dark { scrim() }}
val InverseSurfaceDark = ${dark { inverseSurface() }}
val InverseOnSurfaceDark = ${dark { inverseOnSurface() }}
val InversePrimaryDark = ${dark { inversePrimary() }}
val SurfaceDimDark = ${dark { surfaceDim() }}
val SurfaceBrightDark = ${dark { surfaceBright() }}
val SurfaceContainerLowestDark = ${dark { surfaceContainerLowest() }}
val SurfaceContainerLowDark = ${dark { surfaceContainerLow() }}
val SurfaceContainerDark = ${dark { surfaceContainer() }}
val SurfaceContainerHighDark = ${dark { surfaceContainerHigh() }}
val SurfaceContainerHighestDark = ${dark { surfaceContainerHighest() }}
""".trimIndent()
assertEquals(expected, actual)
}
private fun Color.h() = "Color(0x${toHex(includePrefix = false, alwaysIncludeAlpha = true)})"
}

View file

@ -0,0 +1,573 @@
package com.materialkolor.builder.export.standard
import androidx.compose.ui.graphics.Color
import com.materialkolor.Contrast
import com.materialkolor.builder.export.model.header
import com.materialkolor.builder.export.model.standard.standardThemeKt
import com.materialkolor.builder.settings.model.ColorSettings
import com.materialkolor.builder.settings.model.Settings
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
class StandardThemeTest {
@Test
fun testStandardMultiplatformTheme() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
selectedImage = null,
)
val result = standardThemeKt(
packageName = "com.example",
themeName = "AppTheme",
multiplatform = true,
settings = settings,
)
val expected = """
${header(settings)}
package com.example
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
private val lightColorScheme = lightColorScheme(
primary = PrimaryLight,
onPrimary = OnPrimaryLight,
primaryContainer = PrimaryContainerLight,
onPrimaryContainer = OnPrimaryContainerLight,
secondary = SecondaryLight,
onSecondary = OnSecondaryLight,
secondaryContainer = SecondaryContainerLight,
onSecondaryContainer = OnSecondaryContainerLight,
tertiary = TertiaryLight,
onTertiary = OnTertiaryLight,
tertiaryContainer = TertiaryContainerLight,
onTertiaryContainer = OnTertiaryContainerLight,
error = ErrorLight,
onError = OnErrorLight,
errorContainer = ErrorContainerLight,
onErrorContainer = OnErrorContainerLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
surfaceVariant = SurfaceVariantLight,
onSurfaceVariant = OnSurfaceVariantLight,
outline = OutlineLight,
outlineVariant = OutlineVariantLight,
scrim = ScrimLight,
inverseSurface = InverseSurfaceLight,
inverseOnSurface = InverseOnSurfaceLight,
inversePrimary = InversePrimaryLight,
surfaceDim = SurfaceDimLight,
surfaceBright = SurfaceBrightLight,
surfaceContainerLowest = SurfaceContainerLowestLight,
surfaceContainerLow = SurfaceContainerLowLight,
surfaceContainer = SurfaceContainerLight,
surfaceContainerHigh = SurfaceContainerHighLight,
surfaceContainerHighest = SurfaceContainerHighestLight,
)
private val darkColorScheme = darkColorScheme(
primary = PrimaryDark,
onPrimary = OnPrimaryDark,
primaryContainer = PrimaryContainerDark,
onPrimaryContainer = OnPrimaryContainerDark,
secondary = SecondaryDark,
onSecondary = OnSecondaryDark,
secondaryContainer = SecondaryContainerDark,
onSecondaryContainer = OnSecondaryContainerDark,
tertiary = TertiaryDark,
onTertiary = OnTertiaryDark,
tertiaryContainer = TertiaryContainerDark,
onTertiaryContainer = OnTertiaryContainerDark,
error = ErrorDark,
onError = OnErrorDark,
errorContainer = ErrorContainerDark,
onErrorContainer = OnErrorContainerDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
surfaceVariant = SurfaceVariantDark,
onSurfaceVariant = OnSurfaceVariantDark,
outline = OutlineDark,
outlineVariant = OutlineVariantDark,
scrim = ScrimDark,
inverseSurface = InverseSurfaceDark,
inverseOnSurface = InverseOnSurfaceDark,
inversePrimary = InversePrimaryDark,
surfaceDim = SurfaceDimDark,
surfaceBright = SurfaceBrightDark,
surfaceContainerLowest = SurfaceContainerLowestDark,
surfaceContainerLow = SurfaceContainerLowDark,
surfaceContainer = SurfaceContainerDark,
surfaceContainerHigh = SurfaceContainerHighDark,
surfaceContainerHighest = SurfaceContainerHighestDark,
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit,
) {
val colorScheme = when {
darkTheme -> darkColorScheme
else -> lightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}
""".trimIndent()
assertEquals(expected, result)
}
@Test
fun testStandardAndroidTheme() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
selectedImage = null,
)
val result = standardThemeKt(
packageName = "com.example",
themeName = "AppTheme",
multiplatform = false,
settings = settings,
)
val expected = """
${header(settings)}
package com.example
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val lightColorScheme = lightColorScheme(
primary = PrimaryLight,
onPrimary = OnPrimaryLight,
primaryContainer = PrimaryContainerLight,
onPrimaryContainer = OnPrimaryContainerLight,
secondary = SecondaryLight,
onSecondary = OnSecondaryLight,
secondaryContainer = SecondaryContainerLight,
onSecondaryContainer = OnSecondaryContainerLight,
tertiary = TertiaryLight,
onTertiary = OnTertiaryLight,
tertiaryContainer = TertiaryContainerLight,
onTertiaryContainer = OnTertiaryContainerLight,
error = ErrorLight,
onError = OnErrorLight,
errorContainer = ErrorContainerLight,
onErrorContainer = OnErrorContainerLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
surfaceVariant = SurfaceVariantLight,
onSurfaceVariant = OnSurfaceVariantLight,
outline = OutlineLight,
outlineVariant = OutlineVariantLight,
scrim = ScrimLight,
inverseSurface = InverseSurfaceLight,
inverseOnSurface = InverseOnSurfaceLight,
inversePrimary = InversePrimaryLight,
surfaceDim = SurfaceDimLight,
surfaceBright = SurfaceBrightLight,
surfaceContainerLowest = SurfaceContainerLowestLight,
surfaceContainerLow = SurfaceContainerLowLight,
surfaceContainer = SurfaceContainerLight,
surfaceContainerHigh = SurfaceContainerHighLight,
surfaceContainerHighest = SurfaceContainerHighestLight,
)
private val darkColorScheme = darkColorScheme(
primary = PrimaryDark,
onPrimary = OnPrimaryDark,
primaryContainer = PrimaryContainerDark,
onPrimaryContainer = OnPrimaryContainerDark,
secondary = SecondaryDark,
onSecondary = OnSecondaryDark,
secondaryContainer = SecondaryContainerDark,
onSecondaryContainer = OnSecondaryContainerDark,
tertiary = TertiaryDark,
onTertiary = OnTertiaryDark,
tertiaryContainer = TertiaryContainerDark,
onTertiaryContainer = OnTertiaryContainerDark,
error = ErrorDark,
onError = OnErrorDark,
errorContainer = ErrorContainerDark,
onErrorContainer = OnErrorContainerDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
surfaceVariant = SurfaceVariantDark,
onSurfaceVariant = OnSurfaceVariantDark,
outline = OutlineDark,
outlineVariant = OutlineVariantDark,
scrim = ScrimDark,
inverseSurface = InverseSurfaceDark,
inverseOnSurface = InverseOnSurfaceDark,
inversePrimary = InversePrimaryDark,
surfaceDim = SurfaceDimDark,
surfaceBright = SurfaceBrightDark,
surfaceContainerLowest = SurfaceContainerLowestDark,
surfaceContainerLow = SurfaceContainerLowDark,
surfaceContainer = SurfaceContainerDark,
surfaceContainerHigh = SurfaceContainerHighDark,
surfaceContainerHighest = SurfaceContainerHighestDark,
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable() () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme
else -> lightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}
""".trimIndent()
assertEquals(expected, result)
}
@Test
fun testStandardReducedContrastTheme() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
contrast = Contrast.Reduced,
selectedImage = null,
)
val result = standardThemeKt(
packageName = "com.example",
themeName = "AppTheme",
multiplatform = false,
settings = settings,
)
val colors = """
private val reducedContrastLightColorScheme = lightColorScheme(
primary = PrimaryLightReducedContrast,
onPrimary = OnPrimaryLightReducedContrast,
primaryContainer = PrimaryContainerLightReducedContrast,
onPrimaryContainer = OnPrimaryContainerLightReducedContrast,
secondary = SecondaryLightReducedContrast,
onSecondary = OnSecondaryLightReducedContrast,
secondaryContainer = SecondaryContainerLightReducedContrast,
onSecondaryContainer = OnSecondaryContainerLightReducedContrast,
tertiary = TertiaryLightReducedContrast,
onTertiary = OnTertiaryLightReducedContrast,
tertiaryContainer = TertiaryContainerLightReducedContrast,
onTertiaryContainer = OnTertiaryContainerLightReducedContrast,
error = ErrorLightReducedContrast,
onError = OnErrorLightReducedContrast,
errorContainer = ErrorContainerLightReducedContrast,
onErrorContainer = OnErrorContainerLightReducedContrast,
background = BackgroundLightReducedContrast,
onBackground = OnBackgroundLightReducedContrast,
surface = SurfaceLightReducedContrast,
onSurface = OnSurfaceLightReducedContrast,
surfaceVariant = SurfaceVariantLightReducedContrast,
onSurfaceVariant = OnSurfaceVariantLightReducedContrast,
outline = OutlineLightReducedContrast,
outlineVariant = OutlineVariantLightReducedContrast,
scrim = ScrimLightReducedContrast,
inverseSurface = InverseSurfaceLightReducedContrast,
inverseOnSurface = InverseOnSurfaceLightReducedContrast,
inversePrimary = InversePrimaryLightReducedContrast,
surfaceDim = SurfaceDimLightReducedContrast,
surfaceBright = SurfaceBrightLightReducedContrast,
surfaceContainerLowest = SurfaceContainerLowestLightReducedContrast,
surfaceContainerLow = SurfaceContainerLowLightReducedContrast,
surfaceContainer = SurfaceContainerLightReducedContrast,
surfaceContainerHigh = SurfaceContainerHighLightReducedContrast,
surfaceContainerHighest = SurfaceContainerHighestLightReducedContrast,
)
private val reducedContrastDarkColorScheme = darkColorScheme(
primary = PrimaryDarkReducedContrast,
onPrimary = OnPrimaryDarkReducedContrast,
primaryContainer = PrimaryContainerDarkReducedContrast,
onPrimaryContainer = OnPrimaryContainerDarkReducedContrast,
secondary = SecondaryDarkReducedContrast,
onSecondary = OnSecondaryDarkReducedContrast,
secondaryContainer = SecondaryContainerDarkReducedContrast,
onSecondaryContainer = OnSecondaryContainerDarkReducedContrast,
tertiary = TertiaryDarkReducedContrast,
onTertiary = OnTertiaryDarkReducedContrast,
tertiaryContainer = TertiaryContainerDarkReducedContrast,
onTertiaryContainer = OnTertiaryContainerDarkReducedContrast,
error = ErrorDarkReducedContrast,
onError = OnErrorDarkReducedContrast,
errorContainer = ErrorContainerDarkReducedContrast,
onErrorContainer = OnErrorContainerDarkReducedContrast,
background = BackgroundDarkReducedContrast,
onBackground = OnBackgroundDarkReducedContrast,
surface = SurfaceDarkReducedContrast,
onSurface = OnSurfaceDarkReducedContrast,
surfaceVariant = SurfaceVariantDarkReducedContrast,
onSurfaceVariant = OnSurfaceVariantDarkReducedContrast,
outline = OutlineDarkReducedContrast,
outlineVariant = OutlineVariantDarkReducedContrast,
scrim = ScrimDarkReducedContrast,
inverseSurface = InverseSurfaceDarkReducedContrast,
inverseOnSurface = InverseOnSurfaceDarkReducedContrast,
inversePrimary = InversePrimaryDarkReducedContrast,
surfaceDim = SurfaceDimDarkReducedContrast,
surfaceBright = SurfaceBrightDarkReducedContrast,
surfaceContainerLowest = SurfaceContainerLowestDarkReducedContrast,
surfaceContainerLow = SurfaceContainerLowDarkReducedContrast,
surfaceContainer = SurfaceContainerDarkReducedContrast,
surfaceContainerHigh = SurfaceContainerHighDarkReducedContrast,
surfaceContainerHighest = SurfaceContainerHighestDarkReducedContrast,
)
""".trimIndent()
assertContains(result, colors)
}
@Test
fun testStandardMediumContrastTheme() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
contrast = Contrast.Medium,
selectedImage = null,
)
val result = standardThemeKt(
packageName = "com.example",
themeName = "AppTheme",
multiplatform = true,
settings = settings,
)
val colors = """
private val mediumContrastLightColorScheme = lightColorScheme(
primary = PrimaryLightMediumContrast,
onPrimary = OnPrimaryLightMediumContrast,
primaryContainer = PrimaryContainerLightMediumContrast,
onPrimaryContainer = OnPrimaryContainerLightMediumContrast,
secondary = SecondaryLightMediumContrast,
onSecondary = OnSecondaryLightMediumContrast,
secondaryContainer = SecondaryContainerLightMediumContrast,
onSecondaryContainer = OnSecondaryContainerLightMediumContrast,
tertiary = TertiaryLightMediumContrast,
onTertiary = OnTertiaryLightMediumContrast,
tertiaryContainer = TertiaryContainerLightMediumContrast,
onTertiaryContainer = OnTertiaryContainerLightMediumContrast,
error = ErrorLightMediumContrast,
onError = OnErrorLightMediumContrast,
errorContainer = ErrorContainerLightMediumContrast,
onErrorContainer = OnErrorContainerLightMediumContrast,
background = BackgroundLightMediumContrast,
onBackground = OnBackgroundLightMediumContrast,
surface = SurfaceLightMediumContrast,
onSurface = OnSurfaceLightMediumContrast,
surfaceVariant = SurfaceVariantLightMediumContrast,
onSurfaceVariant = OnSurfaceVariantLightMediumContrast,
outline = OutlineLightMediumContrast,
outlineVariant = OutlineVariantLightMediumContrast,
scrim = ScrimLightMediumContrast,
inverseSurface = InverseSurfaceLightMediumContrast,
inverseOnSurface = InverseOnSurfaceLightMediumContrast,
inversePrimary = InversePrimaryLightMediumContrast,
surfaceDim = SurfaceDimLightMediumContrast,
surfaceBright = SurfaceBrightLightMediumContrast,
surfaceContainerLowest = SurfaceContainerLowestLightMediumContrast,
surfaceContainerLow = SurfaceContainerLowLightMediumContrast,
surfaceContainer = SurfaceContainerLightMediumContrast,
surfaceContainerHigh = SurfaceContainerHighLightMediumContrast,
surfaceContainerHighest = SurfaceContainerHighestLightMediumContrast,
)
private val mediumContrastDarkColorScheme = darkColorScheme(
primary = PrimaryDarkMediumContrast,
onPrimary = OnPrimaryDarkMediumContrast,
primaryContainer = PrimaryContainerDarkMediumContrast,
onPrimaryContainer = OnPrimaryContainerDarkMediumContrast,
secondary = SecondaryDarkMediumContrast,
onSecondary = OnSecondaryDarkMediumContrast,
secondaryContainer = SecondaryContainerDarkMediumContrast,
onSecondaryContainer = OnSecondaryContainerDarkMediumContrast,
tertiary = TertiaryDarkMediumContrast,
onTertiary = OnTertiaryDarkMediumContrast,
tertiaryContainer = TertiaryContainerDarkMediumContrast,
onTertiaryContainer = OnTertiaryContainerDarkMediumContrast,
error = ErrorDarkMediumContrast,
onError = OnErrorDarkMediumContrast,
errorContainer = ErrorContainerDarkMediumContrast,
onErrorContainer = OnErrorContainerDarkMediumContrast,
background = BackgroundDarkMediumContrast,
onBackground = OnBackgroundDarkMediumContrast,
surface = SurfaceDarkMediumContrast,
onSurface = OnSurfaceDarkMediumContrast,
surfaceVariant = SurfaceVariantDarkMediumContrast,
onSurfaceVariant = OnSurfaceVariantDarkMediumContrast,
outline = OutlineDarkMediumContrast,
outlineVariant = OutlineVariantDarkMediumContrast,
scrim = ScrimDarkMediumContrast,
inverseSurface = InverseSurfaceDarkMediumContrast,
inverseOnSurface = InverseOnSurfaceDarkMediumContrast,
inversePrimary = InversePrimaryDarkMediumContrast,
surfaceDim = SurfaceDimDarkMediumContrast,
surfaceBright = SurfaceBrightDarkMediumContrast,
surfaceContainerLowest = SurfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = SurfaceContainerLowDarkMediumContrast,
surfaceContainer = SurfaceContainerDarkMediumContrast,
surfaceContainerHigh = SurfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = SurfaceContainerHighestDarkMediumContrast,
)
""".trimIndent()
assertContains(result, colors)
val darkSchemeName = "darkTheme -> mediumContrastDarkColorScheme"
val lightSchemeName = "else -> mediumContrastLightColorScheme"
assertContains(result, darkSchemeName)
assertContains(result, lightSchemeName)
}
@Test
fun testStandardHighContrastTheme() {
val settings = Settings(
colors = ColorSettings(
seed = Color(0xFF0000FF),
),
isDarkMode = false,
contrast = Contrast.High,
selectedImage = null,
)
val result = standardThemeKt(
packageName = "com.example",
themeName = "AppTheme",
multiplatform = false,
settings = settings,
)
val colors = """
private val highContrastLightColorScheme = lightColorScheme(
primary = PrimaryLightHighContrast,
onPrimary = OnPrimaryLightHighContrast,
primaryContainer = PrimaryContainerLightHighContrast,
onPrimaryContainer = OnPrimaryContainerLightHighContrast,
secondary = SecondaryLightHighContrast,
onSecondary = OnSecondaryLightHighContrast,
secondaryContainer = SecondaryContainerLightHighContrast,
onSecondaryContainer = OnSecondaryContainerLightHighContrast,
tertiary = TertiaryLightHighContrast,
onTertiary = OnTertiaryLightHighContrast,
tertiaryContainer = TertiaryContainerLightHighContrast,
onTertiaryContainer = OnTertiaryContainerLightHighContrast,
error = ErrorLightHighContrast,
onError = OnErrorLightHighContrast,
errorContainer = ErrorContainerLightHighContrast,
onErrorContainer = OnErrorContainerLightHighContrast,
background = BackgroundLightHighContrast,
onBackground = OnBackgroundLightHighContrast,
surface = SurfaceLightHighContrast,
onSurface = OnSurfaceLightHighContrast,
surfaceVariant = SurfaceVariantLightHighContrast,
onSurfaceVariant = OnSurfaceVariantLightHighContrast,
outline = OutlineLightHighContrast,
outlineVariant = OutlineVariantLightHighContrast,
scrim = ScrimLightHighContrast,
inverseSurface = InverseSurfaceLightHighContrast,
inverseOnSurface = InverseOnSurfaceLightHighContrast,
inversePrimary = InversePrimaryLightHighContrast,
surfaceDim = SurfaceDimLightHighContrast,
surfaceBright = SurfaceBrightLightHighContrast,
surfaceContainerLowest = SurfaceContainerLowestLightHighContrast,
surfaceContainerLow = SurfaceContainerLowLightHighContrast,
surfaceContainer = SurfaceContainerLightHighContrast,
surfaceContainerHigh = SurfaceContainerHighLightHighContrast,
surfaceContainerHighest = SurfaceContainerHighestLightHighContrast,
)
private val highContrastDarkColorScheme = darkColorScheme(
primary = PrimaryDarkHighContrast,
onPrimary = OnPrimaryDarkHighContrast,
primaryContainer = PrimaryContainerDarkHighContrast,
onPrimaryContainer = OnPrimaryContainerDarkHighContrast,
secondary = SecondaryDarkHighContrast,
onSecondary = OnSecondaryDarkHighContrast,
secondaryContainer = SecondaryContainerDarkHighContrast,
onSecondaryContainer = OnSecondaryContainerDarkHighContrast,
tertiary = TertiaryDarkHighContrast,
onTertiary = OnTertiaryDarkHighContrast,
tertiaryContainer = TertiaryContainerDarkHighContrast,
onTertiaryContainer = OnTertiaryContainerDarkHighContrast,
error = ErrorDarkHighContrast,
onError = OnErrorDarkHighContrast,
errorContainer = ErrorContainerDarkHighContrast,
onErrorContainer = OnErrorContainerDarkHighContrast,
background = BackgroundDarkHighContrast,
onBackground = OnBackgroundDarkHighContrast,
surface = SurfaceDarkHighContrast,
onSurface = OnSurfaceDarkHighContrast,
surfaceVariant = SurfaceVariantDarkHighContrast,
onSurfaceVariant = OnSurfaceVariantDarkHighContrast,
outline = OutlineDarkHighContrast,
outlineVariant = OutlineVariantDarkHighContrast,
scrim = ScrimDarkHighContrast,
inverseSurface = InverseSurfaceDarkHighContrast,
inverseOnSurface = InverseOnSurfaceDarkHighContrast,
inversePrimary = InversePrimaryDarkHighContrast,
surfaceDim = SurfaceDimDarkHighContrast,
surfaceBright = SurfaceBrightDarkHighContrast,
surfaceContainerLowest = SurfaceContainerLowestDarkHighContrast,
surfaceContainerLow = SurfaceContainerLowDarkHighContrast,
surfaceContainer = SurfaceContainerDarkHighContrast,
surfaceContainerHigh = SurfaceContainerHighDarkHighContrast,
surfaceContainerHighest = SurfaceContainerHighestDarkHighContrast,
)
""".trimIndent()
assertContains(result, colors)
}
}

View file

@ -7,6 +7,8 @@ import com.materialkolor.PaletteStyle
import com.materialkolor.builder.settings.model.KeyColor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@ -35,9 +37,9 @@ class QueryParamsTest {
color_primary=FF00FF00&
color_secondary=FF0000FF&
dark_mode=true&
contrast=1.0&
selected_preset_id=preset1&
style=Expressive&
selected_preset_id=preset1&
contrast=1.0&
extended_fidelity=true
""".trimIndent().replace("\n", "")
@ -83,8 +85,8 @@ class QueryParamsTest {
val queryParams = settingsEntity.toQueryParams()
assertEquals(
expected = "contrast=0.0&style=TonalSpot&extended_fidelity=false",
actual = queryParams.removePrefix("?"),
expected = "?dark_mode=false",
actual = queryParams,
)
}
@ -183,8 +185,9 @@ class QueryParamsTest {
val queryParams = settingsEntity.toQueryParams()
val reconstructed = queryParams.toSettingsEntity()
// Dark mode is always added
assertNotNull(reconstructed.isDarkMode)
assertNull(reconstructed.colors[KeyColor.Seed])
assertNull(reconstructed.isDarkMode)
assertNull(reconstructed.selectedPresetId)
}

View file

@ -1,7 +1,10 @@
package com.materialkolor.builder.ui.ktx
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
@Composable
actual fun windowSizeClass(): WindowSizeClass {
TODO("Not yet implemented")
}
return calculateWindowSizeClass()
}

View file

@ -0,0 +1,7 @@
package com.materialkolor.builder.export
import com.materialkolor.builder.export.model.ExportFile
actual suspend fun exportFiles(list: List<ExportFile>) {
throw UnsupportedOperationException("Exporting files is not supported in non-browser environments")
}

View file

@ -0,0 +1,42 @@
package com.materialkolor.builder.export
import com.materialkolor.builder.export.model.ExportFile
import kotlinx.browser.document
import kotlinx.coroutines.await
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.url.URL
import org.w3c.files.Blob
import kotlin.js.Promise
@JsModule("jszip")
external class JSZip {
fun file(name: String, data: String)
fun generateAsync(options: JsAny = definedExternally): Promise<JsAny>
}
actual suspend fun exportFiles(list: List<ExportFile>) {
val blob = createZipBlob(list)
offerFileForDownload(blob, "theme.zip")
}
private suspend fun createZipBlob(files: List<ExportFile>): Blob {
val zip = JSZip()
files.forEach { file ->
zip.file(file.name, file.content)
}
return zip.generateAsync(createParams()).await()
}
private fun createParams(): JsAny = js("({ type: 'blob' })")
private fun offerFileForDownload(blob: Blob, filename: String) {
val url = URL.createObjectURL(blob)
val a = document.createElement("a") as HTMLAnchorElement
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}

View file

@ -11,8 +11,9 @@ androidx-activityCompose = "1.9.2"
androidx-core = "1.13.1"
androidx-lifecycle = "2.8.2"
calfFilePicker = "0.5.5"
highlights = "0.9.2"
kotlin = "2.1.0-Beta1"
compose-multiplatform = "1.7.0-beta02"
compose-multiplatform = "1.7.0-rc01"
kotlinx-coroutines = "1.9.0"
kotlinx-datetime = "0.6.1"
kotlinx-collections = "0.3.7"
@ -26,6 +27,7 @@ materialKolor = "1.7.0"
composeIcons = "1.1.1"
stateHolder = "1.2.0"
colorpicker = "1.1.2"
buildKonfig = "0.15.2"
[libraries]
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" }
@ -38,6 +40,7 @@ calf-filePicker = { module = "com.mohamedrejeb.calf:calf-file-picker", version.r
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
highlights = { module = "dev.snipme:highlights", version.ref = "highlights" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
@ -56,6 +59,7 @@ material3-adaptive-navigation = { module = "org.jetbrains.compose.material3.adap
material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "compose-multiplatform" }
material3-windowSizeClass = { module = "org.jetbrains.compose.material3:material3-window-size-class", version.ref = "compose-multiplatform" }
materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
materialKolor-utilities = { module = "com.materialkolor:material-color-utilities", version.ref = "materialKolor" }
stateHolder = { module = "dev.stateholder:core", version.ref = "stateHolder" }
stateHolder-compose = { module = "dev.stateholder:extensions-compose", version.ref = "stateHolder" }
composeIcons-fontAwesome = { module = "br.com.devsrsouza.compose.icons:font-awesome", version.ref = "composeIcons" }
@ -67,4 +71,4 @@ compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform"
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
buildKonfig = { id = "com.codingfeline.buildkonfig", version = "0.15.2" }
buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" }

Some files were not shown because too many files have changed in this diff Show more