Files
NebulaLauncher/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs

582 lines
17 KiB
C#
Raw Normal View History

2025-01-10 12:28:29 +03:00
using System;
2025-01-11 20:39:58 +03:00
using System.Collections.Generic;
2025-01-10 12:28:29 +03:00
using System.Collections.ObjectModel;
2025-01-12 15:15:01 +03:00
using System.Diagnostics;
2025-01-11 20:39:58 +03:00
using System.Diagnostics.CodeAnalysis;
2025-01-12 15:15:01 +03:00
using System.IO;
2025-01-11 20:39:58 +03:00
using System.Linq;
using System.Threading;
2025-01-11 20:39:58 +03:00
using System.Threading.Tasks;
2025-01-10 12:28:29 +03:00
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.Models;
2025-01-14 22:10:16 +03:00
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
2025-01-10 12:28:29 +03:00
using Nebula.Launcher.Views.Pages;
using Nebula.Shared.FileApis;
2025-01-10 12:28:29 +03:00
using Nebula.Shared.Models;
2025-01-11 20:39:58 +03:00
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
2025-07-02 21:32:51 +03:00
using Nebula.Shared.ViewHelper;
2025-01-10 12:28:29 +03:00
2025-01-14 22:10:16 +03:00
namespace Nebula.Launcher.ViewModels.Pages;
2025-01-10 12:28:29 +03:00
[ViewModelRegister(typeof(ContentBrowserView))]
2025-01-14 22:10:16 +03:00
[ConstructGenerator]
public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHolder
2025-01-10 12:28:29 +03:00
{
[ObservableProperty] private IContentEntry _currentEntry;
2025-01-14 22:10:16 +03:00
[ObservableProperty] private string _serverText = "";
[ObservableProperty] private string _searchText = "";
2025-01-14 22:10:16 +03:00
[GenerateProperty] private ContentService ContentService { get; } = default!;
[GenerateProperty] private FileService FileService { get; } = default!;
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
[GenerateProperty] private CancellationService CancellationService { get; set; } = default!;
2025-01-22 21:06:05 +03:00
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
2025-01-14 22:10:16 +03:00
2025-01-11 20:39:58 +03:00
public void OnBackEnter()
2025-01-11 20:39:58 +03:00
{
2025-06-15 13:48:56 +03:00
if (CurrentEntry.Parent is null)
{
SetHubRoot();
return;
}
CurrentEntry.Parent?.GoCurrent();
}
2025-01-12 15:15:01 +03:00
public void OnUnpack()
{
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);
2025-01-14 22:10:16 +03:00
Task.Run(() =>
{
ContentService.Unpack(serverEntry.FileApi, myTempDir, loading.CreateLoadingContext());
loading.Dispose();
});
2025-06-19 21:12:42 +03:00
ExplorerHelper.OpenFolder(tmpDir);
}
2025-01-14 22:10:16 +03:00
public void OnGoEnter()
{
if (string.IsNullOrWhiteSpace(ServerText))
{
SetHubRoot();
SearchText = string.Empty;
return;
}
2025-01-12 15:15:01 +03:00
try
{
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
cur.Init(this, ServerText.ToRobustUrl());
var curContent = cur.Go(new ContentPath(SearchText), CancellationService.Token);
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();
2025-01-11 20:39:58 +03:00
}
}
partial void OnCurrentEntryChanged(IContentEntry value)
{
SearchText = value.FullPath.ToString();
if (value.GetRoot() is ServerFolderContentEntry serverEntry)
{
ServerText = serverEntry.ServerUrl.ToString();
}
2025-03-12 14:51:47 +03:00
}
2025-01-14 22:10:16 +03:00
protected override void InitialiseInDesignMode()
2025-01-10 12:28:29 +03:00
{
var root = ViewHelperService.GetViewModel<FolderContentEntry>();
root.Init(this);
var child = root.AddFolder("Biba");
child.AddFolder("Boba");
child.AddFolder("Buba");
CurrentEntry = root;
2025-01-11 20:39:58 +03:00
}
2025-01-14 22:10:16 +03:00
protected override void Initialise()
2025-01-11 20:39:58 +03:00
{
SetHubRoot();
2025-01-10 12:28:29 +03:00
}
public void SetHubRoot()
2025-01-10 12:28:29 +03:00
{
2025-06-15 13:48:56 +03:00
ServerText = string.Empty;
SearchText = string.Empty;
var root = ViewHelperService.GetViewModel<ServerListContentEntry>();
root.InitHubList(this);
CurrentEntry = root;
2025-01-10 12:28:29 +03:00
}
public void Go(RobustUrl url, ContentPath path)
2025-01-11 20:39:58 +03:00
{
ServerText = url.ToString();
SearchText = path.ToString();
OnGoEnter();
2025-01-11 20:39:58 +03:00
}
}
2025-01-10 12:28:29 +03:00
public interface IContentHolder
{
public IContentEntry CurrentEntry { get; set; }
}
public interface IContentEntry
{
public IContentHolder Holder { get; }
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, CancellationToken cancellationToken);
public void GoCurrent()
2025-01-12 22:49:32 +03:00
{
var entry = Go(ContentPath.Empty, CancellationToken.None);
if(entry is not null) Holder.CurrentEntry = entry;
}
public IContentEntry GetRoot()
{
if (Parent is null) return this;
return Parent.GetRoot();
2025-01-12 22:49:32 +03:00
}
}
2025-01-12 22:49:32 +03:00
2025-01-14 22:10:16 +03:00
public sealed class LazyContentEntry : IContentEntry
{
public IContentHolder Holder { get; set; }
public IContentEntry? Parent { get; set; }
public string? Name { get; }
public string IconPath { get; }
2025-01-14 22:10:16 +03:00
private readonly IContentEntry _lazyEntry;
private readonly Action _lazyEntryInit;
2025-01-14 22:10:16 +03:00
public LazyContentEntry (IContentHolder holder,string name, IContentEntry entry, Action lazyEntryInit)
{
Holder = holder;
Name = name;
IconPath = entry.IconPath;
_lazyEntry = entry;
_lazyEntryInit = lazyEntryInit;
}
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
{
_lazyEntryInit?.Invoke();
return _lazyEntry;
}
}
2025-01-14 22:10:16 +03:00
public sealed class ExtContentExecutor
{
public ServerFolderContentEntry _root;
private DecompilerService _decompilerService;
2025-01-14 22:10:16 +03:00
public ExtContentExecutor(ServerFolderContentEntry root, DecompilerService decompilerService)
{
_root = root;
_decompilerService = decompilerService;
}
public bool TryExecute(RobustManifestItem manifestItem, CancellationToken cancellationToken)
{
var ext = Path.GetExtension(manifestItem.Path);
if (ext == ".dll")
2025-01-11 20:39:58 +03:00
{
_decompilerService.OpenServerDecompiler(_root.ServerUrl, cancellationToken);
return true;
2025-01-11 20:39:58 +03:00
}
return false;
2025-01-11 20:39:58 +03:00
}
}
2025-01-11 20:39:58 +03:00
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!;
2025-01-14 22:10:16 +03:00
public void Init(IContentHolder holder, RobustManifestItem manifestItem, HashApi api, ExtContentExecutor executor)
2025-01-10 12:28:29 +03:00
{
Holder = holder;
Name = new ContentPath(manifestItem.Path).GetName();
_manifestItem = manifestItem;
_hashApi = api;
_extContentExecutor = executor;
2025-01-11 20:39:58 +03:00
}
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
2025-04-20 15:43:57 +03:00
{
if (_extContentExecutor.TryExecute(_manifestItem, cancellationToken))
return null;
2025-04-20 15:43:57 +03:00
var ext = Path.GetExtension(_manifestItem.Path);
try
{
if (!_hashApi.TryOpen(_manifestItem, out var stream))
return null;
2025-04-20 15:43:57 +03:00
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
2025-03-12 14:51:47 +03:00
2025-01-11 20:39:58 +03:00
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
stream.CopyTo(sw);
2025-01-11 20:39:58 +03:00
sw.Dispose();
stream.Dispose();
2025-01-14 22:10:16 +03:00
var startInfo = new ProcessStartInfo(myTempFile)
{
UseShellExecute = true
};
2025-01-11 20:39:58 +03:00
Process.Start(startInfo);
}
catch (Exception e)
{
_extContentExecutor._root.PopupService.Popup(e);
}
return null;
2025-01-11 20:39:58 +03:00
}
}
2025-01-11 20:39:58 +03:00
[ViewModelRegister(typeof(FileContentEntryView), false), ConstructGenerator]
public sealed partial class FolderContentEntry : BaseFolderContentEntry
{
[GenerateProperty, DesignConstruct] public override ViewHelperService ViewHelperService { get; } = default!;
public FolderContentEntry AddFolder(string folderName)
2025-01-11 20:39:58 +03:00
{
var folder = ViewHelperService.GetViewModel<FolderContentEntry>();
folder.Init(Holder, folderName);
return AddChild(folder);
2025-01-11 20:39:58 +03:00
}
2025-01-29 12:32:42 +03:00
protected override void InitialiseInDesignMode() { }
protected override void Initialise() { }
2025-01-10 12:28:29 +03:00
}
[ViewModelRegister(typeof(FileContentEntryView), false), ConstructGenerator]
public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
2025-01-10 12:28:29 +03:00
{
[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!;
2025-03-16 09:44:54 +03:00
public RobustUrl ServerUrl { get; private set; }
2025-01-14 22:10:16 +03:00
public HashApi FileApi { get; private set; } = default!;
private ExtContentExecutor _contentExecutor = default!;
public void Init(IContentHolder holder, RobustUrl serverUrl)
2025-01-14 22:10:16 +03:00
{
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();
});
2025-01-11 20:39:58 +03:00
}
public ManifestContentEntry CreateContent(ContentPath path, RobustManifestItem manifestItem)
2025-01-11 20:39:58 +03:00
{
var pathDir = path.GetDirectory();
BaseFolderContentEntry parent = this;
while (pathDir.TryNext(out var dirPart))
2025-01-11 20:39:58 +03:00
{
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();
2025-01-11 20:39:58 +03:00
}
var manifestContent = new ManifestContentEntry();
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor);
parent.AddChild(manifestContent);
return manifestContent;
2025-01-11 20:39:58 +03:00
}
protected override void InitialiseInDesignMode() { }
protected override void Initialise() { }
}
2025-01-11 20:39:58 +03:00
[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)
2025-01-11 20:39:58 +03:00
{
base.Init(holder);
var servers = ConfigurationService.GetConfigValue(LauncherConVar.Hub)!;
foreach (var server in servers)
2025-01-11 20:39:58 +03:00
{
var serverFolder = ServiceProvider.GetService<ServerListContentEntry>()!;
var serverLazy = new LazyContentEntry(Holder, server.Name , serverFolder, () => serverFolder.InitServerList(Holder, server));
AddChild(serverLazy);
2025-01-11 20:39:58 +03:00
}
}
public async void InitServerList(IContentHolder holder, ServerHubRecord hubRecord)
2025-01-11 20:39:58 +03:00
{
base.Init(holder, hubRecord.Name);
2025-01-12 22:49:32 +03:00
IsLoading = true;
var servers =
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(hubRecord.MainUrl), CancellationToken.None);
2025-01-14 22:10:16 +03:00
foreach (var server in servers)
2025-01-11 20:39:58 +03:00
{
var serverFolder = ServiceProvider.GetService<ServerFolderContentEntry>()!;
var serverLazy = new LazyContentEntry(Holder, server.StatusData.Name , serverFolder, () => serverFolder.Init(Holder, server.Address.ToRobustUrl()));
AddChild(serverLazy);
2025-01-11 20:39:58 +03:00
}
IsLoading = true;
2025-01-11 20:39:58 +03:00
}
protected override void InitialiseInDesignMode()
2025-01-11 20:39:58 +03:00
{
}
protected override void Initialise()
2025-01-11 20:39:58 +03:00
{
}
}
2025-01-11 20:39:58 +03:00
public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry
{
public bool IsLoading { get; set; } = false;
public abstract ViewHelperService ViewHelperService { get; }
public ObservableCollection<IContentEntry> Entries { get; } = [];
2025-01-14 22:10:16 +03:00
private Dictionary<string, IContentEntry> _childs = [];
2025-01-11 20:39:58 +03:00
public string IconPath => "/Assets/svg/folder.svg";
2025-09-08 21:26:12 +03:00
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 string? Name { get; private set; }
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
2025-01-11 20:39:58 +03:00
{
if (path.IsEmpty()) return this;
if (_childs.TryGetValue(path.GetNext(), out var child))
return child.Go(path, cancellationToken);
return null;
}
2025-01-12 22:49:32 +03:00
public void Init(IContentHolder holder, string? name = null)
{
Name = name;
2025-09-08 21:26:12 +03:00
_holder = holder;
}
2025-01-14 22:10:16 +03:00
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);
2025-01-11 20:39:58 +03:00
return child;
2025-01-11 20:39:58 +03:00
}
public bool TryGetChild(string name,[NotNullWhen(true)] out IContentEntry? child)
2025-01-11 20:39:58 +03:00
{
return _childs.TryGetValue(name, out child);
2025-01-11 20:39:58 +03:00
}
2025-01-10 12:28:29 +03:00
}
public struct ContentPath : IEquatable<ContentPath>
2025-01-10 12:28:29 +03:00
{
public static readonly ContentPath Empty = new();
2025-01-14 22:10:16 +03:00
public List<string> Pathes { get; }
2025-01-11 20:39:58 +03:00
public ContentPath()
{
Pathes = [];
}
2025-01-11 20:39:58 +03:00
public ContentPath(List<string> pathes)
{
2025-01-14 22:10:16 +03:00
Pathes = pathes;
2025-01-11 20:39:58 +03:00
}
public ContentPath(string? path)
2025-01-11 20:39:58 +03:00
{
2025-01-12 22:49:32 +03:00
Pathes = string.IsNullOrEmpty(path)
? new List<string>()
: path.Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList();
}
public ContentPath With(string? name)
{
if (name != null) return new ContentPath([..Pathes, name]);
return new ContentPath(Pathes);
2025-01-11 20:39:58 +03:00
}
public ContentPath GetDirectory()
{
2025-01-12 22:49:32 +03:00
if (Pathes.Count == 0)
return this;
2025-01-12 22:49:32 +03:00
var directoryPathes = Pathes.Take(Pathes.Count - 1).ToList();
return new ContentPath(directoryPathes);
2025-01-11 20:39:58 +03:00
}
public string GetName()
{
2025-01-12 22:49:32 +03:00
if (Pathes.Count == 0)
throw new InvalidOperationException("Cannot get the name of the root path.");
2025-01-11 20:39:58 +03:00
return Pathes.Last();
}
2025-01-12 22:49:32 +03:00
public string GetNext()
{
if (Pathes.Count == 0)
throw new InvalidOperationException("No elements left to retrieve from the root.");
var nextName = Pathes[0];
Pathes.RemoveAt(0);
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;
}
2025-01-12 15:15:01 +03:00
public ContentPath Clone()
{
2025-01-12 22:49:32 +03:00
return new ContentPath(new List<string>(Pathes));
2025-01-12 15:15:01 +03:00
}
2025-01-12 22:49:32 +03:00
public string Path => Pathes.Count == 0 ? "/" : string.Join("/", Pathes);
public override string ToString()
{
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();
}
2025-03-16 09:44:54 +03:00
}
public sealed class ContentComparer : IComparer<IContentEntry>
2025-03-16 09:44:54 +03:00
{
public int Compare(IContentEntry? x, IContentEntry? y)
2025-03-16 09:44:54 +03:00
{
if (ReferenceEquals(x, y)) return 0;
if (y is null) return 1;
if (x is null) return -1;
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
2025-03-16 09:44:54 +03:00
}
2025-01-12 22:49:32 +03:00
}