arblant
FoxyVPN-MultiAcc / app / src / main / java / com / vauth / foxyvpn / ui / theme / Appearance.kt
Appearance.kt396 lines · 15.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
package com.vauth.foxyvpn.ui.theme

import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.ColorUtils

/**
 * Interface/appearance options ported from ZedSecure's Ui settings page.
 * Design language studied from ZedSecure; all code below is original.
 */

enum class ColorStyle(val label: String) {
    // Enum name "ZedSecure" is kept so existing saved preferences still load.
    // Display label renamed to "Signature" per product request.
    ZedSecure("Signature"),
    TonalSpot("Tonal Spot"),
    Vibrant("Vibrant"),
    Expressive("Expressive"),
    Rainbow("Rainbow"),
    FruitSalad("Fruit Salad"),
    Fidelity("Fidelity"),
    Neutral("Neutral"),
    Monochrome("Monochrome"),
}

enum class ColorContrast(val label: String, val level: Double) {
    Standard("Standard", 0.0),
    Medium("Medium", 0.5),
    High("High", 1.0),
}

enum class AppLanguage(val label: String, val tag: String?) {
    System("System", null),
    English("English", "en"),
    Persian("فارسی", "fa"),
}

enum class UiFontScale(val label: String, val scale: Float) {
    Smallest("Smallest", 0.80f),
    Smaller("Smaller", 0.90f),
    Normal("Normal", 1.00f),
    Larger("Larger", 1.12f),
    Largest("Largest", 1.25f),
}

enum class ScreenTransition(val label: String) {
    Fade("Fade"),
    Slide("Slide"),
    Push("Push"),
    Depth("Depth"),
    Elastic("Elastic"),
    Curtain("Curtain"),
    Flip("Flip"),
}

enum class NavBarStyle(val label: String) {
    FloatingPill("Floating pill"),
    CompactDock("Compact dock"),
    ExpressivePill("Expressive pill"),
    FullBar("Full bar"),
    Underline("Underline"),
    Minimal("Minimal"),
}

enum class ConnectButtonStyle(val label: String) {
    Pill("Pill"),
    Hero("Hero"),
    Ring("Ring"),
    Bar("Bar"),
    Icon("Icon"),
    Switch("Switch"),
}

enum class CardCornerStyle(val label: String, val cardDp: Int) {
    Squared("Squared", 10),
    Rounded("Rounded", 28),
    Pill("Pill", 48),
}

enum class ListDensity(val label: String) {
    Comfortable("Comfortable"),
    Compact("Compact"),
}

data class AccentPreset(val name: String, val light: Color, val dark: Color)

val AccentPresets: List<AccentPreset> = listOf(
    // Indices 0..9 stable so previously saved accentColorIndex values still map
    // to the same colours. Signal blue is appended; fresh installs default to it.
    AccentPreset("Iris", Color(0xFF5646D6), Color(0xFFC7BFFF)),
    AccentPreset("Ocean", Color(0xFF1E63D0), Color(0xFFAAC7FF)),
    AccentPreset("Teal", Color(0xFF00897E), Color(0xFF52DDCF)),
    AccentPreset("Forest", Color(0xFF2E7D32), Color(0xFF9BD89B)),
    AccentPreset("Amber", Color(0xFFB4690E), Color(0xFFFFB870)),
    AccentPreset("Rose", Color(0xFFC4326B), Color(0xFFFFB0C8)),
    AccentPreset("Crimson", Color(0xFFC0362C), Color(0xFFFFB4A9)),
    AccentPreset("Cyan", Color(0xFF00838F), Color(0xFF4DD0E1)),
    AccentPreset("Grape", Color(0xFF8E24AA), Color(0xFFE9B3FF)),
    AccentPreset("Slate", Color(0xFF4A5B7A), Color(0xFFB6C6EA)),
    AccentPreset("Signal blue", Color(0xFF2C6BED), Color(0xFF9DB8FF)),
)

/** Index of Signal blue — default only when no accent has been saved yet. */
val DEFAULT_ACCENT_INDEX: Int =
    AccentPresets.indexOfFirst { it.name == "Signal blue" }.coerceAtLeast(0)

