CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace CVRManager.Services;
|
||||
|
||||
public sealed record ThemePreset(string Name, Color Primary, Color Secondary);
|
||||
|
||||
public static class ThemeService
|
||||
{
|
||||
public const string DefaultName = "Default";
|
||||
public const string GameColoursName = "My in-game colours";
|
||||
|
||||
static Color F(double r, double g, double b) => Color.FromRgb((byte)Math.Round(r * 255), (byte)Math.Round(g * 255), (byte)Math.Round(b * 255));
|
||||
|
||||
public static IReadOnlyList<ThemePreset> Presets { get; } = new[]
|
||||
{
|
||||
new ThemePreset(DefaultName, Color.Parse("#5B8CFF"), Color.Parse("#7AA2FF")),
|
||||
new ThemePreset("ChilloutVR", F(0.894, 0.212, 0), F(1, 0.992, 0.988)),
|
||||
new ThemePreset("Ocean Mist", F(0, 0.5, 0.7), F(0.4, 0.8, 1)),
|
||||
new ThemePreset("Sunset Glow", F(1, 0.4, 0.3), F(1, 0.6, 0.4)),
|
||||
new ThemePreset("Forest Whisper", F(0.2, 0.6, 0.2), F(0.5, 0.8, 0.5)),
|
||||
new ThemePreset("Royal Dusk", F(0.5, 0.2, 0.7), F(0.7, 0.4, 1)),
|
||||
new ThemePreset("Desert Bloom", F(1, 0.7, 0.3), F(1, 0.9, 0.5)),
|
||||
new ThemePreset("Blossom Veil", F(1, 0.5, 0.7), F(1, 0.7, 0.9)),
|
||||
new ThemePreset("Titanium White", F(1, 1, 1), F(1, 1, 1)),
|
||||
new ThemePreset("Where Is My Shader", F(1, 0, 1), F(0, 1, 1)),
|
||||
new ThemePreset("Cotton Candy", F(0.357, 0.812, 0.98), F(0.961, 0.671, 0.725)),
|
||||
new ThemePreset("Basis", F(0.063, 0.059, 0.153), F(0.91, 0.071, 0.212)),
|
||||
new ThemePreset("Unit 01", F(0.435, 0.098, 0.522), F(0.345, 0.722, 0.388)),
|
||||
new ThemePreset("Spooky", F(1, 0.404, 0), F(0.545, 0, 0.545)),
|
||||
new ThemePreset("Cherry", F(0.824, 0.129, 0.204), F(0.278, 0.478, 0)),
|
||||
new ThemePreset("Bongo", F(0.616, 0.035, 0.906), F(0.498, 0, 0.078)),
|
||||
new ThemePreset("Kaffee", F(0.482, 0.259, 0.255), F(0.843, 0.647, 0.545)),
|
||||
};
|
||||
|
||||
public static IEnumerable<string> Names => new[] { GameColoursName }.Concat(Presets.Select(p => p.Name));
|
||||
|
||||
static string GameConfigPath => Path.Combine(Paths.GameDataDir(), "game.config");
|
||||
|
||||
public static (Color Primary, Color Secondary)? ReadGameColours()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(GameConfigPath)) return null;
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(GameConfigPath));
|
||||
var values = new Dictionary<string, double>();
|
||||
foreach (var e in doc.RootElement.GetProperty("settings").EnumerateArray())
|
||||
{
|
||||
var name = e.GetProperty("name").GetString() ?? "";
|
||||
if (!name.StartsWith("PlayerColorPrimary", StringComparison.Ordinal) && !name.StartsWith("PlayerColorSecondary", StringComparison.Ordinal)) continue;
|
||||
if (double.TryParse(e.GetProperty("value").GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) values[name] = v;
|
||||
}
|
||||
Color? Read(string key)
|
||||
{
|
||||
if (!values.TryGetValue(key + "R", out var r) || !values.TryGetValue(key + "G", out var g) || !values.TryGetValue(key + "B", out var b)) return null;
|
||||
var scale = r > 1 || g > 1 || b > 1 ? 1.0 : 255.0;
|
||||
byte B(double x) => (byte)Math.Clamp(Math.Round(x * scale), 0, 255);
|
||||
return Color.FromRgb(B(r), B(g), B(b));
|
||||
}
|
||||
var p = Read("PlayerColorPrimary");
|
||||
var s = Read("PlayerColorSecondary");
|
||||
return p is { } pc && s is { } sc ? (pc, sc) : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
public static bool Apply(string name)
|
||||
{
|
||||
Color primary, secondary;
|
||||
var ok = true;
|
||||
if (name == GameColoursName)
|
||||
{
|
||||
if (ReadGameColours() is { } g) (primary, secondary) = g;
|
||||
else
|
||||
{
|
||||
ok = false;
|
||||
(primary, secondary) = (Presets[1].Primary, Presets[1].Secondary);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var preset = Presets.FirstOrDefault(p => p.Name == name) ?? Presets[0];
|
||||
(primary, secondary) = (preset.Primary, preset.Secondary);
|
||||
}
|
||||
Set(primary, secondary);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static (double H, double S, double L) ToHsl(Color c)
|
||||
{
|
||||
double r = c.R / 255.0, g = c.G / 255.0, b = c.B / 255.0;
|
||||
var max = Math.Max(r, Math.Max(g, b));
|
||||
var min = Math.Min(r, Math.Min(g, b));
|
||||
var l = (max + min) / 2;
|
||||
if (max == min) return (0, 0, l);
|
||||
var d = max - min;
|
||||
var s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
double h;
|
||||
if (max == r) h = (g - b) / d + (g < b ? 6 : 0);
|
||||
else if (max == g) h = (b - r) / d + 2;
|
||||
else h = (r - g) / d + 4;
|
||||
return (h * 60, s, l);
|
||||
}
|
||||
|
||||
static Color FromHsl(double h, double s, double l)
|
||||
{
|
||||
h = ((h % 360) + 360) % 360;
|
||||
s = Math.Clamp(s, 0, 1);
|
||||
l = Math.Clamp(l, 0, 1);
|
||||
var c = (1 - Math.Abs(2 * l - 1)) * s;
|
||||
var x = c * (1 - Math.Abs(h / 60 % 2 - 1));
|
||||
var m = l - c / 2;
|
||||
(double r, double g, double b) = (int)(h / 60) switch
|
||||
{
|
||||
0 => (c, x, 0.0),
|
||||
1 => (x, c, 0.0),
|
||||
2 => (0.0, c, x),
|
||||
3 => (0.0, x, c),
|
||||
4 => (x, 0.0, c),
|
||||
_ => (c, 0.0, x),
|
||||
};
|
||||
return Color.FromRgb((byte)Math.Round((r + m) * 255), (byte)Math.Round((g + m) * 255), (byte)Math.Round((b + m) * 255));
|
||||
}
|
||||
|
||||
static double Luma(Color c) => (0.2126 * c.R + 0.7152 * c.G + 0.0722 * c.B) / 255.0;
|
||||
|
||||
static Color PickAccent(Color primary, Color secondary)
|
||||
{
|
||||
bool Usable(Color c) { var (_, s, l) = ToHsl(c); return Luma(c) >= 0.18 && (s >= 0.25 || l >= 0.8); }
|
||||
if (Usable(primary)) return primary;
|
||||
if (Usable(secondary)) return secondary;
|
||||
return Luma(primary) >= Luma(secondary) ? primary : secondary;
|
||||
}
|
||||
|
||||
static void Set(Color primary, Color secondary)
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
var accent = PickAccent(primary, secondary);
|
||||
var (h, s, l) = ToHsl(accent);
|
||||
var accentL = Math.Clamp(l, 0.50, 0.68);
|
||||
var accentS = s < 0.1 ? s : Math.Max(s, 0.45);
|
||||
var accentColor = FromHsl(h, accentS, accentL);
|
||||
var tint = Math.Min(s, 0.32);
|
||||
var dimS = Math.Min(s, 0.14);
|
||||
|
||||
void Brush(string key, Color c) => app.Resources[key] = new SolidColorBrush(c);
|
||||
void Surface(string key, double lightness, double extraSat = 0) => Brush(key, FromHsl(h, Math.Min(tint + extraSat, 0.5), lightness));
|
||||
|
||||
app.Resources["SystemAccentColor"] = accentColor;
|
||||
app.Resources["SystemAccentColorLight1"] = FromHsl(h, accentS, Math.Min(accentL + 0.08, 0.85));
|
||||
app.Resources["SystemAccentColorLight2"] = FromHsl(h, accentS, Math.Min(accentL + 0.16, 0.9));
|
||||
app.Resources["SystemAccentColorLight3"] = FromHsl(h, accentS, Math.Min(accentL + 0.24, 0.94));
|
||||
app.Resources["SystemAccentColorDark1"] = FromHsl(h, accentS, accentL - 0.08);
|
||||
app.Resources["SystemAccentColorDark2"] = FromHsl(h, accentS, accentL - 0.16);
|
||||
app.Resources["SystemAccentColorDark3"] = FromHsl(h, accentS, accentL - 0.24);
|
||||
|
||||
Brush("AccentBrush", accentColor);
|
||||
Brush("AccentHoverBrush", FromHsl(h, accentS, Math.Min(accentL + 0.08, 0.85)));
|
||||
Brush("AccentPressedBrush", FromHsl(h, accentS, accentL - 0.08));
|
||||
Brush("AccentDisabledBrush", FromHsl(h, accentS * 0.4, 0.22));
|
||||
Brush("AccentFgBrush", Luma(accentColor) > 0.62 ? Color.Parse("#101114") : Colors.White);
|
||||
|
||||
Surface("AppBg", 0.055);
|
||||
Surface("SidebarBg", 0.04);
|
||||
Surface("CardBg", 0.09);
|
||||
Surface("CardBorder", 0.15);
|
||||
Surface("RowHover", 0.125);
|
||||
Surface("ChipBg", 0.16);
|
||||
Surface("SelectedBg", 0.17, 0.08);
|
||||
Surface("OverlayBg", 0.105);
|
||||
Surface("ToastBg", 0.135);
|
||||
Surface("GroupHeaderBg", 0.11);
|
||||
Surface("GhostHover", 0.16);
|
||||
Brush("DimText", FromHsl(h, dimS, 0.64));
|
||||
|
||||
Surface("ButtonBackground", 0.16);
|
||||
Surface("ButtonBackgroundPointerOver", 0.2);
|
||||
Surface("ButtonBackgroundPressed", 0.24);
|
||||
Surface("ButtonBackgroundDisabled", 0.11);
|
||||
Surface("TextControlBackground", 0.075);
|
||||
Surface("TextControlBackgroundPointerOver", 0.095);
|
||||
Surface("TextControlBackgroundFocused", 0.075);
|
||||
Surface("TextControlBorderBrush", 0.18);
|
||||
Surface("TextControlBorderBrushPointerOver", 0.25);
|
||||
Brush("TextControlBorderBrushFocused", accentColor);
|
||||
Surface("ComboBoxBackground", 0.075);
|
||||
Surface("ComboBoxBackgroundPointerOver", 0.095);
|
||||
Surface("ComboBoxBackgroundPressed", 0.095);
|
||||
Surface("ComboBoxBorderBrush", 0.18);
|
||||
Surface("ComboBoxBorderBrushPointerOver", 0.25);
|
||||
Brush("ComboBoxBorderBrushPressed", accentColor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user