almost done

This commit is contained in:
zznty
2022-10-03 21:49:31 +07:00
parent f001aa4a7e
commit 5e508600f9
19 changed files with 435 additions and 145 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View 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));
}
}
}

View File

@@ -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" />

View File

@@ -1 +0,0 @@
global using JsonData = TorchRemote.Plugin.Utils.JsonDataAttribute;

View File

@@ -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>

View File

@@ -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();
}
} }

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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}",

View File

@@ -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 =>

View File

@@ -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()

View 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();
}
}

View File

@@ -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";

View File

@@ -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!;
} }
} }

View File

@@ -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}"