fun accentSeed(index: Int, dark: Boolean): Color {
    val preset = AccentPresets.getOrNull(index) ?: AccentPresets[DEFAULT_ACCENT_INDEX]
    return if (dark) preset.dark else preset.light
}

private fun hslColor(h: Float, s: Float, l: Float): Color {
    val hue = ((h % 360f) + 360f) % 360f
    val packed = ColorUtils.HSLToColor(floatArrayOf(hue, s.coerceIn(0f, 1f), l.coerceIn(0f, 1f)))
    return Color(packed)
}

private fun seedHsl(seed: Color): FloatArray {
    val out = FloatArray(3)
    ColorUtils.RGBToHSL(
        (seed.red * 255f).toInt(),
        (seed.green * 255f).toInt(),
        (seed.blue * 255f).toInt(),
        out,
    )
    return out
}

private fun Color.readableOn(): Color = if (luminance() > 0.5f) Color(0xFF101418) else Color.White

/**
 * Builds a full Material3 [ColorScheme] from a seed color, in the spirit of
 * ZedSecure's material-kolor based generator, using only androidx.core HSL
 * math (no extra dependency). Surfaces carry a subtle tint of the seed hue
 * so the app background feels alive but tasteful.
 */
fun seedColorScheme(
    dark: Boolean,
    seed: Color,
    style: ColorStyle,
    contrast: ColorContrast,
): ColorScheme {
    val hsl = seedHsl(seed)
    val hue = hsl[0]
    val satMul = when (style) {
        ColorStyle.ZedSecure, ColorStyle.TonalSpot -> 0.9f
        ColorStyle.Vibrant -> 1.25f
        ColorStyle.Expressive -> 1.1f
        ColorStyle.Rainbow -> 1.2f
        ColorStyle.FruitSalad -> 1.15f
        ColorStyle.Fidelity -> 1.0f
        ColorStyle.Neutral -> 0.55f
        ColorStyle.Monochrome -> 0.0f
    }
    val hueVar = when (style) {
        ColorStyle.Rainbow -> 90f
        ColorStyle.FruitSalad -> 60f
        ColorStyle.Expressive -> 45f
        else -> 32f
    }
    val s = (hsl[1] * satMul).coerceIn(0f, 1f)

    // Contrast pushes primaries toward vivid and surfaces toward pure.
    val contrastBoost = when (contrast) {
        ColorContrast.Standard -> 0f
        ColorContrast.Medium -> 0.06f
        ColorContrast.High -> 0.12f
    }

    val primary: Color
    val onPrimary: Color
    val primaryContainer: Color
    val onPrimaryContainer: Color
    val secondary: Color
    val onSecondary: Color
    val secondaryContainer: Color
    val onSecondaryContainer: Color
    val tertiary: Color
    val onTertiary: Color
    val tertiaryContainer: Color
    val onTertiaryContainer: Color
    val background: Color
    val onBackground: Color
    val surface: Color
    val onSurface: Color
    val surfaceVariant: Color
    val onSurfaceVariant: Color
    val surfaceTint: Color
    val surfaceBright: Color
    val surfaceDim: Color
    val surfaceContainer: Color
    val surfaceContainerHigh: Color
    val surfaceContainerHighest: Color
    val surfaceContainerLow: Color
    val surfaceContainerLowest: Color
    val outline: Color
    val outlineVariant: Color
    val inverseSurface: Color
    val inverseOnSurface: Color
    val error: Color
    val onError: Color
    val errorContainer: Color
    val onErrorContainer: Color

    if (!dark) {
        primary = hslColor(hue, (s + contrastBoost).coerceIn(0f, 1f), 0.42f)
        onPrimary = primary.readableOn()
        primaryContainer = hslColor(hue, s * 0.45f, 0.90f)
        onPrimaryContainer = hslColor(hue, s * 0.7f, 0.16f)
        secondary = hslColor(hue + hueVar, s * 0.8f, 0.44f)
        onSecondary = secondary.readableOn()
        secondaryContainer = hslColor(hue + hueVar, s * 0.4f, 0.90f)
        onSecondaryContainer = hslColor(hue + hueVar, s * 0.7f, 0.18f)
        tertiary = hslColor(hue - hueVar, s * 0.85f, 0.44f)
        onTertiary = tertiary.readableOn()
        tertiaryContainer = hslColor(hue - hueVar, s * 0.4f, 0.90f)
        onTertiaryContainer = hslColor(hue - hueVar, s * 0.7f, 0.18f)
        // Cheerful subtle tint: background carries ~6% of the seed hue.
        background = hslColor(hue, s * 0.10f, if (contrast == ColorContrast.High) 1f else 0.97f)
        onBackground = if (contrast == ColorContrast.High) Color.Black else Color(0xFF191C22)
        surface = hslColor(hue, s * 0.12f, if (contrast == ColorContrast.High) 1f else 0.96f)
        onSurface = if (contrast == ColorContrast.High) Color.Black else Color(0xFF191C22)
        surfaceVariant = hslColor(hue, s * 0.16f, 0.90f)
        onSurfaceVariant = hslColor(hue, s * 0.25f, 0.32f)
        surfaceTint = primary
        surfaceBright = hslColor(hue, s * 0.10f, 0.98f)
        surfaceDim = hslColor(hue, s * 0.14f, 0.88f)
        surfaceContainerLowest = hslColor(hue, s * 0.08f, 1f)
        surfaceContainerLow = hslColor(hue, s * 0.10f, 0.96f)
        surfaceContainer = hslColor(hue, s * 0.12f, 0.93f)
        surfaceContainerHigh = hslColor(hue, s * 0.14f, 0.90f)
        surfaceContainerHighest = hslColor(hue, s * 0.16f, 0.87f)
        outline = hslColor(hue, s * 0.20f, 0.55f)
        outlineVariant = hslColor(hue, s * 0.14f, 0.80f)
        inverseSurface = Color(0xFF2E3138)
        inverseOnSurface = Color(0xFFF0F1F5)
        error = hslColor(4f, 0.72f, 0.44f)
        onError = Color.White
        errorContainer = hslColor(4f, 0.6f, 0.90f)
        onErrorContainer = hslColor(4f, 0.7f, 0.18f)
    } else {
        primary = hslColor(hue, (s + contrastBoost).coerceIn(0f, 1f), 0.82f)
        onPrimary = primary.readableOn()
        primaryContainer = hslColor(hue, s * 0.7f, 0.30f)
        onPrimaryContainer = hslColor(hue, s * 0.5f, 0.90f)
        secondary = hslColor(hue + hueVar, s * 0.7f, 0.78f)
        onSecondary = secondary.readableOn()
        secondaryContainer = hslColor(hue + hueVar, s * 0.6f, 0.30f)
        onSecondaryContainer = hslColor(hue + hueVar, s * 0.5f, 0.90f)
        tertiary = hslColor(hue - hueVar, s * 0.7f, 0.78f)
        onTertiary = tertiary.readableOn()
        tertiaryContainer = hslColor(hue - hueVar, s * 0.6f, 0.30f)
        onTertiaryContainer = hslColor(hue - hueVar, s * 0.5f, 0.90f)
        background = hslColor(hue, s * 0.16f, if (contrast == ColorContrast.High) 0f else 0.07f)
        onBackground = if (contrast == ColorContrast.High) Color.White else Color(0xFFE3E5EA)
        surface = hslColor(hue, s * 0.16f, if (contrast == ColorContrast.High) 0f else 0.07f)
        onSurface = if (contrast == ColorContrast.High) Color.White else Color(0xFFE3E5EA)
        surfaceVariant = hslColor(hue, s * 0.20f, 0.20f)
        onSurfaceVariant = hslColor(hue, s * 0.25f, 0.72f)
        surfaceTint = primary
        surfaceBright = hslColor(hue, s * 0.16f, 0.16f)
        surfaceDim = hslColor(hue, s * 0.18f, 0.05f)
        surfaceContainerLowest = hslColor(hue, s * 0.18f, 0.04f)
        surfaceContainerLow = hslColor(hue, s * 0.16f, 0.09f)
        surfaceContainer = hslColor(hue, s * 0.16f, 0.12f)
        surfaceContainerHigh = hslColor(hue, s * 0.16f, 0.16f)
        surfaceContainerHighest = hslColor(hue, s * 0.18f, 0.21f)
        outline = hslColor(hue, s * 0.20f, 0.45f)
        outlineVariant = hslColor(hue, s * 0.22f, 0.26f)
        inverseSurface = Color(0xFFE3E5EA)
        inverseOnSurface = Color(0xFF2E3138)
        error = hslColor(4f, 0.72f, 0.72f)
        onError = Color(0xFF40090B)
        errorContainer = hslColor(4f, 0.6f, 0.30f)
        onErrorContainer = hslColor(4f, 0.6f, 0.90f)
    }

    val scheme = if (!dark) {
        lightColorScheme(
            primary = primary, onPrimary = onPrimary,
            primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer,
            inversePrimary = hslColor(hue, s, 0.80f),
            secondary = secondary, onSecondary = onSecondary,
            secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer,
            tertiary = tertiary, onTertiary = onTertiary,
            tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer,
            background = background, onBackground = onBackground,
            surface = surface, onSurface = onSurface,
            surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant,
            surfaceTint = surfaceTint,
            surfaceBright = surfaceBright, surfaceDim = surfaceDim,
            surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh,
            surfaceContainerHighest = surfaceContainerHighest,
            surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest,
            outline = outline, outlineVariant = outlineVariant,
            inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface,
            error = error, onError = onError,
            errorContainer = errorContainer, onErrorContainer = onErrorContainer,
            scrim = Color.Black,
        )
    } else {
        darkColorScheme(
            primary = primary, onPrimary = onPrimary,
            primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer,
            inversePrimary = hslColor(hue, s, 0.42f),
            secondary = secondary, onSecondary = onSecondary,
            secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer,
            tertiary = tertiary, onTertiary = onTertiary,
            tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer,
            background = background, onBackground = onBackground,
            surface = surface, onSurface = onSurface,
            surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant,
            surfaceTint = surfaceTint,
            surfaceBright = surfaceBright, surfaceDim = surfaceDim,
            surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh,
            surfaceContainerHighest = surfaceContainerHighest,
            surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest,
            outline = outline, outlineVariant = outlineVariant,
            inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface,
            error = error, onError = onError,
            errorContainer = errorContainer, onErrorContainer = onErrorContainer,
            scrim = Color.Black,
        )
    }
    return scheme
}

