CVR Manager 1.0.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Exterrata
2026-10-25 02:46:35 -04:00
commit 9bb4f85802
59 changed files with 7913 additions and 0 deletions
+196
View File
@@ -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);
}
}