From 5e508600f92f3b1d8ac2ddf234ca446d3e24a9f3 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Mon, 3 Oct 2022 21:49:31 +0700 Subject: [PATCH] almost done --- .../Responses/SettingInfoResponse.cs | 3 +- .../Shared/Settings/Property.cs | 2 + .../Controllers/SettingsController.cs | 40 ++-- .../Managers/ApiServerManager.cs | 24 +-- TorchRemote.Plugin/Managers/SettingManager.cs | 61 +++++- .../Refiners/DisplayAttributeRefiner.cs | 28 +++ TorchRemote.Plugin/TorchRemote.Plugin.csproj | 2 +- TorchRemote.Plugin/Usings.cs | 1 - TorchRemote.Plugin/manifest.xml | 2 +- TorchRemote/Services/ApiClientService.cs | 57 ++++-- TorchRemote/Services/IRemoteApi.cs | 11 +- TorchRemote/TorchRemote.csproj | 4 +- .../ViewModels/Server/ChatViewModel.cs | 34 ++-- .../ViewModels/Server/DashboardViewModel.cs | 58 +++--- .../Server/ServerConfigViewModel.cs | 68 +++---- .../ViewModels/Server/SettingViewModel.cs | 177 ++++++++++++++++++ .../ViewModels/Server/SettingsViewModel.cs | 2 +- TorchRemote/ViewModels/ViewModelBase.cs | 2 +- TorchRemote/Views/RemoteServerView.axaml | 4 +- 19 files changed, 435 insertions(+), 145 deletions(-) create mode 100644 TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs delete mode 100644 TorchRemote.Plugin/Usings.cs create mode 100644 TorchRemote/ViewModels/Server/SettingViewModel.cs diff --git a/TorchRemote.Models/Responses/SettingInfoResponse.cs b/TorchRemote.Models/Responses/SettingInfoResponse.cs index c5b8035..115bca0 100644 --- a/TorchRemote.Models/Responses/SettingInfoResponse.cs +++ b/TorchRemote.Models/Responses/SettingInfoResponse.cs @@ -2,5 +2,4 @@ namespace TorchRemote.Models.Responses; -public record SettingInfoResponse(string Name, JsonSchema Schema, ICollection PropertyInfos); -public record SettingPropertyInfo(string Name, string PropertyName, string? Description, int? Order); \ No newline at end of file +public record SettingInfoResponse(string Name, JsonSchema Schema); \ No newline at end of file diff --git a/TorchRemote.Models/Shared/Settings/Property.cs b/TorchRemote.Models/Shared/Settings/Property.cs index ef0b120..ceae8e3 100644 --- a/TorchRemote.Models/Shared/Settings/Property.cs +++ b/TorchRemote.Models/Shared/Settings/Property.cs @@ -8,6 +8,7 @@ namespace TorchRemote.Models.Shared.Settings; [JsonDerivedType(typeof(BooleanProperty), "boolean")] [JsonDerivedType(typeof(NumberProperty), "number")] [JsonDerivedType(typeof(ObjectProperty), "object")] +[JsonDerivedType(typeof(ArrayProperty), "array")] [JsonDerivedType(typeof(EnumProperty), "enum")] [JsonDerivedType(typeof(DateTimeProperty), "date-time")] [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 BooleanProperty(string Name, bool? 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 DurationProperty(string Name, TimeSpan? Value) : PropertyBase(Name); public record UriProperty(string Name, Uri? Value) : PropertyBase(Name); diff --git a/TorchRemote.Plugin/Controllers/SettingsController.cs b/TorchRemote.Plugin/Controllers/SettingsController.cs index 14c391f..0bf8cae 100644 --- a/TorchRemote.Plugin/Controllers/SettingsController.cs +++ b/TorchRemote.Plugin/Controllers/SettingsController.cs @@ -1,10 +1,10 @@ -using System.Reflection; +using System.Collections; +using System.Reflection; using System.Text.Json; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using Humanizer; -using Torch.Views; using TorchRemote.Models.Responses; using TorchRemote.Models.Shared.Settings; using TorchRemote.Plugin.Abstractions.Controllers; @@ -17,27 +17,19 @@ public class SettingsController : WebApiController, ISettingsController { private const string RootPath = "/settings"; + [Route(HttpVerbs.Get, RootPath)] + public IEnumerable GetAllSettings() + { + return Statics.SettingManager.Settings.Keys; + } + [Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}")] public SettingInfoResponse Get(string fullName) { if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting)) throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName); - return new(StringExtensions.Humanize(setting.Name), setting.Schema, setting - .Type.GetProperties( - BindingFlags.Public | BindingFlags.Instance) - .Where(b => setting.IncludeDisplayOnly && - b.HasAttribute()) - .Select(b => - { - var attr = b.GetCustomAttribute(); - 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()); + return new(StringExtensions.Humanize(setting.Name), setting.Schema); } [Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}/values")] @@ -59,9 +51,12 @@ public class SettingsController : WebApiController, ISettingsController 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.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( name, (DateTime?)value), _ 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( name, (Guid?)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( name, JsonSerializer.SerializeToElement(value, type, Statics.SerializerOptions)), _ => throw HttpException.NotFound("Property type not found", name), @@ -147,6 +144,9 @@ public class SettingsController : WebApiController, ISettingsController case ObjectProperty objectProperty when type.IsClass: propInfo.SetValue(instance, objectProperty.Value.Deserialize(type, Statics.SerializerOptions)); 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): propInfo.SetValue(instance, stringProperty.Value); break; @@ -168,4 +168,4 @@ public class SettingsController : WebApiController, ISettingsController return true; }).Count(); } -} +} \ No newline at end of file diff --git a/TorchRemote.Plugin/Managers/ApiServerManager.cs b/TorchRemote.Plugin/Managers/ApiServerManager.cs index 52a9731..ac36417 100644 --- a/TorchRemote.Plugin/Managers/ApiServerManager.cs +++ b/TorchRemote.Plugin/Managers/ApiServerManager.cs @@ -51,17 +51,17 @@ public class ApiServerManager : Manager Statics.ChatModule = chatModule; _server = new WebServer(o => o - .WithUrlPrefix(_config.Listener.UrlPrefix) - .WithMicrosoftHttpListener()) - .WithLocalSessionManager() - .WithModule(apiModule - .WithController() - .WithController() - .WithController() - .WithController()) - .WithModule(new LogsModule("/api/live/logs", true)) - .WithModule(chatModule) - .WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider()); + .WithUrlPrefix(_config.Listener.UrlPrefix) + .WithMicrosoftHttpListener()) + .WithLocalSessionManager() + .WithModule(apiModule + .WithController() + .WithController() + .WithController() + .WithController()) + .WithModule(new LogsModule("/api/live/logs", true)) + .WithModule(chatModule) + .WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider()); } public override void Attach() @@ -92,4 +92,4 @@ public class ApiServerManager : Manager return Convert.ToBase64String(aes.Key); } -} +} \ No newline at end of file diff --git a/TorchRemote.Plugin/Managers/SettingManager.cs b/TorchRemote.Plugin/Managers/SettingManager.cs index f2fb34d..269078e 100644 --- a/TorchRemote.Plugin/Managers/SettingManager.cs +++ b/TorchRemote.Plugin/Managers/SettingManager.cs @@ -1,12 +1,16 @@ using System.Collections.Concurrent; +using System.Reflection; using System.Text.Json; using Json.Schema; using Json.Schema.Generation; using NLog; +using Torch; using Torch.API; using Torch.Managers; +using Torch.Server; using Torch.Server.Managers; using Torch.Server.ViewModels; +using TorchRemote.Plugin.Refiners; using TorchRemote.Plugin.Utils; namespace TorchRemote.Plugin.Managers; @@ -17,6 +21,8 @@ public class SettingManager : Manager [Dependency] private readonly InstanceManager _instanceManager = null!; + [Dependency] + private readonly PluginManager _pluginManager = null!; public SettingManager(ITorchBase torchInstance) : base(torchInstance) { @@ -26,25 +32,66 @@ public class SettingManager : Manager { base.Attach(); _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) { - 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() { - PropertyNamingMethod = input => Statics.SerializerOptions.PropertyNamingPolicy!.ConvertName(input) + PropertyNamingMethod = input => Statics.SerializerOptions.PropertyNamingPolicy!.ConvertName(input), + Refiners = { new DisplayAttributeRefiner() } }); - - Settings[type.FullName!] = new(type.Name, type, builder.Build(), value, includeOnlyDisplay); - Log.Info("Registered {0} type", type.FullName); + Settings[type.FullName!] = new(name, type, builder.Build(), value); + Log.Info("Registered {0} type with name {1}", type.FullName, name); } public IDictionary Settings { get; } = new ConcurrentDictionary(); } -public record Setting(string Name, Type Type, JsonSchema Schema, object Value, bool IncludeDisplayOnly); +public record Setting(string Name, Type Type, JsonSchema Schema, object Value); \ No newline at end of file diff --git a/TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs b/TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs new file mode 100644 index 0000000..677b0b2 --- /dev/null +++ b/TorchRemote.Plugin/Refiners/DisplayAttributeRefiner.cs @@ -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().Any(); + } + + public void Run(SchemaGenerationContextBase context) + { + foreach (var displayAttribute in context.GetAttributes().OfType()) + { + 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)); + } + } +} \ No newline at end of file diff --git a/TorchRemote.Plugin/TorchRemote.Plugin.csproj b/TorchRemote.Plugin/TorchRemote.Plugin.csproj index 1cac852..d6a4e47 100644 --- a/TorchRemote.Plugin/TorchRemote.Plugin.csproj +++ b/TorchRemote.Plugin/TorchRemote.Plugin.csproj @@ -75,7 +75,7 @@ - + diff --git a/TorchRemote.Plugin/Usings.cs b/TorchRemote.Plugin/Usings.cs deleted file mode 100644 index fcf1175..0000000 --- a/TorchRemote.Plugin/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using JsonData = TorchRemote.Plugin.Utils.JsonDataAttribute; \ No newline at end of file diff --git a/TorchRemote.Plugin/manifest.xml b/TorchRemote.Plugin/manifest.xml index edc8ec0..9a29c5c 100644 --- a/TorchRemote.Plugin/manifest.xml +++ b/TorchRemote.Plugin/manifest.xml @@ -2,5 +2,5 @@ TorchRemote.Plugin 284017F3-9682-4841-A544-EB04DB8CB9BA - v1.0.1 + v1.0.2 \ No newline at end of file diff --git a/TorchRemote/Services/ApiClientService.cs b/TorchRemote/Services/ApiClientService.cs index 082fcd9..9052685 100644 --- a/TorchRemote/Services/ApiClientService.cs +++ b/TorchRemote/Services/ApiClientService.cs @@ -4,11 +4,16 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Net.WebSockets; +using System.Reactive.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Refit; +using TorchRemote.Models.Responses; using Websocket.Client; +using Websocket.Client.Models; + namespace TorchRemote.Services; public class ApiClientService : IDisposable @@ -23,7 +28,7 @@ public class ApiClientService : IDisposable private readonly CancellationTokenSource _tokenSource = new(); public string BaseUrl { - get => _client.BaseAddress?.ToString() ?? "http://localhost"; + get => _client.BaseAddress?.ToString() ?? $"http://localhost/api/{Version}/"; set => _client.BaseAddress = new($"{value}/api/{Version}/"); } @@ -48,21 +53,23 @@ public class ApiClientService : IDisposable } } - public Task WatchChatAsync() => StartWebsocketConnectionAsync("live/chat"); + public async Task> WatchChatAsync() => + new(await StartWebsocketConnectionAsync("live/chat")); - public Task WatchLogLinesAsync() => StartWebsocketConnectionAsync("live/logs"); + public async Task> WatchLogLinesAsync() => + new(await StartWebsocketConnectionAsync("live/logs")); private async Task StartWebsocketConnectionAsync(string url) { var client = new WebsocketClient(new($"{BaseUrl}{url}" - .Replace($"/{Version}", string.Empty) - .Replace("http", "ws")), - () => - { - var socket = new ClientWebSocket(); - socket.Options.SetRequestHeader("Authorization", $"Bearer {BearerToken}"); - return socket; - }) + .Replace($"/{Version}", string.Empty) + .Replace("http", "ws")), + () => + { + var socket = new ClientWebSocket(); + socket.Options.SetRequestHeader("Authorization", $"Bearer {BearerToken}"); + return socket; + }) { ReconnectTimeout = null, ErrorReconnectTimeout = TimeSpan.FromSeconds(10) @@ -77,4 +84,32 @@ public class ApiClientService : IDisposable _tokenSource.Cancel(); _tokenSource.Dispose(); } + + public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); +} + +public sealed class WebsocketFeed : 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(b.Text, ApiClientService.SerializerOptions)) + .Where(b => b is not null)!; + } + + public IObservable Disconnected { get; } + + public IObservable Connected { get; } + + public IObservable Messages { get; } + + public void Dispose() + { + _client.Dispose(); + } } diff --git a/TorchRemote/Services/IRemoteApi.cs b/TorchRemote/Services/IRemoteApi.cs index 2578852..280bec8 100644 --- a/TorchRemote/Services/IRemoteApi.cs +++ b/TorchRemote/Services/IRemoteApi.cs @@ -5,6 +5,8 @@ using Refit; using TorchRemote.Models.Requests; using TorchRemote.Models.Responses; using TorchRemote.Models.Shared; +using TorchRemote.Models.Shared.Settings; + namespace TorchRemote.Services; public interface IRemoteApi @@ -41,7 +43,12 @@ public interface IRemoteApi #endregion #region Settings - [Get("/settings/{id}")] - Task> GetSetting(Guid id); + [Get("/settings/{fullName}")] + Task> GetSetting(string fullName); + [Get("/settings/{fullName}/values")] + Task>> GetSettingValues(string fullName, [Body] IEnumerable propertyNames); + [Patch("/settings/{fullName}/values")] + Task> PatchSettingValues(string fullName, [Body] IEnumerable properties); + #endregion } diff --git a/TorchRemote/TorchRemote.csproj b/TorchRemote/TorchRemote.csproj index 8221c9c..c324ede 100644 --- a/TorchRemote/TorchRemote.csproj +++ b/TorchRemote/TorchRemote.csproj @@ -7,6 +7,8 @@ copyused true 0.10.17 + preview + True @@ -28,7 +30,7 @@ - + diff --git a/TorchRemote/ViewModels/Server/ChatViewModel.cs b/TorchRemote/ViewModels/Server/ChatViewModel.cs index a21d0bf..b8779f2 100644 --- a/TorchRemote/ViewModels/Server/ChatViewModel.cs +++ b/TorchRemote/ViewModels/Server/ChatViewModel.cs @@ -15,24 +15,22 @@ public class ChatViewModel : ViewModelBase { public ChatViewModel(ApiClientService clientService) { - Observable.FromEventPattern(clientService, nameof(clientService.Connected)) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => - { - var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - Observable.FromAsync(clientService.WatchChatAsync) - .Select(b => b.MessageReceived) - .Concat() - .Select(b => JsonSerializer.Deserialize(b.Text, options)) - .Select(b => b switch - { - ChatMessageResponse msg => $"[{msg.Channel}] {msg.AuthorName}: {msg.Message}", - ChatCommandResponse cmd => $"[Command] {cmd.Author}: {cmd.Message}", - _ => throw new ArgumentOutOfRangeException(nameof(b), b, null) - }) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(s => ChatLines += $"{s}{Environment.NewLine}"); - }); + clientService.Connected + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + { + Observable.FromAsync(clientService.WatchChatAsync) + .Select(b => b.Messages) + .Concat() + .Select(b => b switch + { + ChatMessageResponse msg => $"[{msg.Channel}] {msg.AuthorName}: {msg.Message}", + ChatCommandResponse cmd => $"[Command] {cmd.Author}: {cmd.Message}", + _ => throw new ArgumentOutOfRangeException(nameof(b), b, null) + }) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(s => ChatLines += $"{s}{Environment.NewLine}"); + }); SendMessageCommand = ReactiveCommand.CreateFromTask(s => s.StartsWith("!") ? clientService.Api.InvokeChatCommand(new(s[(s.IndexOf('!') + 1)..])) : diff --git a/TorchRemote/ViewModels/Server/DashboardViewModel.cs b/TorchRemote/ViewModels/Server/DashboardViewModel.cs index ad19692..dfd8f8c 100644 --- a/TorchRemote/ViewModels/Server/DashboardViewModel.cs +++ b/TorchRemote/ViewModels/Server/DashboardViewModel.cs @@ -18,37 +18,35 @@ public class DashboardViewModel : ViewModelBase { _clientService = clientService; - Observable.FromEventPattern(_clientService, nameof(_clientService.Connected)) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => - { - Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10)) - .Select(_ => Observable.FromAsync(() => _clientService.Api.GetServerStatus())) - .Concat() - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(r => - { - var (simSpeed, online, uptime, status) = r.Content!; - SimSpeed = simSpeed; - Status = status; - Uptime = uptime; - MemberCount = online; - }); + _clientService.Connected + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + { + Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(10)) + .Select(_ => Observable.FromAsync(() => _clientService.Api.GetServerStatus())) + .Concat() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(r => + { + var (simSpeed, online, uptime, status) = r.Content!; + SimSpeed = simSpeed; + Status = status; + Uptime = uptime; + MemberCount = online; + }); - var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - Observable.FromAsync(() => _clientService.WatchLogLinesAsync()) - .Select(b => b.MessageReceived) - .Concat() - .Select(b => JsonSerializer.Deserialize(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}") - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(s => - { - if (LogLines.Count(b => b == '\n') > 1000) - LogLines = LogLines['\n'..]; - LogLines += $"{s}\n"; - }); - }); + Observable.FromAsync(() => _clientService.WatchLogLinesAsync()) + .Select(b => b.Messages) + .Concat() + .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) + .Subscribe(s => + { + if (LogLines.Count(b => b == '\n') > 1000) + LogLines = LogLines['\n'..]; + LogLines += $"{s}\n"; + }); + }); StartCommand = ReactiveCommand.CreateFromTask(() => _clientService.Api.StartServer(), this.WhenAnyValue(x => x.Status) diff --git a/TorchRemote/ViewModels/Server/ServerConfigViewModel.cs b/TorchRemote/ViewModels/Server/ServerConfigViewModel.cs index f27d323..75c0a94 100644 --- a/TorchRemote/ViewModels/Server/ServerConfigViewModel.cs +++ b/TorchRemote/ViewModels/Server/ServerConfigViewModel.cs @@ -11,20 +11,20 @@ public class ServerConfigViewModel : ViewModelBase { public ServerConfigViewModel(ApiClientService clientService) { - Observable.FromEventPattern(clientService, nameof(clientService.Connected)) - .ObserveOn(RxApp.MainThreadScheduler) - .Select(_ => Observable.FromAsync(clientService.Api.GetServerSettings)) - .Concat() - .Select(b => b.Content!) - .Subscribe(b => - { - Name = b.ServerName; - MapName = b.MapName; - MemberLimit = b.MemberLimit; - Description = b.ServerDescription; - Ip = b.ListenEndPoint.Ip; - Port = b.ListenEndPoint.Port; - }); + clientService.Connected + .ObserveOn(RxApp.MainThreadScheduler) + .Select(_ => Observable.FromAsync(clientService.Api.GetServerSettings)) + .Concat() + .Select(b => b.Content!) + .Subscribe(b => + { + Name = b.ServerName; + MapName = b.MapName; + MemberLimit = b.MemberLimit; + Description = b.ServerDescription; + Ip = b.ListenEndPoint.Ip; + Port = b.ListenEndPoint.Port; + }); SaveCommand = ReactiveCommand.CreateFromTask(() => clientService.Api.SetServerSettings(new( @@ -35,27 +35,27 @@ public class ServerConfigViewModel : ViewModelBase new(Ip, Port) ))); - Worlds = Observable.FromEventPattern(clientService, nameof(clientService.Connected)) - .ObserveOn(RxApp.MainThreadScheduler) - .Select(_ => Observable.FromAsync(clientService.Api.GetWorlds)) - .Concat() - .Select(b => b.Content!) - .SelectMany(ids => ids) - .Select(id => Observable.FromAsync(() => clientService.Api.GetWorld(id)) - .Select(b => b.Content!) - .Select(b => new World(id, b.Name, b.SizeKb))) - .Concat(); + Worlds = clientService.Connected + .ObserveOn(RxApp.MainThreadScheduler) + .Select(_ => Observable.FromAsync(clientService.Api.GetWorlds)) + .Concat() + .Select(b => b.Content!) + .SelectMany(ids => ids) + .Select(id => Observable.FromAsync(() => clientService.Api.GetWorld(id)) + .Select(b => b.Content!) + .Select(b => new World(id, b.Name, b.SizeKb))) + .Concat(); - Observable.FromEventPattern(clientService, nameof(clientService.Connected)) - .ObserveOn(RxApp.MainThreadScheduler) - .Select(_ => Observable.FromAsync(clientService.Api.GetSelectedWorld)) - .Concat() - .Select(b => b.Content!) - .Select(id => Observable.FromAsync(() => clientService.Api.GetWorld(id)) - .Select(b => b.Content!) - .Select(b => new World(id, b.Name, b.SizeKb))) - .Concat() - .BindTo(this, x => x.SelectedWorld); + clientService.Connected + .ObserveOn(RxApp.MainThreadScheduler) + .Select(_ => Observable.FromAsync(clientService.Api.GetSelectedWorld)) + .Concat() + .Select(b => b.Content!) + .Select(id => Observable.FromAsync(() => clientService.Api.GetWorld(id)) + .Select(b => b.Content!) + .Select(b => new World(id, b.Name, b.SizeKb))) + .Concat() + .BindTo(this, x => x.SelectedWorld); this.ObservableForProperty(x => x.SelectedWorld) .Select(world => Observable.FromAsync(() => clientService.Api.SelectWorld(world.Value!.Id))) diff --git a/TorchRemote/ViewModels/Server/SettingViewModel.cs b/TorchRemote/ViewModels/Server/SettingViewModel.cs new file mode 100644 index 0000000..497860e --- /dev/null +++ b/TorchRemote/ViewModels/Server/SettingViewModel.cs @@ -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 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + public ObservableCollection Properties { get; init; } = new(); + public override string TypeName => "object"; + public static Property FromProperty(Api.ObjectProperty property) + { + return new ObjectProperty(); + } +} \ No newline at end of file diff --git a/TorchRemote/ViewModels/Server/SettingsViewModel.cs b/TorchRemote/ViewModels/Server/SettingsViewModel.cs index 3e4d02e..4af0053 100644 --- a/TorchRemote/ViewModels/Server/SettingsViewModel.cs +++ b/TorchRemote/ViewModels/Server/SettingsViewModel.cs @@ -8,7 +8,7 @@ public class SettingsViewModel : ViewModelBase { private readonly ApiClientService _clientService; [Reactive] - public string BearerToken { get; set; } = "WcdYT5qHjSt5Uzjs54xu8vE9Oq4a5MD2edLxywtJHtc="; + public string BearerToken { get; set; } = "NSN9qSbvKO6PtvoUg+fV5CrSpLqz+F2ssXvzbFbgOpE="; [Reactive] public string RemoteUrl { get; set; } = "http://localhost"; diff --git a/TorchRemote/ViewModels/ViewModelBase.cs b/TorchRemote/ViewModels/ViewModelBase.cs index 823c1ec..2230599 100644 --- a/TorchRemote/ViewModels/ViewModelBase.cs +++ b/TorchRemote/ViewModels/ViewModelBase.cs @@ -5,6 +5,6 @@ namespace TorchRemote.ViewModels public class ViewModelBase : ReactiveObject, IRoutableViewModel { public string? UrlPathSegment { get; set; } - public IScreen HostScreen { get; set; } + public IScreen HostScreen { get; set; } = null!; } } diff --git a/TorchRemote/Views/RemoteServerView.axaml b/TorchRemote/Views/RemoteServerView.axaml index 91a6aae..39ff870 100644 --- a/TorchRemote/Views/RemoteServerView.axaml +++ b/TorchRemote/Views/RemoteServerView.axaml @@ -31,9 +31,7 @@ - - - +