/** AMOLED variant: pure-black surfaces, like ZedSecure's amoled() extension. */
fun ColorScheme.applyAmoled(dark: Boolean): ColorScheme {
    if (!dark) return this
    return copy(
        background = Color.Black,
        surface = Color.Black,
        surfaceBright = Color(0xFF1A1A1A),
        surfaceDim = Color.Black,
        surfaceContainer = Color(0xFF0F0F0F),
        surfaceContainerHigh = Color(0xFF161616),
        surfaceContainerHighest = Color(0xFF1E1E1E),
        surfaceContainerLow = Color(0xFF0B0B0B),
        surfaceContainerLowest = Color.Black,
    )
}

/** Stock theme profiles (original designs): one tap applies accent + key colors. */
data class ThemeProfile(
    val id: String,
    val name: String,
    val accentIndex: Int,
    val amoled: Boolean = false,
    val connect: Long,
    val connected: Long,
    val serverCard: Long,
    val serverActive: Long,
    val configCard: Long,
    val configCardText: Long,
)

val ThemeProfiles: List<ThemeProfile> = listOf(
    ThemeProfile(
        id = "lagoon",
        name = "Lagoon",
        accentIndex = 2,
        connect = 0xFF00897E,
        connected = 0xFF00564F,
        serverCard = 0xFF0E2A28,
        serverActive = 0xFF0B3D3A,
        configCard = 0xFF0E2A28,
        configCardText = 0xFFD7F4F0,
    ),
    ThemeProfile(
        id = "sunset",
        name = "Sunset",
        accentIndex = 4,
        connect = 0xFFB4690E,
        connected = 0xFF7A3E00,
        serverCard = 0xFF2E1E08,
        serverActive = 0xFF3D2809,
        configCard = 0xFF2E1E08,
        configCardText = 0xFFFFEDD2,
    ),
    ThemeProfile(
        id = "onyx",
        name = "Onyx",
        accentIndex = 0,
        amoled = true,
        connect = 0xFF5646D6,
        connected = 0xFF2E2370,
        serverCard = 0xFF101014,
        serverActive = 0xFF1B1830,
        configCard = 0xFF14141A,
        configCardText = 0xFFE4E1FF,
    ),
)