2025-01-10 12:28:29 +03:00
|
|
|
using System;
|
2025-01-11 20:39:58 +03:00
|
|
|
using System.Collections.Frozen;
|
|
|
|
|
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.Tasks;
|
|
|
|
|
using Avalonia.Media;
|
|
|
|
|
using Avalonia.Media.Imaging;
|
|
|
|
|
using Avalonia.Platform;
|
2025-01-10 12:28:29 +03:00
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
2025-01-14 22:10:16 +03:00
|
|
|
using Nebula.Launcher.Services;
|
|
|
|
|
using Nebula.Launcher.ViewModels.Popup;
|
2025-01-10 12:28:29 +03:00
|
|
|
using Nebula.Launcher.Views.Pages;
|
|
|
|
|
using Nebula.Shared.Models;
|
2025-01-11 20:39:58 +03:00
|
|
|
using Nebula.Shared.Services;
|
|
|
|
|
using Nebula.Shared.Utils;
|
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]
|
2025-01-10 12:28:29 +03:00
|
|
|
public sealed partial class ContentBrowserViewModel : ViewModelBase
|
|
|
|
|
{
|
2025-01-11 20:39:58 +03:00
|
|
|
private readonly List<ContentEntry> _root = new();
|
|
|
|
|
|
2025-01-14 22:10:16 +03:00
|
|
|
private readonly List<string> _history = new();
|
|
|
|
|
|
2025-01-10 12:28:29 +03:00
|
|
|
[ObservableProperty] private string _message = "";
|
|
|
|
|
[ObservableProperty] private string _searchText = "";
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
private ContentEntry? _selectedEntry;
|
2025-01-14 22:10:16 +03:00
|
|
|
[ObservableProperty] private string _serverText = "";
|
|
|
|
|
[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 ViewHelperService ViewHelperService { get; } = default!;
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<ContentEntry> Entries { get; } = new();
|
2025-01-11 20:39:58 +03:00
|
|
|
|
|
|
|
|
public ContentEntry? SelectedEntry
|
|
|
|
|
{
|
|
|
|
|
get => _selectedEntry;
|
|
|
|
|
set
|
|
|
|
|
{
|
2025-01-12 15:15:01 +03:00
|
|
|
if (value is { Item: not null })
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
if (FileService.ContentFileApi.TryOpen(value.Item.Value.Hash, out var stream))
|
2025-01-12 15:15:01 +03:00
|
|
|
{
|
|
|
|
|
var ext = Path.GetExtension(value.Item.Value.Path);
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
|
|
|
|
|
|
|
|
|
|
using (var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
2025-01-12 15:15:01 +03:00
|
|
|
{
|
|
|
|
|
stream.CopyTo(sw);
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
stream.Dispose();
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
var startInfo = new ProcessStartInfo(myTempFile)
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
UseShellExecute = true
|
2025-01-12 15:15:01 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Process.Start(startInfo);
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
return;
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
Entries.Clear();
|
|
|
|
|
_selectedEntry = value;
|
|
|
|
|
|
|
|
|
|
if (value == null) return;
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
foreach (var (_, entryCh) in value.Childs) Entries.Add(entryCh);
|
2025-01-11 20:39:58 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 12:28:29 +03:00
|
|
|
|
2025-01-14 22:10:16 +03:00
|
|
|
protected override void InitialiseInDesignMode()
|
2025-01-10 12:28:29 +03:00
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
var a = new ContentEntry(this, "A:", "A", "");
|
|
|
|
|
var b = new ContentEntry(this, "B", "B", "");
|
2025-01-11 20:39:58 +03:00
|
|
|
a.TryAddChild(b);
|
2025-01-14 22:10:16 +03:00
|
|
|
Entries.Add(a);
|
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
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
FillRoot(HubService.ServerList);
|
|
|
|
|
|
|
|
|
|
HubService.HubServerChangedEventArgs += HubServerChangedEventArgs;
|
|
|
|
|
HubService.HubServerLoaded += GoHome;
|
|
|
|
|
|
|
|
|
|
if (!HubService.IsUpdating) GoHome();
|
2025-01-10 12:28:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
private void GoHome()
|
2025-01-10 12:28:29 +03:00
|
|
|
{
|
2025-01-11 20:39:58 +03:00
|
|
|
SelectedEntry = null;
|
2025-01-14 22:10:16 +03:00
|
|
|
foreach (var entry in _root) Entries.Add(entry);
|
2025-01-10 12:28:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
private void HubServerChangedEventArgs(HubServerChangedEventArgs obj)
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
if (obj.Action == HubServerChangeAction.Clear) _root.Clear();
|
|
|
|
|
if (obj.Action == HubServerChangeAction.Add) FillRoot(obj.Items);
|
2025-01-11 20:39:58 +03:00
|
|
|
}
|
2025-01-10 12:28:29 +03:00
|
|
|
|
2025-01-12 22:49:32 +03:00
|
|
|
private void FillRoot(IEnumerable<ServerHubInfo> infos)
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
foreach (var info in infos) _root.Add(new ContentEntry(this, info.StatusData.Name, info.Address, info.Address));
|
2025-01-12 22:49:32 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
public async void Go(ContentPath path, bool appendHistory = true)
|
2025-01-10 12:28:29 +03:00
|
|
|
{
|
2025-01-12 22:49:32 +03:00
|
|
|
if (path.Pathes.Count > 0 && (path.Pathes[0].StartsWith("ss14://") || path.Pathes[0].StartsWith("ss14s://")))
|
|
|
|
|
{
|
|
|
|
|
ServerText = path.Pathes[0];
|
2025-01-13 19:24:12 +03:00
|
|
|
path = new ContentPath("");
|
2025-01-12 22:49:32 +03:00
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 22:49:32 +03:00
|
|
|
if (string.IsNullOrEmpty(ServerText))
|
2025-01-11 20:39:58 +03:00
|
|
|
{
|
|
|
|
|
SearchText = "";
|
|
|
|
|
GoHome();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
if (ServerText != SelectedEntry?.ServerName) SelectedEntry = await CreateEntry(ServerText);
|
|
|
|
|
|
|
|
|
|
DebugService.Debug("Going to:" + path.Path);
|
|
|
|
|
|
2025-01-12 22:49:32 +03:00
|
|
|
var oriPath = path.Clone();
|
2025-01-11 20:39:58 +03:00
|
|
|
try
|
|
|
|
|
{
|
2025-01-12 22:49:32 +03:00
|
|
|
if (SelectedEntry == null || !SelectedEntry.GetRoot().TryGetEntry(path, out var centry))
|
2025-01-13 19:24:12 +03:00
|
|
|
throw new Exception("Not found! " + oriPath.Path);
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
if (appendHistory) AppendHistory(SearchText);
|
2025-01-12 15:15:01 +03:00
|
|
|
SearchText = oriPath.Path;
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 15:15:01 +03:00
|
|
|
SelectedEntry = centry;
|
2025-01-11 20:39:58 +03:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(e);
|
2025-01-12 15:15:01 +03:00
|
|
|
SearchText = oriPath.Path;
|
2025-01-14 22:10:16 +03:00
|
|
|
PopupService.Popup(e);
|
2025-01-11 20:39:58 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void OnBackEnter()
|
|
|
|
|
{
|
2025-01-12 15:15:01 +03:00
|
|
|
Go(new ContentPath(GetHistory()), false);
|
2025-01-10 12:28:29 +03:00
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-10 12:28:29 +03:00
|
|
|
public void OnGoEnter()
|
|
|
|
|
{
|
2025-01-11 20:39:58 +03:00
|
|
|
Go(new ContentPath(SearchText));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<ContentEntry> CreateEntry(string serverUrl)
|
|
|
|
|
{
|
|
|
|
|
var rurl = serverUrl.ToRobustUrl();
|
2025-01-14 22:10:16 +03:00
|
|
|
var info = await ContentService.GetBuildInfo(rurl, CancellationService.Token);
|
|
|
|
|
var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
2025-01-11 20:39:58 +03:00
|
|
|
loading.LoadingName = "Loading entry";
|
2025-01-14 22:10:16 +03:00
|
|
|
PopupService.Popup(loading);
|
|
|
|
|
var items = await ContentService.EnsureItems(info.RobustManifestInfo, loading,
|
|
|
|
|
CancellationService.Token);
|
2025-01-11 20:39:58 +03:00
|
|
|
|
2025-01-14 22:10:16 +03:00
|
|
|
var rootEntry = new ContentEntry(this, "", "", serverUrl);
|
2025-01-11 20:39:58 +03:00
|
|
|
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
var path = new ContentPath(item.Path);
|
|
|
|
|
rootEntry.CreateItem(path, item);
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
loading.Dispose();
|
|
|
|
|
|
|
|
|
|
return rootEntry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AppendHistory(string str)
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
if (_history.Count >= 10) _history.RemoveAt(9);
|
2025-01-11 20:39:58 +03:00
|
|
|
_history.Insert(0, str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetHistory()
|
|
|
|
|
{
|
|
|
|
|
if (_history.Count == 0) return "";
|
|
|
|
|
var h = _history[0];
|
|
|
|
|
_history.RemoveAt(0);
|
|
|
|
|
return h;
|
|
|
|
|
}
|
2025-01-10 12:28:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
public class ContentEntry
|
2025-01-10 12:28:29 +03:00
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
private readonly Dictionary<string, ContentEntry> _childs = new();
|
|
|
|
|
private readonly ContentBrowserViewModel _viewModel;
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
public RobustManifestItem? Item;
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
internal ContentEntry(ContentBrowserViewModel viewModel, string name, string pathName, string serverName)
|
|
|
|
|
{
|
|
|
|
|
Name = name;
|
|
|
|
|
ServerName = serverName;
|
|
|
|
|
PathName = pathName;
|
|
|
|
|
_viewModel = viewModel;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
public bool IsDirectory => Item == null;
|
|
|
|
|
|
|
|
|
|
public string Name { get; private set; }
|
2025-01-14 22:10:16 +03:00
|
|
|
public string PathName { get; }
|
|
|
|
|
public string ServerName { get; }
|
2025-01-18 18:20:11 +03:00
|
|
|
public string IconPath { get; set; } = "/Assets/svg/folder.svg";
|
2025-01-11 20:39:58 +03:00
|
|
|
|
|
|
|
|
public ContentEntry? Parent { get; private set; }
|
|
|
|
|
public bool IsRoot => Parent == null;
|
|
|
|
|
|
|
|
|
|
public IReadOnlyDictionary<string, ContentEntry> Childs => _childs.ToFrozenDictionary();
|
|
|
|
|
|
2025-01-14 22:10:16 +03:00
|
|
|
public bool TryGetChild(string name, [NotNullWhen(true)] out ContentEntry? child)
|
2025-01-11 20:39:58 +03:00
|
|
|
{
|
|
|
|
|
return _childs.TryGetValue(name, out child);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool TryAddChild(ContentEntry contentEntry)
|
|
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
if (_childs.TryAdd(contentEntry.PathName, contentEntry))
|
2025-01-11 20:39:58 +03:00
|
|
|
{
|
|
|
|
|
contentEntry.Parent = this;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ContentPath GetPath()
|
|
|
|
|
{
|
|
|
|
|
if (Parent != null)
|
|
|
|
|
{
|
|
|
|
|
var path = Parent.GetPath();
|
2025-01-12 22:49:32 +03:00
|
|
|
path.Pathes.Add(PathName);
|
2025-01-11 20:39:58 +03:00
|
|
|
return path;
|
|
|
|
|
}
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-12 22:49:32 +03:00
|
|
|
return new ContentPath([PathName]);
|
2025-01-11 20:39:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ContentEntry GetOrCreateDirectory(ContentPath rootPath)
|
|
|
|
|
{
|
|
|
|
|
if (rootPath.Pathes.Count == 0) return this;
|
2025-01-12 22:49:32 +03:00
|
|
|
|
|
|
|
|
var fName = rootPath.GetNext();
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
if (!TryGetChild(fName, out var child))
|
2025-01-11 20:39:58 +03:00
|
|
|
{
|
2025-01-12 22:49:32 +03:00
|
|
|
child = new ContentEntry(_viewModel, fName, fName, ServerName);
|
2025-01-11 20:39:58 +03:00
|
|
|
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);
|
|
|
|
|
|
2025-01-12 22:49:32 +03:00
|
|
|
var name = path.GetName();
|
|
|
|
|
var entry = new ContentEntry(_viewModel, name, name, ServerName)
|
2025-01-11 20:39:58 +03:00
|
|
|
{
|
|
|
|
|
Item = item
|
|
|
|
|
};
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
dirEntry.TryAddChild(entry);
|
2025-01-18 18:20:11 +03:00
|
|
|
entry.IconPath = "/Assets/svg/file.svg";
|
2025-01-11 20:39:58 +03:00
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool TryGetEntry(ContentPath path, out ContentEntry? entry)
|
|
|
|
|
{
|
|
|
|
|
entry = null;
|
2025-01-14 22:10:16 +03:00
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
if (path.Pathes.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
entry = this;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-01-12 22:49:32 +03:00
|
|
|
|
|
|
|
|
var fName = path.GetNext();
|
2025-01-14 22:10:16 +03:00
|
|
|
|
|
|
|
|
if (!TryGetChild(fName, out var child)) return false;
|
2025-01-11 20:39:58 +03:00
|
|
|
|
|
|
|
|
return child.TryGetEntry(path, out entry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnPathGo()
|
|
|
|
|
{
|
|
|
|
|
_viewModel.Go(GetPath());
|
|
|
|
|
}
|
2025-01-10 12:28:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-11 20:39:58 +03:00
|
|
|
public struct ContentPath
|
2025-01-10 12:28:29 +03:00
|
|
|
{
|
2025-01-14 22:10:16 +03:00
|
|
|
public List<string> Pathes { get; }
|
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-12 22:49:32 +03:00
|
|
|
Pathes = string.IsNullOrEmpty(path)
|
|
|
|
|
? new List<string>()
|
|
|
|
|
: path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
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; // Root remains root when getting the directory.
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|