almost done
This commit is contained in:
@@ -2,5 +2,4 @@
|
|||||||
|
|
||||||
namespace TorchRemote.Models.Responses;
|
namespace TorchRemote.Models.Responses;
|
||||||
|
|
||||||
public record SettingInfoResponse(string Name, JsonSchema Schema, ICollection<SettingPropertyInfo> PropertyInfos);
|
public record SettingInfoResponse(string Name, JsonSchema Schema);
|
||||||
public record SettingPropertyInfo(string Name, string PropertyName, string? Description, int? Order);
|
|
@@ -8,6 +8,7 @@ namespace TorchRemote.Models.Shared.Settings;
|
|||||||
[JsonDerivedType(typeof(BooleanProperty), "boolean")]
|
[JsonDerivedType(typeof(BooleanProperty), "boolean")]
|
||||||
[JsonDerivedType(typeof(NumberProperty), "number")]
|
[JsonDerivedType(typeof(NumberProperty), "number")]
|
||||||
[JsonDerivedType(typeof(ObjectProperty), "object")]
|
[JsonDerivedType(typeof(ObjectProperty), "object")]
|
||||||
|
[JsonDerivedType(typeof(ArrayProperty), "array")]
|
||||||
[JsonDerivedType(typeof(EnumProperty), "enum")]
|
[JsonDerivedType(typeof(EnumProperty), "enum")]
|
||||||
[JsonDerivedType(typeof(DateTimeProperty), "date-time")]
|
[JsonDerivedType(typeof(DateTimeProperty), "date-time")]
|
||||||
[JsonDerivedType(typeof(DurationProperty), "duration")]
|
[JsonDerivedType(typeof(DurationProperty), "duration")]
|
||||||
@@ -21,6 +22,7 @@ public record StringProperty(string Name, string? Value) : PropertyBase(Name);
|
|||||||
public record EnumProperty(string Name, string Value) : PropertyBase(Name);
|
public record EnumProperty(string Name, string Value) : PropertyBase(Name);
|
||||||
public record BooleanProperty(string Name, bool? Value) : PropertyBase(Name);
|
public record BooleanProperty(string Name, bool? Value) : PropertyBase(Name);
|
||||||
public record ObjectProperty(string Name, JsonElement Value) : PropertyBase(Name);
|
public record ObjectProperty(string Name, JsonElement Value) : PropertyBase(Name);
|
||||||
|
public record ArrayProperty(string Name, JsonElement Value) : PropertyBase(Name);
|
||||||
public record DateTimeProperty(string Name, DateTime? Value) : PropertyBase(Name);
|
public record DateTimeProperty(string Name, DateTime? Value) : PropertyBase(Name);
|
||||||
public record DurationProperty(string Name, TimeSpan? Value) : PropertyBase(Name);
|
public record DurationProperty(string Name, TimeSpan? Value) : PropertyBase(Name);
|
||||||
public record UriProperty(string Name, Uri? Value) : PropertyBase(Name);
|
public record UriProperty(string Name, Uri? Value) : PropertyBase(Name);
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
using System.Reflection;
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EmbedIO;
|
using EmbedIO;
|
||||||
using EmbedIO.Routing;
|
using EmbedIO.Routing;
|
||||||
using EmbedIO.WebApi;
|
using EmbedIO.WebApi;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using Torch.Views;
|
|
||||||
using TorchRemote.Models.Responses;
|
using TorchRemote.Models.Responses;
|
||||||
using TorchRemote.Models.Shared.Settings;
|
using TorchRemote.Models.Shared.Settings;
|
||||||
using TorchRemote.Plugin.Abstractions.Controllers;
|
using TorchRemote.Plugin.Abstractions.Controllers;
|
||||||
@@ -17,27 +17,19 @@ public class SettingsController : WebApiController, ISettingsController
|
|||||||
{
|
{
|
||||||
private const string RootPath = "/settings";
|
private const string RootPath = "/settings";
|
||||||
|
|
||||||
|
[Route(HttpVerbs.Get, RootPath)]
|
||||||
|
public IEnumerable<string> GetAllSettings()
|
||||||
|
{
|
||||||
|
return Statics.SettingManager.Settings.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
[Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}")]
|
[Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}")]
|
||||||
public SettingInfoResponse Get(string fullName)
|
public SettingInfoResponse Get(string fullName)
|
||||||
{
|
{
|
||||||
if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting))
|
if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting))
|
||||||
throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName);
|
throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName);
|
||||||
|
|
||||||
return new(StringExtensions.Humanize(setting.Name), setting.Schema, setting
|
return new(StringExtensions.Humanize(setting.Name), setting.Schema);
|
||||||
.Type.GetProperties(
|
|
||||||
BindingFlags.Public | BindingFlags.Instance)
|
|
||||||
.Where(b => setting.IncludeDisplayOnly &&
|
|
||||||
b.HasAttribute<DisplayAttribute>())
|
|
||||||
.Select(b =>
|
|
||||||
{
|
|
||||||
var attr = b.GetCustomAttribute<DisplayAttribute>();
|
|
||||||
return new SettingPropertyInfo(
|
|
||||||
attr?.Name ?? StringExtensions.Humanize(b.Name),
|
|
||||||
Statics.SerializerOptions
|
|
||||||
.PropertyNamingPolicy!.ConvertName(b.Name),
|
|
||||||
attr?.Description,
|
|
||||||
attr?.Order is 0 or null ? null : attr.Order);
|
|
||||||
}).ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}/values")]
|
[Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}/values")]
|
||||||
@@ -59,9 +51,12 @@ public class SettingsController : WebApiController, ISettingsController
|
|||||||
|
|
||||||
return type switch
|
return type switch
|
||||||
{
|
{
|
||||||
_ when type == typeof(int) || type == typeof(int?) => (PropertyBase)new IntegerProperty(name, (int?)value),
|
_ when type == typeof(int) || type == typeof(int?) => (PropertyBase)new IntegerProperty(
|
||||||
|
name, (int?)value),
|
||||||
|
_ when type == typeof(bool) || type == typeof(bool?) => new BooleanProperty(name, (bool?)value),
|
||||||
_ when type == typeof(string) => new StringProperty(name, (string?)value),
|
_ when type == typeof(string) => new StringProperty(name, (string?)value),
|
||||||
_ when type.IsPrimitive => new NumberProperty(name, value is null ? null : (double?)Convert.ChangeType(value, typeof(double))),
|
_ when type.IsPrimitive => new NumberProperty(
|
||||||
|
name, value is null ? null : (double?)Convert.ChangeType(value, typeof(double))),
|
||||||
_ when type == typeof(DateTime) || type == typeof(DateTime?) => new DateTimeProperty(
|
_ when type == typeof(DateTime) || type == typeof(DateTime?) => new DateTimeProperty(
|
||||||
name, (DateTime?)value),
|
name, (DateTime?)value),
|
||||||
_ when type == typeof(TimeSpan) || type == typeof(TimeSpan?) => new DurationProperty(
|
_ when type == typeof(TimeSpan) || type == typeof(TimeSpan?) => new DurationProperty(
|
||||||
@@ -71,6 +66,8 @@ public class SettingsController : WebApiController, ISettingsController
|
|||||||
_ when type == typeof(Guid) || type == typeof(Guid?) => new UuidProperty(
|
_ when type == typeof(Guid) || type == typeof(Guid?) => new UuidProperty(
|
||||||
name, (Guid?)value),
|
name, (Guid?)value),
|
||||||
_ when type == typeof(Uri) => new UriProperty(name, (Uri?)value),
|
_ when type == typeof(Uri) => new UriProperty(name, (Uri?)value),
|
||||||
|
_ when typeof(ICollection).IsAssignableFrom(type) =>
|
||||||
|
new ArrayProperty(name, JsonSerializer.SerializeToElement(value, type, Statics.SerializerOptions)),
|
||||||
_ when type.IsClass => new ObjectProperty(
|
_ when type.IsClass => new ObjectProperty(
|
||||||
name, JsonSerializer.SerializeToElement(value, type, Statics.SerializerOptions)),
|
name, JsonSerializer.SerializeToElement(value, type, Statics.SerializerOptions)),
|
||||||
_ => throw HttpException.NotFound("Property type not found", name),
|
_ => throw HttpException.NotFound("Property type not found", name),
|
||||||
@@ -147,6 +144,9 @@ public class SettingsController : WebApiController, ISettingsController
|
|||||||
case ObjectProperty objectProperty when type.IsClass:
|
case ObjectProperty objectProperty when type.IsClass:
|
||||||
propInfo.SetValue(instance, objectProperty.Value.Deserialize(type, Statics.SerializerOptions));
|
propInfo.SetValue(instance, objectProperty.Value.Deserialize(type, Statics.SerializerOptions));
|
||||||
break;
|
break;
|
||||||
|
case ArrayProperty arrayProperty when typeof(ICollection).IsAssignableFrom(type):
|
||||||
|
propInfo.SetValue(instance, arrayProperty.Value.Deserialize(type, Statics.SerializerOptions));
|
||||||
|
break;
|
||||||
case StringProperty stringProperty when type == typeof(string):
|
case StringProperty stringProperty when type == typeof(string):
|
||||||
propInfo.SetValue(instance, stringProperty.Value);
|
propInfo.SetValue(instance, stringProperty.Value);
|
||||||
break;
|
break;
|
||||||
|
@@ -1,12 +1,16 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Json.Schema;
|
using Json.Schema;
|
||||||
using Json.Schema.Generation;
|
using Json.Schema.Generation;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using Torch;
|
||||||
using Torch.API;
|
using Torch.API;
|
||||||
using Torch.Managers;
|
using Torch.Managers;
|
||||||
|
using Torch.Server;
|
||||||
using Torch.Server.Managers;
|
using Torch.Server.Managers;
|
||||||
using Torch.Server.ViewModels;
|
using Torch.Server.ViewModels;
|
||||||
|
using TorchRemote.Plugin.Refiners;
|
||||||
using TorchRemote.Plugin.Utils;
|
using TorchRemote.Plugin.Utils;
|
||||||
|
|
||||||
namespace TorchRemote.Plugin.Managers;
|
namespace TorchRemote.Plugin.Managers;
|
||||||
@@ -17,6 +21,8 @@ public class SettingManager : Manager
|
|||||||
|
|
||||||
[Dependency]
|
[Dependency]
|
||||||
private readonly InstanceManager _instanceManager = null!;
|
private readonly InstanceManager _instanceManager = null!;
|
||||||
|
[Dependency]
|
||||||
|
private readonly PluginManager _pluginManager = null!;
|
||||||
|
|
||||||
public SettingManager(ITorchBase torchInstance) : base(torchInstance)
|
public SettingManager(ITorchBase torchInstance) : base(torchInstance)
|
||||||
{
|
{
|
||||||
@@ -26,25 +32,66 @@ public class SettingManager : Manager
|
|||||||
{
|
{
|
||||||
base.Attach();
|
base.Attach();
|
||||||
_instanceManager.InstanceLoaded += InstanceManagerOnInstanceLoaded;
|
_instanceManager.InstanceLoaded += InstanceManagerOnInstanceLoaded;
|
||||||
|
|
||||||
|
RegisterSetting("Torch Config", Torch.Config, typeof(TorchConfig));
|
||||||
|
|
||||||
|
foreach (var plugin in _pluginManager.Plugins.Values)
|
||||||
|
{
|
||||||
|
var type = plugin.GetType();
|
||||||
|
object persistentInstance;
|
||||||
|
|
||||||
|
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
|
||||||
|
bool IsSuitable(MemberInfo m, Type t) =>
|
||||||
|
t.IsGenericType && typeof(Persistent<>).IsAssignableFrom(t.GetGenericTypeDefinition()) &&
|
||||||
|
(m.Name.Contains(
|
||||||
|
"config", StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
m.Name.Contains(
|
||||||
|
"cfg", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
var fields = type.GetFields(flags).Where(b => IsSuitable(b, b.FieldType)).ToArray();
|
||||||
|
var props = type.GetProperties(flags).Where(b => IsSuitable(b, b.PropertyType)).ToArray();
|
||||||
|
|
||||||
|
if (fields.FirstOrDefault() is { } field)
|
||||||
|
{
|
||||||
|
persistentInstance = field.GetValue(plugin);
|
||||||
|
}
|
||||||
|
else if (props.FirstOrDefault() is { } prop)
|
||||||
|
{
|
||||||
|
persistentInstance = prop.GetValue(plugin);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistentInstance is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var persistentType = persistentInstance.GetType();
|
||||||
|
var getter = persistentType.GetProperty("Data")!;
|
||||||
|
|
||||||
|
RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), persistentType.GenericTypeArguments[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InstanceManagerOnInstanceLoaded(ConfigDedicatedViewModel config)
|
private void InstanceManagerOnInstanceLoaded(ConfigDedicatedViewModel config)
|
||||||
{
|
{
|
||||||
RegisterSetting(config.SessionSettings, typeof(SessionSettingsViewModel));
|
RegisterSetting("Session Settings", config.SessionSettings, typeof(SessionSettingsViewModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterSetting(object value, Type type, bool includeOnlyDisplay = true)
|
public void RegisterSetting(string name, object value, Type type)
|
||||||
{
|
{
|
||||||
var builder = new JsonSchemaBuilder().FromType(type, new()
|
var builder = new JsonSchemaBuilder().FromType(type, new()
|
||||||
{
|
{
|
||||||
PropertyNamingMethod = input => Statics.SerializerOptions.PropertyNamingPolicy!.ConvertName(input)
|
PropertyNamingMethod = input => Statics.SerializerOptions.PropertyNamingPolicy!.ConvertName(input),
|
||||||
|
Refiners = { new DisplayAttributeRefiner() }
|
||||||
});
|
});
|
||||||
|
Settings[type.FullName!] = new(name, type, builder.Build(), value);
|
||||||
Settings[type.FullName!] = new(type.Name, type, builder.Build(), value, includeOnlyDisplay);
|
Log.Info("Registered {0} type with name {1}", type.FullName, name);
|
||||||
Log.Info("Registered {0} type", type.FullName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
|
public IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Setting(string Name, Type Type, JsonSchema Schema, object Value, bool IncludeDisplayOnly);
|
public record Setting(string Name, Type Type, JsonSchema Schema, object Value);
|
28
TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs
Normal file
28
TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Json.Schema.Generation;
|
||||||
|
using Json.Schema.Generation.Intents;
|
||||||
|
using Torch.Views;
|
||||||
|
|
||||||
|
namespace TorchRemote.Plugin.Refiners;
|
||||||
|
|
||||||
|
public class DisplayAttributeRefiner : ISchemaRefiner
|
||||||
|
{
|
||||||
|
public bool ShouldRun(SchemaGenerationContextBase context)
|
||||||
|
{
|
||||||
|
return context.GetAttributes().OfType<DisplayAttribute>().Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(SchemaGenerationContextBase context)
|
||||||
|
{
|
||||||
|
foreach (var displayAttribute in context.GetAttributes().OfType<DisplayAttribute>())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(displayAttribute.Name))
|
||||||
|
context.Intents.Add(new TitleIntent(displayAttribute.Name));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(displayAttribute.Description))
|
||||||
|
context.Intents.Add(new DescriptionIntent(displayAttribute.Description));
|
||||||
|
|
||||||
|
if (displayAttribute.ReadOnly)
|
||||||
|
context.Intents.Add(new ReadOnlyIntent(displayAttribute.ReadOnly));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -75,7 +75,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EmbedIO" Version="3.5.0" />
|
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||||
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
|
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
|
||||||
<PackageReference Include="Json.More.Net" Version="1.7.1" />
|
<PackageReference Include="Json.More.Net" Version="1.7.1" />
|
||||||
<PackageReference Include="JsonPointer.Net" Version="2.2.2" />
|
<PackageReference Include="JsonPointer.Net" Version="2.2.2" />
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
global using JsonData = TorchRemote.Plugin.Utils.JsonDataAttribute;
|
|
@@ -2,5 +2,5 @@
|
|||||||
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<Name>TorchRemote.Plugin</Name>
|
<Name>TorchRemote.Plugin</Name>
|
||||||
<Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid>
|
<Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid>
|
||||||
<Version>v1.0.1</Version>
|
<Version>v1.0.2</Version>
|
||||||
</PluginManifest>
|
</PluginManifest>
|
@@ -4,11 +4,16 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Refit;
|
using Refit;
|
||||||
|
using TorchRemote.Models.Responses;
|
||||||
using Websocket.Client;
|
using Websocket.Client;
|
||||||
|
using Websocket.Client.Models;
|
||||||
|
|
||||||
namespace TorchRemote.Services;
|
namespace TorchRemote.Services;
|
||||||
|
|
||||||
public class ApiClientService : IDisposable
|
public class ApiClientService : IDisposable
|
||||||
@@ -23,7 +28,7 @@ public class ApiClientService : IDisposable
|
|||||||
private readonly CancellationTokenSource _tokenSource = new();
|
private readonly CancellationTokenSource _tokenSource = new();
|
||||||
public string BaseUrl
|
public string BaseUrl
|
||||||
{
|
{
|
||||||
get => _client.BaseAddress?.ToString() ?? "http://localhost";
|
get => _client.BaseAddress?.ToString() ?? $"http://localhost/api/{Version}/";
|
||||||
set => _client.BaseAddress = new($"{value}/api/{Version}/");
|
set => _client.BaseAddress = new($"{value}/api/{Version}/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +53,11 @@ public class ApiClientService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WebsocketClient> WatchChatAsync() => StartWebsocketConnectionAsync("live/chat");
|
public async Task<WebsocketFeed<ChatResponseBase>> WatchChatAsync() =>
|
||||||
|
new(await StartWebsocketConnectionAsync("live/chat"));
|
||||||
|
|
||||||
public Task<WebsocketClient> WatchLogLinesAsync() => StartWebsocketConnectionAsync("live/logs");
|
public async Task<WebsocketFeed<LogLineResponse>> WatchLogLinesAsync() =>
|
||||||
|
new(await StartWebsocketConnectionAsync("live/logs"));
|
||||||
|
|
||||||
private async Task<WebsocketClient> StartWebsocketConnectionAsync(string url)
|
private async Task<WebsocketClient> StartWebsocketConnectionAsync(string url)
|
||||||
{
|
{
|
||||||
@@ -77,4 +84,32 @@ public class ApiClientService : IDisposable
|
|||||||
_tokenSource.Cancel();
|
_tokenSource.Cancel();
|
||||||
_tokenSource.Dispose();
|
_tokenSource.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WebsocketFeed<TMessage> : IDisposable where TMessage : class
|
||||||
|
{
|
||||||
|
private readonly WebsocketClient _client;
|
||||||
|
|
||||||
|
public WebsocketFeed(WebsocketClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
Disconnected = client.DisconnectionHappened;
|
||||||
|
Connected = client.ReconnectionHappened;
|
||||||
|
Messages = client.MessageReceived.Where(b => b.MessageType is WebSocketMessageType.Text)
|
||||||
|
.Select(b => JsonSerializer.Deserialize<TMessage>(b.Text, ApiClientService.SerializerOptions))
|
||||||
|
.Where(b => b is not null)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IObservable<DisconnectionInfo> Disconnected { get; }
|
||||||
|
|
||||||
|
public IObservable<ReconnectionInfo> Connected { get; }
|
||||||
|
|
||||||
|
public IObservable<TMessage> Messages { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,8 @@ using Refit;
|
|||||||
using TorchRemote.Models.Requests;
|
using TorchRemote.Models.Requests;
|
||||||
using TorchRemote.Models.Responses;
|
using TorchRemote.Models.Responses;
|
||||||
using TorchRemote.Models.Shared;
|
using TorchRemote.Models.Shared;
|
||||||
|
using TorchRemote.Models.Shared.Settings;
|
||||||
|
|
||||||
namespace TorchRemote.Services;
|
namespace TorchRemote.Services;
|
||||||
|
|
||||||
public interface IRemoteApi
|
public interface IRemoteApi
|
||||||
@@ -41,7 +43,12 @@ public interface IRemoteApi
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Settings
|
#region Settings
|
||||||
[Get("/settings/{id}")]
|
[Get("/settings/{fullName}")]
|
||||||
Task<IApiResponse<SettingInfoResponse>> GetSetting(Guid id);
|
Task<IApiResponse<SettingInfoResponse>> GetSetting(string fullName);
|
||||||
|
[Get("/settings/{fullName}/values")]
|
||||||
|
Task<IApiResponse<IEnumerable<PropertyBase>>> GetSettingValues(string fullName, [Body] IEnumerable<string> propertyNames);
|
||||||
|
[Patch("/settings/{fullName}/values")]
|
||||||
|
Task<IApiResponse<int>> PatchSettingValues(string fullName, [Body] IEnumerable<PropertyBase> properties);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,8 @@
|
|||||||
<TrimMode>copyused</TrimMode>
|
<TrimMode>copyused</TrimMode>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<AvaloniaVersion>0.10.17</AvaloniaVersion>
|
<AvaloniaVersion>0.10.17</AvaloniaVersion>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<EnablePreviewFeatures>True</EnablePreviewFeatures>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
<PackageReference Include="ReactiveUI.Fody" Version="18.3.1" />
|
<PackageReference Include="ReactiveUI.Fody" Version="18.3.1" />
|
||||||
<PackageReference Include="Refit" Version="6.3.2" />
|
<PackageReference Include="Refit" Version="6.3.2" />
|
||||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Text.Json" Version="7.0.0-preview.6.22324.4" />
|
<PackageReference Include="System.Text.Json" Version="7.0.0-rc.1.22426.10" />
|
||||||
<PackageReference Include="Websocket.Client" Version="4.4.43" />
|
<PackageReference Include="Websocket.Client" Version="4.4.43" />
|
||||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@@ -15,15 +15,13 @@ public class ChatViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
public ChatViewModel(ApiClientService clientService)
|
public ChatViewModel(ApiClientService clientService)
|
||||||
{
|
{
|
||||||
Observable.FromEventPattern(clientService, nameof(clientService.Connected))
|
clientService.Connected
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(_ =>
|
.Subscribe(_ =>
|
||||||
{
|
{
|
||||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
|
||||||
Observable.FromAsync(clientService.WatchChatAsync)
|
Observable.FromAsync(clientService.WatchChatAsync)
|
||||||
.Select(b => b.MessageReceived)
|
.Select(b => b.Messages)
|
||||||
.Concat()
|
.Concat()
|
||||||
.Select(b => JsonSerializer.Deserialize<ChatResponseBase>(b.Text, options))
|
|
||||||
.Select(b => b switch
|
.Select(b => b switch
|
||||||
{
|
{
|
||||||
ChatMessageResponse msg => $"[{msg.Channel}] {msg.AuthorName}: {msg.Message}",
|
ChatMessageResponse msg => $"[{msg.Channel}] {msg.AuthorName}: {msg.Message}",
|
||||||
|
@@ -18,7 +18,7 @@ public class DashboardViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_clientService = clientService;
|
_clientService = clientService;
|
||||||
|
|
||||||
Observable.FromEventPattern(_clientService, nameof(_clientService.Connected))
|
_clientService.Connected
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(_ =>
|
.Subscribe(_ =>
|
||||||
{
|
{
|
||||||
@@ -35,11 +35,9 @@ public class DashboardViewModel : ViewModelBase
|
|||||||
MemberCount = online;
|
MemberCount = online;
|
||||||
});
|
});
|
||||||
|
|
||||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
|
||||||
Observable.FromAsync(() => _clientService.WatchLogLinesAsync())
|
Observable.FromAsync(() => _clientService.WatchLogLinesAsync())
|
||||||
.Select(b => b.MessageReceived)
|
.Select(b => b.Messages)
|
||||||
.Concat()
|
.Concat()
|
||||||
.Select(b => JsonSerializer.Deserialize<LogLineResponse>(b.Text, options))
|
|
||||||
.Select(b => $"{b.Time:hh:mm:ss} [{b.Level}] {(b.Logger.Contains('.') ? b.Logger[(b.Logger.LastIndexOf('.') + 1)..] : b.Logger)}: {b.Message}")
|
.Select(b => $"{b.Time:hh:mm:ss} [{b.Level}] {(b.Logger.Contains('.') ? b.Logger[(b.Logger.LastIndexOf('.') + 1)..] : b.Logger)}: {b.Message}")
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(s =>
|
.Subscribe(s =>
|
||||||
|
@@ -11,7 +11,7 @@ public class ServerConfigViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
public ServerConfigViewModel(ApiClientService clientService)
|
public ServerConfigViewModel(ApiClientService clientService)
|
||||||
{
|
{
|
||||||
Observable.FromEventPattern(clientService, nameof(clientService.Connected))
|
clientService.Connected
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Select(_ => Observable.FromAsync(clientService.Api.GetServerSettings))
|
.Select(_ => Observable.FromAsync(clientService.Api.GetServerSettings))
|
||||||
.Concat()
|
.Concat()
|
||||||
@@ -35,7 +35,7 @@ public class ServerConfigViewModel : ViewModelBase
|
|||||||
new(Ip, Port)
|
new(Ip, Port)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
Worlds = Observable.FromEventPattern(clientService, nameof(clientService.Connected))
|
Worlds = clientService.Connected
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Select(_ => Observable.FromAsync(clientService.Api.GetWorlds))
|
.Select(_ => Observable.FromAsync(clientService.Api.GetWorlds))
|
||||||
.Concat()
|
.Concat()
|
||||||
@@ -46,7 +46,7 @@ public class ServerConfigViewModel : ViewModelBase
|
|||||||
.Select(b => new World(id, b.Name, b.SizeKb)))
|
.Select(b => new World(id, b.Name, b.SizeKb)))
|
||||||
.Concat();
|
.Concat();
|
||||||
|
|
||||||
Observable.FromEventPattern(clientService, nameof(clientService.Connected))
|
clientService.Connected
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Select(_ => Observable.FromAsync(clientService.Api.GetSelectedWorld))
|
.Select(_ => Observable.FromAsync(clientService.Api.GetSelectedWorld))
|
||||||
.Concat()
|
.Concat()
|
||||||
|
177
TorchRemote/ViewModels/Server/SettingViewModel.cs
Normal file
177
TorchRemote/ViewModels/Server/SettingViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using DynamicData.Binding;
|
||||||
|
using Json.Schema;
|
||||||
|
using ReactiveUI;
|
||||||
|
using TorchRemote.Models.Responses;
|
||||||
|
using TorchRemote.Models.Shared.Settings;
|
||||||
|
using TorchRemote.Services;
|
||||||
|
using Api = TorchRemote.Models.Shared.Settings;
|
||||||
|
|
||||||
|
namespace TorchRemote.ViewModels.Server;
|
||||||
|
|
||||||
|
public class SettingViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ApiClientService _service;
|
||||||
|
public ObjectProperty Setting { get; set; } = null!;
|
||||||
|
public string FullName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public SettingViewModel(ApiClientService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
|
||||||
|
/*this.WhenValueChanged(x => x.FullName, false)
|
||||||
|
.Select(b => Observable.FromAsync(() => _service.Api.GetSetting(b!)))
|
||||||
|
.Concat()
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Where(b => b.Content is not null)
|
||||||
|
.Select(b =>
|
||||||
|
{
|
||||||
|
var value = b.Content!;
|
||||||
|
|
||||||
|
|
||||||
|
})*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IProperty<in TProperty> where TProperty : PropertyBase
|
||||||
|
{
|
||||||
|
public static abstract Property FromProperty(TProperty property);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class Property : ViewModelBase
|
||||||
|
{
|
||||||
|
public abstract string TypeName { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IntegerProperty : Property, IProperty<Api.IntegerProperty>
|
||||||
|
{
|
||||||
|
public int Value { get; set; }
|
||||||
|
public override string TypeName => "integer";
|
||||||
|
|
||||||
|
public static Property FromProperty(Api.IntegerProperty property)
|
||||||
|
{
|
||||||
|
return new IntegerProperty
|
||||||
|
{
|
||||||
|
Value = property.Value.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringProperty : Property, IProperty<Api.StringProperty>
|
||||||
|
{
|
||||||
|
public string Value { get; set; } = "value";
|
||||||
|
public override string TypeName => "string";
|
||||||
|
public static Property FromProperty(Api.StringProperty property)
|
||||||
|
{
|
||||||
|
return new StringProperty
|
||||||
|
{
|
||||||
|
Value = property.Value ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BooleanProperty : Property, IProperty<Api.BooleanProperty>
|
||||||
|
{
|
||||||
|
public bool Value { get; set; }
|
||||||
|
public override string TypeName => "boolean";
|
||||||
|
public static Property FromProperty(Api.BooleanProperty property)
|
||||||
|
{
|
||||||
|
return new BooleanProperty
|
||||||
|
{
|
||||||
|
Value = property.Value.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumberProperty : Property, IProperty<Api.NumberProperty>
|
||||||
|
{
|
||||||
|
public double Value { get; set; }
|
||||||
|
public override string TypeName => "number";
|
||||||
|
public static Property FromProperty(Api.NumberProperty property)
|
||||||
|
{
|
||||||
|
return new NumberProperty
|
||||||
|
{
|
||||||
|
Value = property.Value.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DateTimeProperty : Property, IProperty<Api.DateTimeProperty>
|
||||||
|
{
|
||||||
|
public DateTime Value { get; set; }
|
||||||
|
public override string TypeName => "date-time";
|
||||||
|
public static Property FromProperty(Api.DateTimeProperty property)
|
||||||
|
{
|
||||||
|
return new DateTimeProperty
|
||||||
|
{
|
||||||
|
Value = property.Value ?? DateTime.Now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DurationProperty : Property, IProperty<Api.DurationProperty>
|
||||||
|
{
|
||||||
|
public TimeSpan Value { get; set; }
|
||||||
|
public override string TypeName => "duration";
|
||||||
|
public static Property FromProperty(Api.DurationProperty property)
|
||||||
|
{
|
||||||
|
return new DurationProperty
|
||||||
|
{
|
||||||
|
Value = property.Value ?? TimeSpan.Zero
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UuidProperty : Property, IProperty<Api.UuidProperty>
|
||||||
|
{
|
||||||
|
public Guid Value { get; set; }
|
||||||
|
public override string TypeName => "uuid";
|
||||||
|
public static Property FromProperty(Api.UuidProperty property)
|
||||||
|
{
|
||||||
|
return new UuidProperty
|
||||||
|
{
|
||||||
|
Value = property.Value ?? Guid.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UriProperty : Property, IProperty<Api.UriProperty>
|
||||||
|
{
|
||||||
|
public string Value { get; set; } = null!;
|
||||||
|
public override string TypeName => "uri";
|
||||||
|
public static Property FromProperty(Api.UriProperty property)
|
||||||
|
{
|
||||||
|
return new UriProperty
|
||||||
|
{
|
||||||
|
Value = property.Value?.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnumProperty : Property, IProperty<Api.EnumProperty>
|
||||||
|
{
|
||||||
|
public string Value { get; set; } = "value";
|
||||||
|
public IEnumerable<(string Name, string Value)> EnumValues { get; set; } = ArraySegment<(string Name, string Value)>.Empty;
|
||||||
|
public override string TypeName => "enum";
|
||||||
|
public static Property FromProperty(Api.EnumProperty property)
|
||||||
|
{
|
||||||
|
return new EnumProperty
|
||||||
|
{
|
||||||
|
Value = property.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ObjectProperty : Property, IProperty<Api.ObjectProperty>
|
||||||
|
{
|
||||||
|
public ObservableCollection<Property> Properties { get; init; } = new();
|
||||||
|
public override string TypeName => "object";
|
||||||
|
public static Property FromProperty(Api.ObjectProperty property)
|
||||||
|
{
|
||||||
|
return new ObjectProperty();
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,7 @@ public class SettingsViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly ApiClientService _clientService;
|
private readonly ApiClientService _clientService;
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string BearerToken { get; set; } = "WcdYT5qHjSt5Uzjs54xu8vE9Oq4a5MD2edLxywtJHtc=";
|
public string BearerToken { get; set; } = "NSN9qSbvKO6PtvoUg+fV5CrSpLqz+F2ssXvzbFbgOpE=";
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string RemoteUrl { get; set; } = "http://localhost";
|
public string RemoteUrl { get; set; } = "http://localhost";
|
||||||
|
|
||||||
|
@@ -5,6 +5,6 @@ namespace TorchRemote.ViewModels
|
|||||||
public class ViewModelBase : ReactiveObject, IRoutableViewModel
|
public class ViewModelBase : ReactiveObject, IRoutableViewModel
|
||||||
{
|
{
|
||||||
public string? UrlPathSegment { get; set; }
|
public string? UrlPathSegment { get; set; }
|
||||||
public IScreen HostScreen { get; set; }
|
public IScreen HostScreen { get; set; } = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,9 +31,7 @@
|
|||||||
<Panel>
|
<Panel>
|
||||||
<Grid Margin="3" RowDefinitions="Auto,*">
|
<Grid Margin="3" RowDefinitions="Auto,*">
|
||||||
<TextBlock Text="{Binding CurrentNavItem.Title}" FontSize="24" FontWeight="Bold" />
|
<TextBlock Text="{Binding CurrentNavItem.Title}" FontSize="24" FontWeight="Bold" />
|
||||||
<ScrollViewer Grid.Row="1" Margin="0,10" HorizontalScrollBarVisibility="Auto">
|
<reactiveUi:RoutedViewHost Grid.Row="1" Margin="0,10" Router="{Binding Router}" />
|
||||||
<reactiveUi:RoutedViewHost Router="{Binding Router}" />
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="Loading..."
|
<TextBlock Text="Loading..."
|
||||||
IsVisible="{Binding !Connected}"
|
IsVisible="{Binding !Connected}"
|
||||||
|
Reference in New Issue
Block a user