feat(themes): default-fill missing colour/layout slots on theme load
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using HellionChat.Themes.Builtin;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
@@ -11,7 +13,8 @@ internal static class ThemeJsonLoader
|
||||
// policy from the v2.x style refactor: v1 user themes are not migrated,
|
||||
// they're silently ignored so the loader stays free of legacy mapping
|
||||
// code. Any other malformed input still throws FormatException.
|
||||
public static Theme? LoadFromString(string json)
|
||||
// B4b-2: callers must pass the logger or the default-fill warnings go silent.
|
||||
public static Theme? LoadFromString(string json, ILogger? logger = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
throw new FormatException("Theme JSON is empty");
|
||||
@@ -43,8 +46,22 @@ internal static class ThemeJsonLoader
|
||||
var author = ReadString(root, "author");
|
||||
var description = ReadString(root, "description");
|
||||
|
||||
var colors = ReadColors(root.GetProperty("colors"));
|
||||
var layout = ReadLayout(root.GetProperty("layout"));
|
||||
// Missing colours/layout object stays fatal, but as FormatException so the
|
||||
// import path catches it — GetProperty's KeyNotFoundException would crash.
|
||||
if (
|
||||
!root.TryGetProperty("colors", out var colorsEl)
|
||||
|| colorsEl.ValueKind != JsonValueKind.Object
|
||||
)
|
||||
throw new FormatException("Theme JSON missing 'colors' object");
|
||||
if (
|
||||
!root.TryGetProperty("layout", out var layoutEl)
|
||||
|| layoutEl.ValueKind != JsonValueKind.Object
|
||||
)
|
||||
throw new FormatException("Theme JSON missing 'layout' object");
|
||||
|
||||
var fallback = HellionArctic.Build();
|
||||
var colors = ReadColors(colorsEl, fallback.Colors, logger);
|
||||
var layout = ReadLayout(layoutEl, fallback.Layout, logger);
|
||||
var typography = ReadTypography(root);
|
||||
|
||||
ThemeChatColors? chatColors = null;
|
||||
@@ -93,52 +110,72 @@ internal static class ThemeJsonLoader
|
||||
return new ThemeChatColors(dict);
|
||||
}
|
||||
|
||||
public static Theme? LoadFromFile(string path)
|
||||
public static Theme? LoadFromFile(string path, ILogger? logger = null)
|
||||
{
|
||||
// FileShare.Read lets concurrent readers and well-behaved editors share
|
||||
// the handle; atomic-replace editors still raise IOException, caught upstream.
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
return LoadFromString(json);
|
||||
return LoadFromString(json, logger);
|
||||
}
|
||||
|
||||
private static ThemeColors ReadColors(JsonElement el) =>
|
||||
private static ThemeColors ReadColors(JsonElement el, ThemeColors fallback, ILogger? logger) =>
|
||||
new(
|
||||
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
|
||||
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
|
||||
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
|
||||
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
|
||||
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
|
||||
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
|
||||
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
|
||||
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
|
||||
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
|
||||
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
|
||||
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
|
||||
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
|
||||
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
|
||||
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
|
||||
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
|
||||
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
|
||||
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
|
||||
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
|
||||
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
|
||||
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
|
||||
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
|
||||
PrimaryDark: ReadColorOrDefault(el, "primaryDark", fallback.PrimaryDark, logger),
|
||||
Primary: ReadColorOrDefault(el, "primary", fallback.Primary, logger),
|
||||
PrimaryLight: ReadColorOrDefault(el, "primaryLight", fallback.PrimaryLight, logger),
|
||||
PrimaryGlow: ReadColorOrDefault(el, "primaryGlow", fallback.PrimaryGlow, logger),
|
||||
AccentDark: ReadColorOrDefault(el, "accentDark", fallback.AccentDark, logger),
|
||||
Accent: ReadColorOrDefault(el, "accent", fallback.Accent, logger),
|
||||
AccentLight: ReadColorOrDefault(el, "accentLight", fallback.AccentLight, logger),
|
||||
Identity: ReadColorOrDefault(el, "identity", fallback.Identity, logger),
|
||||
WindowBg: ReadColorOrDefault(el, "windowBg", fallback.WindowBg, logger),
|
||||
ChildBg: ReadColorOrDefault(el, "childBg", fallback.ChildBg, logger),
|
||||
FrameBg: ReadColorOrDefault(el, "frameBg", fallback.FrameBg, logger),
|
||||
Surface: ReadColorOrDefault(el, "surface", fallback.Surface, logger),
|
||||
SurfaceHover: ReadColorOrDefault(el, "surfaceHover", fallback.SurfaceHover, logger),
|
||||
Border: ReadColorOrDefault(el, "border", fallback.Border, logger),
|
||||
TextPrimary: ReadColorOrDefault(el, "textPrimary", fallback.TextPrimary, logger),
|
||||
TextMuted: ReadColorOrDefault(el, "textMuted", fallback.TextMuted, logger),
|
||||
TextDim: ReadColorOrDefault(el, "textDim", fallback.TextDim, logger),
|
||||
StatusSuccess: ReadColorOrDefault(el, "statusSuccess", fallback.StatusSuccess, logger),
|
||||
StatusDanger: ReadColorOrDefault(el, "statusDanger", fallback.StatusDanger, logger),
|
||||
StatusWarning: ReadColorOrDefault(el, "statusWarning", fallback.StatusWarning, logger),
|
||||
StatusInfo: ReadColorOrDefault(el, "statusInfo", fallback.StatusInfo, logger)
|
||||
);
|
||||
|
||||
private static ThemeLayout ReadLayout(JsonElement el) =>
|
||||
private static ThemeLayout ReadLayout(JsonElement el, ThemeLayout fallback, ILogger? logger) =>
|
||||
new(
|
||||
WindowRounding: ReadFloat(el, "windowRounding"),
|
||||
ChildRounding: ReadFloat(el, "childRounding"),
|
||||
PopupRounding: ReadFloat(el, "popupRounding"),
|
||||
FrameRounding: ReadFloat(el, "frameRounding"),
|
||||
GrabRounding: ReadFloat(el, "grabRounding"),
|
||||
TabRounding: ReadFloat(el, "tabRounding"),
|
||||
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
|
||||
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
|
||||
FrameBorderSize: ReadFloat(el, "frameBorderSize")
|
||||
WindowRounding: ReadFloatOrDefault(
|
||||
el,
|
||||
"windowRounding",
|
||||
fallback.WindowRounding,
|
||||
logger
|
||||
),
|
||||
ChildRounding: ReadFloatOrDefault(el, "childRounding", fallback.ChildRounding, logger),
|
||||
PopupRounding: ReadFloatOrDefault(el, "popupRounding", fallback.PopupRounding, logger),
|
||||
FrameRounding: ReadFloatOrDefault(el, "frameRounding", fallback.FrameRounding, logger),
|
||||
GrabRounding: ReadFloatOrDefault(el, "grabRounding", fallback.GrabRounding, logger),
|
||||
TabRounding: ReadFloatOrDefault(el, "tabRounding", fallback.TabRounding, logger),
|
||||
ScrollbarRounding: ReadFloatOrDefault(
|
||||
el,
|
||||
"scrollbarRounding",
|
||||
fallback.ScrollbarRounding,
|
||||
logger
|
||||
),
|
||||
WindowBorderSize: ReadFloatOrDefault(
|
||||
el,
|
||||
"windowBorderSize",
|
||||
fallback.WindowBorderSize,
|
||||
logger
|
||||
),
|
||||
FrameBorderSize: ReadFloatOrDefault(
|
||||
el,
|
||||
"frameBorderSize",
|
||||
fallback.FrameBorderSize,
|
||||
logger
|
||||
)
|
||||
);
|
||||
|
||||
// Optional in v2 — themes without a typography block default to the
|
||||
@@ -186,4 +223,54 @@ internal static class ThemeJsonLoader
|
||||
throw new FormatException($"Theme JSON property '{name}' must be a number or null");
|
||||
return (float)v.GetDouble();
|
||||
}
|
||||
|
||||
// Missing / wrong-typed / unparseable colour slot -> built-in default + one warning.
|
||||
private static uint ReadColorOrDefault(
|
||||
JsonElement el,
|
||||
string name,
|
||||
uint fallback,
|
||||
ILogger? logger
|
||||
)
|
||||
{
|
||||
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
logger?.LogWarning(
|
||||
"Theme JSON colour slot '{Slot}' missing or not a string, using built-in default",
|
||||
name
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return ColourUtil.HexToRgba(v.GetString()!);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
logger?.LogWarning(
|
||||
"Theme JSON colour slot '{Slot}' has an invalid hex value, using built-in default",
|
||||
name
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static float ReadFloatOrDefault(
|
||||
JsonElement el,
|
||||
string name,
|
||||
float fallback,
|
||||
ILogger? logger
|
||||
)
|
||||
{
|
||||
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
|
||||
{
|
||||
logger?.LogWarning(
|
||||
"Theme JSON layout slot '{Slot}' missing or not a number, using built-in default",
|
||||
name
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return (float)v.GetDouble();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +626,7 @@ public sealed class ThemeRegistry
|
||||
{
|
||||
try
|
||||
{
|
||||
theme = ThemeJsonLoader.LoadFromFile(path);
|
||||
theme = ThemeJsonLoader.LoadFromFile(path, _logger);
|
||||
// null = hard-cut policy skipped a legacy v1 file. Leave
|
||||
// theme null so the yield-guard below drops the entry.
|
||||
if (theme is not null)
|
||||
|
||||
@@ -150,7 +150,7 @@ internal sealed class ThemeImportExportRow
|
||||
Theme? theme;
|
||||
try
|
||||
{
|
||||
theme = ThemeJsonLoader.LoadFromString(json);
|
||||
theme = ThemeJsonLoader.LoadFromString(json, _logger);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user