Repository: zkhssb/NectarRCON Branch: master Commit: e7349a9bd853 Files: 112 Total size: 224.2 KB Directory structure: gitextract_0vsxp36e/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── dotnet-desktop.yml ├── .gitignore ├── LICENSE.txt ├── NectarRCON/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── Converters/ │ │ ├── BoolToVisibilityConverter.cs │ │ └── IntToVisibilityConverter.cs │ ├── Dp/ │ │ ├── DpFile.cs │ │ └── RconSettingsDp.cs │ ├── Entity/ │ │ ├── ClearLogValueMessage.cs │ │ ├── Config.cs │ │ ├── ETheme.cs │ │ ├── Group.cs │ │ ├── MainPageLoadValueMessage.cs │ │ ├── ServerInformation.cs │ │ └── ServerPassword.cs │ ├── Interfaces/ │ │ ├── IConfigService.cs │ │ ├── IConnectingDialogService.cs │ │ ├── IGroupService.cs │ │ ├── IHistoryService.cs │ │ ├── ILanguageService.cs │ │ ├── ILogService.cs │ │ ├── IMessageBoxService.cs │ │ ├── IRconConnectionInfoService.cs │ │ ├── IServerInformationService.cs │ │ └── IServerPasswordService.cs │ ├── Models/ │ │ ├── GroupModel.cs │ │ ├── GroupServerItemModel.cs │ │ └── ServerModel.cs │ ├── NectarRCON.csproj │ ├── Rcon/ │ │ ├── IRconConnection.cs │ │ ├── RconMultiConnection.cs │ │ └── RconSingleConnection.cs │ ├── Resources/ │ │ └── Languages/ │ │ ├── en_us.xaml │ │ ├── zh_cn.xaml │ │ └── zh_tw.xaml │ ├── Services/ │ │ ├── ApplicationHostService.cs │ │ ├── ConfigService.cs │ │ ├── ConnectingDialogService.cs │ │ ├── GroupService.cs │ │ ├── HistoryService.cs │ │ ├── LanguageService.cs │ │ ├── LogService.cs │ │ ├── MessageBoxService.cs │ │ ├── RconConnectionInfoService.cs │ │ ├── ServerInformationService.cs │ │ └── ServerPasswordService.cs │ ├── ViewModels/ │ │ ├── AboutPageViewModel.cs │ │ ├── AddGroupPageViewModel.cs │ │ ├── AddServerWindowViewModel.cs │ │ ├── EditPasswordWindowViewModel.cs │ │ ├── EditServerWindowViewModel.cs │ │ ├── GroupPageViewModel.cs │ │ ├── JoinGroupWindowViewModel.cs │ │ ├── MainPageViewModel.cs │ │ ├── MainWindowViewModel.cs │ │ ├── ServersPageViewModel.cs │ │ └── SettingPageViewModel.cs │ ├── Views/ │ │ └── Pages/ │ │ ├── AboutPage.xaml │ │ ├── AboutPage.xaml.cs │ │ ├── AddGroupPage.xaml │ │ ├── AddGroupPage.xaml.cs │ │ ├── GroupPage.xaml │ │ ├── GroupPage.xaml.cs │ │ ├── MainPage.xaml │ │ ├── MainPage.xaml.cs │ │ ├── ServersPage.xaml │ │ ├── ServersPage.xaml.cs │ │ ├── SettingPage.xaml │ │ └── SettingPage.xaml.cs │ ├── Windows/ │ │ ├── AddServerWindow.xaml │ │ ├── AddServerWindow.xaml.cs │ │ ├── EditPasswordWindow.xaml │ │ ├── EditPasswordWindow.xaml.cs │ │ ├── EditServerWindow.xaml │ │ ├── EditServerWindow.xaml.cs │ │ ├── JoinGroupWindow.xaml │ │ ├── JoinGroupWindow.xaml.cs │ │ ├── MainWindow.xaml │ │ └── MainWindow.xaml.cs │ └── app.manifest ├── NectarRCON.Adapter.Minecraft/ │ ├── MinecraftRconClient.cs │ ├── NectarRCON.Adapter.Minecraft.csproj │ ├── Packet.cs │ ├── PacketEncoder.cs │ └── README.md ├── NectarRCON.Core/ │ ├── Helper/ │ │ ├── AdapterHelpers.cs │ │ ├── DNSHelpers.cs │ │ └── Win32Helper.cs │ └── NectarRCON.Core.csproj ├── NectarRCON.Export/ │ ├── Client/ │ │ └── BaseTcpClient.cs │ ├── Interfaces/ │ │ ├── IRconAdapter.cs │ │ └── IRconClient.cs │ └── NectarRCON.Export.csproj ├── NectarRCON.Tests/ │ ├── Adapter/ │ │ └── MinecraftRconTest.cs │ ├── DNSHelperTests.cs │ ├── GroupServiceTests.cs │ ├── MessageBoxServiceTests.cs │ ├── NectarRCON.Tests.csproj │ ├── UpdaterTests.cs │ └── Usings.cs ├── NectarRCON.Updater/ │ ├── AppVersion.cs │ ├── GithubUpdater.cs │ ├── IUpdater.cs │ ├── Model/ │ │ ├── Asset.cs │ │ └── Release.cs │ └── NectarRCON.Updater.csproj ├── NectarRCON.sln ├── README.md └── README_EN.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/workflows/dotnet-desktop.yml ================================================ # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow will build, test, sign and package a WPF or Windows Forms desktop application # built on .NET Core. # To learn how to migrate your existing application to .NET Core, # refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework # # To configure this workflow: # # 1. Configure environment variables # GitHub sets default environment variables for every workflow run. # Replace the variables relative to your project in the "env" section below. # # 2. Signing # Generate a signing certificate in the Windows Application # Packaging Project or add an existing signing certificate to the project. # Next, use PowerShell to encode the .pfx file using Base64 encoding # by running the following Powershell script to generate the output string: # # $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte # [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' # # Open the output file, SigningCertificate_Encoded.txt, and copy the # string inside. Then, add the string to the repo as a GitHub secret # and name it "Base64_Encoded_Pfx." # For more information on how to configure your signing certificate for # this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing # # Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". # See "Build the Windows Application Packaging project" below to see how the secret is used. # # For more information on GitHub Actions, refer to https://github.com/features/actions # For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, # refer to https://github.com/microsoft/github-actions-for-desktop-apps name: .NET Core Desktop on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: strategy: matrix: configuration: [Debug, Release] runs-on: windows-latest # For a list of available runner types, refer to # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install .NET Core uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## 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 *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Oo]ut/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) [year] [fullname] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NectarRCON/App.xaml ================================================  1.0.0-beta5 ================================================ FILE: NectarRCON/App.xaml.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NectarRCON.Interfaces; using NectarRCON.Rcon; using NectarRCON.Services; using NectarRCON.ViewModels; using NectarRCON.Windows; using System; using System.IO; using System.Linq; using System.Text; using System.Windows; using Microsoft.Extensions.Logging; using NectarRCON.Dp; using Serilog; using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Services; namespace NectarRCON; /// /// Interaction logic for App.xaml /// public partial class App { private static readonly ILoggerFactory LoggerFactory = new LoggerFactory(); private static string LogFileName = $"logs/program/log{DateTime.Now:yyyyMMddhhmm}.log"; private static readonly IHost Host = Microsoft.Extensions.Hosting.Host .CreateDefaultBuilder() .ConfigureLogging(builder => { Log.Logger = new LoggerConfiguration() .WriteTo.File(LogFileName, rollingInterval: RollingInterval.Infinite, flushToDiskInterval: TimeSpan.FromSeconds(1)) .CreateLogger(); builder.AddSerilog(); }) .ConfigureServices((context, services) => { services.AddSingleton(LoggerFactory); services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); services.AddHostedService(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Rcon Connections services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddTransient(); services.AddTransient(); }).Build(); public static T GetService() where T : class { return (Host.Services.GetService(typeof(T)) as T)!; } public static T GetService(Type type) where T : class { return Host.Services.GetServices().FirstOrDefault(t => t.GetType() == type)!; } private async void OnStartup(object sender, StartupEventArgs e) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); foreach (var rconEncoding in Enum.GetValues()) { rconEncoding.GetEncoding(); } AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; await Host.StartAsync(); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var exception = e.ExceptionObject as Exception; Log.Error("未经处理的异常: {0}", exception); MessageBox.Show( exception + $"\n\n程序遇到异常,即将退出!\n建议前往Github提交Issue\n请前往日志查看详细信息!\nCheck log: {Path.Combine(Environment.CurrentDirectory, LogFileName).Replace("\\", "/")}", "程序崩溃", MessageBoxButton.OK, MessageBoxImage.Error); Environment.Exit(1); } private async void OnExit(object sender, ExitEventArgs e) { await Host.StopAsync(); Host.Dispose(); } } ================================================ FILE: NectarRCON/AssemblyInfo.cs ================================================ using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located //(used if a resource is not found in the page, // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] ================================================ FILE: NectarRCON/Converters/BoolToVisibilityConverter.cs ================================================ using System; using System.Windows.Data; using System.Windows; using System.Globalization; namespace NectarRCON.Converters { public class BoolToVisibilityConverter : IValueConverter { public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) { if(value is bool boolValue) { return boolValue ? Visibility.Visible : Visibility.Hidden; } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } ================================================ FILE: NectarRCON/Converters/IntToVisibilityConverter.cs ================================================ using System; using System.Windows.Data; using System.Windows; using System.Globalization; namespace NectarRCON.Converters { public class IntToVisibilityConverter : IValueConverter { public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) { if(value is int intValue) { if(parameter is string boolStringValue) { bool boolValue = System.Convert.ToBoolean(boolStringValue); // 如果参数为False // <= 0 ? Visible : Hidden // 否则反过来 return boolValue ? (intValue <= 0 ? Visibility.Hidden : Visibility.Visible) : (intValue <= 0 ? Visibility.Visible : Visibility.Hidden); } return intValue <= 0 ? Visibility.Visible : Visibility.Hidden; } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } ================================================ FILE: NectarRCON/Dp/DpFile.cs ================================================ 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)]; } } ================================================ FILE: NectarRCON/Dp/RconSettingsDp.cs ================================================ 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; } ================================================ FILE: NectarRCON/Entity/ClearLogValueMessage.cs ================================================ namespace NectarRCON.Models; public partial class ClearLogValueMessage { } ================================================ FILE: NectarRCON/Entity/Config.cs ================================================ using System.Text.Json.Serialization; namespace NectarRCON.Models; public partial class Config { [JsonPropertyName("language")] public string LanguageName { get; set; } = string.Empty; [JsonPropertyName("theme")] public ETheme Theme { get; set; } [JsonPropertyName("command_list")] public int CommandLimit; } ================================================ FILE: NectarRCON/Entity/ETheme.cs ================================================ namespace NectarRCON.Models; public enum ETheme : short { System = 0, Dark = 1, Light = 2 } ================================================ FILE: NectarRCON/Entity/Group.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; namespace NectarRCON.Models { public class Group { [JsonPropertyName("id")] public required string Id { get; set; } [JsonPropertyName("name")] public required string Name { get; set; } [JsonPropertyName("server_list")] public required List Servers { get; set; } } } ================================================ FILE: NectarRCON/Entity/MainPageLoadValueMessage.cs ================================================ namespace NectarRCON.Models; public partial class MainPageLoadValueMessage { public required bool IsLoaded { get; init; } } ================================================ FILE: NectarRCON/Entity/ServerInformation.cs ================================================ using System.Text.Json.Serialization; namespace NectarRCON.Models; public class ServerInformation { [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("address")] public string Address { get; set; } = string.Empty; [JsonPropertyName("adapter")] public string Adapter { get; set; } = string.Empty; // 适配类名 [JsonPropertyName("port")] public ushort Port { get; set; } = 0; [JsonIgnore] public string DisplayAddress { get => $"{Address}:{Port}"; } } ================================================ FILE: NectarRCON/Entity/ServerPassword.cs ================================================ using System.Text.Json.Serialization; namespace NectarRCON.Models; public class ServerPassword { [JsonPropertyName("password")] public string Password { get; set; } = string.Empty; [JsonPropertyName("is_empty")] public bool IsEmpty { get; set; } [JsonPropertyName("name")] public string ServerName { get; set; } = string.Empty; } ================================================ FILE: NectarRCON/Interfaces/IConfigService.cs ================================================ using NectarRCON.Models; namespace NectarRCON.Interfaces; public interface IConfigService { void Save(); Config GetConfig(); } ================================================ FILE: NectarRCON/Interfaces/IConnectingDialogService.cs ================================================ using System.Windows.Controls; namespace NectarRCON.Interfaces; public interface IConnectingDialogService { void SetDialog(Grid grid); void Show(); void Close(); } ================================================ FILE: NectarRCON/Interfaces/IGroupService.cs ================================================ using NectarRCON.Models; using System.Collections.Generic; namespace NectarRCON.Interfaces; public interface IGroupService { /// /// 获取所有组 /// IReadOnlyList GetGroups(); /// /// 通过组名字获取组 /// /// 组名 Group? FindGroup(string name); /// /// 通过组Id获取组 /// /// 组Id Group? GetGroup(string groupId); /// /// 删除组 /// /// 组Id void Delete(string groupId); /// /// 添加一个组 /// string Add(Group group); } ================================================ FILE: NectarRCON/Interfaces/IHistoryService.cs ================================================ namespace NectarRCON.Interfaces; public class HistoryNode { public string? Cmd { get; set; } public HistoryNode? Prev { get; set; } public HistoryNode? Next { get; set; } } public interface IHistoryService { /// /// 获取前一条命令 /// /// /// HistoryNode? Prev(HistoryNode? current); /// /// 获取后一条命令 /// /// /// HistoryNode? Next(HistoryNode? current); /// /// 输入命令 /// /// HistoryNode? InputCmd(string cmd); } ================================================ FILE: NectarRCON/Interfaces/ILanguageService.cs ================================================ using System.Collections.Generic; using System.Windows; namespace NectarRCON.Interfaces; public interface ILanguageService { public void Refresh(); public ResourceDictionary GetSelectedLanguage(); public Dictionary GetLanguages(); public void SelectLanguage(); public void SelectLanguage(string languageName, bool name); public string GetKey(string key); } ================================================ FILE: NectarRCON/Interfaces/ILogService.cs ================================================ using NectarRCON.Models; namespace NectarRCON.Interfaces; public interface ILogService { string GetText(); string Log(string message); void Clear(); void SetServer(ServerInformation server); void SetGroup(string groupId); } ================================================ FILE: NectarRCON/Interfaces/IMessageBoxService.cs ================================================ using System; using System.Windows; namespace NectarRCON.Interfaces { public interface IMessageBoxService { void Show(string message); void Show(string message, string title); MessageBoxResult Show(string message, string title, MessageBoxButton button); void Show(string message, string title, MessageBoxImage image); MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage image); void Show(Exception exception, string? information); } } ================================================ FILE: NectarRCON/Interfaces/IRconConnectionInfoService.cs ================================================ using NectarRCON.Models; using System.Collections.Generic; namespace NectarRCON.Interfaces; public interface IRconConnectionInfoService { /// /// 清除所有连接信息 /// void Clear(); /// /// 添加一个服务器 /// void AddInformation(string serverName); /// /// 获取最后一个被添加的服务器 /// ServerInformation? GetLastInformation(); /// /// 获取所有的信息 /// IReadOnlyList GetInformation(); /// /// 是否拥有多个服务器信息 /// public bool HasMultipleInformation => GetInformation().Count > 1; } ================================================ FILE: NectarRCON/Interfaces/IServerInformationService.cs ================================================ using NectarRCON.Models; using System.Collections.Generic; namespace NectarRCON.Interfaces; public interface IServerInformationService { ServerInformation? GetServer(string name); List GetServers(); void AddServer(ServerInformation server); void Save(); void RemoveServer(string name); void Update(string name, ServerInformation newInfo); bool ServerIsExist(string name); } ================================================ FILE: NectarRCON/Interfaces/IServerPasswordService.cs ================================================ using NectarRCON.Models; namespace NectarRCON.Interfaces; public interface IServerPasswordService { void Save(); bool IsExist(ServerInformation server); void Set(ServerInformation server, string? password, bool? isEmpty); void Remove(ServerInformation server); ServerPassword? Get(ServerInformation server); ServerInformation GetSelect(); void Select(ServerInformation server); } ================================================ FILE: NectarRCON/Models/GroupModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Rcon; using NectarRCON.Services; using NectarRCON.ViewModels; using NectarRCON.Windows; using System; using System.Collections.ObjectModel; using System.Linq; using System.Net.Sockets; using System.Security.Authentication; using System.Threading.Tasks; using System.Windows; using System.Xml.Linq; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.Models; public partial class GroupModel:ObservableObject { private readonly IGroupService _groupService; private readonly ILanguageService _languageService; private readonly IRconConnectionInfoService _connectionInfoService; private IRconConnection _rconConnection; private readonly INavigationService _navigationService; private readonly IMessageBoxService _messageService; private readonly IConnectingDialogService _connectingDialogService; private readonly ILogService _logService; [ObservableProperty] private string _id; [ObservableProperty] private GroupPageViewModel _baseModel; [ObservableProperty] private string _name; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConnectCommand))] private ObservableCollection _servers = new(); #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 public GroupModel(string name, GroupPageViewModel baseModel) #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 { _groupService = App.GetService(); _languageService = App.GetService(); _connectionInfoService = App.GetService(); _rconConnection = App.GetService(typeof(RconMultiConnection)); _navigationService = App.GetService(); _messageService = App.GetService(); _connectingDialogService = App.GetService(); _logService = App.GetService(); _baseModel = baseModel; Name = name; Id = _groupService.FindGroup(name)?.Id ?? throw new InvalidOperationException(); Refresh(); } private void Refresh() { Group? group = _groupService.GetGroup(_id); if (group != null) { Servers.Clear(); Name = group.Name; foreach (var server in group.Servers) { Servers.Add(new(server, this)); } } } [RelayCommand] public void ItemRemove(string name) { Servers.Remove(Servers.Where(s => s.Name == name).First()); Save(); ConnectCommand.NotifyCanExecuteChanged(); } private bool ConnectCommandCanExecute() => Servers.Count >= 1; [RelayCommand(CanExecute = nameof(ConnectCommandCanExecute))] public void Connect() { _logService.SetGroup(_id); _connectionInfoService.Clear(); _connectingDialogService.Show(); foreach (var server in Servers) { _connectionInfoService.AddInformation(server.Name); } if (_connectionInfoService.HasMultipleInformation) { _rconConnection = App.GetService(typeof(RconMultiConnection)); } else { _rconConnection = App.GetService(typeof(RconSingleConnection)); } _navigationService.Navigate(0); } [RelayCommand] public void Add() { JoinGroupWindow window = new(); foreach (var server in Servers) { window.AddBlackList(server.Name); } window.ShowDialog(); window.Close(); if (window.SelectedServer != null) { Servers.Add(new(window.SelectedServer, this)); Save(); } ConnectCommand.NotifyCanExecuteChanged(); } private void Save() { Group group = _groupService.GetGroup(_id)!; group.Servers.Clear(); foreach (var server in Servers) { group.Servers.Add(server.Name); } _groupService.Delete(group.Id); _groupService.Add(group); Refresh(); } } ================================================ FILE: NectarRCON/Models/GroupServerItemModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; namespace NectarRCON.Models; public partial class GroupServerItemModel : ObservableObject { [ObservableProperty] private string _name; [ObservableProperty] private GroupModel _baseModel; #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 public GroupServerItemModel(string name, GroupModel baseModel) #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 { Name = name; BaseModel = baseModel; } } ================================================ FILE: NectarRCON/Models/ServerModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Rcon; using NectarRCON.Services; using NectarRCON.ViewModels; using NectarRCON.Windows; using System.Net.Sockets; using System.Security.Authentication; using System.Threading.Tasks; using System.Windows; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.Models { public partial class ServerModel : ObservableObject { private readonly IServerPasswordService _passwordService; private readonly IServerInformationService _informationService; private readonly IConnectingDialogService _connectingDialogService; private readonly IRconConnectionInfoService _connectionInfoService; private readonly INavigationService _navigationService; private readonly IServerPasswordService _serverPasswordService; private readonly ILogService _logService; private readonly ILanguageService _languageService; private readonly ServerInformation _information; private readonly ServersPageViewModel _viewModel; private readonly IRconConnection _rconConnection; [ObservableProperty] private string _name; [ObservableProperty] private string _address; #pragma warning disable CS8618 // CS8618 你说的对,但是我已经初始化了 public ServerModel(ServerInformation information, ServersPageViewModel viewModel) #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 { _passwordService = App.GetService(); _informationService = App.GetService(); _languageService = App.GetService(); _connectingDialogService = App.GetService(); _rconConnection = App.GetService(typeof(RconSingleConnection)); _connectionInfoService = App.GetService(); _navigationService = App.GetService(); _serverPasswordService = App.GetService(); _logService = App.GetService(); _information = information; _viewModel = viewModel; Name = information.Name; Address = $"{information.Address}:{information.Port}"; } private void EditPass() { _passwordService.Select(_information); EditPasswordWindow window = new EditPasswordWindow(); window.ShowDialog(); } [RelayCommand] public void EditPassword() { EditPass(); } [RelayCommand] public void Edit() { _passwordService.Select(_information); EditServerWindow window = new EditServerWindow(); window.ShowDialog(); _viewModel.Refresh(); } [RelayCommand] public void Delete() { var result = MessageBox.Show(_languageService.GetKey("ui.server_page.confirm"), _languageService.GetKey("ui.server_page.menu.delete"), MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { _informationService.RemoveServer(Name); _informationService.Save(); _passwordService.Remove(_information); _passwordService.Save(); _viewModel.Refresh(); } } [RelayCommand] public void Connect() { _logService.SetServer(_information); if (_rconConnection.IsConnecting()) return; if (_rconConnection.IsConnected()) { _rconConnection.Close(); } var server = _passwordService.Get(_information); if (server == null) { EditPass(); } else if (server.Password == null && !server.IsEmpty) { EditPass(); } _connectionInfoService.Clear(); _connectionInfoService.AddInformation(Name); _serverPasswordService.Select(_information); _navigationService.Navigate(0); } } } ================================================ FILE: NectarRCON/NectarRCON.csproj ================================================  WinExe net7.0-windows enable true Resources\Icon.ico app.manifest 1.0.0-beta5 12 PreserveNewest ================================================ FILE: NectarRCON/Rcon/IRconConnection.cs ================================================ using NectarRCON.Models; namespace NectarRCON.Rcon; public delegate void MessageEvent(ServerInformation information, string message); public delegate void RconEvent(ServerInformation information); public interface IRconConnection { event MessageEvent OnMessage; event RconEvent OnClosed; event RconEvent OnConnected; event RconEvent OnConnecting; void Connect(); void Send(string command); void Close(); bool IsConnected(); bool IsConnecting(); } ================================================ FILE: NectarRCON/Rcon/RconMultiConnection.cs ================================================ using NectarRCON.Core.Helper; using NectarRCON.Export.Interfaces; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Rcon; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Sockets; using System.Security.Authentication; using System.Windows.Controls; using NectarRCON.Dp; using Serilog; namespace NectarRCON.Services { /// /// Rcon多客户端连接服务 /// internal class RconMultiConnection( IServerPasswordService serverPasswordService, ILanguageService languageService, IRconConnectionInfoService rconConnectionInfoService, IMessageBoxService messageBoxService) : IRconConnection, IDisposable { private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton(); public event MessageEvent? OnMessage; public event RconEvent? OnClosed; public event RconEvent? OnConnected; public event RconEvent? OnConnecting; private readonly Dictionary _connections = new(); private bool _isConnecting; public void Close() { foreach(var connection in _connections) { connection.Value.Dispose(); } _connections.Clear(); } public void Connect() { Close(); _isConnecting = true; var servers = rconConnectionInfoService.GetInformation(); try { foreach (ServerInformation info in servers) { OnConnecting?.Invoke(info); // 准备开始连接,先解析这个地址有没有SRV记录 string address = DNSHelpers.SRVQuery(info.Address); if (string.IsNullOrEmpty(address)) // 如果没有SRV记录,就按照原来的样子设置服务器 address = $"{info.Address.Replace("localhost", "127.0.0.1")}:{info.Port}"; ServerPassword? serverPassword = serverPasswordService.Get(info); // 从设置中读取Rcon密码 string password = serverPassword?.Password ?? string.Empty; // 创建对应的Rcon客户端实例 // 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒) IRconAdapter adapter = AdapterHelpers.CreateAdapterInstance(info.Adapter) ?? throw new InvalidOperationException($"adapter not found: {info.Adapter}"); var host = address.Split(":")[0]; var port = int.Parse(address.Split(":")[1]); Log.Information("[RconMultiConnection] Connecting to {host}:{port}", host, port); try { adapter.Connect(host, port); if (!adapter.Authenticate(password)) throw new AuthenticationException($"Server: \"{info.Name}\"\n"); OnConnected?.Invoke(info); } catch (AuthenticationException) { throw; } catch (Exception ex) { OnClosed?.Invoke(info); messageBoxService.Show(ex, $"Server: \"{info.Name}\""); } //设置编码 adapter.SetEncoding(_settingsDp.Encoding.GetEncoding()); _connections.Add(info, adapter); } } finally { _isConnecting = false; } } public bool IsConnected() => _connections.Any(e => e.Value.IsConnected); public bool IsConnecting() => _isConnecting; public void Send(string command) { foreach(var value in _connections) { IRconAdapter connection = value.Value; ServerInformation info = value.Key; if (connection.IsConnected) { try { string result = connection.Run(command); OnMessage?.Invoke(info, result); }catch (IOException) { connection.Dispose(); // 内部会调用Close OnClosed?.Invoke(info); OnMessage?.Invoke(info, languageService.GetKey("service.rcon.offline")); } } else { OnMessage?.Invoke(info, languageService.GetKey("service.rcon.offline")); } } } public void Dispose() { Close(); } } } ================================================ FILE: NectarRCON/Rcon/RconSingleConnection.cs ================================================ using NectarRCON.Core.Helper; using NectarRCON.Export.Interfaces; using NectarRCON.Interfaces; 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; using Serilog; namespace NectarRCON.Services; public class RconSingleConnection( IServerPasswordService serverPasswordService, ILanguageService languageService, IRconConnectionInfoService rconConnectionInfoService) : IRconConnection { private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton(); private IRconAdapter? _rconClient = null; private bool _connecting; public event MessageEvent? OnMessage; public event RconEvent? OnClosed; public event RconEvent? OnConnected; public event RconEvent? OnConnecting; private ServerInformation _serverInformation = new(); public void Close() { lock (this) { if (IsConnected() && !IsConnecting()) { _connecting = false; _rconClient?.Dispose(); _rconClient = null; OnClosed?.Invoke(_serverInformation); } } } public void Connect() { ServerInformation info = rconConnectionInfoService.GetLastInformation() ?? throw new ArgumentNullException("Internal error, please try again"); _connecting = true; OnConnecting?.Invoke(info); try { if (IsConnected() && _rconClient != null) Close(); // 准备开始连接,先解析这个地址有没有SRV记录 string address = DNSHelpers.SRVQuery(info.Address); if (string.IsNullOrEmpty(address)) // 如果没有SRV记录,就按照原来的样子设置服务器 address = $"{info.Address.Replace("localhost", "127.0.0.1")}:{info.Port}"; ServerPassword? serverPassword = serverPasswordService.Get(info); string password = serverPassword?.Password ?? string.Empty; // 创建对应的Rcon客户端实例 // 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒) _rconClient = AdapterHelpers.CreateAdapterInstance(info.Adapter) ?? throw new InvalidOperationException($"adapter not found: {info.Adapter}"); _rconClient.SetEncoding(_settingsDp.Encoding.GetEncoding()); var host = address.Split(":")[0]; var port = int.Parse(address.Split(":")[1]); Log.Information("[RconSingleConnection] Connecting to {host}:{port}", host, port); _rconClient.Connect(host, port); // 连接 if (!_rconClient.Authenticate(password)) throw new AuthenticationException(); OnConnected?.Invoke(info); _serverInformation = info; } catch (FormatException ex) { MessageBox.Show(languageService.GetKey("ui.text.format_exception") .Replace("%s", ex.Message), languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } finally { _connecting = false; } } public bool IsConnected() => _rconClient?.IsConnected ?? false; public bool IsConnecting() => _connecting; public void Send(string command) { if (IsConnected() && _rconClient != null) { try { string result = _rconClient.Run(command); OnMessage?.Invoke(_serverInformation, result); } catch (Exception ex) { Close(); if (ex is SocketException or IOException && _settingsDp.AutoReconnect) { Log.Information("Rcon连接已经断开,开始自动重连..."); try { Connect(); } catch (Exception e) { Log.Error("自动重连失败 {err}", e); } if (IsConnected()) { try { Log.Information("尝试重发命令..."); string result = _rconClient.Run(command); OnMessage?.Invoke(_serverInformation, result); Log.Information("重发命令成功 -> {command}", command); return; } catch(Exception e) { Log.Error("尝试重发命令失败 {err}", e); } } } MessageBox.Show($"{languageService.GetKey("text.error")}\n{ex.Message}", ex.GetType().FullName, MessageBoxButton.OK, MessageBoxImage.Error); } } else { Close(); MessageBox.Show($"{languageService.GetKey("text.server.not_connect.text")}", languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } } ================================================ FILE: NectarRCON/Resources/Languages/en_us.xaml ================================================  en_us English US(en_us) Server Servers Groups Log Clear Program Settings About CheckUpdate RCONManager UI Language UI Settings UI Theme Dark Theme Light Theme System Theme Rcon passwords... Command Record Limit Rcon Auto Reconnect KeepConnectionWindowOpen Encoding Clear Program Logs Do you want to clear program logs? Connect Edit Delete EditPassword Servers Add Server Search by server name... This action will delete the server! (including recorded password)\nAre you sure you want to delete? Add Server Name Address Port Add Cancel A server with the same name already exists, please change your name! Server name or server address cannot be empty, please change it! Afdian MCBBS Github An error was encountered while parsing the destination address: %s\nPlease check whether the server address is legal! Edit Password Password Empty Pass Run Successful! Offline All clients are offline Reconnect Back to Servers Edit An exception occurred during operation, and the application is about to end. If you cannot understand the error below, please submit an issue on Github and describe what happened in detail Error Warning Information Ok Cancel Not Available Connect Remove Delete Add Connecting... Failed to connect to the server! Unable to connect to the remote server: %s\nPlease check that the server RCON address is configured correctly, and check that the server has enabled the RCON option! The Rcon password is incorrect, please check whether your server password is correct! Connected to the server and authenticated successfully! The connection is disconnected! Starting connection You cannot connect to the server, so you cannot use the remote command function, please try to go back to the server list and reconnect to this server! Unable to instantiate server list /servers.json %s\nClick "Yes" to reset the server list (you need to re-add the server, but the recorded password will not disappear)\nClick "Cancel" to close the program! Unable to instantiate server list /servers.json %s\nClick "Yes" to close the program! Unable to save file /servers.json\nIf you just added a server, please note that this save is not in effect!\n%s Unable to resolve password list /passwords.json %s\nClick "Yes" to reset the password list (password needs to be reset, but the recorded server will not disappear)\nClick "Cancel" to close the program! Unable to resolve password list /passwords.json %s\nClick "Yes" to close the program! Unable to save file /passwords.json\nIf you have just set a password, please note that this save is not in effect!\n%s Unable to resolve configuration file /config.json JSON parsing failed: %s\nClick "Yes" to reset the profile!\nClick "No" to exit the program! Unable to parse configuration file /config.json: %s\nClick "Yes" to reset the profile!\nClick "No" to exit the program! Unable to save configuration file /config.json: %s\nIf you have just changed the configuration file, please note that the save does not take effect! An error occurred while trying to read and parse the group file "{0}", trying to delete or repair the file may resolve this error! The same GroupId already exists (existing value: {0}, duplicate value: {1}) The same Name already exists (existing id: {0}, duplicate id: {1}) A group with the same internal ID already exists in the file, try creating it again The {0} file name does not match the internal ID {1} The same Name already exists Click '+ Add' to add a group! After adding a group, you can broadcast a command to all servers in that group simultaneously. This group has no servers. Click '+ Add' to add a server. Do you want to delete group {0}? New Group Group Name Select Server No server available Offline ================================================ FILE: NectarRCON/Resources/Languages/zh_cn.xaml ================================================  zh_cn 简体中文(zh_cn) 服务器 服务器列表 服务器分组 日志 清空日志 程序 设置 关于 检查更新 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}? 新建组 组名 选择服务器 没有可用服务器 离线 ================================================ FILE: NectarRCON/Resources/Languages/zh_tw.xaml ================================================  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}? 新建分組 分組名稱 選擇伺服器 沒有可用的伺服器 離線 ================================================ FILE: NectarRCON/Services/ApplicationHostService.cs ================================================ using Microsoft.Extensions.Hosting; using NectarRCON.Core.Helper; using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using Microsoft.Extensions.Logging; using Serilog; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.Services; public class ApplicationHostService( IServiceProvider serviceProvider, INavigationService navigationService, IThemeService themeService, ILanguageService languageService, IConfigService configService) : IHostedService { private readonly INavigationService _navigationService = navigationService; private INavigationWindow? _navigationWindow; public async Task StartAsync(CancellationToken cancellationToken) { Log.Information("Starting NectarRCON..."); await HandleActivationAsync(); } public async Task StopAsync(CancellationToken cancellationToken) { await Task.CompletedTask; } private void LoadConfig() { switch (configService.GetConfig().Theme) { case ETheme.System: themeService.SetTheme(Win32Helper.GetWindowsTheme() ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light); break; case ETheme.Dark: themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Dark); break; case ETheme.Light: themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Light); break; default: break; } if (configService.GetConfig().LanguageName == string.Empty) { languageService.SelectLanguage(); configService.GetConfig().LanguageName = languageService.GetKey("file.language"); configService.Save(); } else { languageService.SelectLanguage(configService.GetConfig().LanguageName, false); } } private async Task HandleActivationAsync() { await Task.CompletedTask; if (!Application.Current.Windows.OfType().Any()) { _navigationWindow = (serviceProvider.GetService(typeof(INavigationWindow)) as INavigationWindow)!; Log.Information("Show MainWindow..."); _navigationWindow!.ShowWindow(); } LoadConfig(); await Task.CompletedTask; } } ================================================ FILE: NectarRCON/Services/ConfigService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.Diagnostics; using System.IO; using System.Text.Json; using System.Windows; namespace NectarRCON.Services; public partial class ConfigService : IConfigService { private ILanguageService _languageService; private Config _config = new(); public ConfigService(ILanguageService languageService) { _languageService = languageService; if (!File.Exists("./config.json")) Save(); try { _config = JsonSerializer.Deserialize(File.ReadAllText("./config.json")) ?? new Config(); } catch (Exception ex) { var resutl = MessageBox.Show(_languageService.GetKey(ex.GetType() == typeof(JsonException) ? "text.config.json_error" : "text.config.error") .Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.YesNo, MessageBoxImage.Error); if (resutl == MessageBoxResult.Yes) { _config = new(); Save(); } else { Process.GetCurrentProcess().Kill(); } } } public Config GetConfig() => _config; public void Save() { try { File.WriteAllText("./config.json", JsonSerializer.Serialize(_config)); } catch (Exception ex) { MessageBox.Show(_languageService.GetKey("text.config.save_error") .Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } } ================================================ FILE: NectarRCON/Services/ConnectingDialogService.cs ================================================ using NectarRCON.Interfaces; using System.Windows; using System.Windows.Controls; namespace NectarRCON.Services; internal class ConnectingDialogService : IConnectingDialogService { private Grid? _dialog; public void Close() { if (null == _dialog) return; _dialog.Visibility = Visibility.Hidden; } public void SetDialog(Grid grid) { _dialog = grid; } public void Show() { if (null == _dialog) return; _dialog.Visibility = Visibility.Visible; } } ================================================ FILE: NectarRCON/Services/GroupService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; namespace NectarRCON.Services { public class GroupService : IGroupService { private static string _groupPath = Path.Combine("./groups"); private readonly IMessageBoxService _messageBoxService; private readonly ILanguageService _languageService; private readonly Dictionary _groupMapping = new(); // id, group private readonly Dictionary _groupNameMapping = new(); // name, group public GroupService(IMessageBoxService messageBoxService, ILanguageService languageService) { _messageBoxService = messageBoxService; Refresh(); _languageService = languageService; } private void Refresh() { _groupMapping.Clear(); _groupNameMapping.Clear(); if (Directory.Exists(_groupPath)) { string[] jsonFiles = Directory.GetFiles(_groupPath, "*.json"); foreach (string jsonFile in jsonFiles) { try { using (FileStream fs = File.OpenRead(jsonFile)) { Group group = JsonSerializer.Deserialize(fs) ?? throw new InvalidDataException(jsonFile); if (group.Id.ToLower() != Path.GetFileNameWithoutExtension(jsonFile).ToLower()) throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.file_name_mismatch_exception"), jsonFile, group.Id)); if (_groupMapping.ContainsKey(group.Id)) throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.same_group_id_exception"), _groupMapping[group.Id].Name, group.Name)); if (_groupNameMapping.ContainsKey(group.Name)) throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.same_name_exception"), _groupMapping[group.Id].Id, group.Id)); _groupMapping.Add(group.Id, group); _groupNameMapping.Add(group.Name, group); } } catch (Exception ex) { _messageBoxService.Show(ex.Message, string.Format(_languageService.GetKey("groups.exception.invalid_data_exception"), ex.ToString())); } } } else { Directory.CreateDirectory(_groupPath); } } private void RemoveGroupInDict(string groupId) { if (_groupMapping.TryGetValue(groupId, out Group? group)) { _groupMapping.Remove(groupId); if (_groupNameMapping.ContainsKey(group.Name)) { _groupNameMapping.Remove(group.Name); } } } public string Add(Group group) { string filePath = Path.Combine(_groupPath, group.Id + ".json"); if (!File.Exists(filePath)) { if (_groupNameMapping.ContainsKey(group.Name)) throw new InvalidOperationException(_languageService.GetKey("groups.exception.name_already_exists_exception")); if (_groupMapping.ContainsKey(group.Id)) throw new InvalidOperationException(_languageService.GetKey("groups.exception.group_id_exists_exception")); File.WriteAllBytes(filePath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(group))); _groupMapping.Add(group.Id, group); _groupNameMapping.Add(group.Name,group); return group.Id; } throw new InvalidOperationException(_languageService.GetKey("groups.exception.group_id_exists_exception")); } public void Delete(string groupId) { string filePath = Path.Combine(_groupPath, groupId + ".json"); if (File.Exists(filePath)) { File.Delete(filePath); RemoveGroupInDict(groupId); } } public Group? FindGroup(string name) { _groupNameMapping.TryGetValue(name, out Group? group); return group; } public Group? GetGroup(string groupId) { _groupMapping.TryGetValue(groupId, out Group? group); return group; } public IReadOnlyList GetGroups() => _groupMapping.Select(s => s.Value).ToList(); } } ================================================ FILE: NectarRCON/Services/HistoryService.cs ================================================ using NectarRCON.Interfaces; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace NectarRCON.Services; public class HistoryService : IHistoryService { /// /// 双向链表保存历史记录 /// private HistoryNode? _head; private HistoryNode? _tail; private readonly Dictionary _map; /// /// 限制记录条数 /// private int _limit = 20; public HistoryService() { _map = []; if (!Path.Exists("./logs")) { Directory.CreateDirectory("./logs"); } var stream = File.Open($"./logs/history", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); using StreamReader reader = new StreamReader(stream); string? line; while ((line = reader.ReadLine()) != null) { line = line.Trim(); if (!string.IsNullOrEmpty(line)) { AppendNode(line); } } } public HistoryNode? InputCmd(string cmd) { var node = AppendNode(cmd); Save(); return node; } private HistoryNode? AppendNode(string cmd) { cmd = cmd.Trim(); HistoryNode node; if (_map.ContainsKey(cmd)) { // 已存在的记录,将其移动到链表尾部 node = _map[cmd]; if (node == _tail) { return null; } else if (node == _head) { _head = _head.Next; } if (node.Prev != null) { node.Prev.Next = node.Next; } if (node.Next != null) { node.Next.Prev = node.Prev; } node.Prev = null; node.Next = null; } else { node = new HistoryNode() { Cmd = cmd, }; _map.Add(cmd, node); } if (_head == null || _tail == null) { _head = _tail = node; } else { _tail.Next = node; node.Prev = _tail; _tail = node; } // 超出限制,移除链表头部记录 if (_map.Count() > _limit) { var head = _head; _head = _head.Next; if (_head != null) { _head.Prev = null; } if (!string.IsNullOrEmpty(head.Cmd)) { _map.Remove(head.Cmd); } } return null; } public HistoryNode? Next(HistoryNode? current) { if (current == null) { return null; } return current.Next; } public HistoryNode? Prev(HistoryNode? current) { if (current == null) { return _tail; } else if (current.Prev == null) { return current; } return current.Prev; } private void Save() { using var stream = File.Open($"./logs/history", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); stream.Seek(0, SeekOrigin.Begin); HistoryNode? node = _head; while (node != null) { if (!string.IsNullOrEmpty(node.Cmd)) { string cmd = node.Cmd + "\n"; stream.Write(Encoding.UTF8.GetBytes(cmd)); } node = node.Next; } } } ================================================ FILE: NectarRCON/Services/LanguageService.cs ================================================ using NectarRCON.Interfaces; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; namespace NectarRCON.Services; public class LanguageService : ILanguageService { private readonly Dictionary _defaultLanguages = new(); private Dictionary _languages = new(); private ResourceDictionary _selectedLanguage = new ResourceDictionary() { Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml", UriKind.RelativeOrAbsolute) }; private readonly ResourceDictionary _defLanguage = new ResourceDictionary() { Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml", UriKind.RelativeOrAbsolute) }; public LanguageService() { Refresh(); } public ResourceDictionary SelectedLanguage { get => _selectedLanguage; } public Dictionary Languages { get => _languages; } public Dictionary GetLanguages() => Languages; public ResourceDictionary GetSelectedLanguage() { return _selectedLanguage; } public void Refresh() { _languages.Clear(); // 从本地目录获取xaml string[] files = new string[0]; if (Directory.Exists("./languages/")) files = Directory.GetFiles("./languages/"); foreach (string file in files) { if (Path.GetExtension(file).ToLower() == ".xaml" || Path.GetExtension(file).ToLower() == ".xml") { ResourceDictionary resourceDictionary = new ResourceDictionary(); using (FileStream fs = File.OpenRead(file)) { resourceDictionary = (ResourceDictionary)System.Windows.Markup.XamlReader.Load(fs); } _languages.Add(Path.GetFileNameWithoutExtension(file), resourceDictionary); } } // 从内部文件加载 _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) { if (_languages.ContainsKey(language.Key)) _languages.Remove(language.Key); Languages.Add(language.Key, new ResourceDictionary() { Source = new Uri(language.Value, UriKind.RelativeOrAbsolute) }); } } public void SelectLanguage() { Application.Current.Resources.Remove(SelectedLanguage); string name = System.Globalization.CultureInfo.CurrentCulture.Name; SelectLanguage(name); } public void SelectLanguage(string languageName, bool name = false) { languageName = languageName.Replace("-", "_"); Application.Current.Resources.MergedDictionaries.Remove(SelectedLanguage); var language = Languages.Where(l => { if (name) { return (l.Value["file.name"].ToString() ?? "nullname") == languageName.ToLower(); } else { return l.Key.ToLower() == languageName.ToLower(); } }).FirstOrDefault(); if (null == language.Value) { _selectedLanguage = new ResourceDictionary() { Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/en_us.xaml", UriKind.RelativeOrAbsolute) }; if (Application.Current.Resources.Contains("en_us")) { Application.Current.Resources.Remove("en_us"); } Application.Current.Resources.MergedDictionaries.Add(_selectedLanguage); } else { _selectedLanguage = language.Value; Application.Current.Resources.MergedDictionaries.Add(language.Value); } } public string GetKey(string key) { string valuel; if (_selectedLanguage.Contains(key)) valuel = _selectedLanguage[key].ToString() ?? string.Empty; else if (_defLanguage.Contains(key)) valuel = _defLanguage[key].ToString() ?? string.Empty; else valuel = key; return valuel .Replace("\\n", "\n"); } } ================================================ FILE: NectarRCON/Services/LogService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.IO; using System.Text; using System.Text.Encodings.Web; using System.Text.RegularExpressions; using System.Windows; namespace NectarRCON.Services; public class LogService : ILogService { private FileStream? _logFileStream; public LogService() { if (!Path.Exists("./logs")) Directory.CreateDirectory("./logs"); } public void Clear() { _logFileStream?.SetLength(0); _logFileStream?.Flush(); } public string GetText() { using (MemoryStream ms = new MemoryStream()) { _logFileStream?.CopyTo(ms); return Encoding.UTF8.GetString(ms.ToArray()); } } public string Log(string message) { string logText = $"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {message}\n"; _logFileStream?.Write(Encoding.UTF8.GetBytes(logText)); _logFileStream?.Flush(); return logText; } public void SetServer(ServerInformation server) { OpenFile("server_" + UrlEncoder.Default.Encode(server.Name)); } public void SetGroup(string groupId) { OpenFile("group_" + UrlEncoder.Default.Encode(groupId)); } private void OpenFile(string fileName) { if (!File.Exists($"./logs/{fileName}.log")) File.Create($"./logs/{fileName}.log").Close(); _logFileStream?.Close(); _logFileStream?.Dispose(); _logFileStream = File.Open($"./logs/{fileName}.log", FileMode.Open, FileAccess.ReadWrite, FileShare.Read); } } ================================================ FILE: NectarRCON/Services/MessageBoxService.cs ================================================ using NectarRCON.Interfaces; using System; using System.Windows; namespace NectarRCON.Services; public class MessageBoxService : IMessageBoxService { private readonly ILanguageService _languageService; public MessageBoxService(ILanguageService languageService) { _languageService = languageService; } public void Show(string message) => MessageBox.Show(message, "NectarRcon", MessageBoxButton.OK, MessageBoxImage.Information); public void Show(string message, string title) => MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information); public MessageBoxResult Show(string message, string title, MessageBoxButton button) => MessageBox.Show(message, title, button, MessageBoxImage.Information); public void Show(string message, string title, MessageBoxImage image) => MessageBox.Show(message, title, MessageBoxButton.OK, image); public MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage image) => MessageBox.Show(message, title, button, image); public void Show(Exception exception, string? information = null) => MessageBox.Show($"{_languageService.GetKey("text.went_wrong")}\n{information}\n{exception}", "error", MessageBoxButton.OK, MessageBoxImage.Error); } ================================================ FILE: NectarRCON/Services/RconConnectionInfoService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.Collections.Generic; using System.Linq; namespace NectarRCON.Services; /// /// Rcon连接信息管理服务 /// public class RconConnectionInfoService : IRconConnectionInfoService { private readonly IServerInformationService _serverInformationService; private readonly List _serverInformation = new(); public RconConnectionInfoService(IServerInformationService serverInformationService) { _serverInformationService = serverInformationService; } public void AddInformation(string serverName) => _serverInformation.Add(_serverInformationService.GetServer(serverName) ?? throw new ArgumentNullException(nameof(serverName))); public void Clear() => _serverInformation.Clear(); public IReadOnlyList GetInformation() => _serverInformation; public ServerInformation? GetLastInformation() => _serverInformation.FirstOrDefault(); } ================================================ FILE: NectarRCON/Services/ServerInformationService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; using System.Windows; namespace NectarRCON.Services; public class ServerInformationService : IServerInformationService { private readonly ILanguageService _languageService; private List _servers = new(); public List Servers => _servers; public ServerInformationService(ILanguageService languageService) { _languageService = languageService; if (!File.Exists("./servers.json")) File.WriteAllText("./servers.json", "[]"); try { string jsonString = File.ReadAllText("./servers.json"); _servers = JsonSerializer.Deserialize>(jsonString) ?? new List(); } catch (JsonException ex) { MessageBoxResult result = MessageBox.Show(_languageService.GetKey("text.server_information.error.json_error") .Replace("%s", ex.Message) .Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OKCancel, MessageBoxImage.Error); if (result == MessageBoxResult.OK) File.WriteAllText("./servers.json", "[]"); else Process.GetCurrentProcess().Kill(); } catch (Exception ex) { MessageBox.Show(_languageService.GetKey("text.server_information.error.text") .Replace("%s", ex.Message) .Replace("\\n", "\n") , _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); Process.GetCurrentProcess().Kill(); } } public void AddServer(ServerInformation server) { if (ServerIsExist(server.Name)) return; _servers.Add(server); } public ServerInformation? GetServer(string name) => _servers.Where(s => s.Name == name).FirstOrDefault(); public List GetServers() { return _servers; } public void RemoveServer(string name) { ServerInformation? server = GetServer(name); if (null == server) return; _servers.Remove(server); } public void Save() { try { File.WriteAllText("./servers.json", JsonSerializer.Serialize(_servers)); } catch (Exception ex) { MessageBox.Show(_languageService.GetKey("text.server_information.error.save") .Replace("%s", ex.Message) .Replace("\\n", "\n") , _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } public bool ServerIsExist(string name) => GetServer(name) != null; public void Update(string name, ServerInformation newInfo) { var oldInfo = GetServer(name); if (null == oldInfo) return; for (int i = 0; i < _servers.Count; i++) { if (_servers[i] == oldInfo) { _servers[i] = newInfo; return; } } } } ================================================ FILE: NectarRCON/Services/ServerPasswordService.cs ================================================ using NectarRCON.Interfaces; using NectarRCON.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; using System.Windows; namespace NectarRCON.Services; public class ServerPasswordService : IServerPasswordService { private readonly ILanguageService _languageService; private ServerInformation? _selectedServer; private List _serverPasswordList = new(); public ServerPasswordService(ILanguageService languageService) { _languageService = languageService; if (!File.Exists("./passwords.json")) File.WriteAllText("./passwords.json", "[]"); try { string jsonString = File.ReadAllText("./passwords.json"); _serverPasswordList = JsonSerializer.Deserialize>(jsonString) ?? new List(); } catch (JsonException ex) { MessageBoxResult result = MessageBox.Show(_languageService.GetKey("text.password.error.json_error") .Replace("%s", ex.Message) .Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OKCancel, MessageBoxImage.Error); if (result == MessageBoxResult.OK) File.WriteAllText("./passwords.json", "[]"); else Process.GetCurrentProcess().Kill(); } catch (Exception ex) { MessageBox.Show(_languageService.GetKey("text.password.error.text") .Replace("%s", ex.Message) .Replace("\\n", "\n") , _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); Process.GetCurrentProcess().Kill(); } } public ServerPassword? Get(ServerInformation server) { var password = _serverPasswordList.Where(s => { return s.ServerName == server.Name; }).FirstOrDefault(); return password; } public ServerInformation GetSelect() { return _selectedServer ?? new(); } public bool IsExist(ServerInformation server) => Get(server) != null; public void Remove(ServerInformation server) { ServerPassword? password = Get(server); if (password == null) return; _serverPasswordList.Remove(password); } public void Save() { try { File.WriteAllText("./passwords.json", JsonSerializer.Serialize(_serverPasswordList)); } catch (Exception ex) { MessageBox.Show(_languageService.GetKey("text.password.error.save") .Replace("%s", ex.Message) .Replace("\\n", "\n") , _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } public void Select(ServerInformation server) { _selectedServer = server; } public void Set(ServerInformation server, string? password, bool? isEmpty) { var oldPassword = Get(server); ServerPassword newPassword; if (oldPassword == null) { newPassword = new ServerPassword() { Password = password ?? string.Empty, IsEmpty = isEmpty ?? string.IsNullOrEmpty(password), ServerName = server.Name }; } else { newPassword = new ServerPassword() { Password = password ?? oldPassword.Password, IsEmpty = isEmpty ?? oldPassword.IsEmpty, ServerName = server.Name }; } if (IsExist(server)) _serverPasswordList.Remove(Get(server)!); _serverPasswordList.Add(newPassword); } } ================================================ FILE: NectarRCON/ViewModels/AboutPageViewModel.cs ================================================ namespace NectarRCON.ViewModels { class AboutPageViewModel { } } ================================================ FILE: NectarRCON/ViewModels/AddGroupPageViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Services; using System; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.ViewModels; public partial class AddGroupPageViewModel:ObservableObject { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddCommand))] private string? _groupName; private INavigationService _navigationService; private IMessageBoxService _messageBoxService; private IGroupService _groupService; public AddGroupPageViewModel() { GroupName = string.Empty; _navigationService = App.GetService(); _messageBoxService = App.GetService(); _groupService = App.GetService(); } private bool CanAdd() => !string.IsNullOrWhiteSpace(GroupName); [RelayCommand(CanExecute = nameof(CanAdd))] public void Add() { try { _groupService.Add(new Models.Group() { Id = Guid.NewGuid().ToString(), Name = GroupName!, Servers = new() }); _navigationService.Navigate(4); }catch(Exception ex) { _messageBoxService.Show(ex, string.Empty); } } [RelayCommand] public void Cancel() { _navigationService.Navigate(4); } [RelayCommand] public void Load() { GroupName = null; } } ================================================ FILE: NectarRCON/ViewModels/AddServerWindowViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Windows; using System.Windows; namespace NectarRCON.ViewModels; public partial class AddServerWindowViewModel : ObservableObject { private readonly IServerInformationService _serverInformationService; private readonly ILanguageService _languageService; private AddServerWindow? _serverWindow; [ObservableProperty] private string _serverName = "Rcon"; [ObservableProperty] private string _serverAddress = "localhost"; [ObservableProperty] private string _serverPort = "25575"; public AddServerWindowViewModel(IServerInformationService serverInformationService, ILanguageService languageService) { _serverInformationService = serverInformationService; _languageService = languageService; } public void SetWindow(AddServerWindow window) { _serverWindow = window; } [RelayCommand] private void Ok() { ServerAddress = ServerAddress.Trim(); if (string.IsNullOrWhiteSpace(ServerName) || string.IsNullOrWhiteSpace(ServerAddress)) { MessageBox.Show(_languageService.GetKey("ui.add_server_window.null_text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); return; } var information = new ServerInformation() { Name = ServerName, Address = ServerAddress, Port = ushort.Parse(ServerPort) }; if (_serverInformationService.ServerIsExist(information.Name)) { MessageBox .Show(_languageService.GetKey("ui.add_server_window.already_exist") , _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } else { _serverInformationService.AddServer(information); _serverInformationService.Save(); _serverWindow?.Close(); } } } ================================================ FILE: NectarRCON/ViewModels/EditPasswordWindowViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Windows; namespace NectarRCON.ViewModels; public partial class EditPasswordWindowViewModel : ObservableObject { private readonly IServerPasswordService _serverPasswordService; private readonly ServerInformation _serverInformation; private EditPasswordWindow? _window = null; [ObservableProperty] private string _password = string.Empty; [ObservableProperty] private bool _isEmpty = true; public EditPasswordWindowViewModel() { _serverPasswordService = App.GetService(); _serverInformation = _serverPasswordService.GetSelect()!; var password = _serverPasswordService.Get(_serverInformation); if (password != null) { Password = password.Password; IsEmpty = string.IsNullOrEmpty(Password); } else { Password = string.Empty; IsEmpty = true; } } public void SetWindow(EditPasswordWindow window) { _window = window; } [RelayCommand] public void TextChange() { IsEmpty = string.IsNullOrEmpty(Password); } [RelayCommand] public void SetPassword() { _serverPasswordService.Set(_serverInformation, Password, IsEmpty); _serverPasswordService.Save(); _window?.Close(); } } ================================================ FILE: NectarRCON/ViewModels/EditServerWindowViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Models; using System.Windows; using Wpf.Ui.Controls; using MessageBox = System.Windows.MessageBox; namespace NectarRCON.ViewModels; public partial class EditServerWindowViewModel : ObservableObject { private readonly ILanguageService _languageService; private readonly IServerPasswordService _serverPasswordService; private readonly IServerInformationService _serverInformationService; private UiWindow? _window; [ObservableProperty] private ServerInformation? _selectServer; [ObservableProperty] private string _port = string.Empty; [ObservableProperty] private string _address = string.Empty; public EditServerWindowViewModel() { _languageService = App.GetService(); _serverPasswordService = App.GetService(); _serverInformationService = App.GetService(); } [RelayCommand] private void Load(RoutedEventArgs e) { _window = e.Source as UiWindow; SelectServer = _serverPasswordService.GetSelect(); Port = _selectServer?.Port.ToString() ?? "0"; Address = _selectServer?.Address ?? "localhost"; } [RelayCommand] public void Exit() { _window?.Close(); } [RelayCommand] private void Ok() { Address = Address.Trim(); if (string.IsNullOrWhiteSpace(_selectServer?.Address) || string.IsNullOrEmpty(Address)) MessageBox.Show(_languageService.GetKey("ui.add_server_window.null_text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); else { _selectServer.Port = ushort.Parse(Port); _selectServer.Address = Address; _serverInformationService.Update(_selectServer.Name, _selectServer); _serverInformationService.Save(); Exit(); } } } ================================================ FILE: NectarRCON/ViewModels/GroupPageViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Views.Pages; using System.Collections.ObjectModel; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.ViewModels; public partial class GroupPageViewModel:ObservableObject { [ObservableProperty] private ObservableCollection _groups; private readonly IGroupService _groupService; private readonly ILanguageService _languageService; private readonly IMessageBoxService _messageBoxService; private readonly INavigationService _navigationService; #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 public GroupPageViewModel() #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 { _groupService = App.GetService(); _languageService = App.GetService(); _messageBoxService = App.GetService(); _navigationService = App.GetService(); Groups = new(); } [RelayCommand] public void Load() { Refresh(); } private void Refresh() { Groups.Clear(); foreach(Group group in _groupService.GetGroups()) { Groups.Add(new(group.Name, this)); } } [RelayCommand] public void RemoveGroup(string groupId) { Group group = _groupService.GetGroup(groupId)!; if (_messageBoxService.Show(string.Format(_languageService.GetKey("ui.group.delete_group"), group.Name), _languageService.GetKey("text.delete"), System.Windows.MessageBoxButton.YesNo, System.Windows.MessageBoxImage.Question) == System.Windows.MessageBoxResult.Yes) { _groupService.Delete(groupId); Refresh(); } } [RelayCommand] public void NewGroup() { _navigationService.Navigate(typeof(AddGroupPage)); } } ================================================ FILE: NectarRCON/ViewModels/JoinGroupWindowViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using NectarRCON.Interfaces; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Windows.Data; namespace NectarRCON.ViewModels; public partial class JoinGroupWindowViewModel:ObservableObject { private IServerInformationService _serverInformationService; [ObservableProperty] private ObservableCollection _servers = new(); [ObservableProperty] private ListCollectionView? _serverCollectionView; [ObservableProperty] public ObservableCollection? _blackList; public JoinGroupWindowViewModel() { _serverInformationService = App.GetService(); foreach(var server in _serverInformationService.GetServers()) { Servers.Add(server.Name); } ServerCollectionView = new(Servers); ServerCollectionView.Filter += (s) => { return !BlackList?.Contains(s?.ToString() ?? string.Empty) ?? true; }; BlackList = new(); } partial void OnBlackListChanged(ObservableCollection? value) { ServerCollectionView?.Refresh(); } } ================================================ FILE: NectarRCON/ViewModels/MainPageViewModel.cs ================================================ using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Rcon; using NectarRCON.Services; using NectarRCON.Views.Pages; using System.Net.Sockets; using System.Security.Authentication; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using NectarRCON.Dp; using Serilog; 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 IHistoryService _historyService; private readonly IServerPasswordService _serverPasswordService; private IRconConnection _rconConnectService; private readonly INavigationService _navigationService; private readonly ILanguageService _languageService; private readonly IRconConnectionInfoService _rconConnectionInfoService; private readonly IConnectingDialogService _connectingDialogService; private readonly IMessageBoxService _messageBoxService; private MainPage? _page; private TextBox? _logTextBox; private HistoryNode? _historyNode; [ObservableProperty] private string _commandText = string.Empty; [ObservableProperty] private string _logText = string.Empty; [ObservableProperty] private bool _isMultipleConnection; [ObservableProperty] private bool _isDisconnection; public MainPageViewModel() { _logService = App.GetService(); _historyService = App.GetService(); _serverPasswordService = App.GetService(); _navigationService = App.GetService(); _languageService = App.GetService(); _rconConnectionInfoService = App.GetService(); _messageBoxService = App.GetService(); _connectingDialogService = App.GetService(); WeakReferenceMessenger.Default.Register(this, OnClear); // 选择连接服务 _rconConnectService = _rconConnectionInfoService.HasMultipleInformation ? App.GetService(typeof(RconMultiConnection)) : App.GetService(typeof(RconSingleConnection)); IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation; } private void OnClear(object sender, ClearLogValueMessage msg) { _logService.Clear(); LogText = string.Empty; } private void OnMessage(ServerInformation info, string msg) { Log.Information("[OnMessage] {name}({adapter}) -> {msg}", info.Name, string.IsNullOrEmpty(info.Adapter) ? "TCPRcon" : info.Adapter, string.IsNullOrEmpty(msg) ? "$empty$" : msg); string logMsg = string.IsNullOrEmpty(msg) ? _languageService.GetKey("ui.main_page.successful") : msg; LogText += _logService.Log($"{info.Name}:" + logMsg); _logTextBox?.ScrollToEnd(); } private void OnClosed(ServerInformation info) { LogText += _logService.Log($"{info.Name} {_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] private async void ReConnect() { Log.Information($"[ReConnectCommand] 正在准备重连"); if (_rconConnectService.IsConnected()) _rconConnectService.Close(); IsDisconnection = false; await ConnectAsync(); } private async Task ConnectAsync() { Log.Information($"[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)); WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage { IsLoaded = true, }); Log.Information($"[ConnectAsync] 连接服务: {_rconConnectService.GetType().FullName}, 是否为多连接: {IsMultipleConnection}"); _logTextBox = (TextBox)LogicalTreeHelper.FindLogicalNode(_page, "LogText"); LogText += _logService.Log(_languageService.GetKey("text.server.start")); _logTextBox?.ScrollToEnd(); _rconConnectService.OnConnected += OnConnected; _rconConnectService.OnMessage += OnMessage; _rconConnectService.OnClosed += OnClosed; await Task.Run(_rconConnectService.Connect); } catch (AuthenticationException ex) { Log.Error($"[ConnectAsync] 认证失败: {ex.Message}"); 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)); } catch (Exception ex) { Log.Error($"[ConnectAsync] 连接遇到错误: {ex}"); var msg = _languageService.GetKey("text.server.connect.fail.text") .Replace("\\n", "\n") .Replace("%s", ex.Message); _messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); LogText += _logService.Log(msg); _logTextBox?.ScrollToEnd(); } finally { _connectingDialogService.Close(); } // 当只有一个服务器时IsConnected会返回单个客户端的连接状态 // 当有多个服务器时只要有一个客户端在线,IsConnected就会返回True if (!_rconConnectService.IsConnected() && !RconSettings.IsKeepConnectionWindowOpen) { _navigationService.Navigate(2); } IsDisconnection = !_rconConnectService.IsConnected(); } partial void OnIsDisconnectionChanged(bool value) { Log.Information("当前客户端状态: {0}", _rconConnectService.IsConnected() ? "在线" : "离线"); } private void OnConnected(ServerInformation info) { Log.Information("[OnConnected] {name}({adapter})", info.Name, string.IsNullOrEmpty(info.Adapter) ? "TCPRcon" : info.Adapter); LogText += _logService.Log($"$ {info.Name} {_languageService.GetKey("text.server.connected")}"); IsDisconnection = false; } [RelayCommand] private void BackHome() { _navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation ? typeof(GroupPage) : typeof(ServersPage)); } [RelayCommand] private void Exit() { WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage() { IsLoaded = false, }); if (_rconConnectService.IsConnected()) _rconConnectService.Close(); _rconConnectService.OnMessage -= OnMessage; _rconConnectService.OnClosed -= OnClosed; _rconConnectService.OnConnected -= OnConnected; IsDisconnection = false; // 重置状态 此时没有任何连接 } [RelayCommand] private void Run() { Log.Information("[Run] {0}", CommandText); if (_rconConnectService.IsConnected()) { LogText += _logService.Log($"> {CommandText}"); _logTextBox?.ScrollToEnd(); _rconConnectService.Send(CommandText); CommandText = string.Empty; } else { IsDisconnection = true; _rconConnectService.Close(); MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private void KeyDown(KeyEventArgs e) { var textBox = (System.Windows.Controls.TextBox)e.Source; if (e.Key == Key.Enter) { var text = textBox.Text.Trim(); if (string.IsNullOrEmpty(text)) { return; } _commandText = text; _historyNode = _historyService.InputCmd(_commandText); Run(); } else if (e.Key == Key.Up) { _historyNode = _historyService.Prev(_historyNode); textBox.Text = _historyNode?.Cmd; textBox.Select(textBox.Text?.Length ?? 0, 0); } else if (e.Key == Key.Down) { _historyNode = _historyService.Next(_historyNode); textBox.Text = _historyNode?.Cmd; textBox.Select(textBox.Text?.Length ?? 0, 0); } } } ================================================ FILE: NectarRCON/ViewModels/MainWindowViewModel.cs ================================================ using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using NectarRCON.Interfaces; using NectarRCON.Models; using System.Collections.ObjectModel; using System.IO; using System.Windows; using Serilog; using Wpf.Ui.Common; using Wpf.Ui.Controls; using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Mvvm.Contracts; using MessageBox = System.Windows.MessageBox; namespace NectarRCON.ViewModels { public partial class MainWindowViewModel : ObservableObject { [ObservableProperty] private ObservableCollection _navigationItems = new(); private readonly INavigationService _navigationService; private readonly ILanguageService _languageService; [ObservableProperty] private bool _mainPageIsLoaded; public MainWindowViewModel(INavigationService navigationService, ILanguageService languageService) { NavigationItems = new ObservableCollection() { new NavigationItem { Content = "主页", PageTag = "mainPage", PageType = typeof(Views.Pages.MainPage) }, new NavigationItem { Content = "设置", PageTag = "settingsPage", PageType= typeof(Views.Pages.SettingPage) }, new NavigationItem { Content = "服务器...", PageTag = "serversPage", PageType= typeof(Views.Pages.ServersPage) }, new NavigationItem { Content = "关于", PageTag = "aboutPage", PageType= typeof(Views.Pages.AboutPage) }, new NavigationItem { Content = "群组", PageTag = "groupPage", PageType= typeof(Views.Pages.GroupPage) }, new NavigationItem { Content = "添加服务器", PageTag = "newGroup", PageType= typeof(Views.Pages.AddGroupPage) } }; _navigationService = navigationService; _languageService = languageService; WeakReferenceMessenger.Default.Register(this, OnMainPageChange); } public void OnMainPageChange(object sender, MainPageLoadValueMessage message) { MainPageIsLoaded = message.IsLoaded; } [RelayCommand] private void OnLoad() { _navigationService.Navigate(2); } [RelayCommand] private void ClearButtonClick() { WeakReferenceMessenger.Default.Send(new ClearLogValueMessage()); } [RelayCommand] private void ChangePage(string index) { _navigationService.Navigate(int.Parse(index)); } [RelayCommand] private void ClearProgramLogs() { if (MessageBox.Show(_languageService.GetKey("ui.menu.log.clear_program.ask"), "NectarRcon", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) return; Log.Information("Clear program logs"); foreach (var logFile in Directory.GetFiles(Path.Combine(Environment.CurrentDirectory, "logs", "program"), "*.log")) { try { Log.Information("Delete log file: {0}", logFile); File.Delete(logFile); } catch(Exception ex) { Log.Error(ex, "Delete log file failed: {0}", logFile); } } } } } ================================================ FILE: NectarRCON/ViewModels/ServersPageViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Interfaces; using NectarRCON.Models; using NectarRCON.Windows; using System.Collections.ObjectModel; using System.Windows.Controls; using System.Windows.Data; namespace NectarRCON.ViewModels; public partial class ServersPageViewModel : ObservableObject { [ObservableProperty] private ListCollectionView _serverCollectionView; [ObservableProperty] private ObservableCollection _servers; private string _filterName = string.Empty; #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 public ServersPageViewModel() #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 { Servers = new(); ServerCollectionView = new(Servers); ServerCollectionView.Filter += (s) => { if (_filterName == string.Empty) return true; if (s is ServerModel model) { return model.Name.Contains(_filterName); } return false; }; Refresh(); } public void Refresh() { Servers.Clear(); foreach (ServerInformation information in App.GetService().GetServers()) { Servers.Add(new ServerModel(information, this)); } } [RelayCommand] public void AddServer() { AddServerWindow addServer = App.GetService(); addServer.ShowDialog(); Refresh(); } [RelayCommand] public void FilterTextChanged(TextChangedEventArgs e) { var box = (System.Windows.Controls.TextBox)e.Source; _filterName = box.Text.ToString() ?? string.Empty; _serverCollectionView.Refresh(); } } ================================================ FILE: NectarRCON/ViewModels/SettingPageViewModel.cs ================================================ using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Core.Helper; using NectarRCON.Interfaces; using NectarRCON.Models; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; using NectarRCON.Dp; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.ViewModels; public partial class SettingPageViewModel : ObservableObject { private bool _isLoaded = false; 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 = []; 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) { Languages.Clear(); foreach (var language in _languageService.GetLanguages()) { Languages.Add(language.Value["file.name"].ToString() ?? "NullName"); } foreach (var language in _languageService.GetLanguages()) { LanguageSelectedIndex++; if (language.Key == _configService.GetConfig().LanguageName) break; } ThemeSelectedIndex = (int)_configService.GetConfig().Theme; _isLoaded = true; } [RelayCommand] public void Exit() { _isLoaded = false; } [RelayCommand] public void ThemeSelectionChange() { if (!_isLoaded) return; _configService.GetConfig().Theme = (ETheme)_themeSelectedIndex; _configService.Save(); switch ((ETheme)_themeSelectedIndex) { case ETheme.System: _themeService.SetTheme(Win32Helper.GetWindowsTheme() ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light); break; case ETheme.Dark: _themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Dark); break; case ETheme.Light: _themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Light); break; default: break; } } [RelayCommand] public void LanguageSelectionChange(SelectionChangedEventArgs e) { if (!_isLoaded) return; if (e.AddedItems.Count == 0 || null == e.AddedItems[0]) return; KeyValuePair? lang = _languageService.GetLanguages().Where(l => { return (l.Value["file.name"].ToString() ?? "NullName") == e.AddedItems[0]!.ToString(); }).FirstOrDefault(); if (null == lang.Value.Value) return; _configService.GetConfig().LanguageName = lang.Value.Key; _configService.Save(); _languageService.SelectLanguage(lang.Value.Value["file.name"].ToString() ?? "NullName", true); } } ================================================ FILE: NectarRCON/Views/Pages/AboutPage.xaml ================================================  ================================================ FILE: NectarRCON/Views/Pages/AboutPage.xaml.cs ================================================ namespace NectarRCON.Views.Pages { /// /// AboutPage.xaml 的交互逻辑 /// public partial class AboutPage { public AboutPage() { InitializeComponent(); } } } ================================================ FILE: NectarRCON/Views/Pages/AddGroupPage.xaml ================================================  ================================================ FILE: NectarRCON/Views/Pages/AddGroupPage.xaml.cs ================================================ using System; using Wpf.Ui.Controls; namespace NectarRCON.Views.Pages { /// /// AddGroupPage.xaml 的交互逻辑 /// public partial class AddGroupPage : UiPage { public AddGroupPage() { InitializeComponent(); } } } ================================================ FILE: NectarRCON/Views/Pages/GroupPage.xaml ================================================