init
This commit is contained in:
95
scripts/ApiModelSerializer.cs
Normal file
95
scripts/ApiModelSerializer.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using openF1Manager.scripts;
|
||||
|
||||
public static class ApiModelSerializer
|
||||
{
|
||||
public static string ToQueryString(object model, string baseAddress)
|
||||
{
|
||||
var query = System.Web.HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var (key, value) in EnumerateKeyValues(model))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
query[key] = value;
|
||||
}
|
||||
|
||||
var queryString = query.ToString();
|
||||
return string.IsNullOrEmpty(queryString) ? baseAddress : $"{baseAddress}?{queryString}";
|
||||
}
|
||||
|
||||
public static void PopulateFromDictionary(object model, Godot.Collections.Dictionary dict)
|
||||
{
|
||||
var type = model.GetType();
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (!prop.CanWrite) continue;
|
||||
|
||||
var attr = prop.GetCustomAttribute<ApiFieldAttribute>();
|
||||
var key = attr?.Key ?? ToSnakeCase(prop.Name);
|
||||
|
||||
// Try snake_case first, then camelCase fallbacks
|
||||
if (!TryGet(dict, key, out var str) && !TryGet(dict, SnakeToCamel(key), out str))
|
||||
continue;
|
||||
|
||||
// You can add type conversions if needed; here we only set strings
|
||||
if (prop.PropertyType == typeof(string) || prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(int) || prop.PropertyType == typeof(float))
|
||||
prop.SetValue(model, str);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGet(Dictionary dict, string key, out string value)
|
||||
{
|
||||
if (dict.ContainsKey(key) && dict[key].VariantType != Variant.Type.Nil)
|
||||
{
|
||||
value = dict[key].ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Key, string Value)> EnumerateKeyValues(object model)
|
||||
{
|
||||
var type = model.GetType();
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (!prop.CanRead) continue;
|
||||
|
||||
var value = prop.GetValue(model)?.ToString();
|
||||
var attr = prop.GetCustomAttribute<ApiFieldAttribute>();
|
||||
var key = attr?.Key ?? ToSnakeCase(prop.Name);
|
||||
yield return (key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToSnakeCase(string name)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
var c = name[i];
|
||||
sb.Append(i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : c.ToString());
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string SnakeToCamel(string name)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(char.ToLower(name[0]));
|
||||
for (int i = 1; i < name.Length; i++)
|
||||
{
|
||||
var c = name[i];
|
||||
sb.Append(char.IsUpper(c) ? char.ToLower(c) : c);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
1
scripts/ApiModelSerializer.cs.uid
Normal file
1
scripts/ApiModelSerializer.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kos10xtghum5
|
||||
88
scripts/FormBinder.cs
Normal file
88
scripts/FormBinder.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Godot;
|
||||
|
||||
public sealed class FormBinder<TViewModel> where TViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly TViewModel _vm;
|
||||
private readonly Control _root;
|
||||
private readonly Dictionary<LineEdit, PropertyInfo> _lineToProp = new();
|
||||
private readonly Dictionary<string, LineEdit> _propNameToLine = new(StringComparer.OrdinalIgnoreCase);
|
||||
private bool _suppress;
|
||||
|
||||
public FormBinder(TViewModel vm, Control root)
|
||||
{
|
||||
_vm = vm;
|
||||
_root = root;
|
||||
}
|
||||
|
||||
public FormBinder<TViewModel> Bind()
|
||||
{
|
||||
Discover();
|
||||
_vm.PropertyChanged += OnPropertyChanged;
|
||||
foreach (var (line, prop) in _lineToProp)
|
||||
{
|
||||
line.TextChanged += (string text) =>
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (prop.PropertyType == typeof(string) || prop.PropertyType == typeof(string))
|
||||
prop.SetValue(_vm, text);
|
||||
};
|
||||
}
|
||||
// Initial sync UI <- VM
|
||||
PushAllFromVm();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void Discover()
|
||||
{
|
||||
var props = typeof(TViewModel).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||
var propMap = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in props) if (p.CanRead || p.CanWrite) propMap[p.Name] = p;
|
||||
|
||||
void Walk(Node n)
|
||||
{
|
||||
if (n is LineEdit le)
|
||||
{
|
||||
// Prefer explicit metadata "vm_prop" to map to a property name; fallback to node name.
|
||||
string propName = le.HasMeta("vm_prop") ? (string)le.GetMeta("vm_prop") : StripSuffix(le.Name, "LineEdit");
|
||||
if (propMap.TryGetValue(propName, out var pi))
|
||||
{
|
||||
_lineToProp[le] = pi;
|
||||
_propNameToLine[propName] = le;
|
||||
}
|
||||
}
|
||||
foreach (var c in n.GetChildren())
|
||||
if (c is Node child) Walk(child);
|
||||
}
|
||||
|
||||
Walk(_root);
|
||||
}
|
||||
|
||||
private static string StripSuffix(string s, string suffix) =>
|
||||
s.EndsWith(suffix, StringComparison.Ordinal) ? s[..^suffix.Length] : s;
|
||||
|
||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == null) return;
|
||||
if (_propNameToLine.TryGetValue(e.PropertyName, out var le))
|
||||
{
|
||||
_suppress = true;
|
||||
try { le.Text = _lineToProp[le].GetValue(_vm)?.ToString() ?? string.Empty; }
|
||||
finally { _suppress = false; }
|
||||
}
|
||||
}
|
||||
|
||||
public void PushAllFromVm()
|
||||
{
|
||||
_suppress = true;
|
||||
try
|
||||
{
|
||||
foreach (var (line, prop) in _lineToProp)
|
||||
line.Text = prop.GetValue(_vm)?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally { _suppress = false; }
|
||||
}
|
||||
}
|
||||
1
scripts/FormBinder.cs.uid
Normal file
1
scripts/FormBinder.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4puwe430cmav
|
||||
154
scripts/SessionFilter.cs
Normal file
154
scripts/SessionFilter.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace openF1Manager.scripts;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute that is applied to a property to associate it with a specific API field key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This attribute is used to map a class property to a corresponding field in the API.
|
||||
/// It allows specifying a key that identifies the corresponding API field.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ApiFieldAttribute : Attribute
|
||||
{
|
||||
public string Key { get; }
|
||||
public ApiFieldAttribute(string key) => Key = key;
|
||||
}
|
||||
|
||||
|
||||
public partial class SessionFilter : INotifyPropertyChanged
|
||||
{
|
||||
private string? _meetingKey;
|
||||
|
||||
|
||||
private string? _sessionKey;
|
||||
private string? _location;
|
||||
private DateTime? _dateStart;
|
||||
private DateTime? _dateEnd;
|
||||
private string? _sessionType;
|
||||
private string? _sessionName;
|
||||
private int? _countryKey;
|
||||
private string? _countryCode;
|
||||
private string? _countryName;
|
||||
private int? _circuitKey;
|
||||
private string? _circuitShortName;
|
||||
private string? _gmtOffset;
|
||||
private string? _year;
|
||||
|
||||
[ApiField("meeting_key")]
|
||||
public string MeetingKey
|
||||
{
|
||||
get => _meetingKey;
|
||||
set => SetField(ref _meetingKey, value);
|
||||
}
|
||||
|
||||
[ApiField("session_key")]
|
||||
public string SessionKey
|
||||
{
|
||||
get => _sessionKey;
|
||||
set => SetField(ref _sessionKey, value);
|
||||
}
|
||||
|
||||
[ApiField("location")]
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetField(ref _location, value);
|
||||
}
|
||||
|
||||
[ApiField("date_start")]
|
||||
public DateTime? DateStart
|
||||
{
|
||||
get => _dateStart;
|
||||
set => SetField(ref _dateStart, value);
|
||||
}
|
||||
|
||||
[ApiField("date_en")]
|
||||
public DateTime? DateEnd
|
||||
{
|
||||
get => _dateEnd;
|
||||
set => SetField(ref _dateEnd, value);
|
||||
}
|
||||
|
||||
[ApiField("session_type")]
|
||||
public string SessionType
|
||||
{
|
||||
get => _sessionType;
|
||||
set => SetField(ref _sessionType, value);
|
||||
}
|
||||
|
||||
[ApiField("session_name")]
|
||||
public string SessionName
|
||||
{
|
||||
get => _sessionName;
|
||||
set => SetField(ref _sessionName, value);
|
||||
}
|
||||
|
||||
[ApiField("country_key")]
|
||||
public int? CountryKey
|
||||
{
|
||||
get => _countryKey;
|
||||
set => SetField(ref _countryKey, value);
|
||||
}
|
||||
|
||||
[ApiField("country_code")]
|
||||
public string CountryCode
|
||||
{
|
||||
get => _countryCode;
|
||||
set => SetField(ref _countryCode, value);
|
||||
}
|
||||
|
||||
[ApiField("country_name")]
|
||||
public string CountryName
|
||||
{
|
||||
get => _countryName;
|
||||
set => SetField(ref _countryName, value);
|
||||
}
|
||||
|
||||
[ApiField("circuit_key")]
|
||||
public int? CircuitKey
|
||||
{
|
||||
get => _circuitKey;
|
||||
set => SetField(ref _circuitKey, value);
|
||||
}
|
||||
|
||||
[ApiField("circuit_short_name")]
|
||||
public string CircuitShortName
|
||||
{
|
||||
get => _circuitShortName;
|
||||
set => SetField(ref _circuitShortName, value);
|
||||
}
|
||||
|
||||
[ApiField("gmt_offset")]
|
||||
public string GmtOffset
|
||||
{
|
||||
get => _gmtOffset;
|
||||
set => SetField(ref _gmtOffset, value);
|
||||
}
|
||||
|
||||
[ApiField("year")]
|
||||
public string Year
|
||||
{
|
||||
get => _year;
|
||||
set => SetField(ref _year, value);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1
scripts/SessionFilter.cs.uid
Normal file
1
scripts/SessionFilter.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://db7e6qcg77eky
|
||||
Reference in New Issue
Block a user