using System; 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.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.Utils; using Nebula.Shared.ViewHelper; namespace Nebula.Launcher.ViewModels.Pages; [ViewModelRegister(typeof(ContentBrowserView))] [ConstructGenerator] public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHolder { [ObservableProperty] private IContentEntry _currentEntry; [ObservableProperty] private string _serverText = ""; [ObservableProperty] private string _searchText = ""; [GenerateProperty] private ContentService ContentService { get; } = default!; [GenerateProperty] private FileService FileService { get; } = default!; [GenerateProperty] private PopupMessageService PopupService { get; } = default!; [GenerateProperty] private IServiceProvider ServiceProvider { get; } [GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!; public void OnBackEnter() { if (CurrentEntry.Parent is null) { SetHubRoot(); return; } CurrentEntry.Parent?.GoCurrent(); } public void OnUnpack() { if(CurrentEntry is not ServerFolderContentEntry serverEntry) return; var myTempDir = FileService.EnsureTempDir(out var tmpDir); var loading = ViewHelperService.GetViewModel(); loading.LoadingName = "Unpacking entry"; PopupService.Popup(loading); Task.Run(() => ContentService.Unpack(serverEntry.FileApi, myTempDir, loading)); ExplorerHelper.OpenFolder(tmpDir); } public void OnGoEnter() { if (string.IsNullOrWhiteSpace(ServerText)) { SetHubRoot(); SearchText = string.Empty; return; } try { var cur = ServiceProvider.GetService()!; 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(); } } partial void OnCurrentEntryChanged(IContentEntry value) { SearchText = value.FullPath.ToString(); if (value.GetRoot() is ServerFolderContentEntry serverEntry) { ServerText = serverEntry.ServerUrl.ToString(); } } protected override void InitialiseInDesignMode() { var root = ViewHelperService.GetViewModel(); root.Init(this); var child = root.AddFolder("Biba"); child.AddFolder("Boba"); child.AddFolder("Buba"); CurrentEntry = root; } protected override void Initialise() { SetHubRoot(); } public void SetHubRoot() { ServerText = string.Empty; SearchText = string.Empty; var root = ViewHelperService.GetViewModel(); root.InitHubList(this); CurrentEntry = root; } public void Go(RobustUrl url, ContentPath path) { ServerText = url.ToString(); SearchText = path.ToString(); OnGoEnter(); } } 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); 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; IconPath = entry.IconPath; _lazyEntry = entry; _lazyEntryInit = lazyEntryInit; } public IContentEntry? Go(ContentPath path) { _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 TryExecute(RobustManifestItem manifestItem) { var ext = Path.GetExtension(manifestItem.Path); if (ext == ".dll") { _decompilerService.OpenServerDecompiler(_root.ServerUrl); return true; } return false; } } 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) { 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 { 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); } catch (Exception e) { _extContentExecutor._root.PopupService.Popup(e); } return null; } } [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(); 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(); 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).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()!; 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>(new Uri(hubRecord.MainUrl), CancellationToken.None); foreach (var server in servers) { var serverFolder = ServiceProvider.GetService()!; 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 Entries { get; } = []; private Dictionary _childs = []; public string IconPath => "/Assets/svg/folder.svg"; 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) { 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 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 { public static readonly ContentPath Empty = new(); public List Pathes { get; } public ContentPath() { Pathes = []; } public ContentPath(List pathes) { Pathes = pathes; } public ContentPath(string? path) { Pathes = string.IsNullOrEmpty(path) ? new List() : 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; var directoryPathes = Pathes.Take(Pathes.Count - 1).ToList(); return new ContentPath(directoryPathes); } public string GetName() { if (Pathes.Count == 0) throw new InvalidOperationException("Cannot get the name of the root path."); return Pathes.Last(); } 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; } public ContentPath Clone() { return new ContentPath(new List(Pathes)); } 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(); } } public sealed class ContentComparer : IComparer { 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; return string.Compare(x.Name, y.Name, StringComparison.Ordinal); } }