3 Commits

Author SHA1 Message Date
22a6b3157d - add: DotNet runtime download 2025-06-15 13:48:56 +03:00
30a526a746 - remove: Length of penis 2025-06-15 12:26:29 +03:00
df050b9417 - tweak: Big refactoring of hub and file content overview 2025-06-14 22:33:03 +03:00
38 changed files with 1255 additions and 772 deletions

View File

@@ -8,12 +8,14 @@
<entry key="Nebula.Launcher/Assets/Icons.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Assets/Resources.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Assets/Style.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Controls/ServerListView.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/ViewModels/Styles1.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/ExceptionView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Views/FileContentEntryView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Views/MainView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Views/MainWindow.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
<entry key="Nebula.Launcher/Views/Pages/AccountInfoPage.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
@@ -23,6 +25,7 @@
<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/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/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/InfoPopupView.axaml" value="Nebula.Launcher/Nebula.Launcher.csproj" />
@@ -33,6 +36,7 @@
<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/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/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.UpdateResolver/App.axaml" value="Nebula.UpdateResolver/Nebula.UpdateResolver.csproj" />

View File

@@ -1,5 +1,3 @@
using System;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
@@ -8,7 +6,6 @@ using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.MessageBox;
using Nebula.Launcher.ViewModels.ContentView;
using Nebula.Launcher.Views;
using Nebula.Shared;
using Nebula.Shared.Services;
@@ -63,7 +60,6 @@ public class App : Application
services.AddAvaloniaServices();
services.AddServices();
services.AddViews();
services.AddTransient<DecompilerContentView>();
var serviceProvider = services.BuildServiceProvider();

View File

@@ -0,0 +1,20 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Nebula.Launcher.Controls.ServerListView">
<ScrollViewer
Margin="5,0,0,10"
Padding="0,0,10,0">
<StackPanel Margin="0,0,0,30">
<Label x:Name="LoadingLabel" Margin="10" HorizontalAlignment="Center">Loading... Please wait</Label>
<ItemsControl
x:Name="ErrorList"
Margin="10,0,10,0" />
<ItemsControl
x:Name="ServerList"
Padding="0" />
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,104 @@
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;
}
}

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
using Nebula.Launcher.Models;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared.Services;
@@ -22,8 +24,15 @@ public static class LauncherConVar
"https://auth.fallback.spacestation14.com/"
])
]);
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
new ServerHubRecord("WizDen", "https://hub.spacestation14.com/api/servers", null),
new ServerHubRecord("AltHub","https://web.networkgamez.com/api/servers",null)
]);
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", "en-US");
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");
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Text.Json.Serialization;
using Nebula.Launcher.ServerListProviders;
namespace Nebula.Launcher.Models;
public record ListItemTemplate(Type ModelType, string IconKey, string Label);
public record ServerListTabTemplate(IServerListProvider ServerListProvider, string TabName);
public record ServerHubRecord(
[property:JsonPropertyName("name")] string Name,
[property:JsonPropertyName("url")] string MainUrl,
[property:JsonPropertyName("fallback")] string? Fallback);

View File

