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.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.Services; using Nebula.Launcher.ViewModels.ContentView; using Nebula.Launcher.ViewModels.Popup; 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 { private readonly List _root = new(); private readonly List _history = new(); [ObservableProperty] private string _message = ""; [ObservableProperty] private string _searchText = ""; private ContentEntry? _selectedEntry; private ILogger _logger; [ObservableProperty] private string _serverText = ""; [ObservableProperty] private ContentViewBase? _contentView; public bool IsCustomContenView => ContentView != null; [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, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!; public ObservableCollection Entries { get; } = new(); private Dictionary _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 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)); } public void OnUnpack() { if (SelectedEntry == null) return; var myTempDir = FileService.EnsureTempDir(out var tmpDir); var loading = ViewHelperService.GetViewModel(); loading.LoadingName = "Unpacking entry"; PopupService.Popup(loading); Task.Run(() => ContentService.Unpack(SelectedEntry.FileApi, myTempDir, loading)); var startInfo = new ProcessStartInfo(){ FileName = "explorer.exe", Arguments = tmpDir, }; _logger.Log("Opening " + tmpDir); Process.Start(startInfo); } private async Task CreateEntry(string serverUrl) { var loading = ViewHelperService.GetViewModel(); 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) { var path = new ContentPath(item.Path); rootEntry.CreateItem(path, item); } loading.Dispose(); return rootEntry; } private void AppendHistory(string str) { if (_history.Count >= 10) _history.RemoveAt(9); _history.Insert(0, str); } private string GetHistory() { if (_history.Count == 0) return ""; var h = _history[0]; _history.RemoveAt(0); return h; } public void OnPageOpen(object? args) { } } public class ContentEntry { private readonly Dictionary _childs = new(); 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) { Name = name; ServerName = serverName; PathName = pathName; _viewModel = viewModel; FileApi = fileApi; Item = item; } 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 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) { return _childs.TryGetValue(name, out child); } public bool TryAddChild(ContentEntry contentEntry) { if (_childs.TryAdd(contentEntry.PathName, contentEntry)) { contentEntry.Parent = this; return true; } return false; } public ContentPath GetPath() { if (Parent != null) { var path = Parent.GetPath(); path.Pathes.Add(PathName); return path; } 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)) { child = new ContentEntry(_viewModel, fName, fName, ServerName, FileApi); TryAddChild(child); } 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()); } } public struct ContentPath { 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(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } public ContentPath GetDirectory() { if (Pathes.Count == 0) return this; // Root remains root when getting the directory. 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 ContentPath Clone() { return new ContentPath(new List(Pathes)); } public string Path => Pathes.Count == 0 ? "/" : string.Join("/", Pathes); public override string ToString() { return Path; } } public sealed class ContentComparer : IComparer { public int Compare(ContentEntry? x, ContentEntry? 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); } }