Fixed plugin values which depended on the region settings of the computer (#1194)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-12 08:57:50 +01:00
1 parent 65d50de033
commit a1b493cb13
20 files changed
+397 -137

No files matched your search

@@ -1,3 +1,5 @@
using System.Globalization;
using AIStudio.Tools.PluginSystem.Assistants.Icons;
namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
@@ -9,26 +11,58 @@ internal static class AssistantComponentPropHelper
/// </summary>
private const string LUCIDE_PREFIX = "lucide:";
/// <summary>
/// Reads a prop as text. A number, which Lua hands over as a double, becomes the same text on every computer,
/// the one Lua itself writes: 12.5 stays "12.5", also where the region settings use a decimal comma.
/// </summary>
public static string ReadString(Dictionary<string, object> props, string key)
{
if (props.TryGetValue(key, out var value))
return value.ToString() ?? string.Empty;
if (!props.TryGetValue(key, out var value))
return string.Empty;
return string.Empty;
if (value is IFormattable formattable)
return formattable.ToString(null, CultureInfo.InvariantCulture);
return value.ToString() ?? string.Empty;
}
public static void WriteString(Dictionary<string, object> props, string key, string value) => props[key] = value;
public static int ReadInt(Dictionary<string, object> props, string key, int fallback = 0)
{
return props.TryGetValue(key, out var value) && int.TryParse(value.ToString(), out var i) ? i : fallback;
}
public static int ReadInt(Dictionary<string, object> props, string key, int fallback = 0) => TryReadInt(props, key, out var number) ? number : fallback;
public static void WriteInt(Dictionary<string, object> props, string key, int value) => props[key] = value;
public static int? ReadNullableInt(Dictionary<string, object> props, string key)
public static int? ReadNullableInt(Dictionary<string, object> props, string key) => TryReadInt(props, key, out var number) ? number : null;
/// <summary>
/// Reads a whole number, independent of the region settings of the computer.
/// </summary>
/// <remarks>
/// Lua hands over every number as a double, and the setters of the components store an int. A plugin may also
/// write the number as text. A number with a fraction, or one beyond the range of an int, counts as missing.
/// </remarks>
private static bool TryReadInt(Dictionary<string, object> props, string key, out int number)
{
return props.TryGetValue(key, out var value) && int.TryParse(value.ToString(), out var i) ? i : null;
number = 0;
if (!props.TryGetValue(key, out var value))
return false;
switch (value)
{
case int whole:
number = whole;
return true;
case double real when double.IsInteger(real) && real is >= int.MinValue and <= int.MaxValue:
number = (int)real;
return true;
case string text:
return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out number);
default:
return false;
}
}
public static void WriteNullableInt(Dictionary<string, object> props, string key, int? value)
@@ -32,14 +32,6 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase
set => this.Props[nameof(this.Items)] = value;
}
public string ValueType
{
get => this.Props.TryGetValue(nameof(this.ValueType), out var v)
? v.ToString() ?? "string"
: "string";
set => this.Props[nameof(this.ValueType)] = value;
}
public bool IsMultiselect
{
get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsMultiselect));
@@ -141,28 +133,6 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase
return [this.Default, .. this.Items];
}
public IEnumerable<object> GetParsedDropdownValues()
{
foreach (var item in this.Items)
{
switch (this.ValueType.ToLowerInvariant())
{
case "int":
if (int.TryParse(item.Value, out var i)) yield return i;
break;
case "double":
if (double.TryParse(item.Value, out var d)) yield return d;
break;
case "bool":
if (bool.TryParse(item.Value, out var b)) yield return b;
break;
default:
yield return item.Value;
break;
}
}
}
public string Class
{
get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class));
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using AIStudio.Assistants;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
@@ -856,21 +857,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
this.State.Environment["DateTime"] = new LuaFunction((context, _) =>
{
var format = context.ArgumentCount > 0 ? context.GetArgument<string>(0) : "yyyy-MM-dd HH:mm:ss";
var now = DateTime.Now;
var formattedDate = now.ToString(format);
var table = new LuaTable
{
["year"] = now.Year,
["month"] = now.Month,
["day"] = now.Day,
["hour"] = now.Hour,
["minute"] = now.Minute,
["second"] = now.Second,
["millisecond"] = now.Millisecond,
["formatted"] = formattedDate,
};
return new(context.Return(table));
return new(context.Return(CreateDateTimeTable(DateTime.Now, format)));
});
this.State.Environment["Timestamp"] = new LuaFunction((context, _) =>
@@ -889,6 +876,29 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
});
}
/// <summary>
/// Builds the table which the Lua function DateTime returns for the given moment.
/// </summary>
/// <remarks>
/// The formatted text is the same on every computer. With the culture of the computer, the separators, the
/// calendar, and the names of months and days would follow its region settings, e.g., the year of the Buddhist
/// era in Thailand.
/// </remarks>
/// <param name="moment">The moment to describe.</param>
/// <param name="format">The .NET format of the formatted text.</param>
/// <returns>The parts of the moment and the formatted text.</returns>
internal static LuaTable CreateDateTimeTable(DateTime moment, string format) => new()
{
["year"] = moment.Year,
["month"] = moment.Month,
["day"] = moment.Day,
["hour"] = moment.Hour,
["minute"] = moment.Minute,
["second"] = moment.Second,
["millisecond"] = moment.Millisecond,
["formatted"] = moment.ToString(format, CultureInfo.InvariantCulture)
};
private static void InitializeState(IEnumerable<IAssistantComponent> components, AssistantState state)
{
foreach (var component in components)
@@ -1,4 +1,6 @@
// ReSharper disable MemberCanBePrivate.Global
using System.Globalization;
namespace AIStudio.Tools.PluginSystem;
/// <summary>
@@ -46,9 +48,9 @@ public readonly record struct PluginVersion(int Major, int Minor, int Patch) : I
if (segments.Length != 3)
throw new FormatException("The input string must be in the format 'major.minor.patch'.");
var major = int.Parse(segments[0]);
var minor = int.Parse(segments[1]);
var patch = int.Parse(segments[2]);
var major = int.Parse(segments[0], NumberStyles.Integer, CultureInfo.InvariantCulture);
var minor = int.Parse(segments[1], NumberStyles.Integer, CultureInfo.InvariantCulture);
var patch = int.Parse(segments[2], NumberStyles.Integer, CultureInfo.InvariantCulture);
if(major < 0 || minor < 0 || patch < 0)
throw new FormatException("The major, minor, and patch numbers must be greater than or equal to 0.");