@@ -42,6 +42,10 @@
<DependentUpon>AddFavoriteView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\ServerListView.axaml.cs">
<DependentUpon>ServerListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<Target Name="BuildCheck" AfterTargets="AfterBuild">
@@ -69,4 +73,8 @@
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Controls\ServerListView.axaml" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ServerListProviders;
[ServiceRegister(), ConstructGenerator]
public sealed partial class FavoriteServerListProvider : IServerListProvider, IServerListDirtyInvoker
{
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
[GenerateProperty] private RestService RestService { get; }
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
private List<IFilterConsumer> _serverLists = [];
public bool IsLoaded { get; private set; }
public Action? OnLoaded { get; set; }
public Action? Dirty { get; set; }
public IEnumerable<IFilterConsumer> GetServers()
{
return _serverLists;
}
public IEnumerable<Exception> GetErrors()
{
return [];
}
public void LoadServerList()
{
IsLoaded = false;
_serverLists.Clear();
var servers = GetFavoriteEntries();
_serverLists.AddRange(
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)
{
var servers = GetFavoriteEntries();
servers.Add(robustUrl.ToString());
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
Dirty?.Invoke();
}
public void RemoveFavorite(ServerEntryModelView entryModelView)
{
var servers = GetFavoriteEntries();
servers.Remove(entryModelView.Address.ToString());
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
Dirty?.Invoke();
}
private List<string> GetFavoriteEntries()
{
return ConfigurationService.GetConfigValue(LauncherConVar.Favorites)?.ToList() ?? [];
}
private void Initialise(){}
private void InitialiseInDesignMode(){}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ServerListProviders;
[ServiceRegister(null, false), ConstructGenerator]
public sealed partial class HubServerListProvider : IServerListProvider
{
[GenerateProperty] private RestService RestService { 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 readonly List<ServerEntryModelView> _servers = [];
private readonly List<Exception> _errors = [];
public HubServerListProvider With(string hubUrl)
{
HubUrl = hubUrl;
return this;
}
public IEnumerable<IFilterConsumer> GetServers()
{
return _servers;
}
public IEnumerable<Exception> GetErrors()
{
return _errors;
}
public async void LoadServerList()
{
if (_cts != null)
{
await _cts.CancelAsync();
_cts = null;
}
_servers.Clear();
_errors.Clear();
IsLoaded = false;
_cts = new CancellationTokenSource();
try
{
var servers =
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(HubUrl), _cts.Token);
servers.Sort(new ServerComparer());
if(_cts.Token.IsCancellationRequested) return;
_servers.AddRange(
servers.Select(h=>
ServerViewContainer.Get(h.Address.ToRobustUrl(), h.StatusData)
)
);
}
catch (Exception e)
{
_errors.Add(new Exception($"Some error while loading server list from {HubUrl}. See inner exception", e));
}
IsLoaded = true;
OnLoaded?.Invoke();
}
private void Initialise(){}
private void InitialiseInDesignMode(){}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using Nebula.Launcher.ViewModels;
namespace Nebula.Launcher.ServerListProviders;
public interface IServerListProvider
{
public bool IsLoaded { get; }
public Action? OnLoaded { get; set; }
public IEnumerable<IFilterConsumer> GetServers();
public IEnumerable<Exception> GetErrors();
public void LoadServerList();
}
public interface IServerListDirtyInvoker
{
public Action? Dirty { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using Nebula.Launcher.Controls;
using Nebula.Launcher.ViewModels;
namespace Nebula.Launcher.ServerListProviders;
public sealed class TestServerList : IServerListProvider
{
public bool IsLoaded => true;
public Action? OnLoaded { get; set; }
public IEnumerable<IFilterConsumer> GetServers()
{
return [new ServerEntryModelView(),new ServerEntryModelView()];
}
public IEnumerable<Exception> GetErrors()
{
return [new Exception("On no!")];
}
public void LoadServerList()
{
}
}

View File

@@ -1,14 +0,0 @@
using System;
using System.IO;
using Nebula.Launcher.ViewModels.Pages;
namespace Nebula.Launcher.ViewModels.ContentView;
public abstract class ContentViewBase : ViewModelBase, IDisposable
{
public virtual void InitialiseWithData(ContentPath path, Stream stream, ContentEntry contentEntry)
{
}
public virtual void Dispose()
{
}
}

View File

@@ -1,26 +0,0 @@
using System.IO;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ViewModels.ContentView;
[ConstructGenerator]
public sealed partial class DecompilerContentView: ContentViewBase
{
[GenerateProperty] private DecompilerService decompilerService {get;}
public override void InitialiseWithData(ContentPath path, Stream stream, ContentEntry contentEntry)
{
base.InitialiseWithData(path, stream, contentEntry);
decompilerService.OpenServerDecompiler(contentEntry.ServerName.ToRobustUrl());
}
protected override void Initialise()
{
}
protected override void InitialiseInDesignMode()
{
}
}

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Nebula.Launcher.Models;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.ViewModels.Popup;
@@ -22,11 +23,10 @@ public partial class MainViewModel : ViewModelBase
{
private readonly List<ListItemTemplate> _templates =
[
new ListItemTemplate(typeof(AccountInfoViewModel), "user", "Account", null),
new ListItemTemplate(typeof(ServerListViewModel), "file", "Servers", false),
new ListItemTemplate(typeof(ServerListViewModel), "star", "Favorites", true),
new ListItemTemplate(typeof(ContentBrowserViewModel), "folder", "Content", null),
new ListItemTemplate(typeof(ConfigurationViewModel), "settings", "Settings", null)
new ListItemTemplate(typeof(AccountInfoViewModel), "user", "Account"),
new ListItemTemplate(typeof(ServerOverviewModel), "file", "Servers"),
new ListItemTemplate(typeof(ContentBrowserViewModel), "folder", "Content"),
new ListItemTemplate(typeof(ConfigurationViewModel), "settings", "Settings")
];
private readonly List<PopupViewModelBase> _viewQueue = new();
@@ -93,19 +93,19 @@ public partial class MainViewModel : ViewModelBase
if (!ViewHelperService.TryGetViewModel(value.ModelType, out var vmb)) return;
OpenPage(vmb, value.args, false);
OpenPage(vmb, false);
}
public T RequirePage<T>() where T : ViewModelBase, IViewModelPage
public T RequirePage<T>() where T : ViewModelBase
{
if (CurrentPage is T vam) return vam;
var page = ViewHelperService.GetViewModel<T>();
OpenPage(page, null);
OpenPage(page);
return page;
}
private void OpenPage(ViewModelBase obj, object? args, bool selectListView = true)
private void OpenPage(ViewModelBase obj, bool selectListView = true)
{
var tabItems = Items.Where(vm => vm.ModelType == obj.GetType());
@@ -118,11 +118,6 @@ public partial class MainViewModel : ViewModelBase
}
}
if (obj is IViewModelPage page)
{
page.OnPageOpen(args);
}
CurrentPage = obj;
}

View File

@@ -19,7 +19,7 @@ namespace Nebula.Launcher.ViewModels.Pages;
[ViewModelRegister(typeof(AccountInfoView))]
[ConstructGenerator]
public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
public partial class AccountInfoViewModel : ViewModelBase
{
[ObservableProperty] private bool _authMenuExpand;
@@ -255,11 +255,8 @@ public partial class AccountInfoViewModel : ViewModelBase, IViewModelPage
ConfigurationService.SetConfigValue(LauncherConVar.AuthProfiles,
Accounts.ToArray());
}
public void OnPageOpen(object? args)
{
}
}
public sealed record ProfileAuthCredentials(
string Login,
string Password,

View File

@@ -119,7 +119,7 @@ public partial class StringConfigurationViewModel : ViewModelBase , IConfigurati
private string _oldText = string.Empty;
public ConfigContext<string> Context { get; set; }
public required ConfigContext<string> Context { get; set; }
public void InitializeConfig()
{
ConfigName = Context.ConVar.Name;

View File

@@ -1,397 +1,469 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.Models;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.ContentView;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Launcher.Views.Pages;
using Nebula.Shared.FileApis;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ViewModels.Pages;
[ViewModelRegister(typeof(ContentBrowserView))]
[ConstructGenerator]
public sealed partial class ContentBrowserViewModel : ViewModelBase , IViewModelPage
public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHolder
{
private readonly List<ContentEntry> _root = new();
private readonly List<string> _history = new();
[ObservableProperty] private string _message = "";
[ObservableProperty] private string _searchText = "";
private ContentEntry? _selectedEntry;
private ILogger _logger;
[ObservableProperty] private IContentEntry _currentEntry;
[ObservableProperty] private string _serverText = "";
[ObservableProperty] private ContentViewBase? _contentView;
public bool IsCustomContenView => ContentView != null;
[ObservableProperty] private string _searchText = "";
[GenerateProperty] private ContentService ContentService { get; } = default!;
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
[GenerateProperty] private FileService FileService { get; } = default!;
[GenerateProperty] private DebugService DebugService { get; } = default!;
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
[GenerateProperty] private HubService HubService { get; } = default!;
[GenerateProperty] private IServiceProvider ServiceProvider {get;}
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
public ObservableCollection<ContentEntry> Entries { get; } = new();
private Dictionary<string, Type> _contentContainers = new();
public ContentEntry? SelectedEntry
{
get => _selectedEntry;
set
{
var oldSearchText = SearchText;
SearchText = value?.GetPath().ToString() ?? "";
ContentView = null;
Entries.Clear();
_selectedEntry = value;
if (value == null) return;
if(value.TryOpen(out var stream, out var item)){
var ext = Path.GetExtension(item.Value.Path);
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
if(TryGetContentViewer(ext, out var contentViewBase)){
_logger.Debug($"Opening custom context:{item.Value.Path}");
contentViewBase.InitialiseWithData(value.GetPath(), stream, value);
stream.Dispose();
ContentView = contentViewBase;
return;
}
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
stream.CopyTo(sw);
sw.Dispose();
stream.Dispose();
var startInfo = new ProcessStartInfo(myTempFile)
{
UseShellExecute = true
};
_logger.Log("Opening " + myTempFile);
Process.Start(startInfo);
return;
}
if(SearchText.Length > oldSearchText.Length)
AppendHistory(oldSearchText);
foreach (var entryCh in value.Childs) Entries.Add(entryCh);
}
}
private bool TryGetContentViewer(string type,[NotNullWhen(true)] out ContentViewBase? contentViewBase){
contentViewBase = null;
if(!_contentContainers.TryGetValue(type, out var contentViewType) ||
!contentViewType.IsAssignableTo(typeof(ContentViewBase)))
return false;
contentViewBase = (ContentViewBase)ServiceProvider.GetService(contentViewType)!;
return true;
}
protected override void InitialiseInDesignMode()
{
var a = new ContentEntry(this, "A:", "A", "", default!);
var b = new ContentEntry(this, "B", "B", "", default!);
a.TryAddChild(b);
Entries.Add(a);
}
protected override void Initialise()
{
_logger = DebugService.GetLogger(this);
FillRoot(HubService.ServerList);
HubService.HubServerChangedEventArgs += HubServerChangedEventArgs;
HubService.HubServerLoaded += GoHome;
if (!HubService.IsUpdating) GoHome();
_contentContainers.Add(".dll",typeof(DecompilerContentView));
}
private void GoHome()
{
SelectedEntry = null;
foreach (var entry in _root) Entries.Add(entry);
}
private void HubServerChangedEventArgs(HubServerChangedEventArgs obj)
{
if (obj.Action == HubServerChangeAction.Clear) _root.Clear();
if (obj.Action == HubServerChangeAction.Add) FillRoot(obj.Items);
}
private void FillRoot(IEnumerable<ServerHubInfo> infos)
{
foreach (var info in infos)
_root.Add(new ContentEntry(this, info.StatusData.Name, info.Address, info.Address, default!));
}
public void Go(string server, ContentPath path)
{
ServerText = server;
Go(path);
}
public async void Go(ContentPath path)
{
if (path.Pathes.Count > 0 && (path.Pathes[0].StartsWith("ss14://") || path.Pathes[0].StartsWith("ss14s://")))
{
ServerText = path.Pathes[0];
path = new ContentPath("");
}
if (string.IsNullOrEmpty(ServerText))
{
SearchText = "";
GoHome();
return;
}
if (ServerText != SelectedEntry?.ServerName) SelectedEntry = await CreateEntry(ServerText);
_logger.Debug("Going to:" + path.Path);
var oriPath = path.Clone();
try
{
if (SelectedEntry == null || !SelectedEntry.GetRoot().TryGetEntry(path, out var centry))
throw new Exception("Not found! " + oriPath.Path);
SelectedEntry = centry;
}
catch (Exception e)
{
PopupService.Popup(e);
}
}
public void OnBackEnter()
{
Go(new ContentPath(GetHistory()));
}
public void OnGoEnter()
{
Go(new ContentPath(SearchText));
if (CurrentEntry.Parent is null)
{
SetHubRoot();
return;
}
CurrentEntry.Parent?.GoCurrent();
}
public void OnUnpack()
{
if (SelectedEntry == null) return;
if(CurrentEntry is not ServerFolderContentEntry serverEntry)
return;
var myTempDir = FileService.EnsureTempDir(out var tmpDir);
var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
loading.LoadingName = "Unpacking entry";
PopupService.Popup(loading);
Task.Run(() => ContentService.Unpack(SelectedEntry.FileApi, myTempDir, loading));
Task.Run(() => ContentService.Unpack(serverEntry.FileApi, myTempDir, loading));
var startInfo = new ProcessStartInfo(){
FileName = "explorer.exe",
Arguments = tmpDir,
};
_logger.Log("Opening " + tmpDir);
Process.Start(startInfo);
}
private async Task<ContentEntry> CreateEntry(string serverUrl)
public void OnGoEnter()
{
var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
loading.LoadingName = "Loading entry";
PopupService.Popup(loading);
var rurl = serverUrl.ToRobustUrl();
var info = await ContentService.GetBuildInfo(rurl, CancellationService.Token);
var hashApi = await ContentService.EnsureItems(info.RobustManifestInfo, loading,
CancellationService.Token);
var rootEntry = new ContentEntry(this, "", "", serverUrl, hashApi);
foreach (var item in hashApi.Manifest.Values)
if (string.IsNullOrWhiteSpace(ServerText))
{
var path = new ContentPath(item.Path);
rootEntry.CreateItem(path, item);
SetHubRoot();
SearchText = string.Empty;
return;
}
loading.Dispose();
return rootEntry;
try
{
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
cur.Init(this, ServerText.ToRobustUrl());
var curContent = cur.Go(new ContentPath(SearchText));
if(curContent == null)
throw new NullReferenceException($"{SearchText} not found in {ServerText}");
CurrentEntry = curContent;
}
catch (Exception e)
{
PopupService.Popup(e);
ServerText = string.Empty;
SearchText = string.Empty;
SetHubRoot();
}
}
private void AppendHistory(string str)
partial void OnCurrentEntryChanged(IContentEntry value)
{
if (_history.Count >= 10) _history.RemoveAt(9);
_history.Insert(0, str);
SearchText = value.FullPath.ToString();
if (value.GetRoot() is ServerFolderContentEntry serverEntry)
{
ServerText = serverEntry.ServerUrl.ToString();
}
}
protected override void InitialiseInDesignMode()
{
var root = ViewHelperService.GetViewModel<FolderContentEntry>();
root.Init(this);
var child = root.AddFolder("Biba");
child.AddFolder("Boba");
child.AddFolder("Buba");
CurrentEntry = root;
}
private string GetHistory()
protected override void Initialise()
{
if (_history.Count == 0) return "";
var h = _history[0];
_history.RemoveAt(0);
return h;
SetHubRoot();
}
public void OnPageOpen(object? args)
public void SetHubRoot()
{
ServerText = string.Empty;
SearchText = string.Empty;
var root = ViewHelperService.GetViewModel<ServerListContentEntry>();
root.InitHubList(this);
CurrentEntry = root;
}
public void Go(RobustUrl url, ContentPath path)
{
ServerText = url.ToString();
SearchText = path.ToString();
OnGoEnter();
}
}
public class ContentEntry
public interface IContentHolder
{
private readonly Dictionary<string, ContentEntry> _childs = new();
public IContentEntry CurrentEntry { get; set; }
}
public interface IContentEntry
{
public IContentHolder Holder { get; }
private readonly ContentBrowserViewModel _viewModel;
public readonly HashApi FileApi;
public readonly RobustManifestItem? Item;
internal ContentEntry(ContentBrowserViewModel viewModel, string name, string pathName, string serverName, HashApi fileApi, RobustManifestItem? item = null)
public IContentEntry? Parent { get; set; }
public string? Name { get; }
public string IconPath { get; }
public ContentPath FullPath => Parent?.FullPath.With(Name) ?? new ContentPath(Name);
public IContentEntry? Go(ContentPath path);
public void GoCurrent()
{
var entry = Go(ContentPath.Empty);
if(entry is not null) Holder.CurrentEntry = entry;
}
public IContentEntry GetRoot()
{
if (Parent is null) return this;
return Parent.GetRoot();
}
}
public sealed class LazyContentEntry : IContentEntry
{
public IContentHolder Holder { get; set; }
public IContentEntry? Parent { get; set; }
public string? Name { get; }
public string IconPath { get; }
private readonly IContentEntry _lazyEntry;
private readonly Action _lazyEntryInit;
public LazyContentEntry (IContentHolder holder,string name, IContentEntry entry, Action lazyEntryInit)
{
Holder = holder;
Name = name;
ServerName = serverName;
PathName = pathName;
_viewModel = viewModel;
FileApi = fileApi;
Item = item;
IconPath = entry.IconPath;
_lazyEntry = entry;
_lazyEntryInit = lazyEntryInit;
}
public bool IsDirectory => Item == null;
public string Name { get; private set; }
public string PathName { get; }
public string ServerName { get; }
public string IconPath { get; set; } = "/Assets/svg/folder.svg";
public ContentEntry? Parent { get; private set; }
public bool IsRoot => Parent == null;
//TODO: Remove LINQ later...
public IReadOnlyList<ContentEntry> Childs => _childs.Values.OrderBy(v => v,new ContentComparer()).ToList();
public bool TryOpen([NotNullWhen(true)] out Stream? stream,[NotNullWhen(true)] out RobustManifestItem? item){
stream = null;
item = null;
if(Item is null || !FileApi.TryOpen(Item.Value, out stream))
return false;
item = Item;
return true;
}
public bool TryGetChild(string name, [NotNullWhen(true)] out ContentEntry? child)
public IContentEntry? Go(ContentPath path)
{
return _childs.TryGetValue(name, out child);
_lazyEntryInit?.Invoke();
return _lazyEntry;
}
}
public sealed class ExtContentExecutor
{
public ServerFolderContentEntry _root;
private DecompilerService _decompilerService;
public ExtContentExecutor(ServerFolderContentEntry root, DecompilerService decompilerService)
{
_root = root;
_decompilerService = decompilerService;
}
public bool TryAddChild(ContentEntry contentEntry)
public bool TryExecute(RobustManifestItem manifestItem)
{
if (_childs.TryAdd(contentEntry.PathName, contentEntry))
var ext = Path.GetExtension(manifestItem.Path);
if (ext == ".dll")
{
contentEntry.Parent = this;
_decompilerService.OpenServerDecompiler(_root.ServerUrl);
return true;
}
return false;
}
}
public ContentPath GetPath()
public sealed partial class ManifestContentEntry : IContentEntry
{
public IContentHolder Holder { get; set; } = default!;
public IContentEntry? Parent { get; set; }
public string? Name { get; set; }
public string IconPath => "/Assets/svg/file.svg";
private RobustManifestItem _manifestItem;
private HashApi _hashApi = default!;
private ExtContentExecutor _extContentExecutor = default!;
public void Init(IContentHolder holder, RobustManifestItem manifestItem, HashApi api, ExtContentExecutor executor)
{
if (Parent != null)
Holder = holder;
Name = new ContentPath(manifestItem.Path).GetName();
_manifestItem = manifestItem;
_hashApi = api;
_extContentExecutor = executor;
}
public IContentEntry? Go(ContentPath path)
{
if (_extContentExecutor.TryExecute(_manifestItem))
return null;
var ext = Path.GetExtension(_manifestItem.Path);
try
{
var path = Parent.GetPath();
path.Pathes.Add(PathName);
return path;
if (!_hashApi.TryOpen(_manifestItem, out var stream))
return null;
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
stream.CopyTo(sw);
sw.Dispose();
stream.Dispose();
var startInfo = new ProcessStartInfo(myTempFile)
{
UseShellExecute = true
};
Process.Start(startInfo);
}
return new ContentPath([PathName]);
}
public ContentEntry GetOrCreateDirectory(ContentPath rootPath)
{
if (rootPath.Pathes.Count == 0) return this;
var fName = rootPath.GetNext();
if (!TryGetChild(fName, out var child))
catch (Exception e)
{
child = new ContentEntry(_viewModel, fName, fName, ServerName, FileApi);
TryAddChild(child);
_extContentExecutor._root.PopupService.Popup(e);
}
return child.GetOrCreateDirectory(rootPath);
}
public ContentEntry GetRoot()
{
if (Parent == null) return this;
return Parent.GetRoot();
}
public ContentEntry CreateItem(ContentPath path, RobustManifestItem item)
{
var dir = path.GetDirectory();
var dirEntry = GetOrCreateDirectory(dir);
var name = path.GetName();
var entry = new ContentEntry(_viewModel, name, name, ServerName, FileApi, item);
dirEntry.TryAddChild(entry);
entry.IconPath = "/Assets/svg/file.svg";
return entry;
}
public bool TryGetEntry(ContentPath path, out ContentEntry? entry)
{
entry = null;
if (path.Pathes.Count == 0)
{
entry = this;
return true;
}
var fName = path.GetNext();
if (!TryGetChild(fName, out var child)) return false;
return child.TryGetEntry(path, out entry);
}
public void OnPathGo()
{
_viewModel.Go(GetPath());
return null;
}
}
public struct ContentPath
[ViewModelRegister(typeof(FileContentEntryView), false), ConstructGenerator]
public sealed partial class FolderContentEntry : BaseFolderContentEntry
{
[GenerateProperty, DesignConstruct] public override ViewHelperService ViewHelperService { get; } = default!;
public FolderContentEntry AddFolder(string folderName)
{
var folder = ViewHelperService.GetViewModel<FolderContentEntry>();
folder.Init(Holder, folderName);
return AddChild(folder);
}
protected override void InitialiseInDesignMode() { }
protected override void Initialise() { }
}
[ViewModelRegister(typeof(FileContentEntryView), false), ConstructGenerator]
public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
{
[GenerateProperty, DesignConstruct] public override ViewHelperService ViewHelperService { get; } = default!;
[GenerateProperty] public ContentService ContentService { get; } = default!;
[GenerateProperty] public CancellationService CancellationService { get; } = default!;
[GenerateProperty] public PopupMessageService PopupService { get; } = default!;
[GenerateProperty] public DecompilerService DecompilerService { get; } = default!;
public RobustUrl ServerUrl { get; private set; }
public HashApi FileApi { get; private set; } = default!;
private ExtContentExecutor _contentExecutor = default!;
public void Init(IContentHolder holder, RobustUrl serverUrl)
{
base.Init(holder);
_contentExecutor = new ExtContentExecutor(this, DecompilerService);
IsLoading = true;
var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
loading.LoadingName = "Loading entry";
PopupService.Popup(loading);
ServerUrl = serverUrl;
Task.Run(async () =>
{
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
FileApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loading,
CancellationService.Token);
foreach (var (path, item) in FileApi.Manifest)
{
CreateContent(new ContentPath(path), item);
}
IsLoading = false;
loading.Dispose();
});
}
public ManifestContentEntry CreateContent(ContentPath path, RobustManifestItem manifestItem)
{
var pathDir = path.GetDirectory();
BaseFolderContentEntry parent = this;
while (pathDir.TryNext(out var dirPart))
{
if (!parent.TryGetChild(dirPart, out var folderContentEntry))
{
folderContentEntry = ViewHelperService.GetViewModel<FolderContentEntry>();
((FolderContentEntry)folderContentEntry).Init(Holder, dirPart);
parent.AddChild(folderContentEntry);
}
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
}
var manifestContent = new ManifestContentEntry();
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor);
parent.AddChild(manifestContent);
return manifestContent;
}
protected override void InitialiseInDesignMode() { }
protected override void Initialise() { }
}
[ViewModelRegister(typeof(FileContentEntryView), false), ConstructGenerator]
public sealed partial class ServerListContentEntry : BaseFolderContentEntry
{
[GenerateProperty, DesignConstruct] public override ViewHelperService ViewHelperService { get; } = default!;
[GenerateProperty] public ConfigurationService ConfigurationService { get; } = default!;
[GenerateProperty] public IServiceProvider ServiceProvider { get; } = default!;
[GenerateProperty] public RestService RestService { get; } = default!;
public void InitHubList(IContentHolder holder)
{
base.Init(holder);
var servers = ConfigurationService.GetConfigValue(LauncherConVar.Hub)!;
foreach (var server in servers)
{
var serverFolder = ServiceProvider.GetService<ServerListContentEntry>()!;
var serverLazy = new LazyContentEntry(Holder, server.Name , serverFolder, () => serverFolder.InitServerList(Holder, server));
AddChild(serverLazy);
}
}
public async void InitServerList(IContentHolder holder, ServerHubRecord hubRecord)
{
base.Init(holder, hubRecord.Name);
IsLoading = true;
var servers =
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(hubRecord.MainUrl), CancellationToken.None);
foreach (var server in servers)
{
var serverFolder = ServiceProvider.GetService<ServerFolderContentEntry>()!;
var serverLazy = new LazyContentEntry(Holder, server.StatusData.Name , serverFolder, () => serverFolder.Init(Holder, server.Address.ToRobustUrl()));
AddChild(serverLazy);
}
IsLoading = true;
}
protected override void InitialiseInDesignMode()
{
}
protected override void Initialise()
{
}
}
public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry
{
public bool IsLoading { get; set; } = false;
public abstract ViewHelperService ViewHelperService { get; }
public ObservableCollection<IContentEntry> Entries { get; } = [];
private Dictionary<string, IContentEntry> _childs = [];
public string IconPath => "/Assets/svg/folder.svg";
public IContentHolder Holder { get; private set; }
public IContentEntry? Parent { get; set; }
public string? Name { get; private set; }
public IContentEntry? Go(ContentPath path)
{
if (path.IsEmpty()) return this;
if (_childs.TryGetValue(path.GetNext(), out var child))
return child.Go(path);
return null;
}
public void Init(IContentHolder holder, string? name = null)
{
Name = name;
Holder = holder;
}
public T AddChild<T>(T child) where T: IContentEntry
{
if(child.Name is null) throw new InvalidOperationException();
child.Parent = this;
_childs.Add(child.Name, child);
Entries.Add(child);
return child;
}
public bool TryGetChild(string name,[NotNullWhen(true)] out IContentEntry? child)
{
return _childs.TryGetValue(name, out child);
}
}
public struct ContentPath : IEquatable<ContentPath>
{
public static readonly ContentPath Empty = new();
public List<string> Pathes { get; }
public ContentPath()
@@ -404,17 +476,23 @@ public struct ContentPath
Pathes = pathes;
}
public ContentPath(string path)
public ContentPath(string? path)
{
Pathes = string.IsNullOrEmpty(path)
? new List<string>()
: path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList();
: path.Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList();
}
public ContentPath With(string? name)
{
if (name != null) return new ContentPath([..Pathes, name]);
return new ContentPath(Pathes);
}
public ContentPath GetDirectory()
{
if (Pathes.Count == 0)
return this; // Root remains root when getting the directory.
return this;
var directoryPathes = Pathes.Take(Pathes.Count - 1).ToList();
return new ContentPath(directoryPathes);
@@ -439,6 +517,14 @@ public struct ContentPath
return string.IsNullOrWhiteSpace(nextName) ? GetNext() : nextName;
}
public bool TryNext([NotNullWhen(true)]out string? part)
{
part = null;
if (Pathes.Count == 0) return false;
part = GetNext();
return true;
}
public ContentPath Clone()
{
return new ContentPath(new List<string>(Pathes));
@@ -450,19 +536,35 @@ public struct ContentPath
{
return Path;
}
public bool IsEmpty()
{
return Pathes.Count == 0;
}
public bool Equals(ContentPath other)
{
return Pathes.Equals(other.Pathes);
}
public override bool Equals(object? obj)
{
return obj is ContentPath other && Equals(other);
}
public override int GetHashCode()
{
return Pathes.GetHashCode();
}
}
public sealed class ContentComparer : IComparer<ContentEntry>
public sealed class ContentComparer : IComparer<IContentEntry>
{
public int Compare(ContentEntry? x, ContentEntry? y)
public int Compare(IContentEntry? x, IContentEntry? y)
{
if (ReferenceEquals(x, y)) return 0;
if (y is null) return 1;
if (x is null) return -1;
var iconComparison = string.Compare(x.IconPath, y.IconPath, StringComparison.Ordinal);
if (iconComparison != 0) return -iconComparison;
var nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal);
if (nameComparison != 0) return nameComparison;
return string.Compare(x.ServerName, y.ServerName, StringComparison.Ordinal);
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
}

View File

@@ -1,6 +0,0 @@
namespace Nebula.Launcher.ViewModels.Pages;
public interface IViewModelPage
{
public void OnPageOpen(object? args);
}

View File

@@ -1,60 +0,0 @@
using System.Collections.ObjectModel;
using System.Linq;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ViewModels.Pages;
public partial class ServerListViewModel
{
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
[GenerateProperty] private RestService RestService { get; }
public ObservableCollection<ServerEntryModelView> FavoriteServers { get; } = [];
private void UpdateFavoriteEntries()
{
foreach(var fav in FavoriteServers.ToList()){
FavoriteServers.Remove(fav);
}
var servers = ConfigurationService.GetConfigValue(LauncherConVar.Favorites);
if (servers is null || servers.Length == 0)
{
return;
}
foreach (var server in servers)
{
var s = ServerViewContainer.Get(server.ToRobustUrl());
s.IsFavorite = true;
FavoriteServers.Add(s);
}
ApplyFilter();
}
public void AddFavorite(ServerEntryModelView entryModelView)
{
entryModelView.IsFavorite = true;
AddFavorite(entryModelView.Address);
}
public void AddFavorite(RobustUrl robustUrl)
{
var servers = (ConfigurationService.GetConfigValue(LauncherConVar.Favorites) ?? []).ToList();
servers.Add(robustUrl.ToString());
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
UpdateFavoriteEntries();
}
public void RemoveFavorite(ServerEntryModelView entryModelView)
{
var servers = (ConfigurationService.GetConfigValue(LauncherConVar.Favorites) ?? []).ToList();
servers.Remove(entryModelView.Address.ToString());
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
entryModelView.IsFavorite = false;
UpdateFavoriteEntries();
}
}

View File

@@ -1,67 +1,74 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.Controls;
using Nebula.Launcher.Models;
using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Launcher.Views.Pages;
using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ViewModels.Pages;
[ViewModelRegister(typeof(ServerListView))]
[ViewModelRegister(typeof(ServerOverviewView))]
[ConstructGenerator]
public partial class ServerListViewModel : ViewModelBase, IViewModelPage
public partial class ServerOverviewModel : ViewModelBase
{
[ObservableProperty] private string _searchText = string.Empty;
[ObservableProperty] private bool _isFavoriteMode;
[ObservableProperty] private bool _isFilterVisible;
[ObservableProperty] private ServerListView _currentServerList = new ServerListView();
public ObservableCollection<ServerEntryModelView> Servers { get; }= new();
public ObservableCollection<Exception> HubErrors { get; } = new();
public readonly ServerFilter CurrentFilter = new ServerFilter();
public Action? OnSearchChange;
[GenerateProperty] private HubService HubService { get; }
[GenerateProperty] private PopupMessageService PopupMessageService { get; }
[GenerateProperty] private CancellationService CancellationService { get; }
[GenerateProperty] private DebugService DebugService { get; }
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
private ServerViewContainer ServerViewContainer { get; set; }
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
[ObservableProperty] private ServerListTabTemplate _selectedItem;
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; set; }
private Dictionary<string, ServerListView> _viewCache = [];
private List<ServerHubInfo> UnsortedServers { get; } = new();
//Design think
protected override void InitialiseInDesignMode()
{
ServerViewContainer = new ServerViewContainer(this, ViewHelperService);
HubErrors.Add(new Exception("UVI"));
Items = new ObservableCollection<ServerListTabTemplate>([
new ServerListTabTemplate(new TestServerList(), "Test think"),
new ServerListTabTemplate(new TestServerList(), "Test think2")
]);
SelectedItem = Items[0];
}
//real think
protected override void Initialise()
{
ServerViewContainer = new ServerViewContainer(this, ViewHelperService);
var tempItems = new List<ServerListTabTemplate>();
foreach (var record in ConfigurationService.GetConfigValue(LauncherConVar.Hub) ?? [])
{
tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService<HubServerListProvider>()!.With(record.MainUrl), record.Name));
}
foreach (var info in HubService.ServerList) UnsortedServers.Add(info);
HubService.HubServerChangedEventArgs += HubServerChangedEventArgs;
HubService.HubServerLoaded += UpdateServerEntries;
HubService.HubServerLoadingError += HubServerLoadingError;
OnSearchChange += OnChangeSearch;
if (!HubService.IsUpdating) UpdateServerEntries();
UpdateFavoriteEntries();
tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, "Favorite"));
Items = new ObservableCollection<ServerListTabTemplate>(tempItems);
SelectedItem = Items[0];
}
public void ApplyFilter()
@@ -80,83 +87,45 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage
CurrentFilter.Tags.Remove(args.Tag);
ApplyFilter();
}
private void HubServerLoadingError(Exception obj)
{
HubErrors.Add(obj);
}
private void UpdateServerEntries()
{
foreach(var fav in Servers.ToList()){
Servers.Remove(fav);
}
Task.Run(() =>
{
UnsortedServers.Sort(new ServerComparer());
foreach (var info in UnsortedServers)
{
var view = ServerViewContainer.Get(info.Address.ToRobustUrl(), info.StatusData);
Servers.Add(view);
}
ApplyFilter();
});
}
private void OnChangeSearch()
{
CurrentFilter.SearchText = SearchText;
ApplyFilter();
}
private void HubServerChangedEventArgs(HubServerChangedEventArgs obj)
{
if (obj.Action == HubServerChangeAction.Add)
foreach (var info in obj.Items)
UnsortedServers.Add(info);
if (obj.Action == HubServerChangeAction.Remove)
foreach (var info in obj.Items)
UnsortedServers.Remove(info);
if (obj.Action == HubServerChangeAction.Clear)
{
UnsortedServers.Clear();
ServerViewContainer.Clear();
UpdateFavoriteEntries();
}
}
public void FilterRequired()
{
IsFilterVisible = !IsFilterVisible;
}
public void AddFavoriteRequired()
{
var p = ViewHelperService.GetViewModel<AddFavoriteViewModel>();
PopupMessageService.Popup(p);
}
public void UpdateRequired()
{
HubErrors.Clear();
Task.Run(HubService.UpdateHub);
CurrentServerList.RefreshFromProvider();
}
public void OnPageOpen(object? args)
partial void OnSelectedItemChanged(ServerListTabTemplate value)
{
if (args is bool fav)
if (!_viewCache.TryGetValue(value.TabName, out var view))
{
IsFavoriteMode = fav;
view = ServerListView.TakeFrom(value.ServerListProvider);
_viewCache[value.TabName] = view;
}
CurrentServerList = view;
}
}
public class ServerViewContainer(
ServerListViewModel serverListViewModel,
ViewHelperService viewHelperService
)
[ServiceRegister]
public class ServerViewContainer
{
private readonly ViewHelperService _viewHelperService;
public ServerViewContainer()
{
_viewHelperService = new ViewHelperService();
}
public ServerViewContainer(ViewHelperService viewHelperService)
{
_viewHelperService = viewHelperService;
}
private readonly Dictionary<string, ServerEntryModelView> _entries = new();
public ICollection<ServerEntryModelView> Items => _entries.Values;
@@ -177,17 +146,11 @@ public class ServerViewContainer(
return entry;
}
entry = viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, serverStatus);
entry = _viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, serverStatus);
_entries.Add(url.ToString(), entry);
}
entry.OnFavoriteToggle += () =>
{
if (entry.IsFavorite) serverListViewModel.RemoveFavorite(entry);
else serverListViewModel.AddFavorite(entry);
};
return entry;
}
}

