Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a4b39aa9 | |||
| 8bf665d1f1 | |||
| 7a77af2d80 | |||
| 830cb38d9f | |||
| 8f66bf9f09 | |||
| 755fa51adc | |||
| a15d187550 | |||
| 6e6ebffb62 | |||
| c2ab550329 | |||
| ff31412719 | |||
| 15e4e3fbd7 | |||
| 5306a86d13 | |||
| c304ac94fe | |||
| e0a16f7fb6 | |||
| f7cec5d093 | |||
|
|
0c6bbaadac | ||
| d7f775e80c | |||
| 6eead05308 | |||
| 673a972598 | |||
| f44589454c | |||
| fd06729035 | |||
| a09ace0d39 | |||
| 56c373134f | |||
| fa68b4bcd5 | |||
| b86b65fd66 | |||
| 6c967efd85 | |||
| 6a6bb4f27c | |||
| f6a15e9c45 | |||
| c148f6ed34 | |||
| a475148543 | |||
| 34fd4ebf4c | |||
| 83d116003b | |||
| a3c96174b2 | |||
| 3bfae32c25 | |||
| ae17ff2d89 | |||
| 517fadaab8 | |||
| 9e95d68c4a | |||
| 01ebe3219d | |||
| 147915f684 | |||
| 0a2fe55c7f | |||
| 02e1a14571 | |||
| 37ca8fecf3 | |||
| 875dacaa18 | |||
| 10d317c867 | |||
| 73682acbab | |||
| a7943adb76 | |||
| eb188321af | |||
| 030f680b96 | |||
| ba8153e6b4 |
2
.github/workflows/publish_manifest.yml
vendored
2
.github/workflows/publish_manifest.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3.2.0
|
uses: actions/setup-dotnet@v3.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Set version
|
- name: Set version
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3.2.0
|
uses: actions/setup-dotnet@v3.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Create build
|
- name: Create build
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ obj/
|
|||||||
riderModule.iml
|
riderModule.iml
|
||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
release/
|
release/
|
||||||
publish/
|
publish/
|
||||||
|
/.vs
|
||||||
7
.idea/.idea.Nebula/.idea/avalonia.xml
generated
7
.idea/.idea.Nebula/.idea/avalonia.xml
generated
@@ -12,6 +12,7 @@
|
|||||||
<entry key="Nebula.Launcher/MessageBox/MessageView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/MessageBox/MessageView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/MessageBox/MessageWindow.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/MessageBox/MessageWindow.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/ViewModels/Styles1.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/ViewModels/Styles1.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Config/StringConfigurationView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Controls/PlayerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Controls/PlayerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Controls/ServerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Controls/ServerContainerControl.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ExceptionView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ExceptionView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
@@ -21,24 +22,30 @@
|
|||||||
<entry key="Nebula.Launcher/Views/Pages/AccountInfoPage.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/AccountInfoPage.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/AccountInfoView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/AccountInfoView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/AddFavoriteView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/AddFavoriteView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Pages/ConfigurationView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/ContentBrowserView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/ContentBrowserView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/FavoriteServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/FavoriteServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/ServerListPage.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/ServerListPage.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/ServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/ServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Pages/ServerOverviewView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Pages/ServerOverviewView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Popup/AddFavoriteView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Popup/EditServerNameView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/ExceptionListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/ExceptionListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/ExceptionView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/ExceptionView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/InfoPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/InfoPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/Popup/IsLoginCredentialsNullPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/LoadingContextView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/LoadingContextView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/LogPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/LogPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/MessagePopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/MessagePopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Popup/TfaView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Popup/TfaView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/ServerCompoundEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerContainer.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerContainer.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerList.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerList.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/ServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/ServerListView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Tabs/AccountInfoTab.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Tabs/AccountInfoTab.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.Launcher/Views/Tabs/ServerListTab.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
<entry key="Nebula.Launcher/Views/Tabs/ServerListTab.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
|
<entry key="Nebula.Launcher/Views/VisualErrorView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
|
||||||
<entry key="Nebula.UpdateResolver/App.axaml" value="Nebula.UpdateResolver/Nebula.UpdateResolver.csproj" />
|
<entry key="Nebula.UpdateResolver/App.axaml" value="Nebula.UpdateResolver/Nebula.UpdateResolver.csproj" />
|
||||||
<entry key="Nebula.UpdateResolver/MainWindow.axaml" value="Nebula.UpdateResolver/Nebula.UpdateResolver.csproj" />
|
<entry key="Nebula.UpdateResolver/MainWindow.axaml" value="Nebula.UpdateResolver/Nebula.UpdateResolver.csproj" />
|
||||||
</map>
|
</map>
|
||||||
|
|||||||
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/Robust.LoaderApi" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
"program": "${workspaceFolder}/Nebula.Launcher/bin/Debug/net9.0/Nebula.Launcher.dll",
|
"program": "${workspaceFolder}/Nebula.Launcher/bin/Debug/Nebula.Launcher.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Nebula.Launcher",
|
"cwd": "${workspaceFolder}/Nebula.Launcher",
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
|||||||
6
Directory.Build.props
Normal file
6
Directory.Build.props
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
33
Directory.Packages.props
Normal file
33
Directory.Packages.props
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Avalonia" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.3.0" />
|
||||||
|
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" />
|
||||||
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageVersion Include="Fluent.Net" Version="1.0.63" />
|
||||||
|
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="libsodium" Version="1.0.20" />
|
||||||
|
<PackageVersion Include="Robust.Natives" Version="0.2.3" />
|
||||||
|
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
|
<PackageVersion Include="Lib.Harmony" Version="2.4.2" />
|
||||||
|
<PackageVersion Include="SharpZstd.Interop" Version="1.5.6" />
|
||||||
|
<PackageVersion Include="coverlet.collector" Version="6.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageVersion>
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageVersion Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -28,14 +28,16 @@ public class App : Application
|
|||||||
{
|
{
|
||||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
case IClassicDesktopStyleApplicationLifetime desktop:
|
||||||
DisableAvaloniaDataAnnotationValidation();
|
DisableAvaloniaDataAnnotationValidation();
|
||||||
desktop.MainWindow = new MessageWindow(out provider);
|
desktop.MainWindow = (Window)(provider = new MessageWindow());
|
||||||
break;
|
break;
|
||||||
case ISingleViewApplicationLifetime singleViewPlatform:
|
case ISingleViewApplicationLifetime singleViewPlatform:
|
||||||
singleViewPlatform.MainView = new MessageView(out provider);
|
singleViewPlatform.MainView = (Control)(provider = new MessageView());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
provider?.ShowMessage("Launcher is already running.","hey shithead!");
|
provider?.ShowMessage(
|
||||||
|
"Error: An instance of the application is already running. Please close the existing instance before launching a new one.",
|
||||||
|
"Duplicate instance detected.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages">
|
||||||
|
|
||||||
|
<!-- Base Window Style -->
|
||||||
<Style Selector="Window">
|
<Style Selector="Window">
|
||||||
<Setter Property="Background" Value="{StaticResource DefaultBackground}" />
|
<Setter Property="Background" Value="{StaticResource DefaultBackground}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Common Border Style -->
|
||||||
<Style Selector="Border">
|
<Style Selector="Border">
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Common Label Style -->
|
||||||
<Style Selector="Label">
|
<Style Selector="Label">
|
||||||
<Setter Property="Foreground" Value="#f7f7ff" />
|
<Setter Property="Foreground" Value="#f7f7ff" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Common ItemsControl Style -->
|
||||||
<Style Selector="ItemsControl">
|
<Style Selector="ItemsControl">
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- General Button Style -->
|
||||||
<Style Selector="Button">
|
<Style Selector="Button">
|
||||||
<Setter Property="BorderBrush" Value="#343334" />
|
<Setter Property="BorderBrush" Value="#343334" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
@@ -19,6 +31,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Button State Overrides -->
|
||||||
<Style Selector="Button:pressed">
|
<Style Selector="Button:pressed">
|
||||||
<Setter Property="RenderTransform" Value="{x:Null}" />
|
<Setter Property="RenderTransform" Value="{x:Null}" />
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
||||||
@@ -30,26 +43,33 @@
|
|||||||
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- ViewSelectButton Specialization -->
|
||||||
<Style Selector="Button.ViewSelectButton">
|
<Style Selector="Button.ViewSelectButton">
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
<Setter Property="Margin" Value="0,0,0,5" />
|
<Setter Property="Margin" Value="0,0,0,5" />
|
||||||
<Setter Property="Padding" Value="8" />
|
<Setter Property="Padding" Value="8" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button.ViewSelectButton:pressed">
|
<Style Selector="Button.ViewSelectButton:pressed">
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- TextBox Styles -->
|
||||||
<Style Selector="TextBox">
|
<Style Selector="TextBox">
|
||||||
<Setter Property="Foreground" Value="#f7f7ff" />
|
<Setter Property="Foreground" Value="#f7f7ff" />
|
||||||
<Setter Property="SelectionForegroundBrush" Value="#f7f7ff" />
|
<Setter Property="SelectionForegroundBrush" Value="#f7f7ff" />
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="BorderBrush" Value="#f7f7ff" />
|
<Setter Property="BorderBrush" Value="#f7f7ff" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TextBox" />
|
|
||||||
|
|
||||||
|
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="BorderBrush" Value="White" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ListBoxItem Styles -->
|
||||||
<Style Selector="ListBoxItem /template/ ContentPresenter">
|
<Style Selector="ListBoxItem /template/ ContentPresenter">
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
<Setter Property="Margin" Value="0,0,0,5" />
|
<Setter Property="Margin" Value="0,0,0,5" />
|
||||||
@@ -60,29 +80,37 @@
|
|||||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
<Setter Property="Background">
|
<Setter Property="Background">
|
||||||
<LinearGradientBrush EndPoint="100%,50%" StartPoint="0%,50%">
|
<Setter.Value>
|
||||||
<GradientStop Color="#ae4c47" Offset="0.0" />
|
<LinearGradientBrush EndPoint="100%,50%" StartPoint="0%,50%">
|
||||||
<GradientStop Color="#D95F59" Offset="0.2" />
|
<GradientStop Color="#ae4c47" Offset="0.0" />
|
||||||
<GradientStop Color="#D95F59" Offset="1.0" />
|
<GradientStop Color="#D95F59" Offset="0.2" />
|
||||||
</LinearGradientBrush>
|
<GradientStop Color="#D95F59" Offset="1.0" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
<Setter Property="BoxShadow" Value="0 0 15 1 #1212" />
|
<Setter Property="BoxShadow" Value="0 0 15 1 #1212" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ListBoxItem:pointerover">
|
<Style Selector="ListBoxItem:pointerover">
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
<Setter Property="Margin" Value="0,0,5,0" />
|
<Setter Property="Margin" Value="0,0,5,0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ListBoxItem:pressed /template/ ContentPresenter">
|
<Style Selector="ListBoxItem:pressed /template/ ContentPresenter">
|
||||||
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
<Setter Property="CornerRadius" Value="0,8,8,0" />
|
||||||
<Setter Property="Background" Value="{StaticResource DefaultSelected}" />
|
<Setter Property="Background" Value="{StaticResource DefaultSelected}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox">
|
<!-- Combined ConfigBorder Styles -->
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Style Selector="pages|ComplexUnitConfigControl.ConfigBorder,
|
||||||
</Style>
|
pages|ArrayUnitConfigControl.ConfigBorder,
|
||||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
pages|StringUnitConfigControl.ConfigBorder,
|
||||||
<Setter Property="BorderBrush" Value="White" />
|
pages|IntUnitConfigControl.ConfigBorder,
|
||||||
<Setter Property="Background" Value="Transparent" />
|
pages|FloatUnitConfigControl.ConfigBorder">
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
<Setter Property="Background" Value="#33333355" />
|
||||||
</Style>
|
<Setter Property="CornerRadius" Value="0"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1,0,0,2" />
|
||||||
|
<Setter Property="BorderBrush" Value="#3f3f3f5f" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
||||||
BIN
Nebula.Launcher/Assets/error_presentation/Cinka.png
Normal file
BIN
Nebula.Launcher/Assets/error_presentation/Cinka.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
BIN
Nebula.Launcher/Assets/error_presentation/alex.png
Normal file
BIN
Nebula.Launcher/Assets/error_presentation/alex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@@ -0,0 +1,74 @@
|
|||||||
|
tab-account = Account
|
||||||
|
tab-servers = Servers
|
||||||
|
tab-content = Content
|
||||||
|
tab-settings = Settings
|
||||||
|
vcruntime-check-error = VC runtime dlls are not present on this computer. Install VC runtime dlls.
|
||||||
|
migration-label-task = Migration task, please wait...
|
||||||
|
task-cancel = Cancel
|
||||||
|
|
||||||
|
popup-edit-name = Edit server name
|
||||||
|
popup-add-favorite = Add favorite
|
||||||
|
popup-exception = Exception was thrown
|
||||||
|
popup-information = Information
|
||||||
|
popup-loading = Loading...
|
||||||
|
popup-twofa = 2fa
|
||||||
|
|
||||||
|
account-profiles = Profiles
|
||||||
|
account-profile-select = Select
|
||||||
|
account-profile-delete = Delete
|
||||||
|
account-auth-retry = Retry Authentication
|
||||||
|
account-auth-try-another = Or try another account
|
||||||
|
account-auth-login = Enter your login
|
||||||
|
account-auth-password = Enter your password
|
||||||
|
account-auth-server = Authentication Server
|
||||||
|
account-auth-button = Authenticate
|
||||||
|
account-auth-save = Save Profile
|
||||||
|
account-auth-hello = Hello,
|
||||||
|
account-auth-current-server = Current server auth:
|
||||||
|
account-auth-logout = Log out
|
||||||
|
auth-current-login-name = Current login {$auth_server}: {$login}
|
||||||
|
auth-current-login-no-name = Profile not selected
|
||||||
|
|
||||||
|
auth-processing = Processing authentication request...
|
||||||
|
auth-error = An authentication error has occurred.
|
||||||
|
auth-error-occured = An error occurred during the authentication process.
|
||||||
|
auth-invalid-credentials = Invalid username or password. Please try again.
|
||||||
|
auth-connection-error = Unable to connect to the authentication server.
|
||||||
|
auth-name-resolution-error = Failed to resolve server address. Check your network or server configuration.
|
||||||
|
auth-secure-error = Failed to cinnect to the server using SSL
|
||||||
|
auth-config-read = Reading authentication configuration...
|
||||||
|
auth-try-auth-config = Attempting to authenticate using saved configuration.
|
||||||
|
auth-try-auth-profile = Attempting to authenticate using profile
|
||||||
|
|
||||||
|
config-export-logs = Export logs
|
||||||
|
config-open-data = Open data path
|
||||||
|
config-reset = Reset to default
|
||||||
|
config-save = Save changes
|
||||||
|
config-remove-content-all = Remove all content
|
||||||
|
|
||||||
|
filter-roleplay = Roleplay
|
||||||
|
filter-language = Language
|
||||||
|
favorite-add = Add to favorites
|
||||||
|
popup-add-favorite-invalid-ip = Please enter a valid IP
|
||||||
|
servername-set = Set server name
|
||||||
|
servername-clear = Clear server name
|
||||||
|
twofa-enabled = You have two-factor authentication enabled. Please enter the code.
|
||||||
|
twofa-set = Proceed
|
||||||
|
|
||||||
|
server-search = Server search
|
||||||
|
serverentry-tag = Tag
|
||||||
|
serverentry-map = Map
|
||||||
|
serverentry-preset = Preset
|
||||||
|
|
||||||
|
content-view-server = Server url
|
||||||
|
content-view-path = Path
|
||||||
|
|
||||||
|
popup-login-credentials-warning = Warning! No credentials provided!
|
||||||
|
popup-login-credentials-warning-label = Warning! No credentials provided! The servers will not be able to let you in due to lack of authorization. Please keep this in mind.
|
||||||
|
popup-login-credentials-warning-go-auth = Go to auth page
|
||||||
|
popup-login-credentials-warning-cancel = Cancel
|
||||||
|
popup-login-credentials-warning-proceed = Proceed
|
||||||
|
|
||||||
|
goto-path-home = Root folder
|
||||||
|
tab-favorite = Favorite
|
||||||
|
server-list-loading = Loading server list.. Please wait
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
tab-account = Аккаунт
|
||||||
|
tab-servers = Серверы
|
||||||
|
tab-content = Контент
|
||||||
|
tab-settings = Настройки
|
||||||
|
vcruntime-check-error = VC runtime dll-библиотеки отсутствуют на этом компьютере. Установите VC runtime dll.
|
||||||
|
migration-label-task = Задача миграции, подождите...
|
||||||
|
task-cancel = Отмена
|
||||||
|
|
||||||
|
popup-edit-name = Изменить имя сервера
|
||||||
|
popup-add-favorite = Добавить в избранное
|
||||||
|
popup-exception = Произошло исключение
|
||||||
|
popup-information = Информация
|
||||||
|
popup-loading = Загрузка...
|
||||||
|
popup-twofa = 2FA
|
||||||
|
|
||||||
|
account-profiles = Профили
|
||||||
|
account-profile-select = Выбрать
|
||||||
|
account-profile-delete = Удалить
|
||||||
|
account-auth-retry = Повторить аутентификацию
|
||||||
|
account-auth-try-another = Или попробуйте другой аккаунт
|
||||||
|
account-auth-login = Введите логин
|
||||||
|
account-auth-password = Введите пароль
|
||||||
|
account-auth-server = Сервер аутентификации
|
||||||
|
account-auth-button = Аутентифицировать
|
||||||
|
account-auth-save = Сохранить профиль
|
||||||
|
account-auth-hello = Привет,
|
||||||
|
account-auth-current-server = Текущий сервер авторизации:
|
||||||
|
account-auth-logout = Выйти
|
||||||
|
auth-current-login-name = Текущий профиль {$auth_server}: {$login}
|
||||||
|
auth-current-login-no-name = Профиль не выбран
|
||||||
|
|
||||||
|
auth-processing = Обработка запроса аутентификации...
|
||||||
|
auth-error = Произошла ошибка аутентификации.
|
||||||
|
auth-error-occured = Во время аутентификации произошла ошибка.
|
||||||
|
auth-invalid-credentials = Неверное имя пользователя или пароль. Попробуйте еще раз.
|
||||||
|
auth-connection-error = Не удается подключиться к серверу аутентификации.
|
||||||
|
auth-name-resolution-error = Не удалось разрешить адрес сервера. Проверьте сетевые настройки или конфигурацию сервера.
|
||||||
|
auth-secure-error = Не удалось подключиться к серверу по SSL. Проверьте сетевые настройки.
|
||||||
|
auth-config-read = Чтение конфигурации аутентификации...
|
||||||
|
auth-try-auth-config = Попытка аутентификации с использованием сохраненной конфигурации.
|
||||||
|
auth-try-auth-profile = Попытка аутентификации с использованием профиля
|
||||||
|
|
||||||
|
config-export-logs = Экспортировать логи
|
||||||
|
config-open-data = Открыть путь данных
|
||||||
|
config-reset = Сбросить к значениям по умолчанию
|
||||||
|
config-save = Сохранить изменения
|
||||||
|
config-remove-content-all = Удалить весь контент
|
||||||
|
|
||||||
|
filter-roleplay = Ролевая игра
|
||||||
|
filter-language = Язык
|
||||||
|
favorite-add = Добавить в избранное
|
||||||
|
popup-add-favorite-invalid-ip = Пожалуйста, введите валидный адрес
|
||||||
|
servername-set = Установить имя сервера
|
||||||
|
servername-clear = Очистить
|
||||||
|
twofa-enabled = У вас включена двухфакторная аутентификация. Введите код.
|
||||||
|
twofa-set = Продолжить
|
||||||
|
|
||||||
|
server-search = Поиск сервера
|
||||||
|
serverentry-tag = Тэги
|
||||||
|
serverentry-map = Карта
|
||||||
|
serverentry-preset = Режим
|
||||||
|
|
||||||
|
content-view-server = Сервер
|
||||||
|
content-view-path = Путь
|
||||||
|
|
||||||
|
popup-login-credentials-warning = Предупреждение! Учетные данные не указаны!
|
||||||
|
popup-login-credentials-warning-label = Предупреждение! Учетные данные не указаны! Серверы не смогут пропустить вас из-за отсутствия авторизации. Пожалуйста, имейте это в виду.
|
||||||
|
popup-login-credentials-warning-go-auth = Перейти на страницу авторизации
|
||||||
|
popup-login-credentials-warning-cancel = Отмена
|
||||||
|
popup-login-credentials-warning-proceed = Продолжить
|
||||||
|
|
||||||
|
goto-path-home = Корн. папка
|
||||||
|
tab-favorite = Избранное
|
||||||
|
server-list-loading = Загрузка списка серверов. Пожалуйста, подождите...
|
||||||
7
Nebula.Launcher/Assets/svg/pencil.svg
Normal file
7
Nebula.Launcher/Assets/svg/pencil.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- License: Apache. Made by Remix Design: https://github.com/Remix-Design/remixicon -->
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path fill="white" d="M15.728 9.686l-1.414-1.414L5 17.586V19h1.414l9.314-9.314zm1.414-1.414l1.414-1.414-1.414-1.414-1.414 1.414 1.414 1.414zM7.242 21H3v-4.243L16.435 3.322a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L7.243 21z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
99
Nebula.Launcher/Configurations/ArrayUnitConfigControl.cs
Normal file
99
Nebula.Launcher/Configurations/ArrayUnitConfigControl.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public sealed class ArrayUnitConfigControl : Border, IConfigControl
|
||||||
|
{
|
||||||
|
private readonly List<IConfigControl> _itemControls = [];
|
||||||
|
private readonly StackPanel _itemsPanel = new StackPanel() { Orientation = Orientation.Vertical };
|
||||||
|
private readonly Button _addButton = new Button() { Content = new Label()
|
||||||
|
{
|
||||||
|
Content = "Add Item"
|
||||||
|
}, Classes = { "ConfigBorder" }};
|
||||||
|
private readonly int _oldCount;
|
||||||
|
private readonly Type _elementType;
|
||||||
|
private readonly StackPanel _panel = new();
|
||||||
|
|
||||||
|
public string ConfigName { get; }
|
||||||
|
public bool Dirty => _itemControls.Any(dirty => dirty.Dirty) || _itemControls.Count != _oldCount;
|
||||||
|
|
||||||
|
public ArrayUnitConfigControl(string name, object value)
|
||||||
|
{
|
||||||
|
Classes.Add("ConfigBorder");
|
||||||
|
_elementType = value.GetType().GetElementType()!;
|
||||||
|
|
||||||
|
ConfigName = name;
|
||||||
|
_panel.Orientation = Orientation.Vertical;
|
||||||
|
_panel.Spacing = 4f;
|
||||||
|
_itemsPanel.Spacing = 4f;
|
||||||
|
|
||||||
|
_panel.Children.Add(new Label { Content = name });
|
||||||
|
_panel.Children.Add(_itemsPanel);
|
||||||
|
_panel.Children.Add(_addButton);
|
||||||
|
|
||||||
|
_addButton.Click += (_, _) => AddItem(ConfigControlHelper.CreateDefaultValue(_elementType)!);
|
||||||
|
Child = _panel;
|
||||||
|
SetValue(value);
|
||||||
|
_oldCount = _itemControls.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddItem(object value)
|
||||||
|
{
|
||||||
|
var itemPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 2 };
|
||||||
|
var control = ConfigControlHelper.GetConfigControl(_itemControls.Count.ToString(), value);
|
||||||
|
var removeButton = new Button { Content = new Label(){ Content = "Remove" }, Classes = { "ConfigBorder" }};
|
||||||
|
|
||||||
|
removeButton.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_itemControls.Remove(control);
|
||||||
|
_itemsPanel.Children.Remove(itemPanel);
|
||||||
|
};
|
||||||
|
|
||||||
|
((Control)control).Margin = new Thickness(5);
|
||||||
|
itemPanel.Children.Add((Control)control);
|
||||||
|
itemPanel.Children.Add(removeButton);
|
||||||
|
|
||||||
|
_itemsPanel.Children.Add(itemPanel);
|
||||||
|
_itemControls.Add(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValue(object value)
|
||||||
|
{
|
||||||
|
_itemControls.Clear();
|
||||||
|
_itemsPanel.Children.Clear();
|
||||||
|
|
||||||
|
if (value is IEnumerable list)
|
||||||
|
{
|
||||||
|
foreach (var item in list)
|
||||||
|
{
|
||||||
|
AddItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetValue()
|
||||||
|
{
|
||||||
|
return ConvertArray(_itemControls.Select(c => c.GetValue()).ToArray(), _elementType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Array ConvertArray(Array sourceArray, Type targetType)
|
||||||
|
{
|
||||||
|
int length = sourceArray.Length;
|
||||||
|
var newArray = Array.CreateInstance(targetType, length);
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var value = sourceArray.GetValue(i);
|
||||||
|
var converted = Convert.ChangeType(value, targetType);
|
||||||
|
newArray.SetValue(converted, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Nebula.Launcher/Configurations/ComplexConVarBinder.cs
Normal file
86
Nebula.Launcher/Configurations/ComplexConVarBinder.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Shared.Configurations;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Configurations;
|
||||||
|
|
||||||
|
public abstract class ComplexConVarBinder<T> : INotifyPropertyChanged, INotifyPropertyChanging
|
||||||
|
{
|
||||||
|
private readonly ConVarObserver<T> _baseConVar;
|
||||||
|
private readonly Lock _lock = new();
|
||||||
|
private readonly SemaphoreSlim _valueChangeSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
public T? Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _baseConVar.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ = SetValueAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasValue
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _baseConVar.HasValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ComplexConVarBinder(ConVarObserver<T> baseConVar)
|
||||||
|
{
|
||||||
|
_baseConVar = baseConVar ?? throw new ArgumentNullException(nameof(baseConVar));
|
||||||
|
_baseConVar.PropertyChanged += BaseConVarOnPropertyChanged;
|
||||||
|
_baseConVar.PropertyChanging += BaseConVarOnPropertyChanging;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task SetValueAsync(T? value)
|
||||||
|
{
|
||||||
|
await _valueChangeSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newValue = await OnValueChange(value).ConfigureAwait(false);
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_baseConVar.Value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_valueChangeSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<T?> OnValueChange(T? newValue);
|
||||||
|
|
||||||
|
private void BaseConVarOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasValue)));
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BaseConVarOnPropertyChanging(object? sender, PropertyChangingEventArgs e)
|
||||||
|
{
|
||||||
|
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(HasValue)));
|
||||||
|
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public event PropertyChangingEventHandler? PropertyChanging;
|
||||||
|
}
|
||||||
68
Nebula.Launcher/Configurations/ComplexUnitConfigControl.cs
Normal file
68
Nebula.Launcher/Configurations/ComplexUnitConfigControl.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public sealed class ComplexUnitConfigControl : Border, IConfigControl
|
||||||
|
{
|
||||||
|
private readonly List<(PropertyInfo, IConfigControl)> _units = [];
|
||||||
|
|
||||||
|
private Type _objectType = typeof(object);
|
||||||
|
|
||||||
|
private readonly StackPanel _panel = new();
|
||||||
|
|
||||||
|
public string ConfigName { get; }
|
||||||
|
public bool Dirty => _units.Any(dirty => dirty.Item2.Dirty);
|
||||||
|
|
||||||
|
public ComplexUnitConfigControl(string name, object obj)
|
||||||
|
{
|
||||||
|
Classes.Add("ConfigBorder");
|
||||||
|
_panel.Orientation = Orientation.Vertical;
|
||||||
|
_panel.Spacing = 4f;
|
||||||
|
ConfigName = name;
|
||||||
|
Child = _panel;
|
||||||
|
SetValue(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValue(object value)
|
||||||
|
{
|
||||||
|
_units.Clear();
|
||||||
|
_panel.Children.Clear();
|
||||||
|
_objectType = value.GetType();
|
||||||
|
|
||||||
|
_panel.Children.Add(new Label()
|
||||||
|
{
|
||||||
|
Content = ConfigName
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var propInfo in _objectType.GetProperties())
|
||||||
|
{
|
||||||
|
if(propInfo.PropertyType.IsInterface)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var propValue = propInfo.GetValue(value);
|
||||||
|
|
||||||
|
var control = ConfigControlHelper.GetConfigControl(propInfo.Name, propValue!);
|
||||||
|
|
||||||
|
((Control)control).Margin = new Thickness(5);
|
||||||
|
_panel.Children.Add((Control)control);
|
||||||
|
_units.Add((propInfo,control));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetValue()
|
||||||
|
{
|
||||||
|
var obj = ConfigControlHelper.CreateDefaultValue(_objectType);
|
||||||
|
foreach (var (fieldInfo, configControl) in _units)
|
||||||
|
{
|
||||||
|
fieldInfo.SetValue(obj, configControl.GetValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj!;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Nebula.Launcher/Configurations/ConfigControlHelper.cs
Normal file
39
Nebula.Launcher/Configurations/ConfigControlHelper.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public static class ConfigControlHelper{
|
||||||
|
public static IConfigControl GetConfigControl(string name,object value)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case string stringValue:
|
||||||
|
return new StringUnitConfigControl(name, stringValue);
|
||||||
|
case int intValue:
|
||||||
|
return new IntUnitConfigControl(name, intValue);
|
||||||
|
case float floatValue:
|
||||||
|
return new FloatUnitConfigControl(name, floatValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueType = value.GetType();
|
||||||
|
|
||||||
|
if (valueType.IsArray)
|
||||||
|
return new ArrayUnitConfigControl(name, value);
|
||||||
|
|
||||||
|
return new ComplexUnitConfigControl(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object? CreateDefaultValue(Type type)
|
||||||
|
{
|
||||||
|
if(type.IsValueType)
|
||||||
|
return Activator.CreateInstance(type);
|
||||||
|
|
||||||
|
var ctor = type.GetConstructors().First();
|
||||||
|
var parameters = ctor.GetParameters()
|
||||||
|
.Select(p => CreateDefaultValue(p.ParameterType))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ctor.Invoke(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Nebula.Launcher/Configurations/FloatUnitConfigControl.cs
Normal file
19
Nebula.Launcher/Configurations/FloatUnitConfigControl.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public sealed class FloatUnitConfigControl(string name, float value) : UnitConfigControl<float>(name, value)
|
||||||
|
{
|
||||||
|
|
||||||
|
public CultureInfo CultureInfo = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
public override void SetConfValue(float value)
|
||||||
|
{
|
||||||
|
ConfigValue = value.ToString(CultureInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float GetConfValue()
|
||||||
|
{
|
||||||
|
return float.Parse(ConfigValue, CultureInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Nebula.Launcher/Configurations/IConfigControl.cs
Normal file
9
Nebula.Launcher/Configurations/IConfigControl.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public interface IConfigControl
|
||||||
|
{
|
||||||
|
public string ConfigName { get; }
|
||||||
|
public bool Dirty {get;}
|
||||||
|
public abstract void SetValue(object value);
|
||||||
|
public abstract object GetValue();
|
||||||
|
}
|
||||||
14
Nebula.Launcher/Configurations/IntUnitConfigControl.cs
Normal file
14
Nebula.Launcher/Configurations/IntUnitConfigControl.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public sealed class IntUnitConfigControl(string name, int value) : UnitConfigControl<int>(name, value)
|
||||||
|
{
|
||||||
|
public override void SetConfValue(int value)
|
||||||
|
{
|
||||||
|
ConfigValue = value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetConfValue()
|
||||||
|
{
|
||||||
|
return int.Parse(ConfigValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Nebula.Launcher/Configurations/StringUnitConfigControl.cs
Normal file
14
Nebula.Launcher/Configurations/StringUnitConfigControl.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public sealed class StringUnitConfigControl(string name, string value) : UnitConfigControl<string>(name, value)
|
||||||
|
{
|
||||||
|
public override void SetConfValue(string value)
|
||||||
|
{
|
||||||
|
ConfigValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetConfValue()
|
||||||
|
{
|
||||||
|
return ConfigValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Nebula.Launcher/Configurations/UnitConfigControl.cs
Normal file
53
Nebula.Launcher/Configurations/UnitConfigControl.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
public abstract class UnitConfigControl<T> : Border, IConfigControl where T : notnull
|
||||||
|
{
|
||||||
|
private readonly Label _nameLabel = new();
|
||||||
|
private readonly TextBox _valueLabel = new();
|
||||||
|
private string _originalValue;
|
||||||
|
|
||||||
|
private StackPanel _panel = new();
|
||||||
|
|
||||||
|
public string ConfigName { get; }
|
||||||
|
|
||||||
|
public bool Dirty => _originalValue != ConfigValue;
|
||||||
|
|
||||||
|
protected string ConfigValue
|
||||||
|
{
|
||||||
|
get => _valueLabel.Text ?? string.Empty;
|
||||||
|
set => _valueLabel.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnitConfigControl(string name, T value)
|
||||||
|
{
|
||||||
|
Classes.Add("ConfigBorder");
|
||||||
|
ConfigName = name;
|
||||||
|
_panel.Orientation = Orientation.Horizontal;
|
||||||
|
_panel.Children.Add(_nameLabel);
|
||||||
|
_panel.Children.Add(_valueLabel);
|
||||||
|
|
||||||
|
_nameLabel.Content = name;
|
||||||
|
_nameLabel.VerticalAlignment = VerticalAlignment.Center;
|
||||||
|
Child = _panel;
|
||||||
|
|
||||||
|
SetConfValue(value);
|
||||||
|
_originalValue = ConfigValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void SetConfValue(T value);
|
||||||
|
|
||||||
|
public abstract T GetConfValue();
|
||||||
|
|
||||||
|
public void SetValue(object value)
|
||||||
|
{
|
||||||
|
SetConfValue((T)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetValue()
|
||||||
|
{
|
||||||
|
return GetConfValue()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,13 +19,16 @@ public class FilterBox : UserControl
|
|||||||
|
|
||||||
public Action<FilterBoxChangedEventArgs>? OnFilterChanged {get; set;}
|
public Action<FilterBoxChangedEventArgs>? OnFilterChanged {get; set;}
|
||||||
|
|
||||||
public string? FilterBoxName {
|
public string FilterBoxName {
|
||||||
set => filterName.Text = value;
|
set => filterName.LocalId = value;
|
||||||
get => filterName.Text;
|
get => filterName.LocalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private StackPanel filterPanel;
|
private StackPanel filterPanel;
|
||||||
private TextBox filterName = new TextBox();
|
private LocalizedLabel filterName = new LocalizedLabel()
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
public FilterBox()
|
public FilterBox()
|
||||||
{
|
{
|
||||||
@@ -34,6 +37,8 @@ public class FilterBox : UserControl
|
|||||||
Orientation = Orientation.Horizontal,
|
Orientation = Orientation.Horizontal,
|
||||||
Spacing = 5,
|
Spacing = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
filterPanel.Children.Add(filterName);
|
||||||
|
|
||||||
Content = filterPanel;
|
Content = filterPanel;
|
||||||
}
|
}
|
||||||
|
|||||||
20
Nebula.Launcher/Controls/LocalizedLabel.cs
Normal file
20
Nebula.Launcher/Controls/LocalizedLabel.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Controls;
|
||||||
|
|
||||||
|
public class LocalizedLabel : Label
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string> LocalIdProperty = AvaloniaProperty.Register<LocalizedLabel, string>(nameof(LocalId));
|
||||||
|
|
||||||
|
public string LocalId
|
||||||
|
{
|
||||||
|
get => GetValue(LocalIdProperty);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetValue(LocalIdProperty, value);
|
||||||
|
Content = LocalizationService.GetString(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Nebula.Launcher.ServerListProviders;
|
|
||||||
using Nebula.Launcher.ViewModels;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Controls;
|
|
||||||
|
|
||||||
public partial class ServerListView : UserControl
|
|
||||||
{
|
|
||||||
private IServerListProvider _provider = default!;
|
|
||||||
private ServerFilter? _currentFilter;
|
|
||||||
|
|
||||||
public bool IsLoading { get; private set; }
|
|
||||||
|
|
||||||
public ServerListView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ServerListView TakeFrom(IServerListProvider provider)
|
|
||||||
{
|
|
||||||
var serverListView = new ServerListView();
|
|
||||||
if (provider is IServerListDirtyInvoker invoker)
|
|
||||||
{
|
|
||||||
invoker.Dirty += serverListView.OnDirty;
|
|
||||||
}
|
|
||||||
serverListView._provider = provider;
|
|
||||||
serverListView.RefreshFromProvider();
|
|
||||||
return serverListView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshFromProvider()
|
|
||||||
{
|
|
||||||
if (IsLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Clear();
|
|
||||||
StartLoading();
|
|
||||||
|
|
||||||
_provider.LoadServerList();
|
|
||||||
|
|
||||||
if (_provider.IsLoaded) PasteServersFromList();
|
|
||||||
else _provider.OnLoaded += RefreshRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyFilter(ServerFilter? filter)
|
|
||||||
{
|
|
||||||
_currentFilter = filter;
|
|
||||||
|
|
||||||
if(IsLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (IFilterConsumer? serverView in ServerList.Items)
|
|
||||||
{
|
|
||||||
serverView?.ProcessFilter(filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDirty()
|
|
||||||
{
|
|
||||||
RefreshFromProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
ErrorList.Items.Clear();
|
|
||||||
ServerList.Items.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PasteServersFromList()
|
|
||||||
{
|
|
||||||
foreach (var serverEntry in _provider.GetServers())
|
|
||||||
{
|
|
||||||
ServerList.Items.Add(serverEntry);
|
|
||||||
serverEntry.ProcessFilter(_currentFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in _provider.GetErrors())
|
|
||||||
{
|
|
||||||
ErrorList.Items.Add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshRequired()
|
|
||||||
{
|
|
||||||
PasteServersFromList();
|
|
||||||
_provider.OnLoaded -= RefreshRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartLoading()
|
|
||||||
{
|
|
||||||
Clear();
|
|
||||||
IsLoading = true;
|
|
||||||
LoadingLabel.IsVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndLoading()
|
|
||||||
{
|
|
||||||
IsLoading = false;
|
|
||||||
LoadingLabel.IsVisible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
157
Nebula.Launcher/Controls/SimpleGraph.cs
Normal file
157
Nebula.Launcher/Controls/SimpleGraph.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Reactive;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Controls;
|
||||||
|
|
||||||
|
public class SimpleGraph : Control
|
||||||
|
{
|
||||||
|
// Bindable data: list of doubles or points
|
||||||
|
public static readonly StyledProperty<ObservableCollection<double>> ValuesProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph,ObservableCollection<double>>(nameof(Values));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> GraphBrushProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph, IBrush>(nameof(GraphBrush), Brushes.CornflowerBlue);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> GridBrushProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph, IBrush>(nameof(GridBrush), Brushes.LightGray);
|
||||||
|
|
||||||
|
static SimpleGraph()
|
||||||
|
{
|
||||||
|
ValuesProperty.Changed.Subscribe(
|
||||||
|
new AnonymousObserver<AvaloniaPropertyChangedEventArgs<ObservableCollection<double>>>(args =>
|
||||||
|
{
|
||||||
|
if (args.Sender is not SimpleGraph g)
|
||||||
|
return;
|
||||||
|
|
||||||
|
g.InvalidateVisual();
|
||||||
|
g.Values.CollectionChanged += g.ValuesOnCollectionChanged;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleGraph()
|
||||||
|
{
|
||||||
|
Values = new ObservableCollection<double>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValuesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(InvalidateVisual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<double> Values
|
||||||
|
{
|
||||||
|
get => GetValue(ValuesProperty);
|
||||||
|
set => SetValue(ValuesProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IBrush GraphBrush
|
||||||
|
{
|
||||||
|
get => GetValue(GraphBrushProperty);
|
||||||
|
set => SetValue(GraphBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBrush GridBrush
|
||||||
|
{
|
||||||
|
get => GetValue(GridBrushProperty);
|
||||||
|
set => SetValue(GridBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
base.Render(context);
|
||||||
|
|
||||||
|
|
||||||
|
if (Bounds.Width <= 0 || Bounds.Height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// background grid
|
||||||
|
DrawGrid(context, Bounds);
|
||||||
|
|
||||||
|
if (Values.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
var min = Values.Min();
|
||||||
|
var max = Values.Max();
|
||||||
|
if (Math.Abs(min - max) < 0.001)
|
||||||
|
{
|
||||||
|
min -= 1;
|
||||||
|
max += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var geo = new StreamGeometry();
|
||||||
|
using (var ctx = geo.Open())
|
||||||
|
{
|
||||||
|
if (Values.Count > 1)
|
||||||
|
{
|
||||||
|
Point p0 = Map(0, Values[0]);
|
||||||
|
ctx.BeginFigure(p0, false);
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < Values.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var p1 = Map(i, Values[i]);
|
||||||
|
var p2 = Map(i + 1, Values[i + 1]);
|
||||||
|
|
||||||
|
|
||||||
|
// control points for smoothing
|
||||||
|
var c1 = new Point((p1.X + p2.X) / 2, p1.Y);
|
||||||
|
var c2 = new Point((p1.X + p2.X) / 2, p2.Y);
|
||||||
|
|
||||||
|
|
||||||
|
ctx.CubicBezierTo(c1, c2, p2);
|
||||||
|
}
|
||||||
|
ctx.EndFigure(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// stroke
|
||||||
|
context.DrawGeometry(null, new Pen(GraphBrush, 2), geo);
|
||||||
|
|
||||||
|
// draw points
|
||||||
|
for (var i = 0; i < Values.Count; i++)
|
||||||
|
{
|
||||||
|
var p = Map(i, Values[i]);
|
||||||
|
context.DrawEllipse(GraphBrush, null, p, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
// map data index/value -> point
|
||||||
|
Point Map(int i, double val)
|
||||||
|
{
|
||||||
|
var x = Bounds.X + Bounds.Width * (i / (double)Math.Max(1, Values.Count - 1));
|
||||||
|
var y = Bounds.Y + Bounds.Height - (val - min) / (max - min) * Bounds.Height;
|
||||||
|
return new Point(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGrid(DrawingContext dc, Rect r)
|
||||||
|
{
|
||||||
|
var pen = new Pen(GridBrush, 0.5);
|
||||||
|
var rows = 4;
|
||||||
|
var cols = Math.Max(2, Values?.Count ?? 2);
|
||||||
|
for (var i = 0; i <= rows; i++)
|
||||||
|
{
|
||||||
|
var y = r.Y + i * (r.Height / rows);
|
||||||
|
dc.DrawLine(pen, new Point(r.X, y), new Point(r.Right, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j <= cols; j++)
|
||||||
|
{
|
||||||
|
var x = r.X + j * (r.Width / cols);
|
||||||
|
dc.DrawLine(pen, new Point(x, r.Y), new Point(x, r.Bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
using Avalonia;
|
using System;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Color = System.Drawing.Color;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Converters;
|
namespace Nebula.Launcher.Converters;
|
||||||
|
|
||||||
public class TypeConverters
|
public static class TypeConverters
|
||||||
{
|
{
|
||||||
public static FuncValueConverter<string, string?> IconConverter { get; } =
|
public static FuncValueConverter<string, string?> IconConverter { get; } =
|
||||||
new(iconKey =>
|
new(iconKey =>
|
||||||
@@ -13,4 +16,17 @@ public class TypeConverters
|
|||||||
if (iconKey == null) return null;
|
if (iconKey == null) return null;
|
||||||
return $"/Assets/svg/{iconKey}.svg";
|
return $"/Assets/svg/{iconKey}.svg";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public static FuncValueConverter<string, IImage?> ImageConverter { get; } =
|
||||||
|
new(iconKey =>
|
||||||
|
{
|
||||||
|
if (iconKey == null) return null;
|
||||||
|
return new Avalonia.Media.Imaging.Bitmap(AssetLoader.Open(new Uri($"avares://Nebula.Launcher/Assets/error_presentation/{iconKey}.png")));
|
||||||
|
});
|
||||||
|
|
||||||
|
public static FuncValueConverter<string, Avalonia.Media.Color> NameColorRepresentation { get; } =
|
||||||
|
new((str)=>ColorUtils.GetColorFromString(str ?? throw new ArgumentNullException(nameof(str),"Name of color is null!")));
|
||||||
|
|
||||||
|
public static FuncValueConverter<string, bool> StringIsNotEmpty { get; } =
|
||||||
|
new(iconKey => !string.IsNullOrEmpty(iconKey));
|
||||||
}
|
}
|
||||||
@@ -1,2 +1 @@
|
|||||||
global using Nebula.Shared.Attributes;
|
global using Nebula.Shared.Attributes;
|
||||||
global using Nebula.Launcher.ViewHelper;
|
|
||||||
@@ -1,38 +1,60 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.Models.Auth;
|
||||||
|
using Nebula.Shared.ConfigMigrations;
|
||||||
|
using Nebula.Shared.Configurations;
|
||||||
|
using Nebula.Shared.Configurations.Migrations;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
namespace Nebula.Launcher;
|
namespace Nebula.Launcher;
|
||||||
|
|
||||||
public static class LauncherConVar
|
public static class LauncherConVar
|
||||||
{
|
{
|
||||||
public static readonly ConVar<ProfileAuthCredentials[]> AuthProfiles =
|
public static readonly ConVar<bool> DoMigration =
|
||||||
ConVarBuilder.Build<ProfileAuthCredentials[]>("auth.profiles.v2", []);
|
ConVarBuilder.Build("migration.doMigrate", true);
|
||||||
|
|
||||||
|
public static readonly ConVar<string[]> AuthProfiles =
|
||||||
|
ConVarBuilder.BuildWithMigration<string[]>("auth.profiles.v4",
|
||||||
|
MigrationQueueBuilder.Instance
|
||||||
|
.With(new ProfileMigrationV2("auth.profiles.v2","auth.profiles.v4"))
|
||||||
|
.With(new ProfileMigrationV3V4("auth.profiles.v3","auth.profiles.v4"))
|
||||||
|
.Build(),
|
||||||
|
[]);
|
||||||
|
|
||||||
public static readonly ConVar<CurrentAuthInfo?> AuthCurrent =
|
public static readonly ConVar<AuthTokenCredentials?> AuthCurrent =
|
||||||
ConVarBuilder.Build<CurrentAuthInfo?>("auth.current.v2");
|
ConVarBuilder.Build<AuthTokenCredentials?>("auth.current.v2");
|
||||||
|
|
||||||
public static readonly ConVar<string[]> Favorites =
|
public static readonly ConVar<string[]> Favorites =
|
||||||
ConVarBuilder.Build<string[]>("server.favorites", []);
|
ConVarBuilder.Build<string[]>("server.favorites", []);
|
||||||
|
|
||||||
public static readonly ConVar<AuthServerCredentials[]> AuthServers = ConVarBuilder.Build<AuthServerCredentials[]>("launcher.authServers", [
|
public static readonly ConVar<Dictionary<string,string>> ServerCustomNames =
|
||||||
|
ConVarBuilder.Build<Dictionary<string,string>>("server.names", []);
|
||||||
|
|
||||||
|
public static readonly ConVar<AuthServerCredentials[]> AuthServers =
|
||||||
|
ConVarBuilder.Build<AuthServerCredentials[]>("launcher.authServers", [
|
||||||
new AuthServerCredentials(
|
new AuthServerCredentials(
|
||||||
"WizDen",
|
"WizDen",
|
||||||
[
|
[
|
||||||
"https://harpy.durenko.tatar/auth-api/",
|
"https://feline.durenko.tatar/auth-api/",
|
||||||
"https://auth.fallback.spacestation14.com/"
|
"https://auth.spacestation14.com/",
|
||||||
|
"https://auth.fallback.spacestation14.com/",
|
||||||
|
]),
|
||||||
|
new AuthServerCredentials(
|
||||||
|
"SimpleStation",
|
||||||
|
[
|
||||||
|
"https://auth.simplestation.org/",
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
|
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
|
||||||
new ServerHubRecord("WizDen", "https://harpy.durenko.tatar/hub-api/api/servers", null),
|
new ServerHubRecord("WizDen", "https://feline.durenko.tatar/hub-api/api/servers"),
|
||||||
new ServerHubRecord("AltHub","https://web.networkgamez.com/api/servers",null)
|
new ServerHubRecord("AltHub","https://hub.singularity14.co.uk/api/servers")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", "en-US");
|
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", CultureInfo.CurrentCulture.Name);
|
||||||
public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url",
|
public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url",
|
||||||
"https://github.com/icsharpcode/ILSpy/releases/download/v9.0/ILSpy_binaries_9.0.0.7889-x64.zip");
|
"https://feline.durenko.tatar/ILSpy_selfcontained_10.0.0.8330-x64.zip");
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly ConVar<string> ILSpyVersion = ConVarBuilder.Build<string>("dotnet.version", "10");
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,9 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:views="clr-namespace:Nebula.Launcher.Views"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
Width="600"
|
|
||||||
Height="400"
|
|
||||||
x:Class="Nebula.Launcher.MessageBox.MessageView">
|
x:Class="Nebula.Launcher.MessageBox.MessageView">
|
||||||
<Grid RowDefinitions="50,*" ColumnDefinitions="*">
|
<views:VisualErrorView x:Name="ErrorView"/>
|
||||||
<Border Grid.Column="0" Background="#222222" Padding="10" BorderBrush="#444444" BorderThickness="0,0,0,3">
|
|
||||||
<Label VerticalAlignment="Center" x:Name="Title">Text</Label>
|
|
||||||
</Border>
|
|
||||||
<Panel Margin="5" Grid.Row="1">
|
|
||||||
<Label x:Name="Message">Message</Label>
|
|
||||||
</Panel>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.MessageBox;
|
namespace Nebula.Launcher.MessageBox;
|
||||||
|
|
||||||
public partial class MessageView : UserControl, IMessageContainerProvider
|
public partial class MessageView : UserControl, IMessageContainerProvider
|
||||||
{
|
{
|
||||||
public MessageView(out IMessageContainerProvider provider)
|
private readonly VisualErrorViewModel _context;
|
||||||
|
public MessageView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
provider = this;
|
_context = new VisualErrorViewModel();
|
||||||
|
ErrorView.Content = _context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowMessage(string message, string title)
|
public void ShowMessage(string message, string title)
|
||||||
{
|
{
|
||||||
Title.Content = title;
|
_context.Title = title;
|
||||||
Message.Content = message;
|
_context.Description = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,52 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:messageBox="clr-namespace:Nebula.Launcher.MessageBox"
|
xmlns:messageBox="clr-namespace:Nebula.Launcher.MessageBox"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
SystemDecorations="BorderOnly"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="260"
|
||||||
Width="600"
|
Width="600"
|
||||||
Height="400"
|
Height="260"
|
||||||
|
CanResize="False"
|
||||||
x:Class="Nebula.Launcher.MessageBox.MessageWindow"
|
x:Class="Nebula.Launcher.MessageBox.MessageWindow"
|
||||||
Title="MessageWindow">
|
Title="MessageWindow">
|
||||||
|
<Grid ColumnDefinitions="*" RowDefinitions="30,*">
|
||||||
|
<messageBox:MessageView
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
x:Name="MessageView" />
|
||||||
|
<Border
|
||||||
|
|
||||||
|
BorderThickness="0,0,0,2"
|
||||||
|
CornerRadius="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0">
|
||||||
|
<Border.BorderBrush>
|
||||||
|
<LinearGradientBrush EndPoint="100%,50%" StartPoint="0%,50%">
|
||||||
|
<GradientStop Color="#222222" Offset="0.0" />
|
||||||
|
<GradientStop Color="#442222" Offset="1.0" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Border.BorderBrush>
|
||||||
|
<Panel
|
||||||
|
Height="30"
|
||||||
|
PointerPressed="InputElement_OnPointerPressed">
|
||||||
|
<TextBlock
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="White"
|
||||||
|
IsVisible="False"
|
||||||
|
Margin="15,0"
|
||||||
|
Text="Nebula Launcher"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<StackPanel
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="5,0,5,0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<Button
|
||||||
|
Click="Close_Click"
|
||||||
|
Content="🗙"
|
||||||
|
Foreground="Azure" />
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
namespace Nebula.Launcher.MessageBox;
|
namespace Nebula.Launcher.MessageBox;
|
||||||
|
|
||||||
public partial class MessageWindow : Window
|
public partial class MessageWindow : Window, IMessageContainerProvider
|
||||||
{
|
{
|
||||||
public MessageWindow(out IMessageContainerProvider provider)
|
public MessageWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Content = new MessageView(out provider);
|
}
|
||||||
|
|
||||||
|
public void ShowMessage(string message, string title)
|
||||||
|
{
|
||||||
|
MessageView.ShowMessage(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close_Click(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
BeginMoveDrag(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
Nebula.Launcher/Models/Auth/AuthServerCredentials.cs
Normal file
6
Nebula.Launcher/Models/Auth/AuthServerCredentials.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nebula.Launcher.Models.Auth;
|
||||||
|
|
||||||
|
public sealed record AuthServerCredentials(
|
||||||
|
string Name,
|
||||||
|
string[] Servers
|
||||||
|
);
|
||||||
12
Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs
Normal file
12
Nebula.Launcher/Models/Auth/ProfileAuthCredentials.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Models.Auth;
|
||||||
|
|
||||||
|
public sealed record ProfileEntry(
|
||||||
|
ProfileAuthCredentials Credentials,
|
||||||
|
string AuthName,
|
||||||
|
[property: JsonIgnore] ICommand OnSelect = default!,
|
||||||
|
[property: JsonIgnore] ICommand OnDelete = default!);
|
||||||
67
Nebula.Launcher/Models/ContentLogConsumer.cs
Normal file
67
Nebula.Launcher/Models/ContentLogConsumer.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public sealed class ContentLogConsumer : IProcessLogConsumer
|
||||||
|
{
|
||||||
|
private readonly PopupMessageService _popupMessageService;
|
||||||
|
private readonly List<string> _outMessages = [];
|
||||||
|
|
||||||
|
private LogPopupModelView? _currentLogPopup;
|
||||||
|
|
||||||
|
public int MaxMessages { get; set; } = 100;
|
||||||
|
|
||||||
|
public ContentLogConsumer(PopupMessageService popupMessageService)
|
||||||
|
{
|
||||||
|
_popupMessageService = popupMessageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Popup()
|
||||||
|
{
|
||||||
|
if(_currentLogPopup is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentLogPopup = new LogPopupModelView(_popupMessageService);
|
||||||
|
_currentLogPopup.OnDisposing += OnLogPopupDisposing;
|
||||||
|
|
||||||
|
foreach (var message in _outMessages.ToArray())
|
||||||
|
{
|
||||||
|
_currentLogPopup.Append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_popupMessageService.Popup(_currentLogPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogPopupDisposing(PopupViewModelBase obj)
|
||||||
|
{
|
||||||
|
if(_currentLogPopup == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentLogPopup.OnDisposing -= OnLogPopupDisposing;
|
||||||
|
_currentLogPopup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Out(string text)
|
||||||
|
{
|
||||||
|
_outMessages.Add(text);
|
||||||
|
if(_outMessages.Count >= MaxMessages)
|
||||||
|
_outMessages.RemoveAt(0);
|
||||||
|
|
||||||
|
_currentLogPopup?.Append(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string text)
|
||||||
|
{
|
||||||
|
Out(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string text)
|
||||||
|
{
|
||||||
|
_popupMessageService.Popup(new ExceptionCompound("Error while running program", text));
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Nebula.Launcher/Models/IFilterConsumer.cs
Normal file
8
Nebula.Launcher/Models/IFilterConsumer.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public interface IFilterConsumer
|
||||||
|
{
|
||||||
|
public void ProcessFilter(ServerFilter? serverFilter);
|
||||||
|
}
|
||||||
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public interface IRunningSignalConsumer
|
||||||
|
{
|
||||||
|
public void ProcessRunningSignal(bool isRunning);
|
||||||
|
}
|
||||||
@@ -8,5 +8,4 @@ public record ListItemTemplate(Type ModelType, string IconKey, string Label);
|
|||||||
public record ServerListTabTemplate(IServerListProvider ServerListProvider, string TabName);
|
public record ServerListTabTemplate(IServerListProvider ServerListProvider, string TabName);
|
||||||
public record ServerHubRecord(
|
public record ServerHubRecord(
|
||||||
[property:JsonPropertyName("name")] string Name,
|
[property:JsonPropertyName("name")] string Name,
|
||||||
[property:JsonPropertyName("url")] string MainUrl,
|
[property:JsonPropertyName("url")] string MainUrl);
|
||||||
[property:JsonPropertyName("fallback")] string? Fallback);
|
|
||||||
40
Nebula.Launcher/Models/LogInfo.cs
Normal file
40
Nebula.Launcher/Models/LogInfo.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public sealed class LogInfo
|
||||||
|
{
|
||||||
|
public string Category { get; set; } = "LOG";
|
||||||
|
public IBrush CategoryColor { get; set; } = Brush.Parse("#424242");
|
||||||
|
public string Message { get; set; } = "";
|
||||||
|
|
||||||
|
public static LogInfo FromString(string input)
|
||||||
|
{
|
||||||
|
var matches = Regex.Matches(input, @"(\[(?<c>.*)\] (?<m>.*))|(?<m>.*)");
|
||||||
|
var category = "All";
|
||||||
|
|
||||||
|
if (matches[0].Groups.TryGetValue("c", out var c)) category = c.Value;
|
||||||
|
|
||||||
|
var color = Brush.Parse("#444444");
|
||||||
|
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case "DEBG":
|
||||||
|
color = Brush.Parse("#2436d4");
|
||||||
|
break;
|
||||||
|
case "ERRO":
|
||||||
|
color = Brush.Parse("#d42436");
|
||||||
|
break;
|
||||||
|
case "INFO":
|
||||||
|
color = Brush.Parse("#0ab3c9");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = matches[0].Groups["m"].Value;
|
||||||
|
return new LogInfo
|
||||||
|
{
|
||||||
|
Category = category, Message = message, CategoryColor = color
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
@@ -15,37 +14,24 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
|
<PackageReference Include="AsyncImageLoader.Avalonia"/>
|
||||||
<PackageReference Include="Avalonia" Version="11.2.1"/>
|
<PackageReference Include="Avalonia"/>
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Desktop"/>
|
||||||
<PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2" />
|
<PackageReference Include="Avalonia.Svg.Skia"/>
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Themes.Fluent"/>
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Fonts.Inter"/>
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
|
<PackageReference Include="Avalonia.Diagnostics">
|
||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
|
<PackageReference Include="CommunityToolkit.Mvvm"/>
|
||||||
<PackageReference Include="Fluent.Net" Version="1.0.63" />
|
<PackageReference Include="Fluent.Net"/>
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
|
<PackageReference Include="JetBrains.Annotations"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
|
||||||
<PackageReference Include="libsodium" Version="1.0.20"/>
|
<PackageReference Include="libsodium"/>
|
||||||
</ItemGroup>
|
<PackageReference Include="Robust.Natives"/>
|
||||||
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater"/>
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Views\Tabs\ServerListTab.axaml.cs">
|
|
||||||
<DependentUpon>ServerListTab.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Views\Popup\AddFavoriteView.axaml.cs">
|
|
||||||
<DependentUpon>AddFavoriteView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Controls\ServerListView.axaml.cs">
|
|
||||||
<DependentUpon>ServerListView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="BuildCheck" AfterTargets="AfterBuild">
|
<Target Name="BuildCheck" AfterTargets="AfterBuild">
|
||||||
@@ -53,6 +39,8 @@
|
|||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.pdb" DestinationFolder="$(OutDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.pdb" DestinationFolder="$(OutDir)"/>
|
||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.deps.json" DestinationFolder="$(OutDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.deps.json" DestinationFolder="$(OutDir)"/>
|
||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.runtimeconfig.json" DestinationFolder="$(OutDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.runtimeconfig.json" DestinationFolder="$(OutDir)"/>
|
||||||
|
|
||||||
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\0Harmony.dll" DestinationFolder="$(OutDir)"/>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PublishCheck" AfterTargets="Publish">
|
<Target Name="PublishCheck" AfterTargets="Publish">
|
||||||
@@ -60,6 +48,8 @@
|
|||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.pdb" DestinationFolder="$(PublishDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.pdb" DestinationFolder="$(PublishDir)"/>
|
||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.deps.json" DestinationFolder="$(PublishDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.deps.json" DestinationFolder="$(PublishDir)"/>
|
||||||
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.runtimeconfig.json" DestinationFolder="$(PublishDir)"/>
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\Nebula.Runner.runtimeconfig.json" DestinationFolder="$(PublishDir)"/>
|
||||||
|
|
||||||
|
<Copy SourceFiles="..\Nebula.Runner\bin\$(Configuration)\$(TargetFramework)\0Harmony.dll" DestinationFolder="$(PublishDir)"/>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -72,9 +62,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
|
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
|
||||||
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\Nebula.Runner\Nebula.Runner.csproj"
|
||||||
|
ReferenceOutputAssembly="false" />
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="Controls\ServerListView.axaml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public abstract class DotnetProcessStartInfoProviderBase(DotnetResolverService resolverService) : IProcessStartInfoProvider
|
||||||
|
{
|
||||||
|
protected abstract string GetDllPath();
|
||||||
|
|
||||||
|
public virtual async Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = await resolverService.EnsureDotnet(cancellationToken),
|
||||||
|
Arguments = GetDllPath(),
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
[ServiceRegister(isSingleton:false)]
|
||||||
|
public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverService, AccountInfoViewModel accountInfoViewModel) :
|
||||||
|
DotnetProcessStartInfoProviderBase(resolverService)
|
||||||
|
{
|
||||||
|
private string? _publicKey;
|
||||||
|
private RobustUrl _address = default!;
|
||||||
|
|
||||||
|
protected override string GetDllPath()
|
||||||
|
{
|
||||||
|
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
|
||||||
|
return Path.Join(path, "Nebula.Runner.dll");
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameProcessStartInfoProvider WithBuildInfo(string publicKey, RobustUrl address)
|
||||||
|
{
|
||||||
|
_publicKey = publicKey;
|
||||||
|
_address = address;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var baseStart = await base.GetProcessStartInfo(cancellationToken);
|
||||||
|
|
||||||
|
var authProv = accountInfoViewModel.Credentials.Value;
|
||||||
|
if(authProv is null)
|
||||||
|
throw new Exception("Client is without selected auth");
|
||||||
|
|
||||||
|
baseStart.EnvironmentVariables["ROBUST_AUTH_USERID"] = authProv.UserId.ToString();
|
||||||
|
baseStart.EnvironmentVariables["ROBUST_AUTH_TOKEN"] = authProv.Token.Token;
|
||||||
|
baseStart.EnvironmentVariables["ROBUST_AUTH_SERVER"] = authProv.AuthServer;
|
||||||
|
baseStart.EnvironmentVariables["AUTH_LOGIN"] = authProv.Login;
|
||||||
|
baseStart.EnvironmentVariables["ROBUST_AUTH_PUBKEY"] = _publicKey;
|
||||||
|
baseStart.EnvironmentVariables["GAME_URL"] = _address.ToString();
|
||||||
|
|
||||||
|
return baseStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs
Normal file
45
Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService contentService, EngineService engineService)
|
||||||
|
{
|
||||||
|
public async Task<GameProcessStartInfoProvider> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var buildInfo = await contentService.GetBuildInfo(address, cancellationToken);
|
||||||
|
|
||||||
|
var engine = await engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandlerFactory, cancellationToken);
|
||||||
|
|
||||||
|
if (engine is null)
|
||||||
|
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
|
||||||
|
var hashApi = await contentService.EnsureItems(buildInfo, loadingHandlerFactory, cancellationToken);
|
||||||
|
|
||||||
|
if (hashApi.TryOpen("manifest.yml", out var stream))
|
||||||
|
{
|
||||||
|
var modules = ContentManifestParser.ExtractModules(stream);
|
||||||
|
|
||||||
|
foreach (var moduleStr in modules)
|
||||||
|
{
|
||||||
|
var module = await engineService.EnsureEngineModules(moduleStr, loadingHandlerFactory, buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
if(module is null)
|
||||||
|
throw new Exception("Module not found: " + moduleStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
await stream.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
provider.GetService<GameProcessStartInfoProvider>()!.WithBuildInfo(buildInfo.BuildInfo.Auth.PublicKey,
|
||||||
|
address);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public interface IProcessConsumerCollection
|
||||||
|
{
|
||||||
|
public void RegisterLogger(IProcessLogConsumer consumer);
|
||||||
|
}
|
||||||
8
Nebula.Launcher/ProcessHelper/IProcessLogConsumer.cs
Normal file
8
Nebula.Launcher/ProcessHelper/IProcessLogConsumer.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public interface IProcessLogConsumer
|
||||||
|
{
|
||||||
|
public void Out(string text);
|
||||||
|
public void Error(string text);
|
||||||
|
public void Fatal(string text);
|
||||||
|
}
|
||||||
10
Nebula.Launcher/ProcessHelper/IProcessRunner.cs
Normal file
10
Nebula.Launcher/ProcessHelper/IProcessRunner.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public interface IProcessStartInfoProvider
|
||||||
|
{
|
||||||
|
public Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public sealed class ProcessLogConsumerCollection: IProcessLogConsumer, IProcessConsumerCollection
|
||||||
|
{
|
||||||
|
private readonly List<IProcessLogConsumer> _consumers = [];
|
||||||
|
|
||||||
|
public void RegisterLogger(IProcessLogConsumer consumer)
|
||||||
|
{
|
||||||
|
_consumers.Add(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Out(string text)
|
||||||
|
{
|
||||||
|
foreach (var consumer in _consumers)
|
||||||
|
{
|
||||||
|
consumer.Out(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string text)
|
||||||
|
{
|
||||||
|
foreach (var consumer in _consumers)
|
||||||
|
{
|
||||||
|
consumer.Error(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string text)
|
||||||
|
{
|
||||||
|
foreach (var consumer in _consumers)
|
||||||
|
{
|
||||||
|
consumer.Fatal(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
213
Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs
Normal file
213
Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Services.Logging;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
|
public class ProcessRunHandler : IDisposable
|
||||||
|
{
|
||||||
|
private Process? _process;
|
||||||
|
private readonly IProcessLogConsumer _logConsumer;
|
||||||
|
|
||||||
|
private StringBuilder _lastErrorBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
public bool IsRunning => _process is not null;
|
||||||
|
public Action<ProcessRunHandler>? OnProcessExited;
|
||||||
|
|
||||||
|
public AsyncValueCache<ProcessStartInfo> ProcessStartInfoProvider { get; }
|
||||||
|
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer)
|
||||||
|
{
|
||||||
|
_logConsumer = logConsumer;
|
||||||
|
|
||||||
|
ProcessStartInfoProvider = new AsyncValueCache<ProcessStartInfo>(processStartInfoProvider.GetProcessStartInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckIfDisposed()
|
||||||
|
{
|
||||||
|
if (!Disposed) return;
|
||||||
|
throw new ObjectDisposedException(nameof(ProcessRunHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
CheckIfDisposed();
|
||||||
|
if(_process is not null)
|
||||||
|
throw new InvalidOperationException("Already running");
|
||||||
|
|
||||||
|
_process = Process.Start(ProcessStartInfoProvider.GetValue());
|
||||||
|
|
||||||
|
if (_process is null) return;
|
||||||
|
|
||||||
|
_process.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
_process.BeginOutputReadLine();
|
||||||
|
_process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
_process.OutputDataReceived += OnOutputDataReceived;
|
||||||
|
_process.ErrorDataReceived += OnErrorDataReceived;
|
||||||
|
|
||||||
|
_process.Exited += OnExited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
CheckIfDisposed();
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExited(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_process is null) return;
|
||||||
|
|
||||||
|
_process.OutputDataReceived -= OnOutputDataReceived;
|
||||||
|
_process.ErrorDataReceived -= OnErrorDataReceived;
|
||||||
|
_process.Exited -= OnExited;
|
||||||
|
|
||||||
|
if (_process.ExitCode != 0)
|
||||||
|
_logConsumer.Fatal(_lastErrorBuilder.ToString());
|
||||||
|
|
||||||
|
_process.Dispose();
|
||||||
|
_process = null;
|
||||||
|
|
||||||
|
OnProcessExited?.Invoke(this);
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data == null) return;
|
||||||
|
|
||||||
|
if (!e.Data.StartsWith(" "))
|
||||||
|
_lastErrorBuilder.Clear();
|
||||||
|
|
||||||
|
_lastErrorBuilder.AppendLine(e.Data);
|
||||||
|
_logConsumer.Error(e.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data != null)
|
||||||
|
{
|
||||||
|
_logConsumer.Out(e.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_process is not null)
|
||||||
|
{
|
||||||
|
_process.CloseMainWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessStartInfoProvider.Invalidate();
|
||||||
|
|
||||||
|
CheckIfDisposed();
|
||||||
|
|
||||||
|
Disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DebugLoggerBridge : IProcessLogConsumer
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
public DebugLoggerBridge(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Out(string text)
|
||||||
|
{
|
||||||
|
_logger.Log(LoggerCategory.Log, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string text)
|
||||||
|
{
|
||||||
|
_logger.Log(LoggerCategory.Error, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string text)
|
||||||
|
{
|
||||||
|
_logger.Log(LoggerCategory.Error, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AsyncValueCache<T>
|
||||||
|
{
|
||||||
|
private readonly Func<CancellationToken, Task<T>> _valueFactory;
|
||||||
|
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
private readonly CancellationTokenSource _cacheCts = new();
|
||||||
|
|
||||||
|
private Lazy<Task<T>> _lazyTask = null!;
|
||||||
|
private T _cachedValue = default!;
|
||||||
|
private bool _isCacheValid;
|
||||||
|
|
||||||
|
public AsyncValueCache(Func<CancellationToken, Task<T>> valueFactory)
|
||||||
|
{
|
||||||
|
_valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));
|
||||||
|
ResetLazyTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetValue()
|
||||||
|
{
|
||||||
|
if (_isCacheValid) return _cachedValue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
if (_isCacheValid) return _cachedValue;
|
||||||
|
|
||||||
|
_cachedValue = _lazyTask.Value
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
_isCacheValid = true;
|
||||||
|
return _cachedValue;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
_isCacheValid = false;
|
||||||
|
_cacheCts.Cancel();
|
||||||
|
_cacheCts.Dispose();
|
||||||
|
ResetLazyTask();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetLazyTask()
|
||||||
|
{
|
||||||
|
_lazyTask = new Lazy<Task<T>>(() =>
|
||||||
|
_valueFactory(_cacheCts.Token)
|
||||||
|
.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsCanceled || t.IsFaulted)
|
||||||
|
{
|
||||||
|
_isCacheValid = false;
|
||||||
|
throw t.Exception ?? new Exception();
|
||||||
|
}
|
||||||
|
return t.Result;
|
||||||
|
}, TaskContinuationOptions.ExecuteSynchronously));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
@@ -11,47 +18,28 @@ using Nebula.Shared.Utils;
|
|||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
[ServiceRegister(), ConstructGenerator]
|
[ServiceRegister, ConstructGenerator]
|
||||||
public sealed partial class FavoriteServerListProvider : IServerListProvider, IServerListDirtyInvoker
|
public sealed partial class FavoriteServerListProvider : IServerListProvider
|
||||||
{
|
{
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private RestService RestService { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
|
||||||
private List<IFilterConsumer> _serverLists = [];
|
public Action? OnRefreshRequired;
|
||||||
|
|
||||||
public bool IsLoaded { get; private set; }
|
private string[] _rawServerLists = [];
|
||||||
public Action? OnLoaded { get; set; }
|
|
||||||
public Action? Dirty { get; set; }
|
public void LoadServerList(
|
||||||
public IEnumerable<IFilterConsumer> GetServers()
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
return _serverLists;
|
foreach (var server in _rawServerLists)
|
||||||
}
|
{
|
||||||
|
var container = ServerViewContainer.Get(server);
|
||||||
public IEnumerable<Exception> GetErrors()
|
servers.Add(container);
|
||||||
{
|
}
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadServerList()
|
|
||||||
{
|
|
||||||
IsLoaded = false;
|
|
||||||
_serverLists.Clear();
|
|
||||||
var servers = GetFavoriteEntries();
|
|
||||||
|
|
||||||
_serverLists.AddRange(
|
servers.Add(new AddFavoriteButton(ServiceProvider));
|
||||||
servers.Select(s =>
|
|
||||||
ServerViewContainer.Get(s.ToRobustUrl())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
IsLoaded = true;
|
|
||||||
OnLoaded?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddFavorite(ServerEntryModelView entryModelView)
|
|
||||||
{
|
|
||||||
entryModelView.IsFavorite = true;
|
|
||||||
AddFavorite(entryModelView.Address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddFavorite(RobustUrl robustUrl)
|
public void AddFavorite(RobustUrl robustUrl)
|
||||||
@@ -59,22 +47,64 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
|
|||||||
var servers = GetFavoriteEntries();
|
var servers = GetFavoriteEntries();
|
||||||
servers.Add(robustUrl.ToString());
|
servers.Add(robustUrl.ToString());
|
||||||
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
Dirty?.Invoke();
|
if(ServerViewContainer.Get(robustUrl) is IFavoriteEntryModelView favoriteView) favoriteView.IsFavorite = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFavorite(ServerEntryModelView entryModelView)
|
public void RemoveFavorite(ServerEntryViewModel entryViewModel)
|
||||||
{
|
{
|
||||||
var servers = GetFavoriteEntries();
|
var servers = GetFavoriteEntries();
|
||||||
servers.Remove(entryModelView.Address.ToString());
|
servers.Remove(entryViewModel.Address.ToString());
|
||||||
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveFavorite(RobustUrl url)
|
||||||
|
{
|
||||||
|
var servers = GetFavoriteEntries();
|
||||||
|
servers.Remove(url.ToString());
|
||||||
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
Dirty?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> GetFavoriteEntries()
|
private List<string> GetFavoriteEntries()
|
||||||
{
|
{
|
||||||
return ConfigurationService.GetConfigValue(LauncherConVar.Favorites)?.ToList() ?? [];
|
return _rawServerLists.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise(){}
|
private void Initialise()
|
||||||
|
{
|
||||||
|
ConfigurationService.SubscribeVarChanged(LauncherConVar.Favorites, OnVarChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVarChanged(string[]? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
_rawServerLists = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rawServerLists = value;
|
||||||
|
OnRefreshRequired?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AddFavoriteButton: Border, IListEntryModelView{
|
||||||
|
|
||||||
|
private readonly Button _addFavoriteButton = new();
|
||||||
|
public AddFavoriteButton(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
Margin = new Thickness(5, 5, 5, 20);
|
||||||
|
Background = new SolidColorBrush(Color.Parse("#222222"));
|
||||||
|
CornerRadius = new CornerRadius(20f);
|
||||||
|
_addFavoriteButton.HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
_addFavoriteButton.Click += (sender, args) =>
|
||||||
|
{
|
||||||
|
serviceProvider.GetService<PopupMessageService>()!.Popup(
|
||||||
|
serviceProvider.GetService<AddFavoriteViewModel>()!);
|
||||||
|
};
|
||||||
|
_addFavoriteButton.Content = "Add Favorite";
|
||||||
|
Child = _addFavoriteButton;
|
||||||
|
}
|
||||||
|
public void Dispose(){}
|
||||||
}
|
}
|
||||||
@@ -1,85 +1,125 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Utils;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
[ServiceRegister(null, false), ConstructGenerator]
|
[ServiceRegister(null, false), ConstructGenerator]
|
||||||
public sealed partial class HubServerListProvider : IServerListProvider
|
public sealed partial class HubServerListProvider : IServerListProvider, IDisposable
|
||||||
{
|
{
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private readonly SemaphoreSlim _loadLock = new(1, 1);
|
||||||
|
|
||||||
[GenerateProperty] private RestService RestService { get; }
|
[GenerateProperty] private RestService RestService { get; }
|
||||||
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
|
||||||
public string HubUrl { get; set; }
|
|
||||||
|
|
||||||
public bool IsLoaded { get; private set; }
|
|
||||||
public Action? OnLoaded { get; set; }
|
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private string _hubUrl;
|
||||||
private readonly List<ServerEntryModelView> _servers = [];
|
|
||||||
private readonly List<Exception> _errors = [];
|
|
||||||
|
|
||||||
public HubServerListProvider With(string hubUrl)
|
public HubServerListProvider With(string hubUrl)
|
||||||
{
|
{
|
||||||
HubUrl = hubUrl;
|
_hubUrl = hubUrl;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IFilterConsumer> GetServers()
|
public void LoadServerList(
|
||||||
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
return _servers;
|
servers.Add(new LoadingServerEntry());
|
||||||
|
Task.Run(() => LoadServerListAsync(servers, exceptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Exception> GetErrors()
|
private void SyncServers(List<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<IListEntryModelView> collection)
|
||||||
{
|
{
|
||||||
return _errors;
|
collection.Clear();
|
||||||
|
collection.AddRange(servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void LoadServerList()
|
private async Task LoadServerListAsync(
|
||||||
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
if (_cts != null)
|
CancellationTokenSource localCts;
|
||||||
{
|
|
||||||
await _cts.CancelAsync();
|
|
||||||
_cts = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_servers.Clear();
|
await _loadLock.WaitAsync();
|
||||||
_errors.Clear();
|
try
|
||||||
IsLoaded = false;
|
{
|
||||||
_cts = new CancellationTokenSource();
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
localCts = _cts;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loadLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var servers =
|
var serversRaw = await RestService.GetAsync<List<ServerHubInfo>>(
|
||||||
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(HubUrl), _cts.Token);
|
new Uri(_hubUrl),
|
||||||
|
localCts.Token
|
||||||
servers.Sort(new ServerComparer());
|
|
||||||
|
|
||||||
if(_cts.Token.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
_servers.AddRange(
|
|
||||||
servers.Select(h=>
|
|
||||||
ServerViewContainer.Get(h.Address.ToRobustUrl(), h.StatusData)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
serversRaw.Sort(new ServerComparer());
|
||||||
|
|
||||||
|
localCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
var serverList = new List<IListEntryModelView>();
|
||||||
|
|
||||||
|
foreach (var info in serversRaw)
|
||||||
|
{
|
||||||
|
serverList.Add(ServerViewContainer.Get(info.Address, info.StatusData));
|
||||||
|
}
|
||||||
|
SyncServers(serverList, servers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancel think
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_errors.Add(new Exception($"Some error while loading server list from {HubUrl}. See inner exception", e));
|
Console.WriteLine(e);
|
||||||
|
exceptions.Add(
|
||||||
|
new Exception(
|
||||||
|
$"Some error while loading server list from {_hubUrl}. See inner exception",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoaded = true;
|
|
||||||
OnLoaded?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise(){}
|
private void Initialise(){}
|
||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LoadingServerEntry : Label, IListEntryModelView
|
||||||
|
{
|
||||||
|
public LoadingServerEntry()
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
Content = LocalizationService.GetString("server-list-loading");
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using Avalonia.Collections;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
public interface IServerListProvider
|
public interface IServerListProvider
|
||||||
{
|
{
|
||||||
public bool IsLoaded { get; }
|
public void LoadServerList(
|
||||||
public Action? OnLoaded { get; set; }
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions);
|
||||||
public IEnumerable<IFilterConsumer> GetServers();
|
|
||||||
public IEnumerable<Exception> GetErrors();
|
|
||||||
|
|
||||||
public void LoadServerList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IServerListDirtyInvoker
|
|
||||||
{
|
|
||||||
public Action? Dirty { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.ObjectModel;
|
||||||
using Nebula.Launcher.Controls;
|
using Avalonia.Collections;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
public sealed class TestServerList : IServerListProvider
|
public sealed class TestServerList : IServerListProvider
|
||||||
{
|
{
|
||||||
public bool IsLoaded => true;
|
public void LoadServerList(
|
||||||
public Action? OnLoaded { get; set; }
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
public IEnumerable<IFilterConsumer> GetServers()
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
|
||||||
return [new ServerEntryModelView(),new ServerEntryModelView()];
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Exception> GetErrors()
|
|
||||||
{
|
|
||||||
return [new Exception("On no!")];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadServerList()
|
|
||||||
{
|
{
|
||||||
|
|
||||||
|
//servers.Add(new ServerEntryViewModel());
|
||||||
|
//servers.Add(new ServerEntryViewModel());
|
||||||
|
|
||||||
|
exceptions.Add(new Exception("Oh no!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher;
|
namespace Nebula.Launcher;
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
@@ -14,6 +16,8 @@ using Nebula.Shared.FileApis.Interfaces;
|
|||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Services;
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
@@ -25,35 +29,32 @@ public sealed partial class DecompilerService
|
|||||||
[GenerateProperty] private ViewHelperService ViewHelperService {get;}
|
[GenerateProperty] private ViewHelperService ViewHelperService {get;}
|
||||||
[GenerateProperty] private ContentService ContentService {get;}
|
[GenerateProperty] private ContentService ContentService {get;}
|
||||||
[GenerateProperty] private FileService FileService {get;}
|
[GenerateProperty] private FileService FileService {get;}
|
||||||
[GenerateProperty] private CancellationService CancellationService {get;}
|
|
||||||
[GenerateProperty] private EngineService EngineService {get;}
|
[GenerateProperty] private EngineService EngineService {get;}
|
||||||
[GenerateProperty] private DebugService DebugService {get;}
|
[GenerateProperty] private DebugService DebugService {get;}
|
||||||
|
|
||||||
private HttpClient _httpClient = new HttpClient();
|
private readonly HttpClient _httpClient = new();
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
private static string fullPath = Path.Join(FileService.RootPath,"ILSpy");
|
private string FullPath => Path.Join(AppDataPath.RootPath, $"ILSpy.{ConfigurationService.GetConfigValue(LauncherConVar.ILSpyVersion)}");
|
||||||
private static string executePath = Path.Join(fullPath, "ILSpy.exe");
|
private string ExecutePath => Path.Join(FullPath, "ILSpy.exe");
|
||||||
|
|
||||||
public async void OpenDecompiler(string arguments){
|
public async void OpenDecompiler(string arguments){
|
||||||
await EnsureILSpy();
|
await EnsureILSpy();
|
||||||
var startInfo = new ProcessStartInfo(){
|
var startInfo = new ProcessStartInfo(){
|
||||||
FileName = executePath,
|
FileName = ExecutePath,
|
||||||
Arguments = arguments
|
Arguments = arguments
|
||||||
};
|
};
|
||||||
Process.Start(startInfo);
|
Process.Start(startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenServerDecompiler(RobustUrl url)
|
public async void OpenServerDecompiler(RobustUrl url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var myTempDir = FileService.EnsureTempDir(out var tmpDir);
|
var myTempDir = FileService.EnsureTempDir(out var tmpDir);
|
||||||
|
|
||||||
ILoadingHandler loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
using var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
|
||||||
var buildInfo =
|
var buildInfo =
|
||||||
await ContentService.GetBuildInfo(url, CancellationService.Token);
|
await ContentService.GetBuildInfo(url, cancellationToken);
|
||||||
var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion);
|
var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandler, cancellationToken);
|
||||||
|
|
||||||
if (engine is null)
|
if (engine is null)
|
||||||
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
|
||||||
@@ -63,18 +64,16 @@ public sealed partial class DecompilerService
|
|||||||
myTempDir.Save(file, stream);
|
myTempDir.Save(file, stream);
|
||||||
await stream.DisposeAsync();
|
await stream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, CancellationService.Token);
|
var hashApi = await ContentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
|
||||||
|
|
||||||
foreach (var (file, hash) in hashApi.Manifest)
|
foreach (var file in hashApi.AllFiles)
|
||||||
{
|
{
|
||||||
if(!file.Contains(".dll") || !hashApi.TryOpen(hash, out var stream)) continue;
|
if(!file.Contains(".dll") || !hashApi.TryOpen(file, out var stream)) continue;
|
||||||
myTempDir.Save(Path.GetFileName(file), stream);
|
myTempDir.Save(Path.GetFileName(file), stream);
|
||||||
await stream.DisposeAsync();
|
await stream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
((IDisposable)loadingHandler).Dispose();
|
|
||||||
|
|
||||||
_logger.Log("File extracted. " + tmpDir);
|
_logger.Log("File extracted. " + tmpDir);
|
||||||
|
|
||||||
OpenDecompiler(string.Join(' ', myTempDir.AllFiles.Select(f=>Path.Join(tmpDir, f))) + " --newinstance");
|
OpenDecompiler(string.Join(' ', myTempDir.AllFiles.Select(f=>Path.Join(tmpDir, f))) + " --newinstance");
|
||||||
@@ -87,18 +86,25 @@ public sealed partial class DecompilerService
|
|||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
|
|
||||||
private async Task EnsureILSpy(){
|
private async Task EnsureILSpy(){
|
||||||
if(!Directory.Exists(fullPath))
|
if(!Directory.Exists(FullPath))
|
||||||
await Download();
|
await Download();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Download(){
|
private async Task Download(){
|
||||||
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loading.LoadingName = "Download ILSpy";
|
loading.LoadingName = "Download ILSpy";
|
||||||
loading.SetJobsCount(1);
|
var context = loading.CreateLoadingContext();
|
||||||
PopupMessageService.Popup(loading);
|
PopupMessageService.Popup(loading);
|
||||||
using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl));
|
using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl));
|
||||||
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
|
Console.WriteLine(response.StatusCode);
|
||||||
Directory.CreateDirectory(fullPath);
|
context.SetJobsCount(response.Content.Headers.ContentLength ?? 1000);
|
||||||
zipArchive.ExtractToDirectory(fullPath);
|
|
||||||
|
using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
stream.CopyTo(memoryStream, context);
|
||||||
|
|
||||||
|
using var zipArchive = new ZipArchive(memoryStream);
|
||||||
|
Directory.CreateDirectory(FullPath);
|
||||||
|
zipArchive.ExtractToDirectory(FullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Services.Logging;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class GameRunnerService
|
||||||
|
{
|
||||||
|
private readonly PopupMessageService _popupMessageService;
|
||||||
|
private readonly ViewHelperService _viewHelperService;
|
||||||
|
private readonly GameRunnerPreparer _gameRunnerPreparer;
|
||||||
|
private readonly InstanceRunningContainer _instanceRunningContainer;
|
||||||
|
private readonly AccountInfoViewModel _accountInfoViewModel;
|
||||||
|
private readonly ServerViewContainer _container;
|
||||||
|
private readonly MainViewModel _mainViewModel;
|
||||||
|
private readonly FavoriteServerListProvider _favoriteServerListProvider;
|
||||||
|
private readonly RestService _restService;
|
||||||
|
private readonly CancellationService _cancellationService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private readonly Dictionary<InstanceKey, RobustUrl> _robustUrls = new();
|
||||||
|
private readonly Dictionary<RobustUrl, InstanceKey> _robustKeys = new();
|
||||||
|
|
||||||
|
public GameRunnerService(PopupMessageService popupMessageService,
|
||||||
|
DebugService debugService,
|
||||||
|
ViewHelperService viewHelperService,
|
||||||
|
GameRunnerPreparer gameRunnerPreparer,
|
||||||
|
InstanceRunningContainer instanceRunningContainer,
|
||||||
|
AccountInfoViewModel accountInfoViewModel,
|
||||||
|
ServerViewContainer container,
|
||||||
|
MainViewModel mainViewModel,
|
||||||
|
FavoriteServerListProvider favoriteServerListProvider,
|
||||||
|
RestService restService,
|
||||||
|
CancellationService cancellationService)
|
||||||
|
{
|
||||||
|
_popupMessageService = popupMessageService;
|
||||||
|
_viewHelperService = viewHelperService;
|
||||||
|
_gameRunnerPreparer = gameRunnerPreparer;
|
||||||
|
_instanceRunningContainer = instanceRunningContainer;
|
||||||
|
_accountInfoViewModel = accountInfoViewModel;
|
||||||
|
_container = container;
|
||||||
|
_mainViewModel = mainViewModel;
|
||||||
|
_favoriteServerListProvider = favoriteServerListProvider;
|
||||||
|
_restService = restService;
|
||||||
|
_cancellationService = cancellationService;
|
||||||
|
|
||||||
|
_logger = debugService.GetLogger("GameRunnerService");
|
||||||
|
_instanceRunningContainer.IsRunningChanged += IsRunningChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IsRunningChanged(InstanceKey key, bool isRunning)
|
||||||
|
{
|
||||||
|
_logger.Debug($"IsRunningChanged {key}: {isRunning}");
|
||||||
|
if (!_robustUrls.TryGetValue(key, out var robustUrl)) return;
|
||||||
|
|
||||||
|
if (_container.Get(robustUrl) is IRunningSignalConsumer signalConsumer)
|
||||||
|
{
|
||||||
|
_logger.Debug($"IsRunningChanged conf {robustUrl}: {isRunning}");
|
||||||
|
signalConsumer.ProcessRunningSignal(isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
_robustKeys.Remove(robustUrl);
|
||||||
|
_robustUrls.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInstance(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||||
|
{
|
||||||
|
_instanceRunningContainer.Stop(instanceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReadInstanceLog(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||||
|
{
|
||||||
|
_instanceRunningContainer.Popup(instanceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenContentViewer(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_mainViewModel.RequirePage<ContentBrowserViewModel>().Go(robustUrl, ContentPath.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFavorite(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_favoriteServerListProvider.AddFavorite(robustUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveFavorite(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_favoriteServerListProvider.RemoveFavorite(robustUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditName(RobustUrl robustUrl, string? oldName)
|
||||||
|
{
|
||||||
|
var popup = _viewHelperService.GetViewModel<EditServerNameViewModel>();
|
||||||
|
popup.IpInput = robustUrl.ToString();
|
||||||
|
popup.NameInput = oldName ?? string.Empty;
|
||||||
|
_popupMessageService.Popup(popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InstanceKey?> RunInstanceAsync(ServerEntryViewModel serverEntryViewModel, CancellationToken cancellationToken, bool ignoreLoginCredentials = false)
|
||||||
|
{
|
||||||
|
_logger.Log("Running instance..." + serverEntryViewModel.RealName);
|
||||||
|
if (!ignoreLoginCredentials && _accountInfoViewModel.Credentials.Value is null)
|
||||||
|
{
|
||||||
|
var warningContext = _viewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
|
||||||
|
.WithServerEntry(serverEntryViewModel);
|
||||||
|
|
||||||
|
_popupMessageService.Popup(warningContext);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var viewModelLoading = _viewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
viewModelLoading.LoadingName = "Loading instance...";
|
||||||
|
|
||||||
|
_popupMessageService.Popup(viewModelLoading);
|
||||||
|
var currProcessStartProvider =
|
||||||
|
await _gameRunnerPreparer.GetGameProcessStartInfoProvider(serverEntryViewModel.Address, viewModelLoading, cancellationToken);
|
||||||
|
_logger.Log("Preparing instance...");
|
||||||
|
var instanceKey = _instanceRunningContainer.RegisterInstance(currProcessStartProvider);
|
||||||
|
_robustUrls.Add(instanceKey, serverEntryViewModel.Address);
|
||||||
|
_robustKeys.Add(serverEntryViewModel.Address, instanceKey);
|
||||||
|
_instanceRunningContainer.Run(instanceKey);
|
||||||
|
_logger.Log($"Starting instance... {instanceKey.Id} " + serverEntryViewModel.RealName);
|
||||||
|
return instanceKey;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var error = new Exception("Error while attempt run instance", e);
|
||||||
|
_logger.Error(error);
|
||||||
|
_popupMessageService.Popup(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEntryViewModel GetServerEntry(RobustUrl url, string customName, ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
return new ServerEntryViewModel(_restService, _cancellationService, this)
|
||||||
|
.WithData(url, customName, serverStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
Nebula.Launcher/Services/InstanceRunningContainer.cs
Normal file
91
Nebula.Launcher/Services/InstanceRunningContainer.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public sealed class InstanceRunningContainer(
|
||||||
|
PopupMessageService popupMessageService,
|
||||||
|
DebugService debugService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private readonly InstanceKeyPool _keyPool = new();
|
||||||
|
private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new();
|
||||||
|
private readonly Dictionary<InstanceKey, ContentLogConsumer> _contentLoggerCache = new();
|
||||||
|
private readonly Dictionary<ProcessRunHandler, InstanceKey> _keyCache = new();
|
||||||
|
|
||||||
|
public Action<InstanceKey, bool>? IsRunningChanged;
|
||||||
|
|
||||||
|
public InstanceKey RegisterInstance(IProcessStartInfoProvider provider)
|
||||||
|
{
|
||||||
|
var id = _keyPool.Take();
|
||||||
|
|
||||||
|
var currentContentLogConsumer = new ContentLogConsumer(popupMessageService);
|
||||||
|
var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id));
|
||||||
|
var logContainer = new ProcessLogConsumerCollection();
|
||||||
|
logContainer.RegisterLogger(currentContentLogConsumer);
|
||||||
|
logContainer.RegisterLogger(logBridge);
|
||||||
|
|
||||||
|
var handler = new ProcessRunHandler(provider, logContainer);
|
||||||
|
handler.OnProcessExited += OnProcessExited;
|
||||||
|
|
||||||
|
_processCache[id] = handler;
|
||||||
|
_contentLoggerCache[id] = currentContentLogConsumer;
|
||||||
|
_keyCache[handler] = id;
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Popup(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler))
|
||||||
|
return;
|
||||||
|
|
||||||
|
handler.Popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_processCache.TryGetValue(instanceKey, out var process))
|
||||||
|
return;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
IsRunningChanged?.Invoke(instanceKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_processCache.TryGetValue(instanceKey, out var process))
|
||||||
|
return;
|
||||||
|
|
||||||
|
process.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRunning(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
return _processCache.ContainsKey(instanceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveProcess(ProcessRunHandler handler)
|
||||||
|
{
|
||||||
|
if(handler.Disposed) return;
|
||||||
|
|
||||||
|
var key = _keyCache[handler];
|
||||||
|
IsRunningChanged?.Invoke(key, false);
|
||||||
|
_processCache.Remove(key);
|
||||||
|
_keyCache.Remove(handler);
|
||||||
|
_contentLoggerCache.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProcessExited(ProcessRunHandler obj)
|
||||||
|
{
|
||||||
|
obj.OnProcessExited -= OnProcessExited;
|
||||||
|
RemoveProcess(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using Avalonia.Platform;
|
|
||||||
using Fluent.Net;
|
|
||||||
using Nebula.Shared;
|
|
||||||
using Nebula.Shared.Services;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Services;
|
|
||||||
|
|
||||||
[ConstructGenerator, ServiceRegister]
|
|
||||||
public partial class LocalisationService
|
|
||||||
{
|
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
|
||||||
|
|
||||||
private CultureInfo _currentCultureInfo = CultureInfo.CurrentCulture;
|
|
||||||
private MessageContext _currentMessageContext;
|
|
||||||
|
|
||||||
private void Initialise()
|
|
||||||
{
|
|
||||||
// LoadLanguage(CultureInfo.GetCultureInfo(ConfigurationService.GetConfigValue(LauncherConVar.CurrentLang)!));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadLanguage(CultureInfo cultureInfo)
|
|
||||||
{
|
|
||||||
_currentCultureInfo = cultureInfo;
|
|
||||||
using var fs = AssetLoader.Open(new Uri($@"Assets/lang/{_currentCultureInfo.EnglishName}.ftl"));
|
|
||||||
using var sr = new StreamReader(fs);
|
|
||||||
|
|
||||||
var options = new MessageContextOptions { UseIsolating = false };
|
|
||||||
var mc = new MessageContext(cultureInfo.EnglishName, options);
|
|
||||||
var errors = mc.AddMessages(sr);
|
|
||||||
foreach (var error in errors)
|
|
||||||
{
|
|
||||||
Console.WriteLine(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentMessageContext = mc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitialiseInDesignMode()
|
|
||||||
{
|
|
||||||
Initialise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
78
Nebula.Launcher/Services/LocalizationService.cs
Normal file
78
Nebula.Launcher/Services/LocalizationService.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Fluent.Net;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ConstructGenerator, ServiceRegister]
|
||||||
|
public sealed partial class LocalizationService
|
||||||
|
{
|
||||||
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
|
[GenerateProperty] private DebugService DebugService { get; }
|
||||||
|
|
||||||
|
private CultureInfo _currentCultureInfo = CultureInfo.CurrentCulture;
|
||||||
|
private static MessageContext? _currentMessageContext;
|
||||||
|
|
||||||
|
private void Initialise()
|
||||||
|
{
|
||||||
|
LoadLanguage(CultureInfo.GetCultureInfo(ConfigurationService.GetConfigValue(LauncherConVar.CurrentLang)!));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadLanguage(CultureInfo cultureInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_currentCultureInfo = cultureInfo;
|
||||||
|
using var fs = AssetLoader.Open(new Uri($@"avares://Nebula.Launcher/Assets/lang/{_currentCultureInfo.Name}.ftl"));
|
||||||
|
using var sr = new StreamReader(fs);
|
||||||
|
|
||||||
|
var options = new MessageContextOptions { UseIsolating = false };
|
||||||
|
var mc = new MessageContext(cultureInfo.Name, options);
|
||||||
|
var errors = mc.AddMessages(sr);
|
||||||
|
foreach (var error in errors)
|
||||||
|
{
|
||||||
|
Console.WriteLine(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentMessageContext = mc;
|
||||||
|
} catch (Exception e) {
|
||||||
|
DebugService.GetLogger("localisationService").Error(e);
|
||||||
|
LoadLanguage(CultureInfo.GetCultureInfo("en-US"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
Initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetString(string locale, Dictionary<string, object>? options = null)
|
||||||
|
{
|
||||||
|
if (_currentMessageContext is null)
|
||||||
|
{
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
var message = _currentMessageContext.GetMessage(locale);
|
||||||
|
if (message == null) return locale;
|
||||||
|
return _currentMessageContext.Format(message, options ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocaledText : MarkupExtension
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public Dictionary<string, object>? Options { get; set; }
|
||||||
|
|
||||||
|
public LocaledText(string key) => Key = key;
|
||||||
|
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return LocalizationService.GetString(Key, Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Nebula.Launcher/Utils/ColorUtils.cs
Normal file
20
Nebula.Launcher/Utils/ColorUtils.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class ColorUtils
|
||||||
|
{
|
||||||
|
public static Color GetColorFromString(string input)
|
||||||
|
{
|
||||||
|
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
|
|
||||||
|
var r = byte.Clamp(hash[0], 10, 200);
|
||||||
|
var g = byte.Clamp(hash[1], 10, 100);
|
||||||
|
var b = byte.Clamp(hash[2], 10, 100);
|
||||||
|
|
||||||
|
return Color.FromArgb(Byte.MaxValue, r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Nebula.Launcher/Utils/ExplorerUtils.cs
Normal file
33
Nebula.Launcher/Utils/ExplorerUtils.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
|
||||||
|
public static class ExplorerUtils
|
||||||
|
{
|
||||||
|
public static void OpenFolder(string path)
|
||||||
|
{
|
||||||
|
string command;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
command = "explorer.exe";
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
command = "xdg-open";
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
command = "open";
|
||||||
|
else
|
||||||
|
throw new PlatformNotSupportedException("Unsupported OS platform");
|
||||||
|
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = command,
|
||||||
|
Arguments = path,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Nebula.Launcher/Utils/VCRuntimeDllChecker.cs
Normal file
29
Nebula.Launcher/Utils/VCRuntimeDllChecker.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class VCRuntimeDllChecker
|
||||||
|
{
|
||||||
|
public static bool AreVCRuntimeDllsPresent()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return true;
|
||||||
|
|
||||||
|
string systemDir = Environment.SystemDirectory;
|
||||||
|
string[] requiredDlls = {
|
||||||
|
"msvcp140.dll",
|
||||||
|
"vcruntime140.dll"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var dll in requiredDlls)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(systemDir, dll);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewHelper;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
|
||||||
public class ViewModelRegisterAttribute : Attribute
|
|
||||||
{
|
|
||||||
public ViewModelRegisterAttribute(Type? type = null, bool isSingleton = true)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
IsSingleton = isSingleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type? Type { get; }
|
|
||||||
public bool IsSingleton { get; }
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.Templates;
|
using Avalonia.Controls.Templates;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher;
|
namespace Nebula.Launcher;
|
||||||
|
|
||||||
|
|||||||
39
Nebula.Launcher/ViewModels/ExceptionCompound.cs
Normal file
39
Nebula.Launcher/ViewModels/ExceptionCompound.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ExceptionView), false)]
|
||||||
|
public class ExceptionCompound : ViewModelBase
|
||||||
|
{
|
||||||
|
public ExceptionCompound()
|
||||||
|
{
|
||||||
|
Message = "Test exception";
|
||||||
|
StackTrace = "Stack trace";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionCompound(string message, string stackTrace)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
StackTrace = stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionCompound(Exception ex)
|
||||||
|
{
|
||||||
|
Message = ex.Message;
|
||||||
|
StackTrace = ex.StackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string? StackTrace { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
public record struct InstanceKey(int Id):
|
||||||
|
IEquatable<int>,
|
||||||
|
IComparable<InstanceKey>
|
||||||
|
{
|
||||||
|
public static implicit operator InstanceKey(int id) => new InstanceKey(id);
|
||||||
|
public static implicit operator int(InstanceKey id) => id.Id;
|
||||||
|
public bool Equals(int other) => Id == other;
|
||||||
|
public int CompareTo(InstanceKey other) => Id.CompareTo(other.Id);
|
||||||
|
};
|
||||||
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
public sealed class InstanceKeyPool
|
||||||
|
{
|
||||||
|
private int _nextId = 1;
|
||||||
|
|
||||||
|
public InstanceKey Take()
|
||||||
|
{
|
||||||
|
return new InstanceKey(_nextId++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Free(InstanceKey id)
|
||||||
|
{
|
||||||
|
// TODO: make some free logic later
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,17 @@ using System.Collections.ObjectModel;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly List<ListItemTemplate> _templates =
|
private readonly List<ListItemTemplate> _templates =
|
||||||
[
|
[
|
||||||
new ListItemTemplate(typeof(AccountInfoViewModel), "user", "Account"),
|
new ListItemTemplate(typeof(AccountInfoViewModel), "user", "tab-account"),
|
||||||
new ListItemTemplate(typeof(ServerOverviewModel), "file", "Servers"),
|
new ListItemTemplate(typeof(ServerOverviewModel), "file", "tab-servers"),
|
||||||
new ListItemTemplate(typeof(ContentBrowserViewModel), "folder", "Content"),
|
new ListItemTemplate(typeof(ContentBrowserViewModel), "folder", "tab-content"),
|
||||||
new ListItemTemplate(typeof(ConfigurationViewModel), "settings", "Settings")
|
new ListItemTemplate(typeof(ConfigurationViewModel), "settings", "tab-settings")
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly List<PopupViewModelBase> _viewQueue = new();
|
private readonly List<PopupViewModelBase> _viewQueue = new();
|
||||||
@@ -40,12 +41,15 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isPopupClosable = true;
|
[ObservableProperty] private bool _isPopupClosable = true;
|
||||||
[ObservableProperty] private bool _popup;
|
[ObservableProperty] private bool _popup;
|
||||||
[ObservableProperty] private ListItemTemplate? _selectedListItem;
|
[ObservableProperty] private ListItemTemplate? _selectedListItem;
|
||||||
|
[ObservableProperty] private string? _loginText = LocalizationService.GetString("auth-current-login-no-name");
|
||||||
|
|
||||||
|
[GenerateProperty] private LocalizationService LocalizationService { get; } // Не убирать! Без этой хуйни вся локализация идет в пизду!
|
||||||
|
[GenerateProperty] private AccountInfoViewModel AccountInfoViewModel { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
||||||
[GenerateProperty] private ContentService ContentService { get; } = default!;
|
[GenerateProperty] private ContentService ContentService { get; } = default!;
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
||||||
[GenerateProperty] private FileService FileService { get; } = default!;
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; } = default!;
|
||||||
|
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
@@ -53,12 +57,25 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
Items = new ObservableCollection<ListItemTemplate>(_templates);
|
Items = new ObservableCollection<ListItemTemplate>(_templates.Select(a=>
|
||||||
|
{
|
||||||
|
return a with { Label = LocalizationService.GetString(a.Label) };
|
||||||
|
}
|
||||||
|
));
|
||||||
RequirePage<AccountInfoViewModel>();
|
RequirePage<AccountInfoViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
|
AccountInfoViewModel.Credentials.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName is not nameof(AccountInfoViewModel.Credentials.Value))
|
||||||
|
return;
|
||||||
|
UpdateCredentialsInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
UpdateCredentialsInfo();
|
||||||
|
|
||||||
_logger = DebugService.GetLogger(this);
|
_logger = DebugService.GetLogger(this);
|
||||||
|
|
||||||
using var stream = typeof(MainViewModel).Assembly
|
using var stream = typeof(MainViewModel).Assembly
|
||||||
@@ -73,18 +90,54 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
PopupMessageService.OnCloseRequired += OnPopupCloseRequired;
|
PopupMessageService.OnCloseRequired += OnPopupCloseRequired;
|
||||||
|
|
||||||
CheckMigration();
|
CheckMigration();
|
||||||
|
|
||||||
|
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
loadingHandler.LoadingName = LocalizationService.GetString("migration-config-task");
|
||||||
|
loadingHandler.IsCancellable = false;
|
||||||
|
ConfigurationService.MigrateConfigs(loadingHandler);
|
||||||
|
|
||||||
|
if (!VCRuntimeDllChecker.AreVCRuntimeDllsPresent())
|
||||||
|
{
|
||||||
|
OnPopupRequired(LocalizationService.GetString("vcruntime-check-error"));
|
||||||
|
Helper.OpenBrowser("https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCredentialsInfo()
|
||||||
|
{
|
||||||
|
if(AccountInfoViewModel.Credentials.HasValue)
|
||||||
|
{
|
||||||
|
LoginText =
|
||||||
|
LocalizationService.GetString("auth-current-login-name",
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" },
|
||||||
|
{
|
||||||
|
"auth_server",
|
||||||
|
AccountInfoViewModel.GetServerAuthName(AccountInfoViewModel.Credentials.Value?.AuthServer) ?? ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoginText = LocalizationService.GetString("auth-current-login-no-name");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckMigration()
|
private void CheckMigration()
|
||||||
{
|
{
|
||||||
|
if (!ConfigurationService.GetConfigValue(LauncherConVar.DoMigration))
|
||||||
|
return;
|
||||||
|
|
||||||
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loadingHandler.LoadingName = "Migration task, please wait...";
|
loadingHandler.LoadingName = LocalizationService.GetString("migration-label-task");
|
||||||
loadingHandler.IsCancellable = false;
|
loadingHandler.IsCancellable = false;
|
||||||
|
|
||||||
if (!ContentService.CheckMigration(loadingHandler))
|
if (!ContentService.CheckMigration(loadingHandler))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
OnPopupRequired(loadingHandler);
|
OnPopupRequired(loadingHandler);
|
||||||
|
ConfigurationService.SetConfigValue(LauncherConVar.DoMigration, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedListItemChanged(ListItemTemplate? value)
|
partial void OnSelectedListItemChanged(ListItemTemplate? value)
|
||||||
@@ -148,6 +201,16 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
Popup = true;
|
Popup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenAuthPage()
|
||||||
|
{
|
||||||
|
RequirePage<AccountInfoViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenRootPath()
|
||||||
|
{
|
||||||
|
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
|
||||||
|
}
|
||||||
|
|
||||||
public void OpenLink()
|
public void OpenLink()
|
||||||
{
|
{
|
||||||
Helper.OpenBrowser("https://durenko.tatar/nebula");
|
Helper.OpenBrowser("https://durenko.tatar/nebula");
|
||||||
@@ -167,6 +230,11 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
case PopupViewModelBase @base:
|
case PopupViewModelBase @base:
|
||||||
PopupMessage(@base);
|
PopupMessage(@base);
|
||||||
break;
|
break;
|
||||||
|
case ExceptionCompound error:
|
||||||
|
var errViewModel = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
||||||
|
errViewModel.AppendError(error);
|
||||||
|
PopupMessage(errViewModel);
|
||||||
|
break;
|
||||||
case Exception error:
|
case Exception error:
|
||||||
var err = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
var err = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
||||||
_logger.Error(error);
|
_logger.Error(error);
|
||||||
@@ -185,16 +253,18 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
else
|
else
|
||||||
_viewQueue.Remove(viewModelBase);
|
_viewQueue.Remove(viewModelBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TriggerPane()
|
||||||
[RelayCommand]
|
|
||||||
private void TriggerPane()
|
|
||||||
{
|
{
|
||||||
IsPaneOpen = !IsPaneOpen;
|
IsPaneOpen = !IsPaneOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
public void CloseCurrentPopup()
|
||||||
public void ClosePopup()
|
{
|
||||||
|
CurrentPopup?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClosePopup()
|
||||||
{
|
{
|
||||||
var viewModelBase = _viewQueue.FirstOrDefault();
|
var viewModelBase = _viewQueue.FirstOrDefault();
|
||||||
if (viewModelBase is null)
|
if (viewModelBase is null)
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using Nebula.Launcher.Configurations;
|
||||||
|
using Nebula.Launcher.Models.Auth;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared.Configurations;
|
||||||
|
using Nebula.Shared.Models.Auth;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -22,62 +24,52 @@ namespace Nebula.Launcher.ViewModels.Pages;
|
|||||||
public partial class AccountInfoViewModel : ViewModelBase
|
public partial class AccountInfoViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
[ObservableProperty] private bool _authMenuExpand;
|
[ObservableProperty] private bool _authMenuExpand;
|
||||||
|
|
||||||
[ObservableProperty] private bool _authUrlConfigExpand;
|
[ObservableProperty] private bool _authUrlConfigExpand;
|
||||||
|
|
||||||
[ObservableProperty] private int _authViewSpan = 1;
|
[ObservableProperty] private int _authViewSpan = 1;
|
||||||
|
|
||||||
[ObservableProperty] private string _currentAuthServer = string.Empty;
|
[ObservableProperty] private string _currentAuthServer = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty] private string _currentLogin = string.Empty;
|
[ObservableProperty] private string _currentLogin = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty] private string _currentPassword = string.Empty;
|
[ObservableProperty] private string _currentPassword = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty] private bool _isLogged;
|
[ObservableProperty] private bool _isLogged;
|
||||||
|
[ObservableProperty] private bool _doRetryAuth;
|
||||||
|
[ObservableProperty] private AuthServerCredentials _authItemSelect;
|
||||||
|
[ObservableProperty] private string _authServerName;
|
||||||
|
|
||||||
private bool _isProfilesEmpty;
|
private bool _isProfilesEmpty;
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; } = default!;
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; }
|
[GenerateProperty] private DebugService DebugService { get; }
|
||||||
[GenerateProperty] private AuthService AuthService { get; } = default!;
|
[GenerateProperty] private AuthService AuthService { get; }
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
|
||||||
|
|
||||||
public ObservableCollection<ProfileAuthCredentials> Accounts { get; } = new();
|
public ObservableCollection<ProfileEntry> Accounts { get; } = new();
|
||||||
public ObservableCollection<AuthServerCredentials> AuthUrls { get; } = new();
|
public ObservableCollection<AuthServerCredentials> AuthUrls { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private AuthServerCredentials _authItemSelect;
|
public ComplexConVarBinder<AuthTokenCredentials?> Credentials { get; private set; }
|
||||||
|
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
//Design think
|
//Design think
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
AddAccount(new AuthLoginPassword("Binka", "12341", ""));
|
AuthUrls.Add(new AuthServerCredentials("Test",["example.com","variant.lab"]));
|
||||||
AddAccount(new AuthLoginPassword("Binka", "12341", ""));
|
|
||||||
|
|
||||||
AuthUrls.Add(new AuthServerCredentials("Test",["example.com"]));
|
AddAccount(new ProfileAuthCredentials("Binka", "","example.com"));
|
||||||
|
AddAccount(new ProfileAuthCredentials("Vilka","", "variant.lab"));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Real think
|
//Real think
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
_logger = DebugService.GetLogger(this);
|
_logger = DebugService.GetLogger(this);
|
||||||
ReadAuthConfig();
|
Credentials = new AuthTokenCredentialsVar(this);
|
||||||
|
Task.Run(ReadAuthConfig);
|
||||||
|
Credentials.Value = Credentials.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AuthByProfile(ProfileAuthCredentials credentials)
|
|
||||||
{
|
|
||||||
CurrentLogin = credentials.Login;
|
|
||||||
CurrentPassword = credentials.Password;
|
|
||||||
CurrentAuthServer = credentials.AuthServer;
|
|
||||||
|
|
||||||
DoAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DoAuth(string? code = null)
|
public void DoAuth(string? code = null)
|
||||||
{
|
{
|
||||||
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = "Auth think, please wait...";
|
message.InfoText = LocalizationService.GetString("auth-processing");
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
PopupMessageService.Popup(message);
|
PopupMessageService.Popup(message);
|
||||||
|
|
||||||
@@ -91,69 +83,131 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Exception? exception = null;
|
Exception? exception = null;
|
||||||
|
|
||||||
foreach (var server in serverCandidates)
|
foreach (var server in serverCandidates)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await TryAuth(CurrentLogin, CurrentPassword, server,code);
|
await CatchAuthError(async() =>
|
||||||
|
{
|
||||||
|
Credentials.Value = await AuthService.Auth(CurrentLogin, CurrentPassword, server, code);
|
||||||
|
}, ()=> message.Dispose());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
exception = e;
|
exception = new Exception(LocalizationService.GetString("auth-error"), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message.Dispose();
|
message.Dispose();
|
||||||
|
|
||||||
if (!IsLogged)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
PopupMessageService.Popup(new Exception("No one of auth server is available.", exception));
|
PopupMessageService.Popup(new Exception("Error while auth", exception));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryAuth(string login, string password, string authServer,string? code)
|
private async Task CatchAuthError(Func<Task> a, Action? onError)
|
||||||
{
|
{
|
||||||
|
DoRetryAuth = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await AuthService.Auth(new AuthLoginPassword(login, password, authServer), code);
|
await a();
|
||||||
CurrentLogin = login;
|
|
||||||
CurrentPassword = password;
|
|
||||||
CurrentAuthServer = authServer;
|
|
||||||
IsLogged = true;
|
|
||||||
ConfigurationService.SetConfigValue(LauncherConVar.AuthCurrent, AuthService.SelectedAuth);
|
|
||||||
}
|
}
|
||||||
catch (AuthException e)
|
catch (AuthException e)
|
||||||
{
|
{
|
||||||
|
onError?.Invoke();
|
||||||
switch (e.Error.Code)
|
switch (e.Error.Code)
|
||||||
{
|
{
|
||||||
case AuthenticateDenyCode.TfaRequired:
|
case AuthenticateDenyCode.TfaRequired:
|
||||||
case AuthenticateDenyCode.TfaInvalid:
|
case AuthenticateDenyCode.TfaInvalid:
|
||||||
var p = ViewHelperService.GetViewModel<TfaViewModel>();
|
var p = ViewHelperService.GetViewModel<TfaViewModel>();
|
||||||
p.OnTfaEntered += OnTfaEntered;
|
|
||||||
PopupMessageService.Popup(p);
|
PopupMessageService.Popup(p);
|
||||||
_logger.Log("TFA required");
|
_logger.Log("TFA required");
|
||||||
break;
|
break;
|
||||||
case AuthenticateDenyCode.InvalidCredentials:
|
case AuthenticateDenyCode.InvalidCredentials:
|
||||||
PopupMessageService.Popup("Invalid Credentials!");
|
PopupError(LocalizationService.GetString("auth-invalid-credentials"), e);
|
||||||
_logger.Error($"Invalid credentials");
|
break;
|
||||||
|
case AuthenticateDenyCode.AccountLocked:
|
||||||
|
PopupError(LocalizationService.GetString("auth-account-locked"), e);
|
||||||
|
break;
|
||||||
|
case AuthenticateDenyCode.AccountUnconfirmed:
|
||||||
|
PopupError(LocalizationService.GetString("auth-account-unconfirmed"), e);
|
||||||
|
break;
|
||||||
|
case AuthenticateDenyCode.None:
|
||||||
|
PopupError(LocalizationService.GetString("auth-none"),e);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw;
|
PopupError(LocalizationService.GetString("auth-error-fuck"), e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
onError?.Invoke();
|
||||||
|
switch (e.HttpRequestError)
|
||||||
|
{
|
||||||
|
case HttpRequestError.ConnectionError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-connection-error"), e);
|
||||||
|
DoRetryAuth = true;
|
||||||
|
break;
|
||||||
|
case HttpRequestError.NameResolutionError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-name-resolution-error"), e);
|
||||||
|
DoRetryAuth = true;
|
||||||
|
break;
|
||||||
|
case HttpRequestError.SecureConnectionError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-secure-error"), e);
|
||||||
|
DoRetryAuth = true;
|
||||||
|
break;
|
||||||
|
case HttpRequestError.UserAuthenticationError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-user-authentication-error"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.Unknown:
|
||||||
|
PopupError(LocalizationService.GetString("auth-unknown"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.HttpProtocolError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-http-protocol-error"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.ExtendedConnectNotSupported:
|
||||||
|
PopupError(LocalizationService.GetString("auth-extended-connect-not-support"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.VersionNegotiationError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-version-negotiation-error"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.ProxyTunnelError:
|
||||||
|
PopupError(LocalizationService.GetString("auth-proxy-tunnel-error"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.InvalidResponse:
|
||||||
|
PopupError(LocalizationService.GetString("auth-invalid-response"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.ResponseEnded:
|
||||||
|
PopupError(LocalizationService.GetString("auth-response-ended"), e);
|
||||||
|
break;
|
||||||
|
case HttpRequestError.ConfigurationLimitExceeded:
|
||||||
|
PopupError(LocalizationService.GetString("auth-configuration-limit-exceeded"), e);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var authError = new Exception(LocalizationService.GetString("auth-error"), e);
|
||||||
|
_logger.Error(authError);
|
||||||
|
PopupMessageService.Popup(authError);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTfaEntered(string code)
|
|
||||||
{
|
|
||||||
DoAuth(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Logout()
|
public void Logout()
|
||||||
{
|
{
|
||||||
IsLogged = false;
|
Credentials.Value = null;
|
||||||
AuthService.ClearAuth();
|
CurrentAuthServer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetServerAuthName(string? url)
|
||||||
|
{
|
||||||
|
if (url is null) return "";
|
||||||
|
return AuthUrls.FirstOrDefault(p => p.Servers.Contains(url))?.Name ?? "CustomAuth";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAuthMenu()
|
private void UpdateAuthMenu()
|
||||||
@@ -164,15 +218,22 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
AuthViewSpan = 1;
|
AuthViewSpan = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAccount(AuthLoginPassword authLoginPassword)
|
private void AddAccount(ProfileAuthCredentials credentials)
|
||||||
{
|
{
|
||||||
var onDelete = new DelegateCommand<ProfileAuthCredentials>(OnDeleteProfile);
|
var onDelete = new DelegateCommand<ProfileEntry>(OnDeleteProfile);
|
||||||
var onSelect = new DelegateCommand<ProfileAuthCredentials>(AuthByProfile);
|
var onSelect = new DelegateCommand<ProfileEntry>((p) =>
|
||||||
|
{
|
||||||
|
CurrentLogin = p.Credentials.Login;
|
||||||
|
CurrentPassword = p.Credentials.Password;
|
||||||
|
CurrentAuthServer = p.Credentials.AuthServer;
|
||||||
|
DoAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
var serverName = GetServerAuthName(credentials.AuthServer);
|
||||||
|
|
||||||
var alpm = new ProfileAuthCredentials(
|
var alpm = new ProfileEntry(
|
||||||
authLoginPassword.Login,
|
credentials,
|
||||||
authLoginPassword.Password,
|
serverName,
|
||||||
authLoginPassword.AuthServer,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onDelete);
|
onDelete);
|
||||||
|
|
||||||
@@ -182,54 +243,100 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
Accounts.Add(alpm);
|
Accounts.Add(alpm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ReadAuthConfig()
|
private async Task ReadAuthConfig()
|
||||||
{
|
{
|
||||||
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = "Read configuration file, please wait...";
|
message.InfoText = LocalizationService.GetString("auth-config-read");
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
PopupMessageService.Popup(message);
|
PopupMessageService.Popup(message);
|
||||||
foreach (var profile in
|
|
||||||
ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!)
|
_logger.Log("Reading auth config");
|
||||||
AddAccount(new AuthLoginPassword(profile.Login, profile.Password, profile.AuthServer));
|
|
||||||
|
|
||||||
if (Accounts.Count == 0) UpdateAuthMenu();
|
|
||||||
|
|
||||||
AuthUrls.Clear();
|
AuthUrls.Clear();
|
||||||
var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!;
|
var authUrls = ConfigurationService.GetConfigValue(LauncherConVar.AuthServers)!;
|
||||||
foreach (var url in authUrls) AuthUrls.Add(url);
|
foreach (var url in authUrls) AuthUrls.Add(url);
|
||||||
if(authUrls.Length > 0) AuthItemSelect = authUrls[0];
|
if(authUrls.Length > 0) AuthItemSelect = authUrls[0];
|
||||||
|
|
||||||
var currProfile = ConfigurationService.GetConfigValue(LauncherConVar.AuthCurrent);
|
var profileCandidates = new List<string>();
|
||||||
|
|
||||||
if (currProfile != null)
|
foreach (var profileRaw in
|
||||||
|
ConfigurationService.GetConfigValue(LauncherConVar.AuthProfiles)!)
|
||||||
{
|
{
|
||||||
|
_logger.Log($"Decrypting profile...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentLogin = currProfile.Login;
|
var decoded =
|
||||||
CurrentAuthServer = currProfile.AuthServer;
|
await CryptographicStore.Decrypt<ProfileAuthCredentials>(profileRaw,
|
||||||
|
CryptographicStore.GetComputerKey());
|
||||||
IsLogged = await AuthService.SetAuth(currProfile);
|
|
||||||
|
_logger.Log($"Decrypted profile: {decoded.Login}");
|
||||||
|
|
||||||
|
profileCandidates.Add(profileRaw);
|
||||||
|
AddAccount(decoded);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
message.Dispose();
|
_logger.Error("Error while decrypting profile");
|
||||||
PopupMessageService.Popup(e);
|
_logger.Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles, profileCandidates.ToArray());
|
||||||
|
|
||||||
|
if (Accounts.Count == 0) UpdateAuthMenu();
|
||||||
|
|
||||||
message.Dispose();
|
message.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
public void DoCurrentAuth()
|
||||||
private void OnSaveProfile()
|
|
||||||
{
|
{
|
||||||
AddAccount(new AuthLoginPassword(CurrentLogin, CurrentPassword, CurrentAuthServer));
|
DoAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AuthTokenCredentials?> CheckOrRenewToken(AuthTokenCredentials? authTokenCredentials)
|
||||||
|
{
|
||||||
|
if(authTokenCredentials is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var daysLeft = (int)(authTokenCredentials.Token.ExpireTime - DateTime.Now).TotalDays;
|
||||||
|
|
||||||
|
if(daysLeft >= 4)
|
||||||
|
{
|
||||||
|
_logger.Log("Token " + authTokenCredentials.Login + " is active, "+daysLeft+" days left, undo renewing!");
|
||||||
|
return authTokenCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Log($"Renewing token for {authTokenCredentials.Login}");
|
||||||
|
return await ExceptionHelper.TryRun(() => AuthService.Refresh(authTokenCredentials), 3,
|
||||||
|
(attempt, e) => { _logger.Error(new Exception("Error while renewing, attempts: " + attempt, e)); });
|
||||||
|
}
|
||||||
|
catch (AuthTokenExpiredException e)
|
||||||
|
{
|
||||||
|
_logger.Error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), e);
|
||||||
|
_logger.Error(unexpectedError);
|
||||||
|
return authTokenCredentials;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnSaveProfile()
|
||||||
|
{
|
||||||
|
if(Credentials.Value is null ||
|
||||||
|
string.IsNullOrEmpty(CurrentPassword)) return;
|
||||||
|
|
||||||
|
AddAccount(new ProfileAuthCredentials(CurrentLogin, CurrentPassword, Credentials.Value.AuthServer));
|
||||||
_isProfilesEmpty = Accounts.Count == 0;
|
_isProfilesEmpty = Accounts.Count == 0;
|
||||||
UpdateAuthMenu();
|
UpdateAuthMenu();
|
||||||
DirtyProfile();
|
DirtyProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDeleteProfile(ProfileAuthCredentials account)
|
private void OnDeleteProfile(ProfileEntry account)
|
||||||
{
|
{
|
||||||
Accounts.Remove(account);
|
Accounts.Remove(account);
|
||||||
_isProfilesEmpty = Accounts.Count == 0;
|
_isProfilesEmpty = Accounts.Count == 0;
|
||||||
@@ -237,14 +344,23 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
DirtyProfile();
|
DirtyProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
private void PopupError(string message, Exception e)
|
||||||
private void OnExpandAuthUrl()
|
{
|
||||||
|
message = LocalizationService.GetString("auth-error-occured") + message;
|
||||||
|
_logger.Error(new Exception(message, e));
|
||||||
|
|
||||||
|
var messageView = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
|
messageView.InfoText = message;
|
||||||
|
messageView.IsInfoClosable = true;
|
||||||
|
PopupMessageService.Popup(messageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnExpandAuthUrl()
|
||||||
{
|
{
|
||||||
AuthUrlConfigExpand = !AuthUrlConfigExpand;
|
AuthUrlConfigExpand = !AuthUrlConfigExpand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
public void OnExpandAuthView()
|
||||||
private void OnExpandAuthView()
|
|
||||||
{
|
{
|
||||||
AuthMenuExpand = !AuthMenuExpand;
|
AuthMenuExpand = !AuthMenuExpand;
|
||||||
UpdateAuthMenu();
|
UpdateAuthMenu();
|
||||||
@@ -253,19 +369,79 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
private void DirtyProfile()
|
private void DirtyProfile()
|
||||||
{
|
{
|
||||||
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles,
|
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles,
|
||||||
Accounts.ToArray());
|
Accounts.Select(a => CryptographicStore.Encrypt(a.Credentials, CryptographicStore.GetComputerKey())).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AuthTokenCredentialsVar(AccountInfoViewModel accountInfoViewModel)
|
||||||
|
: ComplexConVarBinder<AuthTokenCredentials?>(
|
||||||
|
accountInfoViewModel.ConfigurationService.SubscribeVarChanged(LauncherConVar.AuthCurrent))
|
||||||
|
{
|
||||||
|
protected override async Task<AuthTokenCredentials?> OnValueChange(AuthTokenCredentials? currProfile)
|
||||||
|
{
|
||||||
|
if (currProfile is null)
|
||||||
|
{
|
||||||
|
accountInfoViewModel.IsLogged = false;
|
||||||
|
accountInfoViewModel._logger.Log("clearing credentials");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = accountInfoViewModel.ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
|
message.InfoText = LocalizationService.GetString("auth-try-auth-config");
|
||||||
|
message.IsInfoClosable = false;
|
||||||
|
accountInfoViewModel.PopupMessageService.Popup(message);
|
||||||
|
|
||||||
|
accountInfoViewModel._logger.Log($"trying auth with {currProfile.Login}");
|
||||||
|
|
||||||
|
var errorRun = false;
|
||||||
|
|
||||||
|
currProfile = await accountInfoViewModel.CheckOrRenewToken(currProfile);
|
||||||
|
|
||||||
|
if (currProfile is null)
|
||||||
|
{
|
||||||
|
message.Dispose();
|
||||||
|
|
||||||
|
accountInfoViewModel._logger.Log("profile credentials update required!");
|
||||||
|
|
||||||
|
accountInfoViewModel.PopupMessageService.Popup("profile credentials update required!");
|
||||||
|
|
||||||
|
accountInfoViewModel.IsLogged = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await accountInfoViewModel.CatchAuthError(async () =>
|
||||||
|
{
|
||||||
|
await accountInfoViewModel.AuthService.EnsureToken(currProfile);
|
||||||
|
}, () =>
|
||||||
|
{
|
||||||
|
message.Dispose();
|
||||||
|
errorRun = true;
|
||||||
|
});
|
||||||
|
message.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
accountInfoViewModel.CurrentLogin = currProfile.Login;
|
||||||
|
accountInfoViewModel.CurrentAuthServer = currProfile.AuthServer;
|
||||||
|
var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), ex);
|
||||||
|
accountInfoViewModel._logger.Error(unexpectedError);
|
||||||
|
accountInfoViewModel.PopupMessageService.Popup(unexpectedError);
|
||||||
|
errorRun = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorRun)
|
||||||
|
{
|
||||||
|
accountInfoViewModel.IsLogged = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
accountInfoViewModel.IsLogged = true;
|
||||||
|
|
||||||
|
accountInfoViewModel.AuthServerName = accountInfoViewModel.GetServerAuthName(currProfile.AuthServer);
|
||||||
|
|
||||||
|
return currProfile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record ProfileAuthCredentials(
|
|
||||||
string Login,
|
|
||||||
string Password,
|
|
||||||
string AuthServer,
|
|
||||||
[property: JsonIgnore] ICommand OnSelect = default!,
|
|
||||||
[property: JsonIgnore] ICommand OnDelete = default!
|
|
||||||
);
|
|
||||||
|
|
||||||
public sealed record AuthServerCredentials(
|
|
||||||
string Name,
|
|
||||||
string[] Servers
|
|
||||||
);
|
|
||||||
@@ -1,142 +1,117 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.IO.Compression;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using Nebula.Launcher.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.Views.Config;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Configurations;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using BindingFlags = System.Reflection.BindingFlags;
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
[ViewModelRegister(typeof(ConfigurationView))]
|
[ViewModelRegister(typeof(ConfigurationView))]
|
||||||
[ConstructGenerator]
|
[ConstructGenerator]
|
||||||
public partial class ConfigurationViewModel : ViewModelBase, IConfigContext
|
public partial class ConfigurationViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public ObservableCollection<ViewModelBase> ConfigurationVerbose { get; } = new();
|
public ObservableCollection<IConfigControl> ConfigurationVerbose { get; } = new();
|
||||||
|
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; } = default!;
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; } = default!;
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
|
||||||
|
[GenerateProperty] private FileService FileService { get; set; } = default!;
|
||||||
|
[GenerateProperty] private ContentService ContentService { get; set; } = default!;
|
||||||
|
[GenerateProperty] private CancellationService CancellationService { get; set; } = default!;
|
||||||
|
[GenerateProperty] private ViewHelperService ViewHelperService { get; set; } = default!;
|
||||||
|
|
||||||
|
|
||||||
public void AddConfiguration<T,T1>(ConVar<T> convar) where T1: ViewModelBase, IConfigurationVerbose<T>
|
private readonly List<(object, Type)> _conVarList = new();
|
||||||
|
|
||||||
|
public void AddCvarConf<T>(ConVar<T> cvar)
|
||||||
{
|
{
|
||||||
var configurationVerbose = ServiceProvider.GetService<T1>()!;
|
ConfigurationVerbose.Add(
|
||||||
configurationVerbose.Context = new ConfigContext<T>(convar, this);
|
ConfigControlHelper.GetConfigControl(cvar.Name, ConfigurationService.GetConfigValue(cvar)!));
|
||||||
configurationVerbose.InitializeConfig();
|
_conVarList.Add((cvar, cvar.Type));
|
||||||
ConfigurationVerbose.Add(configurationVerbose);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InvokeUpdateConfiguration()
|
public void InvokeUpdateConfiguration()
|
||||||
{
|
{
|
||||||
foreach (var verbose in ConfigurationVerbose)
|
for (int i = 0; i < ConfigurationVerbose.Count; i++)
|
||||||
{
|
{
|
||||||
if(verbose is not IUpdateInvoker invoker) continue;
|
var conVarControl = ConfigurationVerbose[i];
|
||||||
invoker.UpdateConfiguration();
|
if(!conVarControl.Dirty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var conVar = _conVarList[i];
|
||||||
|
var methodInfo = ConfigurationService.GetType().GetMethod("SetConfigValue")!.MakeGenericMethod(conVar.Item2);
|
||||||
|
methodInfo.Invoke(ConfigurationService, [conVar.Item1, conVarControl.GetValue()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetConfig()
|
||||||
protected override void InitialiseInDesignMode()
|
|
||||||
{
|
{
|
||||||
AddConfiguration<string, StringConfigurationViewModel>(LauncherConVar.ILSpyUrl);
|
foreach (var conVar in _conVarList)
|
||||||
|
{
|
||||||
|
var methodInfo = ConfigurationService.GetType().GetMethod("SetConfigValue")!.MakeGenericMethod(conVar.Item2);
|
||||||
|
methodInfo.Invoke(ConfigurationService, [conVar.Item1, null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_conVarList.Clear();
|
||||||
|
ConfigurationVerbose.Clear();
|
||||||
|
|
||||||
|
InitConfiguration();
|
||||||
|
|
||||||
|
PopupService.Popup("Configuration has been reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Initialise()
|
public void OpenDataFolder()
|
||||||
{
|
{
|
||||||
InitialiseInDesignMode();
|
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue<T>(ConVar<T> conVar, T value)
|
public void ExportLogs()
|
||||||
{
|
{
|
||||||
ConfigurationService.SetConfigValue(conVar, value);
|
var logPath = Path.Join(AppDataPath.RootPath, "log");
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), "tempThink"+Path.GetRandomFileName());
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
|
||||||
|
ZipFile.CreateFromDirectory(logPath, Path.Join(path, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"));
|
||||||
|
ExplorerUtils.OpenFolder(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T? GetValue<T>(ConVar<T> convar)
|
public void RemoveAllContent()
|
||||||
{
|
{
|
||||||
return ConfigurationService.GetConfigValue<T>(convar);
|
Task.Run(() =>
|
||||||
}
|
{
|
||||||
}
|
using var loader = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
loader.LoadingName = "Removing content";
|
||||||
public interface IConfigContext
|
PopupService.Popup(loader);
|
||||||
{
|
ContentService.RemoveAllContent(loader.CreateLoadingContext(), CancellationService.Token);
|
||||||
public void SetValue<T>(ConVar<T> conVar, T value);
|
});
|
||||||
public T? GetValue<T>(ConVar<T> convar);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConfigContext<T> : IConfigContext
|
|
||||||
{
|
|
||||||
public ConfigContext(ConVar<T> conVar, IConfigContext parent)
|
|
||||||
{
|
|
||||||
ConVar = conVar;
|
|
||||||
Parent = parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConVar<T> ConVar { get; }
|
private void InitConfiguration()
|
||||||
public IConfigContext Parent { get; }
|
|
||||||
|
|
||||||
public T? GetValue()
|
|
||||||
{
|
{
|
||||||
return GetValue(ConVar);
|
AddCvarConf(LauncherConVar.ILSpyUrl);
|
||||||
}
|
AddCvarConf(LauncherConVar.Hub);
|
||||||
|
AddCvarConf(LauncherConVar.AuthServers);
|
||||||
public void SetValue(T? value)
|
AddCvarConf(CurrentConVar.EngineManifestUrl);
|
||||||
{
|
AddCvarConf(CurrentConVar.RobustAssemblyName);
|
||||||
SetValue(ConVar!, value);
|
AddCvarConf(CurrentConVar.ManifestDownloadProtocolVersion);
|
||||||
}
|
|
||||||
|
|
||||||
public void SetValue<T1>(ConVar<T1> conVar, T1 value)
|
|
||||||
{
|
|
||||||
Parent.SetValue(conVar, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T1? GetValue<T1>(ConVar<T1> convar)
|
|
||||||
{
|
|
||||||
return Parent.GetValue(convar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IConfigurationVerbose<T>
|
|
||||||
{
|
|
||||||
public ConfigContext<T> Context { get; set; }
|
|
||||||
public void InitializeConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IUpdateInvoker
|
|
||||||
{
|
|
||||||
public void UpdateConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
[ViewModelRegister(typeof(StringConfigurationView))]
|
|
||||||
public partial class StringConfigurationViewModel : ViewModelBase , IConfigurationVerbose<string>, IUpdateInvoker
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _configText = string.Empty;
|
|
||||||
[ObservableProperty] private string? _configName = string.Empty;
|
|
||||||
|
|
||||||
private string _oldText = string.Empty;
|
|
||||||
|
|
||||||
public required ConfigContext<string> Context { get; set; }
|
|
||||||
public void InitializeConfig()
|
|
||||||
{
|
|
||||||
ConfigName = Context.ConVar.Name;
|
|
||||||
_oldText = Context.GetValue() ?? string.Empty;
|
|
||||||
ConfigText = _oldText;
|
|
||||||
}
|
|
||||||
public void UpdateConfiguration()
|
|
||||||
{
|
|
||||||
if (_oldText == ConfigText) return;
|
|
||||||
Context.SetValue(ConfigText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
|
InitConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
|
InitConfiguration();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,15 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
using Nebula.Shared.FileApis;
|
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -29,11 +31,10 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
[ObservableProperty] private string _serverText = "";
|
[ObservableProperty] private string _serverText = "";
|
||||||
[ObservableProperty] private string _searchText = "";
|
[ObservableProperty] private string _searchText = "";
|
||||||
[GenerateProperty] private ContentService ContentService { get; } = default!;
|
[GenerateProperty] private ContentService ContentService { get; } = default!;
|
||||||
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
|
|
||||||
[GenerateProperty] private FileService FileService { get; } = default!;
|
[GenerateProperty] private FileService FileService { get; } = default!;
|
||||||
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
|
||||||
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
|
[GenerateProperty] private CancellationService CancellationService { get; set; } = default!;
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
||||||
|
|
||||||
|
|
||||||
@@ -58,13 +59,12 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
loading.LoadingName = "Unpacking entry";
|
loading.LoadingName = "Unpacking entry";
|
||||||
PopupService.Popup(loading);
|
PopupService.Popup(loading);
|
||||||
|
|
||||||
Task.Run(() => ContentService.Unpack(serverEntry.FileApi, myTempDir, loading));
|
Task.Run(() =>
|
||||||
var startInfo = new ProcessStartInfo(){
|
{
|
||||||
FileName = "explorer.exe",
|
ContentService.Unpack(serverEntry.FileApi, myTempDir, loading.CreateLoadingContext());
|
||||||
Arguments = tmpDir,
|
loading.Dispose();
|
||||||
};
|
});
|
||||||
|
ExplorerUtils.OpenFolder(tmpDir);
|
||||||
Process.Start(startInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnGoEnter()
|
public void OnGoEnter()
|
||||||
@@ -80,11 +80,8 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
{
|
{
|
||||||
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
|
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
|
||||||
cur.Init(this, ServerText.ToRobustUrl());
|
cur.Init(this, ServerText.ToRobustUrl());
|
||||||
var curContent = cur.Go(new ContentPath(SearchText));
|
var curContent = cur.Go(new ContentPath(SearchText), CancellationService.Token);
|
||||||
if(curContent == null)
|
CurrentEntry = curContent ?? throw new NullReferenceException($"{SearchText} not found in {ServerText}");
|
||||||
throw new NullReferenceException($"{SearchText} not found in {ServerText}");
|
|
||||||
|
|
||||||
CurrentEntry = curContent;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -150,11 +147,11 @@ public interface IContentEntry
|
|||||||
public string IconPath { get; }
|
public string IconPath { get; }
|
||||||
public ContentPath FullPath => Parent?.FullPath.With(Name) ?? new ContentPath(Name);
|
public ContentPath FullPath => Parent?.FullPath.With(Name) ?? new ContentPath(Name);
|
||||||
|
|
||||||
public IContentEntry? Go(ContentPath path);
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken);
|
||||||
|
|
||||||
public void GoCurrent()
|
public void GoCurrent()
|
||||||
{
|
{
|
||||||
var entry = Go(ContentPath.Empty);
|
var entry = Go(ContentPath.Empty, CancellationToken.None);
|
||||||
if(entry is not null) Holder.CurrentEntry = entry;
|
if(entry is not null) Holder.CurrentEntry = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +181,7 @@ public sealed class LazyContentEntry : IContentEntry
|
|||||||
_lazyEntry = entry;
|
_lazyEntry = entry;
|
||||||
_lazyEntryInit = lazyEntryInit;
|
_lazyEntryInit = lazyEntryInit;
|
||||||
}
|
}
|
||||||
public IContentEntry? Go(ContentPath path)
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_lazyEntryInit?.Invoke();
|
_lazyEntryInit?.Invoke();
|
||||||
return _lazyEntry;
|
return _lazyEntry;
|
||||||
@@ -202,13 +199,13 @@ public sealed class ExtContentExecutor
|
|||||||
_decompilerService = decompilerService;
|
_decompilerService = decompilerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryExecute(RobustManifestItem manifestItem)
|
public bool TryExecute(IFileApi api, ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(manifestItem.Path);
|
var ext = Path.GetExtension(path.GetName());
|
||||||
|
|
||||||
if (ext == ".dll")
|
if (ext == ".dll")
|
||||||
{
|
{
|
||||||
_decompilerService.OpenServerDecompiler(_root.ServerUrl);
|
_decompilerService.OpenServerDecompiler(_root.ServerUrl, cancellationToken);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,42 +214,39 @@ public sealed class ExtContentExecutor
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class ManifestContentEntry : IContentEntry
|
public sealed partial class FileContentEntry : IContentEntry
|
||||||
{
|
{
|
||||||
public IContentHolder Holder { get; set; } = default!;
|
public IContentHolder Holder { get; set; } = default!;
|
||||||
public IContentEntry? Parent { get; set; }
|
public IContentEntry? Parent { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string IconPath => "/Assets/svg/file.svg";
|
public string IconPath => "/Assets/svg/file.svg";
|
||||||
|
|
||||||
private RobustManifestItem _manifestItem;
|
private IFileApi _fileApi = default!;
|
||||||
private HashApi _hashApi = default!;
|
|
||||||
private ExtContentExecutor _extContentExecutor = default!;
|
private ExtContentExecutor _extContentExecutor = default!;
|
||||||
|
|
||||||
public void Init(IContentHolder holder, RobustManifestItem manifestItem, HashApi api, ExtContentExecutor executor)
|
public void Init(IContentHolder holder, IFileApi api, string fileName, ExtContentExecutor executor)
|
||||||
{
|
{
|
||||||
Holder = holder;
|
Holder = holder;
|
||||||
Name = new ContentPath(manifestItem.Path).GetName();
|
Name = fileName;
|
||||||
_manifestItem = manifestItem;
|
_fileApi = api;
|
||||||
_hashApi = api;
|
|
||||||
_extContentExecutor = executor;
|
_extContentExecutor = executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IContentEntry? Go(ContentPath path)
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_extContentExecutor.TryExecute(_manifestItem))
|
var fullPath = ((IContentEntry)this).FullPath;
|
||||||
|
if (_extContentExecutor.TryExecute(_fileApi, fullPath, cancellationToken))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var ext = Path.GetExtension(_manifestItem.Path);
|
var ext = Path.GetExtension(fullPath.GetName());
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_hashApi.TryOpen(_manifestItem, out var stream))
|
if (!_fileApi.TryOpen(fullPath.Path, out var stream))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|
||||||
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
|
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
|
||||||
|
|
||||||
|
|
||||||
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
stream.CopyTo(sw);
|
stream.CopyTo(sw);
|
||||||
|
|
||||||
@@ -301,7 +295,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
|
|
||||||
public RobustUrl ServerUrl { get; private set; }
|
public RobustUrl ServerUrl { get; private set; }
|
||||||
|
|
||||||
public HashApi FileApi { get; private set; } = default!;
|
public IFileApi FileApi { get; private set; } = default!;
|
||||||
|
|
||||||
private ExtContentExecutor _contentExecutor = default!;
|
private ExtContentExecutor _contentExecutor = default!;
|
||||||
|
|
||||||
@@ -318,20 +312,20 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
|
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
|
||||||
FileApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loading,
|
FileApi = await ContentService.EnsureItems(buildInfo, loading,
|
||||||
CancellationService.Token);
|
CancellationService.Token);
|
||||||
|
|
||||||
foreach (var (path, item) in FileApi.Manifest)
|
foreach (var path in FileApi.AllFiles)
|
||||||
{
|
{
|
||||||
CreateContent(new ContentPath(path), item);
|
CreateContent(new ContentPath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
loading.Dispose();
|
loading.Dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ManifestContentEntry CreateContent(ContentPath path, RobustManifestItem manifestItem)
|
public FileContentEntry CreateContent(ContentPath path)
|
||||||
{
|
{
|
||||||
var pathDir = path.GetDirectory();
|
var pathDir = path.GetDirectory();
|
||||||
BaseFolderContentEntry parent = this;
|
BaseFolderContentEntry parent = this;
|
||||||
@@ -348,8 +342,8 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
|
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestContent = new ManifestContentEntry();
|
var manifestContent = new FileContentEntry();
|
||||||
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor);
|
manifestContent.Init(Holder, FileApi, path.GetName(), _contentExecutor);
|
||||||
|
|
||||||
parent.AddChild(manifestContent);
|
parent.AddChild(manifestContent);
|
||||||
|
|
||||||
@@ -422,15 +416,28 @@ public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry
|
|||||||
private Dictionary<string, IContentEntry> _childs = [];
|
private Dictionary<string, IContentEntry> _childs = [];
|
||||||
|
|
||||||
public string IconPath => "/Assets/svg/folder.svg";
|
public string IconPath => "/Assets/svg/folder.svg";
|
||||||
public IContentHolder Holder { get; private set; }
|
|
||||||
|
private IContentHolder? _holder = null;
|
||||||
|
public IContentHolder Holder
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if(_holder == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
GetType().Name + " was not initialised! Call Init(IContentHolder holder, string? name = null) before using it.");
|
||||||
|
|
||||||
|
return _holder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IContentEntry? Parent { get; set; }
|
public IContentEntry? Parent { get; set; }
|
||||||
public string? Name { get; private set; }
|
public string? Name { get; private set; }
|
||||||
|
|
||||||
public IContentEntry? Go(ContentPath path)
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (path.IsEmpty()) return this;
|
if (path.IsEmpty()) return this;
|
||||||
if (_childs.TryGetValue(path.GetNext(), out var child))
|
if (_childs.TryGetValue(path.GetNext(), out var child))
|
||||||
return child.Go(path);
|
return child.Go(path, cancellationToken);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -438,7 +445,7 @@ public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry
|
|||||||
public void Init(IContentHolder holder, string? name = null)
|
public void Init(IContentHolder holder, string? name = null)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Holder = holder;
|
_holder = holder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T AddChild<T>(T child) where T: IContentEntry
|
public T AddChild<T>(T child) where T: IContentEntry
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using Avalonia.Controls;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.Controls;
|
using Nebula.Launcher.Controls;
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
@@ -12,6 +16,8 @@ using Nebula.Launcher.Views.Pages;
|
|||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -20,29 +26,17 @@ namespace Nebula.Launcher.ViewModels.Pages;
|
|||||||
public partial class ServerOverviewModel : ViewModelBase
|
public partial class ServerOverviewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
[ObservableProperty] private string _searchText = string.Empty;
|
[ObservableProperty] private string _searchText = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty] private bool _isFilterVisible;
|
[ObservableProperty] private bool _isFilterVisible;
|
||||||
|
|
||||||
[ObservableProperty] private ServerListView _currentServerList = new ServerListView();
|
|
||||||
|
|
||||||
public readonly ServerFilter CurrentFilter = new ServerFilter();
|
|
||||||
|
|
||||||
public Action? OnSearchChange;
|
|
||||||
|
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; }
|
|
||||||
[GenerateProperty] private CancellationService CancellationService { get; }
|
|
||||||
[GenerateProperty] private DebugService DebugService { get; }
|
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
|
|
||||||
|
|
||||||
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
|
|
||||||
[ObservableProperty] private ServerListTabTemplate _selectedItem;
|
[ObservableProperty] private ServerListTabTemplate _selectedItem;
|
||||||
|
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
[GenerateProperty, DesignConstruct] public ServerListViewModel CurrentServerList { get; }
|
||||||
|
|
||||||
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; set; }
|
public ServerFilter CurrentFilter { get; } = new();
|
||||||
|
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
|
||||||
private Dictionary<string, ServerListView> _viewCache = [];
|
|
||||||
|
|
||||||
|
|
||||||
//Design think
|
//Design think
|
||||||
@@ -57,34 +51,44 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
|
|
||||||
//real think
|
//real think
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
FavoriteServerListProvider.OnRefreshRequired += OnFavoriteRefreshRequired;
|
||||||
|
ConfigurationService.SubscribeVarChanged(LauncherConVar.Hub, OnHubListChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFavoriteRefreshRequired()
|
||||||
|
{
|
||||||
|
if(CurrentServerList.Provider is FavoriteServerListProvider favoriteServerListProvider)
|
||||||
|
{
|
||||||
|
RefreshProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHubListChanged(ServerHubRecord[]? value)
|
||||||
{
|
{
|
||||||
var tempItems = new List<ServerListTabTemplate>();
|
var tempItems = new List<ServerListTabTemplate>();
|
||||||
foreach (var record in ConfigurationService.GetConfigValue(LauncherConVar.Hub) ?? [])
|
|
||||||
|
foreach (var record in value ?? [])
|
||||||
{
|
{
|
||||||
tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService<HubServerListProvider>()!.With(record.MainUrl), record.Name));
|
tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService<HubServerListProvider>()!.With(record.MainUrl), record.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, "Favorite"));
|
tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, LocalizationService.GetString("tab-favorite")));
|
||||||
|
|
||||||
Items = new ObservableCollection<ServerListTabTemplate>(tempItems);
|
Items = new ObservableCollection<ServerListTabTemplate>(tempItems);
|
||||||
|
|
||||||
SelectedItem = Items[0];
|
SelectedItem = Items[0];
|
||||||
|
|
||||||
OnSearchChange += SearchChangeEvent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SearchChangeEvent()
|
partial void OnSearchTextChanged(string value)
|
||||||
{
|
{
|
||||||
CurrentFilter.SearchText = SearchText;
|
CurrentFilter.SearchText = value;
|
||||||
ApplyFilter();
|
ApplyFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyFilter()
|
private void ApplyFilter()
|
||||||
{
|
{
|
||||||
foreach (var entry in ServerViewContainer.Items)
|
ServerViewContainer.ApplyFilter(CurrentFilter);
|
||||||
{
|
|
||||||
entry.ProcessFilter(CurrentFilter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnFilterChanged(FilterBoxChangedEventArgs args)
|
public void OnFilterChanged(FilterBoxChangedEventArgs args)
|
||||||
@@ -103,64 +107,226 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
|
|
||||||
public void UpdateRequired()
|
public void UpdateRequired()
|
||||||
{
|
{
|
||||||
|
ServerViewContainer.Clear();
|
||||||
|
RefreshProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshProvider()
|
||||||
|
{
|
||||||
|
CurrentServerList.ClearProvider();
|
||||||
CurrentServerList.RefreshFromProvider();
|
CurrentServerList.RefreshFromProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedItemChanged(ServerListTabTemplate value)
|
partial void OnSelectedItemChanged(ServerListTabTemplate value)
|
||||||
{
|
{
|
||||||
if (!_viewCache.TryGetValue(value.TabName, out var view))
|
CurrentServerList.ClearProvider();
|
||||||
{
|
CurrentServerList.SetProvider(value.ServerListProvider);
|
||||||
view = ServerListView.TakeFrom(value.ServerListProvider);
|
ApplyFilter();
|
||||||
_viewCache[value.TabName] = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentServerList = view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public class ServerViewContainer
|
public sealed class ServerViewContainer
|
||||||
{
|
{
|
||||||
private readonly ViewHelperService _viewHelperService;
|
private readonly ViewHelperService _viewHelperService;
|
||||||
|
private readonly List<string> _favorites = [];
|
||||||
|
private readonly Dictionary<string, string> _customNames = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
|
||||||
|
private ServerFilter? _currentFilter;
|
||||||
|
|
||||||
|
public ICollection<IListEntryModelView> Items =>
|
||||||
|
_entries.Values
|
||||||
|
.Select(wr => wr.TryGetTarget(out var target) ? target : null)
|
||||||
|
.Where(t => t != null)
|
||||||
|
.ToList()!;
|
||||||
|
|
||||||
public ServerViewContainer()
|
public ServerViewContainer()
|
||||||
{
|
{
|
||||||
_viewHelperService = new ViewHelperService();
|
_viewHelperService = new ViewHelperService();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerViewContainer(ViewHelperService viewHelperService)
|
[UsedImplicitly]
|
||||||
|
public ServerViewContainer(ViewHelperService viewHelperService, ConfigurationService configurationService)
|
||||||
{
|
{
|
||||||
_viewHelperService = viewHelperService;
|
_viewHelperService = viewHelperService;
|
||||||
|
configurationService.SubscribeVarChanged(LauncherConVar.Favorites, OnFavoritesChange, true);
|
||||||
|
configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, ServerEntryModelView> _entries = new();
|
|
||||||
|
|
||||||
public ICollection<ServerEntryModelView> Items => _entries.Values;
|
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
foreach (var (_, weakRef) in _entries)
|
||||||
|
{
|
||||||
|
if (weakRef.TryGetTarget(out var value) && value is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
_entries.Clear();
|
_entries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
|
public IListEntryModelView Get(string url, ServerStatus? serverStatus = null) => Get(url.ToRobustUrl(), serverStatus);
|
||||||
|
|
||||||
|
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
|
||||||
{
|
{
|
||||||
ServerEntryModelView? entry;
|
var key = url.ToString();
|
||||||
|
IListEntryModelView? entry;
|
||||||
|
|
||||||
lock (_entries)
|
lock (_entries)
|
||||||
{
|
{
|
||||||
if (_entries.TryGetValue(url.ToString(), out entry))
|
if (_entries.TryGetValue(key, out var weakEntry)
|
||||||
|
&& weakEntry.TryGetTarget(out entry))
|
||||||
{
|
{
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry = _viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, serverStatus);
|
entry = Create(url, serverStatus);
|
||||||
|
|
||||||
_entries.Add(url.ToString(), entry);
|
_entries[key] = new WeakReference<IListEntryModelView>(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IListEntryModelView Create(RobustUrl url, ServerStatus? serverStatus = null)
|
||||||
|
{
|
||||||
|
IListEntryModelView? entry;
|
||||||
|
var key = url.ToString();
|
||||||
|
|
||||||
|
_customNames.TryGetValue(key, out var customName);
|
||||||
|
|
||||||
|
if (serverStatus is not null)
|
||||||
|
{
|
||||||
|
entry = _viewHelperService
|
||||||
|
.GetViewModel<ServerEntryViewModel>()
|
||||||
|
.WithData(url, customName, serverStatus);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry = _viewHelperService
|
||||||
|
.GetViewModel<ServerCompoundEntryViewModel>()
|
||||||
|
.LoadServerEntry(url, customName, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is IFavoriteEntryModelView fav)
|
||||||
|
{
|
||||||
|
fav.IsFavorite = _favorites.Contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is IFilterConsumer filterConsumer)
|
||||||
|
{
|
||||||
|
filterConsumer.ProcessFilter(_currentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyFilter(ServerFilter? filter)
|
||||||
|
{
|
||||||
|
_currentFilter = filter;
|
||||||
|
|
||||||
|
foreach (var serverView in Items)
|
||||||
|
{
|
||||||
|
if(serverView is IFilterConsumer filterConsumer)
|
||||||
|
filterConsumer.ProcessFilter(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFavoritesChange(string[]? value)
|
||||||
|
{
|
||||||
|
_favorites.Clear();
|
||||||
|
if (value == null) return;
|
||||||
|
|
||||||
|
foreach (var favorite in value)
|
||||||
|
{
|
||||||
|
_favorites.Add(favorite);
|
||||||
|
if (_entries.TryGetValue(favorite, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IFavoriteEntryModelView fav)
|
||||||
|
{
|
||||||
|
fav.IsFavorite = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCustomNamesChanged(Dictionary<string, string>? value)
|
||||||
|
{
|
||||||
|
var oldNames = _customNames.ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
_customNames.Clear();
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
foreach (var (ip, _) in oldNames)
|
||||||
|
{
|
||||||
|
ResetName(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (oldIp, oldName) in oldNames)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue(oldIp, out var newName))
|
||||||
|
{
|
||||||
|
if (oldName == newName)
|
||||||
|
value.Remove(newName);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetName(oldIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (ip, name) in value)
|
||||||
|
{
|
||||||
|
_customNames.Add(ip, name);
|
||||||
|
|
||||||
|
if (_entries.TryGetValue(ip, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IEntryNameHolder holder)
|
||||||
|
{
|
||||||
|
holder.Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetName(string ip)
|
||||||
|
{
|
||||||
|
if (_entries.TryGetValue(ip, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IEntryNameHolder holder)
|
||||||
|
{
|
||||||
|
holder.Name = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IListEntryModelView
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ExampleEntry : StackPanel, IListEntryModelView
|
||||||
|
{
|
||||||
|
public ExampleEntry(string name)
|
||||||
|
{
|
||||||
|
Children.Add(new Label { Content = name });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFavoriteEntryModelView
|
||||||
|
{
|
||||||
|
public bool IsFavorite { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IEntryNameHolder
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ServerComparer : IComparer<ServerHubInfo>, IComparer<ServerStatus>, IComparer<(RobustUrl,ServerStatus)>
|
public class ServerComparer : IComparer<ServerHubInfo>, IComparer<ServerStatus>, IComparer<(RobustUrl,ServerStatus)>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
using AddFavoriteView = Nebula.Launcher.Views.Popup.AddFavoriteView;
|
using AddFavoriteView = Nebula.Launcher.Views.Popup.AddFavoriteView;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
@@ -28,7 +31,8 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
public override PopupMessageService PopupMessageService { get; }
|
public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; }
|
[GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; }
|
[GenerateProperty] private DebugService DebugService { get; }
|
||||||
public override string Title => "Add to favorite";
|
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
||||||
|
public override string Title => LocalizationService.GetString("popup-add-favorite");
|
||||||
public override bool IsClosable => true;
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
[ObservableProperty] private string _ipInput;
|
[ObservableProperty] private string _ipInput;
|
||||||
@@ -38,7 +42,11 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(IpInput))
|
||||||
|
throw new Exception(LocalizationService.GetString("popup-add-favorite-invalid-ip"));
|
||||||
|
|
||||||
var uri = IpInput.ToRobustUrl();
|
var uri = IpInput.ToRobustUrl();
|
||||||
|
FavoriteServerListProvider.AddFavorite(uri);
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -47,4 +55,10 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
_logger.Error(e);
|
_logger.Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnDispose()
|
||||||
|
{
|
||||||
|
base.OnDispose();
|
||||||
|
_logger.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
63
Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs
Normal file
63
Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Views.Popup;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(EditServerNameView), false)]
|
||||||
|
[ConstructGenerator]
|
||||||
|
public sealed partial class EditServerNameViewModel : PopupViewModelBase
|
||||||
|
{
|
||||||
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
|
[GenerateProperty] public ConfigurationService ConfigurationService { get; }
|
||||||
|
public override string Title => LocalizationService.GetString("popup-edit-name");
|
||||||
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _ipInput;
|
||||||
|
[ObservableProperty] private string _nameInput;
|
||||||
|
|
||||||
|
public void OnEnter()
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(IpInput))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(NameInput))
|
||||||
|
{
|
||||||
|
OnClear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddServerName();
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClear()
|
||||||
|
{
|
||||||
|
RemoveServerName();
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddServerName()
|
||||||
|
{
|
||||||
|
var currentNames = ConfigurationService.GetConfigValue(LauncherConVar.ServerCustomNames)!;
|
||||||
|
currentNames.Add(IpInput, NameInput);
|
||||||
|
ConfigurationService.SetConfigValue(LauncherConVar.ServerCustomNames, currentNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveServerName()
|
||||||
|
{
|
||||||
|
var currentNames = ConfigurationService.GetConfigValue(LauncherConVar.ServerCustomNames)!;
|
||||||
|
currentNames.Remove(IpInput);
|
||||||
|
ConfigurationService.SetConfigValue(LauncherConVar.ServerCustomNames, currentNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
@@ -10,10 +12,10 @@ namespace Nebula.Launcher.ViewModels.Popup;
|
|||||||
public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
||||||
{
|
{
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
public override string Title => "Exception was thrown";
|
public override string Title => LocalizationService.GetString("popup-exception");
|
||||||
public override bool IsClosable => true;
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
public ObservableCollection<Exception> Errors { get; } = new();
|
public ObservableCollection<ExceptionCompound> Errors { get; } = new();
|
||||||
|
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
@@ -21,13 +23,18 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
|||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
var e = new Exception("TEST");
|
var e = new ExceptionCompound("TEST", "thrown in design mode");
|
||||||
AppendError(e);
|
AppendError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AppendError(ExceptionCompound exception)
|
||||||
|
{
|
||||||
|
Errors.Add(exception);
|
||||||
|
}
|
||||||
|
|
||||||
public void AppendError(Exception exception)
|
public void AppendError(Exception exception)
|
||||||
{
|
{
|
||||||
Errors.Add(exception);
|
AppendError(new ExceptionCompound(exception));
|
||||||
if (exception.InnerException != null)
|
if (exception.InnerException != null)
|
||||||
AppendError(exception.InnerException);
|
AppendError(exception.InnerException);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ public partial class InfoPopupViewModel : PopupViewModelBase
|
|||||||
|
|
||||||
[ObservableProperty] private string _infoText = "Test";
|
[ObservableProperty] private string _infoText = "Test";
|
||||||
|
|
||||||
public override string Title => "Info";
|
public override string Title => LocalizationService.GetString("popup-information");
|
||||||
public bool IsInfoClosable { get; set; } = true;
|
public bool IsInfoClosable { get; set; } = true;
|
||||||
public override bool IsClosable => IsInfoClosable;
|
public override bool IsClosable => IsInfoClosable;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.Views.Popup;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
|
[ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))]
|
||||||
|
public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
|
||||||
|
{
|
||||||
|
private ServerEntryViewModel _entryView;
|
||||||
|
|
||||||
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IsLoginCredentialsNullPopupViewModel WithServerEntry(ServerEntryViewModel entryViewModel)
|
||||||
|
{
|
||||||
|
_entryView = entryViewModel;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Proceed()
|
||||||
|
{
|
||||||
|
_entryView.RunInstanceIgnoreAuth();
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GotoAuthPage()
|
||||||
|
{
|
||||||
|
ViewHelperService.GetViewModel<MainViewModel>().RequirePage<AccountInfoViewModel>();
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Title => LocalizationService.GetString("popup-login-credentials-warning");
|
||||||
|
public override bool IsClosable => true;
|
||||||
|
}
|
||||||
@@ -1,58 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
[ViewModelRegister(typeof(LoadingContextView), false)]
|
[ViewModelRegister(typeof(LoadingContextView), false)]
|
||||||
[ConstructGenerator]
|
[ConstructGenerator]
|
||||||
public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandler
|
public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandlerFactory, IConnectionSpeedHandler
|
||||||
{
|
{
|
||||||
|
public ObservableCollection<LoadingContext> LoadingContexts { get; } = [];
|
||||||
|
public ObservableCollection<double> Values { get; } = [];
|
||||||
|
[ObservableProperty] private string _speedText = "";
|
||||||
|
[ObservableProperty] private bool _showSpeed;
|
||||||
|
[ObservableProperty] private int _loadingColumnSize = 2;
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] public CancellationService CancellationService { get; }
|
[GenerateProperty] public CancellationService CancellationService { get; }
|
||||||
|
|
||||||
[ObservableProperty] private int _currJobs;
|
|
||||||
|
|
||||||
[ObservableProperty] private int _resolvedJobs;
|
public string LoadingName { get; set; } = LocalizationService.GetString("popup-loading");
|
||||||
|
|
||||||
public string LoadingName { get; set; } = "Loading...";
|
|
||||||
public bool IsCancellable { get; set; } = true;
|
public bool IsCancellable { get; set; } = true;
|
||||||
public override bool IsClosable => false;
|
public override bool IsClosable => false;
|
||||||
|
|
||||||
public override string Title => LoadingName;
|
public override string Title => LocalizationService.GetString("popup-loading");
|
||||||
|
|
||||||
public void SetJobsCount(int count)
|
public void Cancel()
|
||||||
{
|
{
|
||||||
CurrJobs = count;
|
if (!IsCancellable) return;
|
||||||
}
|
|
||||||
|
|
||||||
public int GetJobsCount()
|
|
||||||
{
|
|
||||||
return CurrJobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetResolvedJobsCount(int count)
|
|
||||||
{
|
|
||||||
ResolvedJobs = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetResolvedJobsCount()
|
|
||||||
{
|
|
||||||
return ResolvedJobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel(){
|
|
||||||
if(!IsCancellable) return;
|
|
||||||
CancellationService.Cancel();
|
CancellationService.Cancel();
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PasteSpeed(int speed)
|
||||||
|
{
|
||||||
|
if (Values.Count == 0)
|
||||||
|
{
|
||||||
|
ShowSpeed = true;
|
||||||
|
LoadingColumnSize = 1;
|
||||||
|
}
|
||||||
|
SpeedText = FileLoadingFormater.FormatBytes(speed) + " / s";
|
||||||
|
Values.Add(speed);
|
||||||
|
if(Values.Count > 10) Values.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null)
|
||||||
|
{
|
||||||
|
var instance = new LoadingContext(this, loadingFormater ?? DefaultLoadingFormater.Instance);
|
||||||
|
LoadingContexts.Add(instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveContextInstance(LoadingContext loadingContext)
|
||||||
|
{
|
||||||
|
LoadingContexts.Remove(loadingContext);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
|
var context = CreateLoadingContext();
|
||||||
|
context.SetJobsCount(5);
|
||||||
|
context.SetResolvedJobsCount(2);
|
||||||
|
context.SetLoadingMessage("message");
|
||||||
|
|
||||||
|
var ctx1 = CreateLoadingContext(new FileLoadingFormater());
|
||||||
|
ctx1.SetJobsCount(1020120);
|
||||||
|
ctx1.SetResolvedJobsCount(12331);
|
||||||
|
ctx1.SetLoadingMessage("File data");
|
||||||
|
|
||||||
|
for (var i = 0; i < 14; i++)
|
||||||
|
{
|
||||||
|
PasteSpeed(Random.Shared.Next(10000000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed partial class LoadingContext : ObservableObject, ILoadingHandler
|
||||||
|
{
|
||||||
|
private readonly LoadingContextViewModel _master;
|
||||||
|
private readonly ILoadingFormater _loadingFormater;
|
||||||
|
public string LoadingText => _loadingFormater.Format(this);
|
||||||
|
|
||||||
|
[ObservableProperty] private string _message = string.Empty;
|
||||||
|
[ObservableProperty] private long _currJobs;
|
||||||
|
[ObservableProperty] private long _resolvedJobs;
|
||||||
|
|
||||||
|
public LoadingContext(LoadingContextViewModel master, ILoadingFormater loadingFormater)
|
||||||
|
{
|
||||||
|
_master = master;
|
||||||
|
_loadingFormater = loadingFormater;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetJobsCount(long count)
|
||||||
|
{
|
||||||
|
CurrJobs = count;
|
||||||
|
OnPropertyChanged(nameof(LoadingText));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetJobsCount()
|
||||||
|
{
|
||||||
|
return CurrJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetResolvedJobsCount(long count)
|
||||||
|
{
|
||||||
|
ResolvedJobs = count;
|
||||||
|
OnPropertyChanged(nameof(LoadingText));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetResolvedJobsCount()
|
||||||
|
{
|
||||||
|
return ResolvedJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLoadingMessage(string message)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_master.RemoveContextInstance(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
@@ -35,4 +37,9 @@ public sealed partial class LogPopupModelView : PopupViewModelBase
|
|||||||
{
|
{
|
||||||
Logs.Add(LogInfo.FromString(str));
|
Logs.Add(LogInfo.FromString(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
Logs.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,14 @@ public abstract class PopupViewModelBase : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public abstract string Title { get; }
|
public abstract string Title { get; }
|
||||||
public abstract bool IsClosable { get; }
|
public abstract bool IsClosable { get; }
|
||||||
|
public Action<PopupViewModelBase>? OnDisposing;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
OnDispose();
|
||||||
|
OnDisposing?.Invoke(this);
|
||||||
PopupMessageService.ClosePopup(this);
|
PopupMessageService.ClosePopup(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void OnDispose(){}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Popup;
|
namespace Nebula.Launcher.ViewModels.Popup;
|
||||||
|
|
||||||
[ConstructGenerator, ViewModelRegister(typeof(TfaView))]
|
[ConstructGenerator, ViewModelRegister(typeof(TfaView))]
|
||||||
public partial class TfaViewModel : PopupViewModelBase
|
public partial class TfaViewModel : PopupViewModelBase
|
||||||
{
|
{
|
||||||
public Action<string>? OnTfaEntered;
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
|
[GenerateProperty] public AccountInfoViewModel AccountInfo { get; }
|
||||||
|
public override string Title => LocalizationService.GetString("popup-twofa");
|
||||||
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
@@ -19,11 +25,7 @@ public partial class TfaViewModel : PopupViewModelBase
|
|||||||
|
|
||||||
public void OnTfaEnter(string code)
|
public void OnTfaEnter(string code)
|
||||||
{
|
{
|
||||||
OnTfaEntered?.Invoke(code);
|
AccountInfo.DoAuth(code);
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
|
||||||
public override string Title => "2fa";
|
|
||||||
public override bool IsClosable => true;
|
|
||||||
}
|
}
|
||||||
133
Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs
Normal file
133
Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ServerCompoundEntryView), false)]
|
||||||
|
[ConstructGenerator]
|
||||||
|
public sealed partial class ServerCompoundEntryViewModel :
|
||||||
|
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _message = "Loading server entry...";
|
||||||
|
[ObservableProperty] private bool _isFavorite;
|
||||||
|
[ObservableProperty] private bool _loading = true;
|
||||||
|
|
||||||
|
private string? _name;
|
||||||
|
private RobustUrl? _url;
|
||||||
|
private ServerFilter? _currentFilter;
|
||||||
|
|
||||||
|
public ServerEntryViewModel? CurrentEntry
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == field) return;
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
if (field != null)
|
||||||
|
{
|
||||||
|
field.IsFavorite = IsFavorite;
|
||||||
|
field.Name = Name;
|
||||||
|
field.ProcessFilter(_currentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading = field == null;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_name = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
if (CurrentEntry != null)
|
||||||
|
CurrentEntry.Name = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GenerateProperty] private RestService RestService { get; }
|
||||||
|
[GenerateProperty] private IServiceProvider ServiceProvider{ get; }
|
||||||
|
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
Name = "TEST.TEST";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryViewModel? entry)
|
||||||
|
{
|
||||||
|
CurrentEntry = entry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url, string? name, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_url = url;
|
||||||
|
_name = name;
|
||||||
|
Task.Run(LoadServer, cancellationToken);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadServer()
|
||||||
|
{
|
||||||
|
if (_url is null)
|
||||||
|
{
|
||||||
|
Message = "Url is not set";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Message = "Loading server entry...";
|
||||||
|
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None);
|
||||||
|
|
||||||
|
CurrentEntry = ServiceProvider.GetService<ServerEntryViewModel>()!.WithData(_url, null, status);
|
||||||
|
|
||||||
|
Loading = false;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Message = "Error while fetching data from " + _url + " : " + e.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleFavorites()
|
||||||
|
{
|
||||||
|
if (CurrentEntry is null && _url is not null)
|
||||||
|
{
|
||||||
|
IsFavorite = !IsFavorite;
|
||||||
|
if(IsFavorite) FavoriteServerListProvider.AddFavorite(_url);
|
||||||
|
else FavoriteServerListProvider.RemoveFavorite(_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentEntry?.ToggleFavorites();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ProcessFilter(ServerFilter? serverFilter)
|
||||||
|
{
|
||||||
|
_currentFilter = serverFilter;
|
||||||
|
if(CurrentEntry is IFilterConsumer filterConsumer)
|
||||||
|
filterConsumer.ProcessFilter(serverFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Nebula.Launcher.ServerListProviders;
|
|
||||||
using Nebula.Launcher.Services;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
|
||||||
using Nebula.Launcher.Views;
|
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
|
||||||
using Nebula.Shared.Services.Logging;
|
|
||||||
using Nebula.Shared.Utils;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
|
||||||
|
|
||||||
[ViewModelRegister(typeof(ServerEntryView), false)]
|
|
||||||
[ConstructGenerator]
|
|
||||||
public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _description = "Fetching info...";
|
|
||||||
[ObservableProperty] private bool _expandInfo;
|
|
||||||
[ObservableProperty] private bool _isFavorite;
|
|
||||||
[ObservableProperty] private bool _isVisible;
|
|
||||||
|
|
||||||
private string _lastError = "";
|
|
||||||
private Process? _p;
|
|
||||||
private ILogger _logger;
|
|
||||||
private ILogger? _processLogger;
|
|
||||||
|
|
||||||
private ServerInfo? _serverInfo;
|
|
||||||
[ObservableProperty] private bool _tagDataVisible;
|
|
||||||
|
|
||||||
public LogPopupModelView CurrLog;
|
|
||||||
public RobustUrl Address { get; private set; }
|
|
||||||
|
|
||||||
[GenerateProperty] private AuthService AuthService { get; } = default!;
|
|
||||||
[GenerateProperty] private ContentService ContentService { get; } = default!;
|
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; } = default!;
|
|
||||||
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
|
|
||||||
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
|
||||||
[GenerateProperty] private RunnerService RunnerService { get; } = default!;
|
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
|
||||||
[GenerateProperty] private ViewHelperService ViewHelperService { get; } = default!;
|
|
||||||
[GenerateProperty] private RestService RestService { get; } = default!;
|
|
||||||
[GenerateProperty] private MainViewModel MainViewModel { get; } = default!;
|
|
||||||
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; } = default!;
|
|
||||||
[GenerateProperty] private DotnetResolverService DotnetResolverService { get; } = default!;
|
|
||||||
|
|
||||||
public ServerStatus Status { get; private set; } =
|
|
||||||
new(
|
|
||||||
"Fetching data...",
|
|
||||||
"Loading...", [],
|
|
||||||
"",
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
false,
|
|
||||||
DateTime.Now,
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
|
|
||||||
public ObservableCollection<ServerLink> Links { get; } = new();
|
|
||||||
public bool RunVisible => Process == null;
|
|
||||||
|
|
||||||
public ObservableCollection<string> Tags { get; } = [];
|
|
||||||
|
|
||||||
public ICommand OnLinkGo { get; } = new LinkGoCommand();
|
|
||||||
|
|
||||||
private Process? Process
|
|
||||||
{
|
|
||||||
get => _p;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_p = value;
|
|
||||||
OnPropertyChanged(nameof(RunVisible));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ServerInfo?> GetServerInfo()
|
|
||||||
{
|
|
||||||
if (_serverInfo == null)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Description = e.Message;
|
|
||||||
_logger.Error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _serverInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
|
||||||
{
|
|
||||||
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
|
|
||||||
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
|
|
||||||
Status = new ServerStatus("Ameba",
|
|
||||||
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
|
|
||||||
["rp:hrp", "18+"],
|
|
||||||
"Antag", 15, 5, 1, false
|
|
||||||
, DateTime.Now, 100);
|
|
||||||
Address = "ss14://localhost".ToRobustUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialise()
|
|
||||||
{
|
|
||||||
_logger = DebugService.GetLogger(this);
|
|
||||||
CurrLog = ViewHelperService.GetViewModel<LogPopupModelView>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProcessFilter(ServerFilter? serverFilter)
|
|
||||||
{
|
|
||||||
if (serverFilter == null)
|
|
||||||
{
|
|
||||||
IsVisible = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetStatus(ServerStatus serverStatus)
|
|
||||||
{
|
|
||||||
Status = serverStatus;
|
|
||||||
Tags.Clear();
|
|
||||||
foreach (var tag in Status.Tags) Tags.Add(tag);
|
|
||||||
OnPropertyChanged(nameof(Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerEntryModelView WithData(RobustUrl url, ServerStatus? serverStatus)
|
|
||||||
{
|
|
||||||
Address = url;
|
|
||||||
if (serverStatus is not null)
|
|
||||||
SetStatus(serverStatus);
|
|
||||||
else
|
|
||||||
FetchStatus();
|
|
||||||
|
|
||||||
IsFavorite = GetFavoriteEntries().Contains(Address.ToString());
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> GetFavoriteEntries()
|
|
||||||
{
|
|
||||||
return ConfigurationService.GetConfigValue(LauncherConVar.Favorites)?.ToList() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void FetchStatus()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SetStatus(await RestService.GetAsync<ServerStatus>(Address.StatusUri, CancellationService.Token));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.Error(e);
|
|
||||||
Status = new ServerStatus("ErrorLand", $"ERROR: {e.Message}", [], "", -1, -1, -1, false,
|
|
||||||
DateTime.Now,
|
|
||||||
-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenContentViewer()
|
|
||||||
{
|
|
||||||
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address, ContentPath.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleFavorites()
|
|
||||||
{
|
|
||||||
IsFavorite = !IsFavorite;
|
|
||||||
if(IsFavorite)
|
|
||||||
FavoriteServerListProvider.AddFavorite(this);
|
|
||||||
else
|
|
||||||
FavoriteServerListProvider.RemoveFavorite(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunInstance()
|
|
||||||
{
|
|
||||||
Task.Run(RunAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var authProv = AuthService.SelectedAuth;
|
|
||||||
|
|
||||||
var buildInfo =
|
|
||||||
await ContentService.GetBuildInfo(Address, CancellationService.Token);
|
|
||||||
|
|
||||||
using (var loadingContext = ViewHelperService.GetViewModel<LoadingContextViewModel>())
|
|
||||||
{
|
|
||||||
loadingContext.LoadingName = "Loading instance...";
|
|
||||||
((ILoadingHandler)loadingContext).AppendJob();
|
|
||||||
|
|
||||||
PopupMessageService.Popup(loadingContext);
|
|
||||||
|
|
||||||
await RunnerService.PrepareRun(buildInfo, loadingContext, CancellationService.Token);
|
|
||||||
|
|
||||||
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
|
|
||||||
|
|
||||||
Process = Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = await DotnetResolverService.EnsureDotnet(),
|
|
||||||
Arguments = Path.Join(path, "Nebula.Runner.dll"),
|
|
||||||
Environment =
|
|
||||||
{
|
|
||||||
{ "ROBUST_AUTH_USERID", authProv?.UserId.ToString() },
|
|
||||||
{ "ROBUST_AUTH_TOKEN", authProv?.Token.Token },
|
|
||||||
{ "ROBUST_AUTH_SERVER", authProv?.AuthServer },
|
|
||||||
{ "ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey },
|
|
||||||
{ "GAME_URL", Address.ToString() },
|
|
||||||
{ "AUTH_LOGIN", authProv?.Login }
|
|
||||||
},
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
StandardOutputEncoding = Encoding.UTF8
|
|
||||||
});
|
|
||||||
|
|
||||||
((ILoadingHandler)loadingContext).AppendResolvedJob();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Process is null) return;
|
|
||||||
|
|
||||||
_processLogger = DebugService.GetLogger($"PROCESS_{Process.Id}");
|
|
||||||
|
|
||||||
Process.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
Process.BeginOutputReadLine();
|
|
||||||
Process.BeginErrorReadLine();
|
|
||||||
|
|
||||||
Process.OutputDataReceived += OnOutputDataReceived;
|
|
||||||
Process.ErrorDataReceived += OnErrorDataReceived;
|
|
||||||
|
|
||||||
Process.Exited += OnExited;
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException e)
|
|
||||||
{
|
|
||||||
PopupMessageService.Popup("Task canceled: " + e.Message);
|
|
||||||
_logger.Error("Task canceled");
|
|
||||||
_logger.Error(e);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PopupMessageService.Popup(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnExited(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (Process is null) return;
|
|
||||||
|
|
||||||
Process.OutputDataReceived -= OnOutputDataReceived;
|
|
||||||
Process.ErrorDataReceived -= OnErrorDataReceived;
|
|
||||||
Process.Exited -= OnExited;
|
|
||||||
|
|
||||||
_processLogger?.Log("PROCESS EXIT WITH CODE " + Process.ExitCode);
|
|
||||||
|
|
||||||
if (Process.ExitCode != 0)
|
|
||||||
PopupMessageService.Popup($"Game exit with code {Process.ExitCode}.\nReason: {_lastError}");
|
|
||||||
|
|
||||||
_processLogger?.Dispose();
|
|
||||||
|
|
||||||
Process.Dispose();
|
|
||||||
Process = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Data != null)
|
|
||||||
{
|
|
||||||
_lastError = e.Data;
|
|
||||||
_processLogger?.Error(e.Data);
|
|
||||||
CurrLog.Append(e.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Data != null)
|
|
||||||
{
|
|
||||||
_processLogger?.Log(e.Data);
|
|
||||||
CurrLog.Append(e.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReadLog()
|
|
||||||
{
|
|
||||||
PopupMessageService.Popup(CurrLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopInstance()
|
|
||||||
{
|
|
||||||
Process?.CloseMainWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void ExpandInfoRequired()
|
|
||||||
{
|
|
||||||
ExpandInfo = !ExpandInfo;
|
|
||||||
if (Design.IsDesignMode) return;
|
|
||||||
|
|
||||||
var info = await GetServerInfo();
|
|
||||||
if (info == null) return;
|
|
||||||
|
|
||||||
Description = info.Desc;
|
|
||||||
|
|
||||||
Links.Clear();
|
|
||||||
if (info.Links is null) return;
|
|
||||||
foreach (var link in info.Links) Links.Add(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FindDotnetPath()
|
|
||||||
{
|
|
||||||
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
|
||||||
var paths = pathEnv?.Split(Path.PathSeparator);
|
|
||||||
if (paths != null)
|
|
||||||
foreach (var path in paths)
|
|
||||||
{
|
|
||||||
var dotnetPath = Path.Combine(path, "dotnet");
|
|
||||||
if (File.Exists(dotnetPath)) return dotnetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "dotnet";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class LogInfo
|
|
||||||
{
|
|
||||||
public string Category { get; set; } = "LOG";
|
|
||||||
public IBrush CategoryColor { get; set; } = Brush.Parse("#424242");
|
|
||||||
public string Message { get; set; } = "";
|
|
||||||
|
|
||||||
public static LogInfo FromString(string input)
|
|
||||||
{
|
|
||||||
var matches = Regex.Matches(input, @"(\[(?<c>.*)\] (?<m>.*))|(?<m>.*)");
|
|
||||||
var category = "All";
|
|
||||||
|
|
||||||
if (matches[0].Groups.TryGetValue("c", out var c)) category = c.Value;
|
|
||||||
|
|
||||||
var color = Brush.Parse("#444444");
|
|
||||||
|
|
||||||
switch (category)
|
|
||||||
{
|
|
||||||
case "DEBG":
|
|
||||||
color = Brush.Parse("#2436d4");
|
|
||||||
break;
|
|
||||||
case "ERRO":
|
|
||||||
color = Brush.Parse("#d42436");
|
|
||||||
break;
|
|
||||||
case "INFO":
|
|
||||||
color = Brush.Parse("#0ab3c9");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = matches[0].Groups["m"].Value;
|
|
||||||
return new LogInfo
|
|
||||||
{
|
|
||||||
Category = category, Message = message, CategoryColor = color
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LinkGoCommand : ICommand
|
|
||||||
{
|
|
||||||
public LinkGoCommand()
|
|
||||||
{
|
|
||||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExecute(object? parameter)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(object? parameter)
|
|
||||||
{
|
|
||||||
if (parameter is not string str) return;
|
|
||||||
Helper.SafeOpenBrowser(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IFilterConsumer
|
|
||||||
{
|
|
||||||
public void ProcessFilter(ServerFilter? serverFilter);
|
|
||||||
}
|
|
||||||
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ServerEntryView), false)]
|
||||||
|
public sealed partial class ServerEntryViewModel(
|
||||||
|
RestService restService,
|
||||||
|
CancellationService cancellationService,
|
||||||
|
GameRunnerService gameRunnerService
|
||||||
|
) :
|
||||||
|
ViewModelBase,
|
||||||
|
IFilterConsumer,
|
||||||
|
IListEntryModelView,
|
||||||
|
IFavoriteEntryModelView,
|
||||||
|
IEntryNameHolder,
|
||||||
|
IRunningSignalConsumer
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _description = "Fetching info...";
|
||||||
|
[ObservableProperty] private bool _expandInfo;
|
||||||
|
[ObservableProperty] private bool _isFavorite;
|
||||||
|
[ObservableProperty] private bool _isVisible;
|
||||||
|
[ObservableProperty] private bool _runVisible = true;
|
||||||
|
[ObservableProperty] private string _realName = string.Empty;
|
||||||
|
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
get => RealName;
|
||||||
|
set => RealName = value ?? Status.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerInfo? _serverInfo;
|
||||||
|
|
||||||
|
public RobustUrl Address { get; private set; }
|
||||||
|
|
||||||
|
public ServerStatus Status { get; private set; } =
|
||||||
|
new(
|
||||||
|
"Fetching data...",
|
||||||
|
"Loading...", [],
|
||||||
|
"",
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
DateTime.Now,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
public ObservableCollection<ServerLink> Links { get; } = new();
|
||||||
|
public ObservableCollection<string> Tags { get; } = [];
|
||||||
|
public ICommand OnLinkGo { get; } = new LinkGoCommand();
|
||||||
|
|
||||||
|
public async Task<ServerInfo?> GetServerInfo()
|
||||||
|
{
|
||||||
|
if (_serverInfo != null)
|
||||||
|
return _serverInfo;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_serverInfo = await restService.GetAsync<ServerInfo>(Address.InfoUri, cancellationService.Token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Description = e.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _serverInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
RealName = "TEST.TEST";
|
||||||
|
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
|
||||||
|
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
|
||||||
|
Status = new ServerStatus("Ameba",
|
||||||
|
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
|
||||||
|
["rp:hrp", "18+"],
|
||||||
|
"Antag", 15, 5, 1, false
|
||||||
|
, DateTime.Now, 100);
|
||||||
|
Address = "ss14://localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessFilter(ServerFilter? serverFilter)
|
||||||
|
{
|
||||||
|
if (serverFilter == null)
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStatus(ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
Status = serverStatus;
|
||||||
|
Tags.Clear();
|
||||||
|
foreach (var tag in Status.Tags) Tags.Add(tag);
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEntryViewModel WithData(RobustUrl url, string? name, ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
Address = url;
|
||||||
|
SetStatus(serverStatus);
|
||||||
|
Name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenContentViewer()
|
||||||
|
{
|
||||||
|
gameRunnerService.OpenContentViewer(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleFavorites()
|
||||||
|
{
|
||||||
|
IsFavorite = !IsFavorite;
|
||||||
|
if(IsFavorite)
|
||||||
|
gameRunnerService.AddFavorite(Address);
|
||||||
|
else
|
||||||
|
gameRunnerService.RemoveFavorite(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RunInstance()
|
||||||
|
{
|
||||||
|
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RunInstanceIgnoreAuth()
|
||||||
|
{
|
||||||
|
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInstance()
|
||||||
|
{
|
||||||
|
gameRunnerService.StopInstance(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReadLog()
|
||||||
|
{
|
||||||
|
gameRunnerService.ReadInstanceLog(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditName()
|
||||||
|
{
|
||||||
|
gameRunnerService.EditName(Address, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ExpandInfoRequired()
|
||||||
|
{
|
||||||
|
ExpandInfo = !ExpandInfo;
|
||||||
|
if (Design.IsDesignMode) return;
|
||||||
|
|
||||||
|
var info = await GetServerInfo();
|
||||||
|
if (info == null) return;
|
||||||
|
|
||||||
|
Description = info.Desc;
|
||||||
|
|
||||||
|
Links.Clear();
|
||||||
|
if (info.Links is null) return;
|
||||||
|
foreach (var link in info.Links) Links.Add(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ProcessRunningSignal(bool isRunning)
|
||||||
|
{
|
||||||
|
RunVisible = !isRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LinkGoCommand : ICommand
|
||||||
|
{
|
||||||
|
public LinkGoCommand()
|
||||||
|
{
|
||||||
|
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(object? parameter)
|
||||||
|
{
|
||||||
|
if (parameter is not string str) return;
|
||||||
|
Helper.SafeOpenBrowser(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
}
|
||||||
57
Nebula.Launcher/ViewModels/ServerListViewModel.cs
Normal file
57
Nebula.Launcher/ViewModels/ServerListViewModel.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ServerListView), false)]
|
||||||
|
public class ServerListViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public AvaloniaList<IListEntryModelView> ServerList { get; private set; } = new();
|
||||||
|
public AvaloniaList<Exception> ErrorList { get; private set; } = new();
|
||||||
|
public IServerListProvider? Provider { get; private set; }
|
||||||
|
|
||||||
|
public void ClearProvider()
|
||||||
|
{
|
||||||
|
foreach (var serverEntry in ServerList)
|
||||||
|
{
|
||||||
|
if (serverEntry is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerList.Clear();
|
||||||
|
ErrorList.Clear();
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProvider(IServerListProvider provider)
|
||||||
|
{
|
||||||
|
Provider = provider;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(ServerList));
|
||||||
|
OnPropertyChanged(nameof(ErrorList));
|
||||||
|
|
||||||
|
RefreshFromProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshFromProvider()
|
||||||
|
{
|
||||||
|
Provider?.LoadServerList(ServerList, ErrorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
SetProvider(new TestServerList());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Nebula.Launcher/ViewModels/VisualErrorViewModel.cs
Normal file
22
Nebula.Launcher/ViewModels/VisualErrorViewModel.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(VisualErrorView))]
|
||||||
|
public partial class VisualErrorViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _imgPath = "cinka";
|
||||||
|
[ObservableProperty] private string _title = "Error";
|
||||||
|
[ObservableProperty] private string _description = "This is an error.";
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Nebula.Launcher.Views.Config.StringConfigurationView"
|
|
||||||
x:DataType="pages:StringConfigurationViewModel">
|
|
||||||
<Design.DataContext>
|
|
||||||
<pages:StringConfigurationViewModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5" Margin="5">
|
|
||||||
<TextBlock VerticalAlignment="Center" Text="{Binding ConfigName}"/>
|
|
||||||
<TextBlock VerticalAlignment="Center" Text=":"/>
|
|
||||||
<TextBox Text="{Binding ConfigText}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views.Config;
|
|
||||||
|
|
||||||
public partial class StringConfigurationView : UserControl
|
|
||||||
{
|
|
||||||
public StringConfigurationView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringConfigurationView(StringConfigurationViewModel viewModel)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="Nebula.Launcher.Views.ExceptionView"
|
x:Class="Nebula.Launcher.Views.ExceptionView"
|
||||||
x:DataType="system:Exception"
|
x:DataType="viewModels:ExceptionCompound"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<system:Exception />
|
<viewModels:ExceptionCompound />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<Border
|
<Border
|
||||||
BoxShadow="{StaticResource DefaultShadow}"
|
BoxShadow="{StaticResource DefaultShadow}"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views;
|
namespace Nebula.Launcher.Views;
|
||||||
|
|
||||||
@@ -14,6 +16,6 @@ public partial class ExceptionView : UserControl
|
|||||||
|
|
||||||
public ExceptionView(Exception exception): this()
|
public ExceptionView(Exception exception): this()
|
||||||
{
|
{
|
||||||
DataContext = exception;
|
DataContext = new ExceptionCompound(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,11 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views;
|
namespace Nebula.Launcher.Views;
|
||||||
|
|
||||||
public partial class FileContentEntryView : UserControl
|
public partial class FileContentEntryView : UserControl
|
||||||
{
|
{
|
||||||
// This constructor is used when the view is created by the XAML Previewer
|
|
||||||
public FileContentEntryView()
|
public FileContentEntryView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This constructor is used when the view is created via dependency injection
|
|
||||||
public FileContentEntryView(FolderContentEntry viewModel)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
|
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:models="clr-namespace:Nebula.Shared.Models;assembly=Nebula.Shared"
|
|
||||||
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
|
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:models1="clr-namespace:Nebula.Launcher.Models">
|
xmlns:models1="clr-namespace:Nebula.Launcher.Models"
|
||||||
|
xmlns:services="clr-namespace:Nebula.Launcher.Services">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:MainViewModel />
|
<viewModels:MainViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</ListBox>
|
</ListBox>
|
||||||
<Button
|
<Button
|
||||||
Classes="ViewSelectButton"
|
Classes="ViewSelectButton"
|
||||||
Command="{Binding TriggerPaneCommand}"
|
Command="{Binding TriggerPane}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Padding="5,0,5,0"
|
Padding="5,0,5,0"
|
||||||
@@ -121,7 +121,41 @@
|
|||||||
https://durenko.tatar/nebula/
|
https://durenko.tatar/nebula/
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center" Text="{Binding VersionInfo}"/>
|
<StackPanel Spacing="5" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
CornerRadius="0"
|
||||||
|
Command="{Binding OpenAuthPage}">
|
||||||
|
<StackPanel Spacing="5" Orientation="Horizontal">
|
||||||
|
<Svg
|
||||||
|
Height="40"
|
||||||
|
Path="/Assets/svg/user.svg"
|
||||||
|
Width="10" />
|
||||||
|
<Panel>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="#777777"
|
||||||
|
Text="{Binding LoginText}"/>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock>|</TextBlock>
|
||||||
|
<Button
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
CornerRadius="0"
|
||||||
|
Command="{Binding OpenRootPath}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<Svg
|
||||||
|
Height="40"
|
||||||
|
Path="/Assets/svg/folder.svg"
|
||||||
|
Width="10" />
|
||||||
|
<TextBlock Foreground="#777777" Text="{services:LocaledText goto-path-home}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock>|</TextBlock>
|
||||||
|
<TextBlock Text="{Binding VersionInfo}"/>
|
||||||
|
</StackPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Label>
|
</Label>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -144,7 +178,7 @@
|
|||||||
<Label Content="{Binding CurrentTitle}" VerticalAlignment="Center" />
|
<Label Content="{Binding CurrentTitle}" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button
|
<Button
|
||||||
Command="{Binding ClosePopupCommand}"
|
Command="{Binding CloseCurrentPopup}"
|
||||||
Content="X"
|
Content="X"
|
||||||
CornerRadius="0,10,0,0"
|
CornerRadius="0,10,0,0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Nebula.Launcher.ViewModels;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views;
|
namespace Nebula.Launcher.Views;
|
||||||
|
|
||||||
public partial class MainView : UserControl
|
public partial class MainView : UserControl
|
||||||
{
|
{
|
||||||
// This constructor is used when the view is created by the XAML Previewer
|
|
||||||
public MainView()
|
public MainView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This constructor is used when the view is created via dependency injection
|
|
||||||
public MainView(MainViewModel viewModel)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
ExtendClientAreaChromeHints="NoChrome"
|
ExtendClientAreaChromeHints="NoChrome"
|
||||||
ExtendClientAreaTitleBarHeightHint="-1"
|
ExtendClientAreaTitleBarHeightHint="-1"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
Height="500"
|
Height="550"
|
||||||
Icon="/Assets/nebula.ico"
|
Icon="/Assets/nebula.ico"
|
||||||
MinHeight="500"
|
MinHeight="550"
|
||||||
MinWidth="800"
|
MinWidth="800"
|
||||||
SystemDecorations="BorderOnly"
|
SystemDecorations="BorderOnly"
|
||||||
Title="Nebula.Launcher"
|
Title="Nebula.Launcher"
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<UserControl
|
<UserControl
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="1000"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="Nebula.Launcher.Views.Pages.AccountInfoView"
|
x:Class="Nebula.Launcher.Views.Pages.AccountInfoView"
|
||||||
x:DataType="pages:AccountInfoViewModel"
|
x:DataType="pages:AccountInfoViewModel"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:customControls="clr-namespace:Nebula.Launcher.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
|
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:auth="clr-namespace:Nebula.Launcher.Models.Auth"
|
||||||
|
xmlns:converters="clr-namespace:Nebula.Launcher.Converters">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<pages:AccountInfoViewModel />
|
<pages:AccountInfoViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
<GradientStop Color="#292222" Offset="1.0" />
|
<GradientStop Color="#292222" Offset="1.0" />
|
||||||
</LinearGradientBrush>
|
</LinearGradientBrush>
|
||||||
</Border.Background>
|
</Border.Background>
|
||||||
<Label HorizontalAlignment="Center">Profiles:</Label>
|
<customControls:LocalizedLabel HorizontalAlignment="Center" LocalId="account-profiles"/>
|
||||||
</Border>
|
</Border>
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
<ItemsControl
|
<ItemsControl
|
||||||
@@ -36,44 +39,58 @@
|
|||||||
ItemsSource="{Binding Accounts}"
|
ItemsSource="{Binding Accounts}"
|
||||||
Padding="0">
|
Padding="0">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type pages:ProfileAuthCredentials}">
|
<DataTemplate DataType="{x:Type auth:ProfileEntry}">
|
||||||
<Border
|
<Grid ColumnDefinitions="4*,*">
|
||||||
BoxShadow="0 1 15 -2 #121212"
|
<Border
|
||||||
CornerRadius="0,10,0,10"
|
BoxShadow="0 1 15 -2 #121212"
|
||||||
Margin="5,5,5,0"
|
CornerRadius="0,10,0,10"
|
||||||
VerticalAlignment="Center">
|
Margin="5,5,5,0">
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<LinearGradientBrush EndPoint="50%,100%" StartPoint="50%,0%">
|
<LinearGradientBrush EndPoint="100%,50%" StartPoint="20%,50%">
|
||||||
<GradientStop Color="#292222" Offset="0.0" />
|
<GradientStop Color="{Binding Credentials.AuthServer,
|
||||||
<GradientStop Color="#222222" Offset="1.0" />
|
Converter={x:Static converters:TypeConverters.NameColorRepresentation}}" Offset="0.0" />
|
||||||
</LinearGradientBrush>
|
<GradientStop Color="#222222" Offset="1.0" />
|
||||||
</Border.Background>
|
</LinearGradientBrush>
|
||||||
<Panel>
|
</Border.Background>
|
||||||
<StackPanel Margin="10,5,5,5" Orientation="Horizontal">
|
<Label>
|
||||||
|
<TextBlock Text="{Binding AuthName}" Margin="5"/>
|
||||||
|
</Label>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
CornerRadius="0,10,0,10"
|
||||||
|
Margin="5,5,5,0">
|
||||||
|
<Border.Background>
|
||||||
|
<LinearGradientBrush EndPoint="100%,50%" StartPoint="20%,50%">
|
||||||
|
<GradientStop Color="#aa222222" Offset="0.0" />
|
||||||
|
<GradientStop Color="#222222" Offset="0.4" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Border.Background>
|
||||||
|
<Button
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding OnSelect}">
|
||||||
<Label>
|
<Label>
|
||||||
<TextBlock Text="{Binding Login}" />
|
<TextBlock Text="{Binding Credentials.Login}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
</Label>
|
</Label>
|
||||||
</StackPanel>
|
</Button>
|
||||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
</Border>
|
||||||
<Button
|
<Border
|
||||||
Command="{Binding OnSelect}"
|
BoxShadow="0 1 15 -2 #121212"
|
||||||
CornerRadius="0,0,0,10"
|
CornerRadius="0,10,0,10"
|
||||||
Padding="5">
|
Margin="0,5,5,0" Grid.Column="1" Padding="0">
|
||||||
<Label>
|
<Border.Background>
|
||||||
Select
|
<LinearGradientBrush EndPoint="100%,50%" StartPoint="20%,50%">
|
||||||
</Label>
|
<GradientStop Color="#292222" Offset="1.0" />
|
||||||
</Button>
|
<GradientStop Color="#222222" Offset="1.0" />
|
||||||
<Button
|
</LinearGradientBrush>
|
||||||
Command="{Binding OnDelete}"
|
</Border.Background>
|
||||||
CornerRadius="0,10,0,0"
|
<Button Command="{Binding OnDelete}" CornerRadius="0,10,0,10" HorizontalAlignment="Stretch">
|
||||||
Padding="5">
|
<Svg
|
||||||
<Label>
|
Height="15"
|
||||||
Delete
|
Path="/Assets/svg/delete.svg"
|
||||||
</Label>
|
Width="15" />
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</Border>
|
||||||
</Panel>
|
</Grid>
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
@@ -102,23 +119,28 @@
|
|||||||
Margin="0,0,0,20"
|
Margin="0,0,0,20"
|
||||||
Path="/Assets/svg/user.svg" />
|
Path="/Assets/svg/user.svg" />
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<StackPanel IsVisible="{Binding DoRetryAuth}">
|
||||||
|
<Border Background="{StaticResource DefaultSelected}" BoxShadow="{StaticResource DefaultShadow}">
|
||||||
|
<Button
|
||||||
|
Command="{Binding DoCurrentAuth}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center">
|
||||||
|
<customControls:LocalizedLabel LocalId="account-auth-retry"/>
|
||||||
|
</Button>
|
||||||
|
</Border>
|
||||||
|
<customControls:LocalizedLabel HorizontalAlignment="Center" LocalId="account-auth-try-another"/>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Label VerticalAlignment="Center">
|
<customControls:LocalizedLabel VerticalAlignment="Center" LocalId="account-auth-login"/>
|
||||||
Login:
|
|
||||||
</Label>
|
|
||||||
<TextBox Text="{Binding CurrentLogin}" MinWidth="200" />
|
<TextBox Text="{Binding CurrentLogin}" MinWidth="200" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Label HorizontalAlignment="Left" VerticalAlignment="Center">
|
<customControls:LocalizedLabel HorizontalAlignment="Left" VerticalAlignment="Center" LocalId="account-auth-password"/>
|
||||||
Password:
|
|
||||||
</Label>
|
|
||||||
<TextBox PasswordChar="#" MinWidth="200" Text="{Binding CurrentPassword}" />
|
<TextBox PasswordChar="#" MinWidth="200" Text="{Binding CurrentPassword}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Label VerticalAlignment="Center">
|
<customControls:LocalizedLabel VerticalAlignment="Center" LocalId="account-auth-server"/>
|
||||||
Auth server:
|
<Button Command="{Binding OnExpandAuthUrl}" VerticalAlignment="Stretch">
|
||||||
</Label>
|
|
||||||
<Button Command="{Binding ExpandAuthUrlCommand}" VerticalAlignment="Stretch">
|
|
||||||
<Label>+</Label>
|
<Label>+</Label>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -150,18 +172,10 @@
|
|||||||
Command="{Binding DoAuth}"
|
Command="{Binding DoAuth}"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center">
|
HorizontalContentAlignment="Center">
|
||||||
<Label>Auth</Label>
|
<customControls:LocalizedLabel LocalId="account-auth-button"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Border>
|
</Border>
|
||||||
<Border BoxShadow="{StaticResource DefaultShadow}">
|
<Button Command="{Binding OnExpandAuthView}" HorizontalAlignment="Right">
|
||||||
<Button
|
|
||||||
Command="{Binding SaveProfileCommand}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
HorizontalContentAlignment="Center">
|
|
||||||
<Label>Save profile</Label>
|
|
||||||
</Button>
|
|
||||||
</Border>
|
|
||||||
<Button Command="{Binding ExpandAuthViewCommand}" HorizontalAlignment="Right">
|
|
||||||
<Label>
|
<Label>
|
||||||
>
|
>
|
||||||
</Label>
|
</Label>
|
||||||
@@ -174,9 +188,15 @@
|
|||||||
Margin="0,0,0,20"
|
Margin="0,0,0,20"
|
||||||
Path="/Assets/svg/user.svg" />
|
Path="/Assets/svg/user.svg" />
|
||||||
<Label>
|
<Label>
|
||||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
<StackPanel Spacing="15">
|
||||||
<TextBlock>Hello,</TextBlock>
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
||||||
<TextBlock Text="{Binding CurrentLogin}" />
|
<customControls:LocalizedLabel LocalId="account-auth-hello"/>
|
||||||
|
<TextBlock Text="{Binding Credentials.Value.Login}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
||||||
|
<customControls:LocalizedLabel LocalId="account-auth-current-server"/>
|
||||||
|
<TextBlock Text="{Binding AuthServerName}" />
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Label>
|
</Label>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
@@ -186,12 +206,14 @@
|
|||||||
Spacing="5">
|
Spacing="5">
|
||||||
<Border BoxShadow="{StaticResource DefaultShadow}">
|
<Border BoxShadow="{StaticResource DefaultShadow}">
|
||||||
<Button Command="{Binding Logout}">
|
<Button Command="{Binding Logout}">
|
||||||
<Label>Logout</Label>
|
<customControls:LocalizedLabel LocalId="account-auth-logout"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Border>
|
</Border>
|
||||||
<Border BoxShadow="{StaticResource DefaultShadow}">
|
<Border BoxShadow="{StaticResource DefaultShadow}"
|
||||||
<Button Command="{Binding SaveProfileCommand}">
|
IsVisible="{Binding CurrentPassword,
|
||||||
<Label>Save profile</Label>
|
Converter={x:Static converters:TypeConverters.StringIsNotEmpty}}">
|
||||||
|
<Button Command="{Binding OnSaveProfile}">
|
||||||
|
<customControls:LocalizedLabel LocalId="account-auth-save"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using AccountInfoViewModel = Nebula.Launcher.ViewModels.Pages.AccountInfoViewModel;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views.Pages;
|
namespace Nebula.Launcher.Views.Pages;
|
||||||
|
|
||||||
@@ -9,10 +8,4 @@ public partial class AccountInfoView : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountInfoView(AccountInfoViewModel viewModel)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
|
xmlns:pages="clr-namespace:Nebula.Launcher.ViewModels.Pages"
|
||||||
|
xmlns:customControls="clr-namespace:Nebula.Launcher.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Nebula.Launcher.Views.Pages.ConfigurationView"
|
x:Class="Nebula.Launcher.Views.Pages.ConfigurationView"
|
||||||
x:DataType="pages:ConfigurationViewModel">
|
x:DataType="pages:ConfigurationViewModel">
|
||||||
@@ -10,31 +11,67 @@
|
|||||||
<pages:ConfigurationViewModel />
|
<pages:ConfigurationViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Panel>
|
<ScrollViewer>
|
||||||
<Border
|
<StackPanel Spacing="10" Margin="10">
|
||||||
VerticalAlignment="Top"
|
<ItemsControl
|
||||||
Margin="5" Padding="5,2,5,2">
|
ItemsSource="{Binding ConfigurationVerbose}"
|
||||||
<Border.Background>
|
Padding="0" >
|
||||||
<LinearGradientBrush EndPoint="50%,100%" StartPoint="50%,0%">
|
<ItemsControl.ItemsPanel>
|
||||||
<GradientStop Color="#222222" Offset="0.0" />
|
<ItemsPanelTemplate>
|
||||||
<GradientStop Color="#292222" Offset="1.0" />
|
<StackPanel Spacing="10" />
|
||||||
</LinearGradientBrush>
|
</ItemsPanelTemplate>
|
||||||
</Border.Background>
|
</ItemsControl.ItemsPanel>
|
||||||
<ScrollViewer >
|
</ItemsControl>
|
||||||
<StackPanel>
|
|
||||||
<ItemsControl
|
<WrapPanel Orientation="Horizontal">
|
||||||
ItemsSource="{Binding ConfigurationVerbose}"
|
<Button
|
||||||
Padding="0" />
|
Classes="ConfigBorder"
|
||||||
</StackPanel>
|
VerticalAlignment="Bottom"
|
||||||
</ScrollViewer>
|
HorizontalAlignment="Stretch"
|
||||||
</Border>
|
Padding="5"
|
||||||
<Button
|
Margin="5"
|
||||||
VerticalAlignment="Bottom"
|
Command="{Binding InvokeUpdateConfiguration}">
|
||||||
HorizontalAlignment="Stretch"
|
<customControls:LocalizedLabel LocalId="config-save"/>
|
||||||
Padding="5"
|
</Button>
|
||||||
Margin="5"
|
<Button
|
||||||
Command="{Binding InvokeUpdateConfiguration}"
|
Classes="ConfigBorder"
|
||||||
>Save
|
VerticalAlignment="Bottom"
|
||||||
</Button>
|
HorizontalAlignment="Stretch"
|
||||||
</Panel>
|
Padding="5"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding ResetConfig}">
|
||||||
|
<customControls:LocalizedLabel LocalId="config-reset"/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Classes="ConfigBorder"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding OpenDataFolder}">
|
||||||
|
<customControls:LocalizedLabel LocalId="config-open-data"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Classes="ConfigBorder"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding ExportLogs}">
|
||||||
|
<customControls:LocalizedLabel LocalId="config-export-logs"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Classes="ConfigBorder"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Padding="5"
|
||||||
|
Margin="5"
|
||||||
|
Command="{Binding RemoveAllContent}">
|
||||||
|
<customControls:LocalizedLabel LocalId="config-remove-content-all"/>
|
||||||
|
</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views.Pages;
|
namespace Nebula.Launcher.Views.Pages;
|
||||||
|
|
||||||
@@ -11,10 +8,4 @@ public partial class ConfigurationView : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConfigurationView(ConfigurationViewModel viewModel)
|
|
||||||
: this()
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user