diff --git a/.gitignore b/.gitignore
index 9491a2f..ffae5ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,11 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+# NectarRcon Config
+config.json
+servers.json
+passwords.json
+
# User-specific files
*.rsuser
*.suo
diff --git a/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs b/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs
index a1ac925..0bca3cc 100644
--- a/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs
+++ b/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs
@@ -1,6 +1,7 @@
using NectarRCON.Export.Client;
using NectarRCON.Export.Interfaces;
using System.ComponentModel;
+using System.Text;
namespace NectarRCON.Adapter.Minecraft
{
@@ -11,8 +12,13 @@ public class MinecraftRconClient : BaseTcpClient, IRconAdapter
private static readonly int MaxMessageSize = 4110;
private readonly MemoryStream _buffer = new();
- private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
- private int lastId = 0;
+ private readonly SemaphoreSlim _semaphore = new(1);
+ private int _lastId;
+ ///
+ /// 编码
+ /// 默认编码 UTF8
+ ///
+ private new Encoding _encoding = Encoding.UTF8;
public void Disconnect()
{
@@ -56,6 +62,14 @@ public string Run(string command)
}
}
+ public Encoding GetEncoding()
+ => _encoding;
+
+ public void SetEncoding(Encoding encoding)
+ {
+ _encoding = encoding;
+ }
+
public bool Connect(string address, int port)
{
_semaphore.Wait();
@@ -76,7 +90,7 @@ public bool Authenticate(string password)
try
{
Packet packet = Send(new Packet(PacketType.Authenticate, password));
- return packet.Id == lastId;
+ return packet.Id == _lastId;
}
finally
{
@@ -86,9 +100,9 @@ public bool Authenticate(string password)
private Packet Send(Packet packet)
{
- Interlocked.Increment(ref lastId);
- packet.SetId(lastId);
- return PacketEncoder.Decode(Send(packet.Encode()));
+ Interlocked.Increment(ref _lastId);
+ packet.SetId(_lastId);
+ return PacketEncoder.Decode(Send(packet.Encode(_encoding)), _encoding);
}
}
}
\ No newline at end of file
diff --git a/NectarRCON.Adapter.Minecraft/Packet.cs b/NectarRCON.Adapter.Minecraft/Packet.cs
index 29fc944..e118be8 100644
--- a/NectarRCON.Adapter.Minecraft/Packet.cs
+++ b/NectarRCON.Adapter.Minecraft/Packet.cs
@@ -27,10 +27,10 @@ public void SetId(int id)
Id = id;
}
- public byte[] Encode()
+ public byte[] Encode(Encoding? encoding = null)
{
List bytes = new List();
- var data = Encoding.UTF8.GetBytes(Body);
+ var data = (encoding?? Encoding.UTF8).GetBytes(Body);
bytes.AddRange(BitConverter.GetBytes(PacketEncoder.HeaderLength + data.Length));
bytes.AddRange(BitConverter.GetBytes(Id));
bytes.AddRange(BitConverter.GetBytes((int)Type));
diff --git a/NectarRCON.Adapter.Minecraft/PacketEncoder.cs b/NectarRCON.Adapter.Minecraft/PacketEncoder.cs
index b105953..be3b5c6 100644
--- a/NectarRCON.Adapter.Minecraft/PacketEncoder.cs
+++ b/NectarRCON.Adapter.Minecraft/PacketEncoder.cs
@@ -10,24 +10,24 @@ public enum PacketType : int
Authenticate // 3: Login
}
- public class PacketEncoder
+ public abstract class PacketEncoder
{
public const int HeaderLength = 10;
- public static byte[] Encode(Packet msg)
+ public static byte[] Encode(Packet msg, Encoding? encoding = null)
{
List bytes = new List();
bytes.AddRange(BitConverter.GetBytes(msg.Length));
bytes.AddRange(BitConverter.GetBytes(msg.Id));
bytes.AddRange(BitConverter.GetBytes((int)msg.Type));
- bytes.AddRange(Encoding.UTF8.GetBytes(msg.Body));
+ bytes.AddRange((encoding??Encoding.UTF8).GetBytes(msg.Body));
bytes.AddRange(new byte[] { 0, 0 });
return bytes.ToArray();
}
- public static Packet Decode(byte[] bytes)
+ public static Packet Decode(byte[] bytes, Encoding? encoding = null)
{
if (bytes.Length < HeaderLength) { throw new ArgumentException("packet length too short"); }
int len = BitConverter.ToInt32(bytes, 0);
@@ -37,7 +37,7 @@ public static Packet Decode(byte[] bytes)
string body = string.Empty;
if (bodyLen > 0)
{
- body = Encoding.UTF8.GetString(bytes, 12, bodyLen);
+ body = (encoding??Encoding.UTF8).GetString(bytes, 12, bodyLen);
}
return new Packet(len, id, (PacketType)type, body);
}
diff --git a/NectarRCON.Export/Interfaces/IRconAdapter.cs b/NectarRCON.Export/Interfaces/IRconAdapter.cs
index 4cc0106..edfd9d6 100644
--- a/NectarRCON.Export/Interfaces/IRconAdapter.cs
+++ b/NectarRCON.Export/Interfaces/IRconAdapter.cs
@@ -1,4 +1,6 @@
-namespace NectarRCON.Export.Interfaces;
+using System.Text;
+
+namespace NectarRCON.Export.Interfaces;
///
/// Rcon协议兼容接口
///
@@ -30,4 +32,7 @@ public interface IRconAdapter : IDisposable
///
string Run(string command);
+
+ Encoding GetEncoding();
+ void SetEncoding(Encoding encoding);
}
diff --git a/NectarRCON.Export/NectarRCON.Export.csproj b/NectarRCON.Export/NectarRCON.Export.csproj
index cfadb03..88eb041 100644
--- a/NectarRCON.Export/NectarRCON.Export.csproj
+++ b/NectarRCON.Export/NectarRCON.Export.csproj
@@ -1,9 +1,13 @@
- net7.0
+ net7.0-windows
enable
enable
+
+
+
+
diff --git a/NectarRCON.Tests/NectarRCON.Tests.csproj b/NectarRCON.Tests/NectarRCON.Tests.csproj
index b53a7d2..49e183a 100644
--- a/NectarRCON.Tests/NectarRCON.Tests.csproj
+++ b/NectarRCON.Tests/NectarRCON.Tests.csproj
@@ -20,6 +20,7 @@
+
diff --git a/NectarRCON.Tests/UpdaterTests.cs b/NectarRCON.Tests/UpdaterTests.cs
new file mode 100644
index 0000000..396921e
--- /dev/null
+++ b/NectarRCON.Tests/UpdaterTests.cs
@@ -0,0 +1,40 @@
+using NectarRCON.Updater;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NectarRCON.Tests
+{
+ [TestClass]
+ public class UpdaterTests
+ {
+ [TestMethod]
+ public void Github()
+ {
+ IUpdater updater = new GithubUpdater();
+ updater.SetVersion("NectarRcon-x86-1.0.0");
+ updater.IsLatestVersion();
+ }
+
+ [TestMethod]
+ public void AppVersionTest()
+ {
+ AppVersion versionA = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta1");
+ AppVersion versionB = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta2");
+
+ Assert.IsTrue(versionA.Equals(versionA));
+ Assert.IsFalse(versionA.Equals(versionB));
+
+#pragma warning disable CS1718 // 对同一变量进行了比较
+ Assert.IsTrue(versionA == versionA);
+ Assert.IsFalse(versionA != versionA);
+ Assert.IsFalse(versionA > versionA);
+#pragma warning restore CS1718 // 对同一变量进行了比较
+
+ Assert.IsTrue(versionB > versionA);
+ Assert.IsFalse(versionB < versionA);
+ }
+ }
+}
diff --git a/NectarRCON.Updater/AppVersion.cs b/NectarRCON.Updater/AppVersion.cs
new file mode 100644
index 0000000..618fe16
--- /dev/null
+++ b/NectarRCON.Updater/AppVersion.cs
@@ -0,0 +1,105 @@
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+
+namespace NectarRCON.Updater
+{
+ public class AppVersion
+ {
+ public string AppName { get; set; } = string.Empty;
+ public int Version { get; set; }
+ public int Major { get;set; }
+ public int Minor { get;set; }
+ public int Patch { get;set; }
+ public int? Build { get; set; }
+ public string PreReleaseType { get; set; } = string.Empty;
+ public string Platform { get; set; } = string.Empty;
+ public bool IsPreRelease
+ => !string.IsNullOrEmpty(PreReleaseType);
+
+ public override string ToString()
+ {
+ return $"{AppName}-{Platform}-{Major}.{Minor}.{Patch}" + (IsPreRelease ? $"-{PreReleaseType}{Build}" : string.Empty);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj?.ToString() == ToString();
+ }
+
+ public static bool operator <(AppVersion a, AppVersion b)
+ {
+ return a.Version < b.Version || (a.Build ?? 0) < (b.Build ?? 0);
+ }
+
+ public static bool operator >(AppVersion a, AppVersion b)
+ {
+ return a.Version > b.Version || (a.Build ?? 0) > (b.Build ?? 0);
+ }
+
+ public static bool operator ==(AppVersion a, AppVersion b)
+ {
+ return a.Version == b.Version && (a.Build ?? 0) == (b.Build ?? 0);
+ }
+
+ public static bool operator !=(AppVersion a, AppVersion b)
+ {
+ return a.Version != b.Version || (a.Build ?? 0) != (b.Build ?? 0);
+ }
+
+ private AppVersion() { }
+
+ public static AppVersion ParseVersion(string version)
+ {
+ string[] versionParts = version.Split("-");
+ if (versionParts.Length > 2)
+ {
+ AppVersion result = new();
+ string name = versionParts[0];
+ string platform = versionParts[1];
+ string ver = versionParts[2];
+ string preRelease = string.Empty;
+
+ if (versionParts.Length > 3)
+ {
+ preRelease = versionParts[3];
+ }
+
+ Regex versionRegex = new(@"(?\d+)\.(?\d+)\.(?\d+)");
+ Match versionMatch = versionRegex.Match(ver);
+
+ if (versionMatch.Success)
+ {
+ result.Version = int.Parse(versionMatch.Groups["major"].Value + versionMatch.Groups["minor"].Value + versionMatch.Groups["patch"].Value);
+ result.Major = int.Parse(versionMatch.Groups["major"].Value);
+ result.Minor = int.Parse(versionMatch.Groups["minor"].Value);
+ result.Patch = int.Parse(versionMatch.Groups["patch"].Value);
+ }
+
+ Regex preReleaseRegex = new(@"(?[a-zA-Z]+)(?\d+)");
+ Match preReleaseMatch = preReleaseRegex.Match(preRelease);
+
+ if (preReleaseMatch.Success)
+ {
+ if (preReleaseMatch.Groups["build"].Success)
+ {
+ result.Build = int.Parse(preReleaseMatch.Groups["build"].Value);
+ }
+ if (preReleaseMatch.Groups["preRelease"].Success)
+ {
+ result.PreReleaseType = preReleaseMatch.Groups["preRelease"].Value;
+ }
+ }
+
+ result.Platform = platform;
+ result.AppName = name;
+ return result;
+ }
+ throw new InvalidOperationException("Invalid version format");
+ }
+
+ public override int GetHashCode()
+ {
+ return RuntimeHelpers.GetHashCode(ToString());
+ }
+ }
+}
diff --git a/NectarRCON.Updater/GithubUpdater.cs b/NectarRCON.Updater/GithubUpdater.cs
new file mode 100644
index 0000000..1f43cf3
--- /dev/null
+++ b/NectarRCON.Updater/GithubUpdater.cs
@@ -0,0 +1,91 @@
+using NectarRCON.Updater.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace NectarRCON.Updater
+{
+ public class GithubUpdater : IUpdater
+ {
+ private static readonly HttpClient _client = new()
+ {
+ BaseAddress = new Uri("https://api.github.com/repos/zkhssb/NectarRcon/")
+ };
+ private bool _preEnable = false;
+ private AppVersion? _version;
+
+ ///
+ /// 获取最新版本, null为没找到
+ ///
+ /// 是否允许pre版本
+ private AppVersion? GetLatestVersion(bool enablePre)
+ {
+ if (_version is null)
+ return null;
+ using(HttpRequestMessage request = new(HttpMethod.Get, "releases/latest"))
+ {
+ request.Headers.Add("User-Agent", $"{_version.AppName}-AppUpdater");
+ using(HttpResponseMessage response = _client.Send(request))
+ {
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException(response.StatusCode.ToString());
+ string resultString = string.Empty;
+ Task.Run(async () =>
+ {
+ resultString = await response.Content.ReadAsStringAsync();
+ }).Wait();
+ Release release = JsonSerializer.Deserialize(resultString) ?? throw new JsonException();
+ foreach(var asset in release.Assets)
+ {
+ string fileName = Path.GetFileNameWithoutExtension(asset.Name);
+ try
+ {
+ fileName = "NectarRcon-x86-1.0.0-beta2";
+ AppVersion version = AppVersion.ParseVersion(fileName);
+ if(version.AppName.ToLower() == _version.AppName.ToLower() && version.Platform.ToLower() == _version.Platform.ToLower())
+ {
+ if (version.IsPreRelease && !enablePre)
+ continue;
+ if (version > _version)
+ {
+ return version;
+ }
+ }
+ }
+ catch (InvalidOperationException) { } // Invalid version format
+ }
+ return null;
+ }
+ }
+ }
+
+ public bool IsLatestVersion()
+ {
+ GetLatestVersion(_preEnable);
+ return true;
+ }
+
+ public void Setup()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetVersion(string version)
+ {
+ _version = AppVersion.ParseVersion(version);
+ }
+
+ public void SetPreEnable(bool value)
+ {
+ _preEnable = value;
+ }
+
+ public AppVersion GetLatestVersion()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/NectarRCON.Updater/IUpdater.cs b/NectarRCON.Updater/IUpdater.cs
new file mode 100644
index 0000000..ca0067f
--- /dev/null
+++ b/NectarRCON.Updater/IUpdater.cs
@@ -0,0 +1,26 @@
+namespace NectarRCON.Updater
+{
+ public interface IUpdater
+ {
+ ///
+ /// 设置版本
+ ///
+ void SetVersion(string version);
+ ///
+ /// 是最新版
+ ///
+ bool IsLatestVersion();
+ ///
+ /// 获取最新版本
+ ///
+ AppVersion GetLatestVersion();
+ ///
+ /// 开始安装
+ ///
+ void Setup();
+ ///
+ /// 设置是否启用获取预发布版本更新
+ ///
+ void SetPreEnable(bool value);
+ }
+}
diff --git a/NectarRCON.Updater/Model/Asset.cs b/NectarRCON.Updater/Model/Asset.cs
new file mode 100644
index 0000000..235c83b
--- /dev/null
+++ b/NectarRCON.Updater/Model/Asset.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace NectarRCON.Updater.Model
+{
+ public class Asset
+ {
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+ [JsonPropertyName("url")]
+ public required string Url { get; set; }
+ [JsonPropertyName("created_at")]
+ public required DateTime CreatedAt { get; set; }
+ }
+}
diff --git a/NectarRCON.Updater/Model/Release.cs b/NectarRCON.Updater/Model/Release.cs
new file mode 100644
index 0000000..c753d84
--- /dev/null
+++ b/NectarRCON.Updater/Model/Release.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace NectarRCON.Updater.Model
+{
+ public class Release
+ {
+ [JsonPropertyName("tag_name")]
+ public required string TagName { get; set; }
+
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public required DateTime CreatedAt { get; set; }
+
+ [JsonPropertyName("assets")]
+ public required IEnumerable Assets { get; set; }
+
+ [JsonPropertyName("body")]
+ public required string Body { get; set; }
+ }
+}
diff --git a/NectarRCON.Updater/NectarRCON.Updater.csproj b/NectarRCON.Updater/NectarRCON.Updater.csproj
new file mode 100644
index 0000000..cb6a55a
--- /dev/null
+++ b/NectarRCON.Updater/NectarRCON.Updater.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net7.0-windows
+ enable
+ enable
+
+
+
diff --git a/NectarRCON.sln b/NectarRCON.sln
index 2a952cb..d3a99e5 100644
--- a/NectarRCON.sln
+++ b/NectarRCON.sln
@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Export", "Nectar
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Adapter.Minecraft", "NectarRCON.Adapter.Minecraft\NectarRCON.Adapter.Minecraft.csproj", "{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NectarRCON.Core", "NectarRCON.Core\NectarRCON.Core.csproj", "{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Core", "NectarRCON.Core\NectarRCON.Core.csproj", "{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NectarRCON.Updater", "NectarRCON.Updater\NectarRCON.Updater.csproj", "{D6C910A7-3590-492B-9CA1-C3D586FF2C41}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +41,10 @@ Global
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/NectarRCON/App.xaml b/NectarRCON/App.xaml
index c5ea934..e71476f 100644
--- a/NectarRCON/App.xaml
+++ b/NectarRCON/App.xaml
@@ -14,7 +14,7 @@
- 1.0.0-beta3
+ 1.0.0-beta4
diff --git a/NectarRCON/App.xaml.cs b/NectarRCON/App.xaml.cs
index 506de6a..314c866 100644
--- a/NectarRCON/App.xaml.cs
+++ b/NectarRCON/App.xaml.cs
@@ -7,7 +7,9 @@
using NectarRCON.Windows;
using System;
using System.Linq;
+using System.Text;
using System.Windows;
+using NectarRCON.Dp;
using Wpf.Ui.Mvvm.Contracts;
using Wpf.Ui.Mvvm.Services;
@@ -66,6 +68,13 @@ public static T GetService(Type type)
private async void OnStartup(object sender, StartupEventArgs e)
{
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ foreach (var rconEncoding in Enum.GetValues())
+ {
+ rconEncoding.GetEncoding();
+ }
+
await _host.StartAsync();
}
diff --git a/NectarRCON/Dp/DpFile.cs b/NectarRCON/Dp/DpFile.cs
new file mode 100644
index 0000000..349bdf0
--- /dev/null
+++ b/NectarRCON/Dp/DpFile.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+
+namespace NectarRCON.Dp;
+
+///
+/// 数据持久化文件
+///
+public abstract class DpFile
+{
+ ///
+ /// 文件名
+ ///
+ protected abstract string Name { get; }
+
+ ///
+ /// 文件路径
+ ///
+ protected virtual string BasePath => string.Empty;
+
+ ///
+ /// 实例映射
+ ///
+ private static readonly Dictionary InstanceMapping = [];
+
+ ///
+ /// 保存数据
+ ///
+ public void Save()
+ {
+ var json = JsonSerializer.Serialize((object)this);
+ var filePath = Path.Combine(AppContext.BaseDirectory,"dp", BasePath, Name);
+ Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
+ File.WriteAllText(filePath, json);
+ }
+
+ ///
+ /// 加载数据
+ ///
+ /// 文件名
+ /// 文件路径
+ /// 类型
+ /// 实例
+ private static T? Load(string name, string? basePath = null)
+ where T : DpFile
+ {
+ var filePath = Path.Combine(AppContext.BaseDirectory, "dp", basePath ?? string.Empty, name);
+ if (!File.Exists(filePath)) return null;
+ var json = File.ReadAllText(filePath);
+ return JsonSerializer.Deserialize(json);
+ }
+
+ ///
+ /// 以单例模式加载数据
+ ///
+ /// 类型
+ /// 实例
+ public static T LoadSingleton()
+ where T:DpFile
+ {
+ // 先从_instanceMapping拿数据
+ if (InstanceMapping.TryGetValue(typeof(T), out var cachedInstance))
+ {
+ return (T)cachedInstance;
+ }
+
+ // 如果缓存没有 则使用找到此DPFile的无参构造函数 使用反射实例化后存放到_instanceMapping
+ var instance = Activator.CreateInstance();
+ // 从instance中获取Name 随后load
+ InstanceMapping[typeof(T)] = Load(instance.Name, instance.BasePath) ?? instance;
+ return (T)InstanceMapping[typeof(T)];
+ }
+}
\ No newline at end of file
diff --git a/NectarRCON/Dp/RconSettingsDp.cs b/NectarRCON/Dp/RconSettingsDp.cs
new file mode 100644
index 0000000..a2bf195
--- /dev/null
+++ b/NectarRCON/Dp/RconSettingsDp.cs
@@ -0,0 +1,59 @@
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace NectarRCON.Dp;
+
+public enum RconEncoding
+{
+ Utf8 = 0,
+ Utf16 = 1,
+ Utf32 = 2,
+ Gb2312 = 3,
+ Gbk = 4,
+ Gb18030 = 5,
+ Ascii = 6,
+ Big5 = 7,
+ HzGb2312 = 8,
+}
+
+public static class RconEncodingExtensions
+{
+ public static Encoding GetEncoding(this RconEncoding encoding)
+ => encoding switch
+ {
+ RconEncoding.Utf8 => Encoding.UTF8,
+ RconEncoding.Utf16 => Encoding.GetEncoding("UTF-16"),
+ RconEncoding.Utf32 => Encoding.UTF32,
+ RconEncoding.Gb2312 => Encoding.GetEncoding("gb2312"),
+ RconEncoding.Gbk => Encoding.GetEncoding("gbk"),
+ RconEncoding.Gb18030 => Encoding.GetEncoding("gb18030"),
+ RconEncoding.Ascii => Encoding.ASCII,
+ RconEncoding.Big5 => Encoding.GetEncoding("big5"),
+ RconEncoding.HzGb2312 => Encoding.GetEncoding("hz-gb-2312"),
+ _ => Encoding.UTF8,
+ };
+}
+
+public class RconSettingsDp : DpFile
+{
+ protected override string Name => "rcon_settings.json";
+
+ ///
+ /// 连接时掉线自动尝试重连
+ ///
+ [JsonPropertyName("auto_reconnect")]
+ public bool AutoReconnect { get; set; } = true;
+
+ ///
+ /// 掉线后不关闭连接窗口
+ ///
+ ///
+ [JsonPropertyName("is_keep_connection_window_open")]
+ public bool IsKeepConnectionWindowOpen { get; set; }
+
+ ///
+ /// 文本编码
+ ///
+ [JsonPropertyName("encoding")]
+ public RconEncoding Encoding { get; set; } = RconEncoding.Utf8;
+}
\ No newline at end of file
diff --git a/NectarRCON/NectarRCON.csproj b/NectarRCON/NectarRCON.csproj
index 4f7bd6e..c205304 100644
--- a/NectarRCON/NectarRCON.csproj
+++ b/NectarRCON/NectarRCON.csproj
@@ -8,6 +8,7 @@
Resources\Icon.ico
app.manifest
1.0.0-beta3
+ 12
@@ -23,6 +24,7 @@
+
diff --git a/NectarRCON/Rcon/RconMultiConnection.cs b/NectarRCON/Rcon/RconMultiConnection.cs
index b1bc748..18dc859 100644
--- a/NectarRCON/Rcon/RconMultiConnection.cs
+++ b/NectarRCON/Rcon/RconMultiConnection.cs
@@ -10,6 +10,7 @@
using System.Net.Sockets;
using System.Security.Authentication;
using System.Windows.Controls;
+using NectarRCON.Dp;
namespace NectarRCON.Services
{
@@ -18,6 +19,7 @@ namespace NectarRCON.Services
///
internal class RconMultiConnection : IRconConnection, IDisposable
{
+ private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton();
public event MessageEvent? OnMessage;
public event RconEvent? OnClosed;
public event RconEvent? OnConnected;
@@ -90,6 +92,8 @@ public void Connect()
_messageBoxService.Show(ex, $"Server: \"{info.Name}\"");
}
+ //设置编码
+ adapter.SetEncoding(_settingsDp.Encoding.GetEncoding());
_connections.Add(info, adapter);
}
}
diff --git a/NectarRCON/Rcon/RconSingleConnection.cs b/NectarRCON/Rcon/RconSingleConnection.cs
index 7761f63..95ab132 100644
--- a/NectarRCON/Rcon/RconSingleConnection.cs
+++ b/NectarRCON/Rcon/RconSingleConnection.cs
@@ -4,12 +4,16 @@
using NectarRCON.Models;
using NectarRCON.Rcon;
using System;
+using System.IO;
+using System.Net.Sockets;
using System.Security.Authentication;
using System.Windows;
+using NectarRCON.Dp;
namespace NectarRCON.Services;
public class RconSingleConnection : IRconConnection
{
+ private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton();
private readonly IServerPasswordService _serverPasswordService;
private readonly ILanguageService _languageService;
private readonly IRconConnectionInfoService _rconConnectionInfoService;
@@ -67,7 +71,7 @@ public void Connect()
// 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒)
_rconClient = AdapterHelpers.CreateAdapterInstance(info.Adapter)
?? throw new InvalidOperationException($"adapter not found: {info.Adapter}");
-
+ _rconClient.SetEncoding(_settingsDp.Encoding.GetEncoding());
string host = address.Split(":")[0];
int port = int.Parse(address.Split(":")[1]);
@@ -101,12 +105,37 @@ public void Send(string command)
{
try
{
- string result = _rconClient.Run(command) ?? string.Empty;
+ string result = _rconClient.Run(command);
OnMessage?.Invoke(_serverInformation, result);
}
catch (Exception ex)
{
Close();
+ if (ex is SocketException or IOException && _settingsDp.AutoReconnect)
+ {
+ try
+ {
+ Connect();
+ }
+ catch
+ {
+ // ignored
+ }
+
+ if (IsConnected())
+ {
+ try
+ {
+ string result = _rconClient.Run(command);
+ OnMessage?.Invoke(_serverInformation, result);
+ return;
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ }
MessageBox.Show($"{_languageService.GetKey("text.error")}\n{ex.Message}", ex.GetType().FullName, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
diff --git a/NectarRCON/Resources/Languages/en_us.xaml b/NectarRCON/Resources/Languages/en_us.xaml
index b698aec..42c8c56 100644
--- a/NectarRCON/Resources/Languages/en_us.xaml
+++ b/NectarRCON/Resources/Languages/en_us.xaml
@@ -15,13 +15,18 @@
CheckUpdate
RCONManager
UI Language
+ UI Settings
UI Theme
Dark Theme
Light Theme
System Theme
Rcon passwords...
Command Record Limit
-
+ Rcon
+ Auto Reconnect
+ KeepConnectionWindowOpen
+ Encoding
+
Connect
Edit
Delete
@@ -52,6 +57,11 @@
Run
Successful!
+
+ Offline
+ All clients are offline
+ Reconnect
+ Back to Servers
Edit
diff --git a/NectarRCON/Resources/Languages/zh_cn.xaml b/NectarRCON/Resources/Languages/zh_cn.xaml
index 5d5d02a..b56a714 100644
--- a/NectarRCON/Resources/Languages/zh_cn.xaml
+++ b/NectarRCON/Resources/Languages/zh_cn.xaml
@@ -15,13 +15,18 @@
检查更新
RCON管理器
UI语言
+ 界面设置
UI主题
深色主题
浅色主题
跟随系统
Rcon密码管理
服务器命令回溯记录数
-
+ Rcon全局设置
+ 掉线自动重连
+ 掉线不回到主页
+ 文本编码
+
连接
编辑
删除
@@ -52,7 +57,12 @@
执行
执行成功
-
+
+ 已掉线
+ 所有客户端都掉线了
+ 重新连接
+ 回到主页
+
编辑
运行过程中出现异常,应用即将结束。 如果您无法理解以下错误,请在 Github 上提交问题并详细描述发生的情况
diff --git a/NectarRCON/Resources/Languages/zh_tw.xaml b/NectarRCON/Resources/Languages/zh_tw.xaml
new file mode 100644
index 0000000..5f0fe90
--- /dev/null
+++ b/NectarRCON/Resources/Languages/zh_tw.xaml
@@ -0,0 +1,122 @@
+
+ zh_tw
+ 繁体中文(zh_tw)
+ 伺服器
+ 伺服器列表
+ 伺服器分組
+ 日誌
+ 清空日誌
+ 程式
+ 設定
+ 關於
+ 檢查更新
+ RCON管理器
+ UI語言
+ 界面設定
+ UI主題
+ 深色主題
+ 淺色主題
+ 跟隨系統
+ Rcon密碼管理
+ 伺服器命令回溯記錄數
+ Rcon全局設定
+ 斷綫自動重連
+ 斷線不回到主頁
+ 文本編碼
+
+ 連線
+ 編輯
+ 刪除
+ 密碼
+ 伺服器列表
+ 新增伺服器
+ 輸入伺服器名稱來搜尋...
+ 此操作將會刪除伺服器! (包括記錄的密碼)\n您確定要刪除嗎?
+
+ 新增伺服器
+ 伺服器名稱
+ 伺服器地址
+ 伺服器端口
+ 新增
+ 取消
+ 相同名稱的伺服器已經存在,請換一個名稱!
+ 伺服器名稱或伺服器地址不可為空,請更改!
+
+ 爱发电
+ MCBBS
+ Github
+
+ 解析目标地址時遇到錯誤:%s\n請檢查伺服器地址是否合法!
+
+ 編輯密碼
+ 密碼
+ 無需密碼
+
+ 執行
+ 執行成功
+
+ 斷線
+ 所有客戶端都斷線了
+ 重新連接
+ 返回伺服器
+
+ 編輯
+
+ 執行過程中出現異常,應用即將結束。如果您無法理解以下錯誤,請在 GitHub 上提交問題並詳細描述發生的情況。
+ 錯誤
+ 警告
+ 資訊
+ 確定
+ 取消
+ 不可使用
+ 連接
+ 移除
+ 刪除
+ 新增
+
+ 正在連接伺服器...
+ 連接伺服器失敗!
+ 無法連接到遠程伺服器:%s\n請檢查伺服器RCON地址配置是否正確,以及檢查服務端是否開啟了RCON選項!
+ Rcon密碼錯誤,請檢查您的伺服器密碼是否正確!
+
+ 連接伺服器並認證成功!
+ 連接已斷開!
+ 開始連接
+
+ 無法連接到伺服器,因此無法使用遠程命令功能,請嘗試回到伺服器列表重新連接到本伺服器!
+
+ 無法解析伺服器列表 /servers.json %s\n點選"是"重置伺服器列表(需重新添加伺服器,但記錄的密碼不會消失)\n點選"取消"關閉程式!
+ 無法解析伺服器列表 /servers.json %s\n點選"是"關閉程式!
+ 無法儲存檔案 /servers.json\n如果您剛剛進行了添加伺服器操作,請注意,本次儲存並未生效!\n%s
+
+ 無法解析密碼列表 /passwords.json %s\n點選"是"重置密碼列表(需重新設定密碼,但記錄的伺服器不會消失)\n點選"取消"關閉程式!
+ 無法解析密碼列表 /passwords.json %s\n點選"是"關閉程式!
+ 無法儲存檔案 /passwords.json\n如果您剛剛進行了設定密碼操作,請注意,本次儲存並未生效!\n%s
+
+ 無法解析配置檔案 /config.json JSON解析失敗:%s\n點選"是"重置配置檔案!\n點選"否"退出程式!
+ 無法解析配置檔案 /config.json:%s\n點選"是"重置配置檔案!\n點選"否"退出程式!
+ 無法儲存配置檔案 /config.json:%s\n如果您剛剛更改了配置檔案,請注意,儲存並未生效!
+
+ 讀取和解析組檔案"{0}"時發生錯誤,請嘗試刪除或修復檔案以解決此錯誤!
+ 相同的 GroupId 已存在(現有值: {0},重複值: {1})
+ 相同的 Name 已存在(現有Id: {0},重複Id: {1})
+ 檔案中已存在具有相同內部 ID 的分組,請再次嘗試建立
+ {0} 檔案名與內部 ID {1} 不匹配
+ 相同的組名已存在
+
+ 點擊「+添加」添加一個分組吧!
+ 添加分組後,您可以將命令廣播到該分組的所有伺服器
+ 該分組沒有任何伺服器,請點擊「+添加」添加一個伺服器吧!
+ 是否要刪除分組 {0}?
+
+ 新建分組
+ 分組名稱
+
+ 選擇伺服器
+ 沒有可用的伺服器
+
+ 離線
+
\ No newline at end of file
diff --git a/NectarRCON/Services/LanguageService.cs b/NectarRCON/Services/LanguageService.cs
index 2351f22..3d680e9 100644
--- a/NectarRCON/Services/LanguageService.cs
+++ b/NectarRCON/Services/LanguageService.cs
@@ -57,6 +57,7 @@ public void Refresh()
}
// 从内部文件加载
_defaultLanguages.Add("zh_cn", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml");
+ _defaultLanguages.Add("zh_tw", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_tw.xaml");
_defaultLanguages.Add("en_us", "pack://application:,,,/NectarRCON;component/Resources/Languages/en_us.xaml");
foreach (KeyValuePair language in _defaultLanguages)
{
diff --git a/NectarRCON/ViewModels/MainPageViewModel.cs b/NectarRCON/ViewModels/MainPageViewModel.cs
index f575a0b..9b0888d 100644
--- a/NectarRCON/ViewModels/MainPageViewModel.cs
+++ b/NectarRCON/ViewModels/MainPageViewModel.cs
@@ -11,13 +11,16 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
+using NectarRCON.Dp;
using Wpf.Ui.Mvvm.Contracts;
using MessageBox = System.Windows.MessageBox;
using TextBox = Wpf.Ui.Controls.TextBox;
namespace NectarRCON.ViewModels;
+
public partial class MainPageViewModel : ObservableObject
{
+ private static readonly RconSettingsDp RconSettings = DpFile.LoadSingleton();
private readonly ILogService _logService;
private readonly IServerPasswordService _serverPasswordService;
private IRconConnection _rconConnectService;
@@ -27,13 +30,14 @@ public partial class MainPageViewModel : ObservableObject
private readonly IConnectingDialogService _connectingDialogService;
private readonly IMessageBoxService _messageBoxService;
- private MainPage? _page = null;
- private TextBox? _logTextBox = null;
+ private MainPage? _page;
+ private TextBox? _logTextBox;
+
+ [ObservableProperty] private string _commandText = string.Empty;
+ [ObservableProperty] private string _logText = string.Empty;
+ [ObservableProperty] private bool _isMultipleConnection;
+ [ObservableProperty] private bool _isDisconnection;
- [ObservableProperty]
- private string _commandText = string.Empty;
- [ObservableProperty]
- private string _logText = string.Empty;
public MainPageViewModel()
{
_logService = App.GetService();
@@ -46,16 +50,19 @@ public MainPageViewModel()
WeakReferenceMessenger.Default.Register(this, OnClear);
// 选择连接服务
- _rconConnectService = _rconConnectionInfoService.HasMultipleInformation ?
- App.GetService(typeof(RconMultiConnection)) :
- App.GetService(typeof(RconSingleConnection));
+ _rconConnectService = _rconConnectionInfoService.HasMultipleInformation
+ ? App.GetService(typeof(RconMultiConnection))
+ : App.GetService(typeof(RconSingleConnection));
+ IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation;
}
- public void OnClear(object sender, ClearLogValueMessage msg)
+
+ private void OnClear(object sender, ClearLogValueMessage msg)
{
_logService.Clear();
LogText = string.Empty;
}
+
private void OnMessage(ServerInformation info, string msg)
{
string logMsg = string.IsNullOrEmpty(msg)
@@ -64,32 +71,56 @@ private void OnMessage(ServerInformation info, string msg)
LogText += _logService.Log($"{info.Name}:" + logMsg);
_logTextBox?.ScrollToEnd();
}
+
private void OnClosed(ServerInformation info)
{
LogText += _logService.Log($"{info.Name}\t{_languageService.GetKey("text.server.closed")}");
+ IsDisconnection = !_rconConnectService.IsConnected();
+ }
+
+ [RelayCommand]
+ private async void Load(RoutedEventArgs e)
+ {
+ // GetLogs
+ LogText = string.Empty;
+ LogText = _logService.GetText();
+
+ _page = e.Source as MainPage;
+ await ConnectAsync();
}
+
[RelayCommand]
- public async void Load(RoutedEventArgs e)
+ private async void ReConnect()
+ {
+ if (_rconConnectService.IsConnected())
+ _rconConnectService.Close();
+ IsDisconnection = false;
+ await ConnectAsync();
+ }
+
+ private async Task ConnectAsync()
{
+ IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation;
+ _rconConnectService.OnConnected -= OnConnected;
+ _rconConnectService.OnMessage -= OnMessage;
+ _rconConnectService.OnClosed -= OnClosed;
+ await Task.CompletedTask;
try
{
_connectingDialogService.Show();
// 选择连接服务
- _rconConnectService = _rconConnectionInfoService.HasMultipleInformation ?
- App.GetService(typeof(RconMultiConnection)) :
- App.GetService(typeof(RconSingleConnection));
+ _rconConnectService = _rconConnectionInfoService.HasMultipleInformation
+ ? App.GetService(typeof(RconMultiConnection))
+ : App.GetService(typeof(RconSingleConnection));
- WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage()
+ WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage
{
IsLoaded = true,
});
- _page = e.Source as MainPage;
_logTextBox = (TextBox)LogicalTreeHelper.FindLogicalNode(_page, "LogText");
-
- LogText = string.Empty;
- LogText = _logService.GetText();
LogText += _logService.Log(_languageService.GetKey("text.server.start"));
+ _logTextBox?.ScrollToEnd();
_rconConnectService.OnConnected += OnConnected;
_rconConnectService.OnMessage += OnMessage;
_rconConnectService.OnClosed += OnClosed;
@@ -97,22 +128,26 @@ public async void Load(RoutedEventArgs e)
}
catch (SocketException ex)
{
- _messageBoxService.Show(_languageService.GetKey("text.server.connect.fail.text")
+ var msg = _languageService.GetKey("text.server.connect.fail.text")
.Replace("\\n", "\n")
- .Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
+ .Replace("%s", ex.Message);
+ _messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ LogText += _logService.Log(msg);
+ _logTextBox?.ScrollToEnd();
}
catch (AuthenticationException ex)
{
- _messageBoxService.Show(ex.Message + _languageService.GetKey("text.server.connect.auth_fail")
- .Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
- if (_rconConnectionInfoService.HasMultipleInformation)
- {
- _navigationService.Navigate(typeof(GroupPage));
- }
- else
- {
- _navigationService.Navigate(typeof(ServersPage));
- }
+ var msg = ex.Message + _languageService.GetKey("text.server.connect.auth_fail")
+ .Replace("\\n", "\n");
+ _messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ LogText += _logService.Log(msg);
+
+ // 如果认证失败 就根据当前模式返回对应页面
+ _navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation
+ ? typeof(GroupPage)
+ : typeof(ServersPage));
}
finally
{
@@ -121,19 +156,30 @@ public async void Load(RoutedEventArgs e)
// 当只有一个服务器时IsConnected会返回单个客户端的连接状态
// 当有多个服务器时只要有一个客户端在线,IsConnected就会返回True
- if (!_rconConnectService.IsConnected())
+ if (!_rconConnectService.IsConnected() && !RconSettings.IsKeepConnectionWindowOpen)
{
_navigationService.Navigate(2);
}
+
+ IsDisconnection = !_rconConnectService.IsConnected();
}
private void OnConnected(ServerInformation info)
{
LogText += _logService.Log($"$ {info.Name}\t{_languageService.GetKey("text.server.connected")}");
+ IsDisconnection = false;
}
[RelayCommand]
- public void Exit()
+ private void BackHome()
+ {
+ _navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation
+ ? typeof(GroupPage)
+ : typeof(ServersPage));
+ }
+
+ [RelayCommand]
+ private void Exit()
{
WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage()
{
@@ -143,9 +189,12 @@ public void Exit()
_rconConnectService.Close();
_rconConnectService.OnMessage -= OnMessage;
_rconConnectService.OnClosed -= OnClosed;
+ _rconConnectService.OnConnected -= OnConnected;
+ IsDisconnection = false; // 重置状态 此时没有任何连接
}
+
[RelayCommand]
- public void Run()
+ private void Run()
{
if (_rconConnectService.IsConnected())
{
@@ -156,12 +205,15 @@ public void Run()
}
else
{
+ IsDisconnection = true;
_rconConnectService.Close();
- MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
+ MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"),
+ _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
+
[RelayCommand]
- public void KeyDown(KeyEventArgs e)
+ private void KeyDown(KeyEventArgs e)
{
var textBox = (System.Windows.Controls.TextBox)e.Source;
_commandText = textBox.Text;
diff --git a/NectarRCON/ViewModels/SettingPageViewModel.cs b/NectarRCON/ViewModels/SettingPageViewModel.cs
index 1d1609a..0a19b0f 100644
--- a/NectarRCON/ViewModels/SettingPageViewModel.cs
+++ b/NectarRCON/ViewModels/SettingPageViewModel.cs
@@ -1,4 +1,5 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Core.Helper;
using NectarRCON.Interfaces;
@@ -8,6 +9,7 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
+using NectarRCON.Dp;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.ViewModels;
@@ -17,20 +19,64 @@ public partial class SettingPageViewModel : ObservableObject
private readonly ILanguageService _languageService;
private readonly IConfigService _configService;
private readonly IThemeService _themeService;
+ private readonly RconSettingsDp _rconSettingsDp = DpFile.LoadSingleton();
[ObservableProperty]
private int _languageSelectedIndex = -1;
[ObservableProperty]
private int _themeSelectedIndex = -1;
+
+ [ObservableProperty]
+ private bool _rconAutoReconnect;
+
+ [ObservableProperty]
+ private bool _isKeepConnectionWindowOpen;
+
+ [ObservableProperty]
+ private ObservableCollection _rconEncoding = [];
+
+ [ObservableProperty]
+ private string _selectedRconEncoding;
[ObservableProperty]
- private ObservableCollection _languages = new();
+ private ObservableCollection _languages = [];
+
public SettingPageViewModel()
{
_languageService = App.GetService();
_configService = App.GetService();
_themeService = App.GetService();
+
+ RconAutoReconnect = _rconSettingsDp.AutoReconnect;
+ IsKeepConnectionWindowOpen = _rconSettingsDp.IsKeepConnectionWindowOpen;
+
+ RconEncoding.Clear();
+ foreach (var encoding in Enum.GetNames(typeof(RconEncoding)))
+ {
+ RconEncoding.Add(encoding);
+ }
+
+ SelectedRconEncoding = _rconSettingsDp.Encoding.ToString();
+ }
+
+ partial void OnRconAutoReconnectChanged(bool value)
+ {
+ _rconSettingsDp.AutoReconnect = value;
+ _rconSettingsDp.Save();
+ }
+
+ partial void OnIsKeepConnectionWindowOpenChanged(bool value)
+ {
+ _rconSettingsDp.IsKeepConnectionWindowOpen = value;
+ _rconSettingsDp.Save();
}
+
+ partial void OnSelectedRconEncodingChanged(string value)
+ {
+ _rconSettingsDp.Encoding = Enum.GetValues().FirstOrDefault(e => e.ToString() == value);
+ _rconSettingsDp.Save();
+ }
+
[RelayCommand]
public void PageLoad(RoutedEventArgs e)
{
diff --git a/NectarRCON/Views/Pages/MainPage.xaml b/NectarRCON/Views/Pages/MainPage.xaml
index feaf82f..9d4dcb2 100644
--- a/NectarRCON/Views/Pages/MainPage.xaml
+++ b/NectarRCON/Views/Pages/MainPage.xaml
@@ -13,6 +13,9 @@
+
+
+
@@ -78,5 +81,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NectarRCON/Views/Pages/SettingPage.xaml b/NectarRCON/Views/Pages/SettingPage.xaml
index 2fb6d5c..acbdfba 100644
--- a/NectarRCON/Views/Pages/SettingPage.xaml
+++ b/NectarRCON/Views/Pages/SettingPage.xaml
@@ -28,9 +28,15 @@
+
+
@@ -72,6 +78,32 @@
+
+
+
+
+
+
+
+
+
+
+