View File

@@ -26,7 +26,7 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
[GenerateProperty]
public override PopupMessageService PopupMessageService { get; }
[GenerateProperty] private ServerListViewModel ServerListViewModel { get; }
[GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; }
[GenerateProperty] private DebugService DebugService { get; }
public override string Title => "Add to favorite";
public override bool IsClosable => true;
@@ -39,7 +39,6 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
try
{
var uri = IpInput.ToRobustUrl();
ServerListViewModel.AddFavorite(uri);
Dispose();
}
catch (Exception e)

View File

@@ -1,7 +1,9 @@
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;
@@ -10,6 +12,7 @@ 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;
@@ -23,7 +26,7 @@ namespace Nebula.Launcher.ViewModels;
[ViewModelRegister(typeof(ServerEntryView), false)]
[ConstructGenerator]
public partial class ServerEntryModelView : ViewModelBase
public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer
{
[ObservableProperty] private string _description = "Fetching info...";
[ObservableProperty] private bool _expandInfo;
@@ -39,11 +42,11 @@ public partial class ServerEntryModelView : ViewModelBase
[ObservableProperty] private bool _tagDataVisible;
public LogPopupModelView CurrLog;
public Action? OnFavoriteToggle;
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!;
@@ -51,6 +54,8 @@ public partial class ServerEntryModelView : ViewModelBase
[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(
@@ -116,8 +121,14 @@ public partial class ServerEntryModelView : ViewModelBase
CurrLog = ViewHelperService.GetViewModel<LogPopupModelView>();
}
public void ProcessFilter(ServerFilter serverFilter)
public void ProcessFilter(ServerFilter? serverFilter)
{
if (serverFilter == null)
{
IsVisible = true;
return;
}
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
}
@@ -136,9 +147,16 @@ public partial class ServerEntryModelView : ViewModelBase
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()
{
@@ -157,12 +175,16 @@ public partial class ServerEntryModelView : ViewModelBase
public void OpenContentViewer()
{
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address.ToString(), new ContentPath());
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address, ContentPath.Empty);
}
public void ToggleFavorites()
{
OnFavoriteToggle?.Invoke();
IsFavorite = !IsFavorite;
if(IsFavorite)
FavoriteServerListProvider.AddFavorite(this);
else
FavoriteServerListProvider.RemoveFavorite(this);
}
public void RunInstance()
@@ -188,11 +210,11 @@ public partial class ServerEntryModelView : ViewModelBase
await RunnerService.PrepareRun(buildInfo, loadingContext, CancellationService.Token);
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
Process = Process.Start(new ProcessStartInfo
{
FileName = "dotnet.exe",
FileName = await DotnetResolverService.EnsureDotnet(),
Arguments = Path.Join(path, "Nebula.Runner.dll"),
Environment =
{
@@ -372,4 +394,9 @@ public class LinkGoCommand : ICommand
}
public event EventHandler? CanExecuteChanged;
}
public interface IFilterConsumer
{
public void ProcessFilter(ServerFilter? serverFilter);
}

View File

@@ -0,0 +1,52 @@
<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:DataType="pages:FolderContentEntry"
x:Class="Nebula.Launcher.Views.FileContentEntryView">
<Design.DataContext>
<pages:FolderContentEntry/>
</Design.DataContext>
<ScrollViewer
Grid.Column="0"
Grid.ColumnSpan="4"
Grid.Row="1"
Margin="0,0,0,5">
<ItemsControl ItemsSource="{Binding Entries}" Padding="0,0,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type pages:IContentEntry}">
<Button
Command="{Binding GoCurrent}"
CornerRadius="0"
Height="30"
HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="15">
<Border
Background="#00000000"
BorderThickness="0,0,2,0"
CornerRadius="0">
<Svg
Height="15"
Margin="10,0,10,0"
Path="{Binding IconPath}" />
<Border.BorderBrush>
<LinearGradientBrush EndPoint="100%,50%" StartPoint="0%,50%">
<GradientStop Color="#442222" Offset="0.0" />
<GradientStop Color="#222222" Offset="1.0" />
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
<Label>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
</Label>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Nebula.Launcher.ViewModels.Pages;
namespace Nebula.Launcher.Views;
public partial class FileContentEntryView : UserControl
{
// This constructor is used when the view is created by the XAML Previewer
public FileContentEntryView()
{
InitializeComponent();
}
// This constructor is used when the view is created via dependency injection
public FileContentEntryView(FolderContentEntry viewModel)
: this()
{
DataContext = viewModel;
}
}

View File

@@ -10,7 +10,8 @@
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:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models1="clr-namespace:Nebula.Launcher.Models">
<Design.DataContext>
<viewModels:MainViewModel />
</Design.DataContext>
@@ -63,7 +64,7 @@
Padding="0"
SelectedItem="{Binding SelectedListItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ListItemTemplate}">
<DataTemplate DataType="{x:Type models1:ListItemTemplate}">
<StackPanel Orientation="Horizontal" Spacing="19">
<Svg
Height="40"

View File

@@ -61,44 +61,11 @@
<Svg Path="/Assets/svg/next.svg" />
</Button>
<ScrollViewer
Grid.Column="0"
Grid.ColumnSpan="4"
Grid.Row="1"
Margin="0,0,0,5">
<ItemsControl ItemsSource="{Binding Entries}" Padding="0,0,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type pages:ContentEntry}">
<Button
Command="{Binding OnPathGo}"
CornerRadius="0"
Height="30"
HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="15">
<Border
Background="#00000000"
BorderThickness="0,0,2,0"
CornerRadius="0">
<Svg
Height="15"
Margin="10,0,10,0"
Path="{Binding IconPath}" />
<Border.BorderBrush>
<LinearGradientBrush EndPoint="100%,50%" StartPoint="0%,50%">
<GradientStop Color="#442222" Offset="0.0" />
<GradientStop Color="#222222" Offset="1.0" />
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
<Label>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
</Label>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ScrollViewer Grid.Column="0"
Grid.ColumnSpan="4"
Grid.Row="1" Content="{Binding CurrentEntry}">
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -2,41 +2,50 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="Nebula.Launcher.Views.Pages.ServerListView"
x:DataType="pages:ServerListViewModel"
x:Class="Nebula.Launcher.Views.Pages.ServerOverviewView"
x:DataType="pages:ServerOverviewModel"
xmlns="https://github.com/avaloniaui"
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"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Nebula.Launcher.Controls">
xmlns:controls="clr-namespace:Nebula.Launcher.Controls"
xmlns:models="clr-namespace:Nebula.Launcher.Models">
<Design.DataContext>
<pages:ServerListViewModel />
<pages:ServerOverviewModel />
</Design.DataContext>
<Grid
ColumnDefinitions="*"
Margin="0"
RowDefinitions="*,40">
<ScrollViewer
Grid.RowSpan="2"
Margin="5,0,0,10"
Padding="0,0,10,0">
<StackPanel Margin="0,0,0,30">
<ItemsControl ItemsSource="{Binding HubErrors}" Margin="10,0,10,0" />
<ItemsControl
IsVisible="{Binding IsFavoriteMode}"
ItemsSource="{Binding FavoriteServers}"
Padding="0" />
<ItemsControl
IsVisible="{Binding !IsFavoriteMode}"
ItemsSource="{Binding Servers}"
Padding="0" />
</StackPanel>
</ScrollViewer>
RowDefinitions="40,*,40">
<Border Grid.Row="0"
<ListBox
Background="Transparent"
ItemsSource="{Binding Items}"
Padding="0"
SelectedItem="{Binding SelectedItem}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ServerListTabTemplate}">
<StackPanel Orientation="Horizontal" Spacing="19">
<TextBlock Text="{Binding TabName}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border
Child="{Binding CurrentServerList}"
Grid.Row="1"
Grid.RowSpan="2" />
<Border Grid.Row="1"
Background="{StaticResource DefaultGrad}"
Margin="0,0,0,0" CornerRadius="20,20,0,0"
VerticalAlignment="Bottom"
@@ -51,11 +60,11 @@
Background="{StaticResource DefaultGrad}"
BoxShadow="0 2 25 0 #121212"
CornerRadius="0"
Grid.Row="1" />
Grid.Row="2" />
<Grid
ColumnDefinitions="*,40,40,40"
Grid.Row="1"
Grid.Row="2"
Margin="-25,0,0,0"
RowDefinitions="*">
<TextBox
@@ -64,12 +73,6 @@
TextChanged="TextBox_OnTextChanged"
VerticalAlignment="Center"
Watermark="Server name..." />
<Button
Command="{Binding AddFavoriteRequired}"
Grid.Column="1"
Padding="10">
<Svg IsVisible="{Binding IsFavoriteMode}" Path="/Assets/svg/star.svg" />
</Button>
<Button
Command="{Binding FilterRequired}"
Grid.Column="2"

View File

@@ -1,12 +1,12 @@
using Avalonia.Controls;
using ServerListViewModel = Nebula.Launcher.ViewModels.Pages.ServerListViewModel;
using Nebula.Launcher.ViewModels.Pages;
namespace Nebula.Launcher.Views.Pages;
public partial class ServerListView : UserControl
public partial class ServerOverviewView : UserControl
{
// This constructor is used when the view is created by the XAML Previewer
public ServerListView()
public ServerOverviewView()
{
InitializeComponent();
@@ -21,7 +21,7 @@ public partial class ServerListView : UserControl
}
// This constructor is used when the view is created via dependency injection
public ServerListView(ServerListViewModel viewModel)
public ServerOverviewView(ServerOverviewModel viewModel)
: this()
{
DataContext = viewModel;
@@ -29,7 +29,7 @@ public partial class ServerListView : UserControl
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
{
var context = (ServerListViewModel?)DataContext;
var context = (ServerOverviewModel?)DataContext;
context?.OnSearchChange?.Invoke();
}
}

View File

@@ -24,15 +24,15 @@ public static class CurrentConVar
public static readonly ConVar<string> RobustAssemblyName =
ConVarBuilder.Build("engine.robustAssemblyName", "Robust.Client");
public static readonly ConVar<string[][]> Hub = ConVarBuilder.Build<string[][]>("launcher.hub", [
[
"https://hub.spacestation14.com/api/servers",
"https://auth.fallback.spacestation14.com/"
]
]);
public static readonly ConVar<Dictionary<string, EngineVersionInfo>> EngineManifestBackup =
ConVarBuilder.Build<Dictionary<string, EngineVersionInfo>>("engine.manifest.backup");
public static readonly ConVar<ModulesInfo> ModuleManifestBackup =
ConVarBuilder.Build<ModulesInfo>("module.manifest.backup");
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
new(){
{"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip"},
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x86.zip"},
{"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz"}
});
}

View File

@@ -1,3 +0,0 @@
namespace Nebula.Shared.Models;
public record ListItemTemplate(Type ModelType, string IconKey, string Label, object? args);

View File

@@ -12,22 +12,32 @@ public static class ServiceExt
public static void AddServices(this IServiceCollection services, Assembly assembly)
{
foreach (var (type, inference) in GetServicesWithHelpAttribute(assembly))
foreach (var (type, inference, isSingleton) in GetServicesWithHelpAttribute(assembly))
{
Console.WriteLine("[ServiceMng] ADD SERVICE " + type);
if (inference is null)
services.AddSingleton(type);
if (isSingleton)
{
if (inference is null)
services.AddSingleton(type);
else
services.AddSingleton(inference, type);
}
else
services.AddSingleton(inference, type);
{
if (inference is null)
services.AddTransient(type);
else
services.AddTransient(inference, type);
}
}
}
private static IEnumerable<(Type, Type?)> GetServicesWithHelpAttribute(Assembly assembly)
private static IEnumerable<(Type, Type?, bool)> GetServicesWithHelpAttribute(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
var attr = type.GetCustomAttribute<ServiceRegisterAttribute>();
if (attr is not null) yield return (type, attr.Inference);
if (attr is not null) yield return (type, attr.Inference, attr.IsSingleton);
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
namespace Nebula.Shared.Services;
[ServiceRegister]
public class DotnetResolverService(DebugService debugService, ConfigurationService configurationService)
{
private HttpClient _httpClient = new HttpClient();
private static readonly string FullPath = Path.Join(FileService.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
public async Task<string> EnsureDotnet(){
if(!Directory.Exists(FullPath))
await Download();
return ExecutePath;
}
private async Task Download(){
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var ridExt =
DotnetUrlHelper.GetCurrentPlatformDotnetUrl(configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!);
using var response = await _httpClient.GetAsync(ridExt);
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
Directory.CreateDirectory(FullPath);
zipArchive.ExtractToDirectory(FullPath);
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet complete.");
}
}
public static class DotnetUrlHelper
{
public static string GetExtension()
{
if (OperatingSystem.IsWindows()) return ".exe";
return "";
}
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
{
string? rid = GetRuntimeIdentifier();
if (dotnetUrl.TryGetValue(rid, out var url))
{
return url;
}
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
}
public static string GetRuntimeIdentifier()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux-x64";
}
throw new PlatformNotSupportedException("Unsupported operating system");
}
}

View File

@@ -1,90 +0,0 @@
using Nebula.Shared.Models;
using Nebula.Shared.Services.Logging;
namespace Nebula.Shared.Services;
[ServiceRegister]
public class HubService
{
private readonly ConfigurationService _configurationService;
private readonly RestService _restService;
private readonly ILogger _logger;
private readonly List<ServerHubInfo> _serverList = new();
public Action<HubServerChangedEventArgs>? HubServerChangedEventArgs;
public Action? HubServerLoaded;
public Action<Exception>? HubServerLoadingError;
public HubService(ConfigurationService configurationService, RestService restService, DebugService debugService)
{
_configurationService = configurationService;
_restService = restService;
_logger = debugService.GetLogger(this);
UpdateHub();
}
public IReadOnlyList<ServerHubInfo> ServerList => _serverList;
public bool IsUpdating { get; private set; }
public async void UpdateHub()
{
if (IsUpdating) return;
_serverList.Clear();
IsUpdating = true;
HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs([], HubServerChangeAction.Clear));
foreach (var urlStr in _configurationService.GetConfigValue(CurrentConVar.Hub)!)
{
var invoked = false;
Exception? exception = null;
foreach (var uri in urlStr)
{
try
{
var servers =
await _restService.GetAsync<List<ServerHubInfo>>(new Uri(uri), CancellationToken.None);
_serverList.AddRange(servers);
HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(servers, HubServerChangeAction.Add));
invoked = true;
break;
}
catch (Exception e)
{
_logger.Error($"Failed to get servers for {uri}");
_logger.Error(e);
exception = e;
}
}
if(exception is not null && !invoked)
HubServerLoadingError?.Invoke(new Exception("No hub is available.", exception));
}
IsUpdating = false;
HubServerLoaded?.Invoke();
}
}
public class HubServerChangedEventArgs : EventArgs
{
public HubServerChangeAction Action;
public List<ServerHubInfo> Items;
public HubServerChangedEventArgs(List<ServerHubInfo> items, HubServerChangeAction action)
{
Items = items;
Action = action;
}
}
public enum HubServerChangeAction
{
Add,
Remove,
Clear
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Nebula.UpdateResolver.Configuration;
namespace Nebula.UpdateResolver;
public static class DotnetStandalone
{
private static readonly HttpClient HttpClient = new HttpClient();
private static readonly string FullPath = Path.Join(MainWindow.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
public static async Task<Process?> Run(string dllPath)
{
await EnsureDotnet();
return Process.Start(new ProcessStartInfo
{
FileName = ExecutePath,
Arguments = dllPath,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8
});
}
private static async Task EnsureDotnet(){
if(!Directory.Exists(FullPath))
await Download();
}
private static async Task Download(){
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var ridExt =
DotnetUrlHelper.GetCurrentPlatformDotnetUrl(ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!);
using var response = await HttpClient.GetAsync(ridExt);
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
Directory.CreateDirectory(FullPath);
zipArchive.ExtractToDirectory(FullPath);
LogStandalone.Log($"Downloading dotnet complete.");
}
}
public static class DotnetUrlHelper
{
public static string GetExtension()
{
if (OperatingSystem.IsWindows()) return ".exe";
return "";
}
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
{
string? rid = GetRuntimeIdentifier();
if (dotnetUrl.TryGetValue(rid, out var url))
{
return url;
}
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
}
public static string GetRuntimeIdentifier()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux-x64";
}
throw new PlatformNotSupportedException("Unsupported operating system");
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace Nebula.UpdateResolver;
public static class LogStandalone
{
public static Action<string, int>? OnLog;
public static void LogError(Exception e){
Log($"{e.GetType().Name}: "+ e.Message);
Log(e.StackTrace);
if(e.InnerException != null)
LogError(e.InnerException);
}
public static void Log(string? message, int percentage = 0)
{
if(message is null) return;
OnLog?.Invoke(message, percentage);
}
}

View File

@@ -23,6 +23,19 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
LogStandalone.OnLog += (message, percentage) =>
{
ProgressLabel.Content = message;
if (percentage == 0)
PercentLabel.Content = "";
else
PercentLabel.Content = percentage + "%";
var messageOut =
$"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}";
Console.WriteLine(messageOut);
LogStr += messageOut + "\n";
};
Start();
}
@@ -31,11 +44,11 @@ public partial class MainWindow : Window
try
{
var info = await EnsureFiles();
Log("Downloading files...");
LogStandalone.Log("Downloading files...");
foreach (var file in info.ToDelete)
{
Log("Deleting " + file.Path);
LogStandalone.Log("Deleting " + file.Path);
FileApi.Remove(file.Path);
}
@@ -55,30 +68,21 @@ public partial class MainWindow : Window
await using var stream = await response.Content.ReadAsStreamAsync();
FileApi.Save(file.Path, stream);
resolved++;
Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
loadedManifest.Add(file);
Save(loadedManifest);
}
Log("Download finished. Running launcher...");
LogStandalone.Log("Download finished. Running launcher...");
var process = Process.Start(new ProcessStartInfo
{
FileName = "dotnet.exe",
Arguments = Path.Join(FileApi.RootPath, "Nebula.Launcher.dll"),
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8
});
await DotnetStandalone.Run(Path.Join(FileApi.RootPath, "Nebula.Launcher.dll"));
}
catch(HttpRequestException e){
LogError(e);
Log("Проблемы с интернет-соединением...");
LogStandalone.LogError(e);
LogStandalone.Log("Network connection error...");
var logPath = Path.Join(RootPath,"updateResloverError.txt");
File.WriteAllText(logPath, LogStr);
await File.WriteAllTextAsync(logPath, LogStr);
Process.Start(new ProcessStartInfo(){
FileName = "notepad",
Arguments = logPath
@@ -86,9 +90,9 @@ public partial class MainWindow : Window
}
catch (Exception e)
{
LogError(e);
LogStandalone.LogError(e);
var logPath = Path.Join(RootPath,"updateResloverError.txt");
File.WriteAllText(logPath, LogStr);
await File.WriteAllTextAsync(logPath, LogStr);
Process.Start(new ProcessStartInfo(){
FileName = "notepad",
Arguments = logPath
@@ -102,7 +106,7 @@ public partial class MainWindow : Window
private async Task<ManifestEnsureInfo> EnsureFiles()
{
Log("Ensuring launcher manifest...");
LogStandalone.Log("Ensuring launcher manifest...");
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
@@ -110,10 +114,10 @@ public partial class MainWindow : Window
var toDelete = new HashSet<LauncherManifestEntry>();
var filesExist = new HashSet<LauncherManifestEntry>();
Log("Manifest loaded!");
LogStandalone.Log("Manifest loaded!");
if (ConfigurationStandalone.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
{
Log("Delta manifest loaded!");
LogStandalone.Log("Delta manifest loaded!");
foreach (var file in currentManifest.Entries)
{
if (!manifest.Entries.Contains(file))
@@ -133,31 +137,11 @@ public partial class MainWindow : Window
toDownload = manifest.Entries;
}
Log("Saving launcher manifest...");
LogStandalone.Log("Saving launcher manifest...");
return new ManifestEnsureInfo(toDownload, toDelete, filesExist);
}
private void LogError(Exception e){
Log($"{e.GetType().Name}: "+ e.Message);
Log(e.StackTrace);
if(e.InnerException != null)
LogError(e.InnerException);
}
private void Log(string? message, int percentage = 0)
{
if(message is null) return;
ProgressLabel.Content = message;
if (percentage == 0)
PercentLabel.Content = "";
else
PercentLabel.Content = percentage + "%";
var messageOut = $"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}";
Console.WriteLine(messageOut);
LogStr += messageOut + "\n";
}
private void Save(HashSet<LauncherManifestEntry> entries)
{

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Nebula.UpdateResolver.Configuration;
namespace Nebula.UpdateResolver;
@@ -8,4 +9,11 @@ public static class UpdateConVars
ConVarBuilder.Build<string>("update.url","https://durenko.tatar/nebula/manifest/");
public static readonly ConVar<LauncherManifest> CurrentLauncherManifest =
ConVarBuilder.Build<LauncherManifest>("update.manifest");
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
new(){
{"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip"},
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x86.zip"},
{"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz"}
});
}

View File

@@ -1,9 +1,13 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaXamlLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F80462644bd1cc7e0b229dc4f5752b48c01cb67b46ae563b1b5078cc2556b98_003FAvaloniaXamlLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABorder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5fda7f1253ea19edc15f91b94a33322b857f1a9319fbffea8d26e9d304178_003FBorder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AButton_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcc84c38d8785b88e166e6741b6a4c0dfa09eaf6e41eb151b255817e11f27570_003FButton_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACancellationToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2565b9d99fdde488bc7801b84387b2cc864959cfb63212e1ff576fc9c6bb7e_003FCancellationToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F50341a469131fa51e5443b9bd96c4ca1c96bfa709f7f41fd15941ff6296a8dc_003FCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConsole_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1_003FConsole_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F57361bc4e5442f644ff63ec7e745da2eb4bbb6c769d1fb683bab5f6f952b1ab_003FControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADecorator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Feecfe7fcb95caaf3978fdce4ae36e346b34986d1d844b0dce2fcb67e5952c_003FDecorator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADrawingContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7f67edd2b798d6c80b015913cde68b729bfe416b62cf075ea3953ffeff639c_003FDrawingContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFrozenDictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89dff9063ddb01ff8125b579122b88bf4de94526490d77bcbbef7d0ee662a_003FFrozenDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -11,8 +15,11 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializer_002ERead_002EString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27c4858128168eda568c1334d70d5241efb9461e2a3209258a04deee5d9c367_003FJsonSerializer_002ERead_002EString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AListBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3a6cdc26ff4d30986a9a16b6bbc9bb6a7f2657431c82cde5c66dd377cf51e2b_003FListBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AScrollBar_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fda7bce95d5f888176a5f93c8965e402ca33cba794ac7e7aa776363c664488d_003FScrollBar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProviderServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4f1fdec7cbfe4433a7ec3a6d1bd0e54210118_003F04_003Fe2f5322d_003FServiceProviderServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>