diff --git a/.idea/.idea.Nebula/.idea/avalonia.xml b/.idea/.idea.Nebula/.idea/avalonia.xml index 0f301fd..1245061 100644 --- a/.idea/.idea.Nebula/.idea/avalonia.xml +++ b/.idea/.idea.Nebula/.idea/avalonia.xml @@ -8,6 +8,7 @@ + @@ -23,6 +24,7 @@ + @@ -33,6 +35,7 @@ + diff --git a/Nebula.Launcher/App.axaml.cs b/Nebula.Launcher/App.axaml.cs index 55d7c70..9628f97 100644 --- a/Nebula.Launcher/App.axaml.cs +++ b/Nebula.Launcher/App.axaml.cs @@ -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(); var serviceProvider = services.BuildServiceProvider(); diff --git a/Nebula.Launcher/Controls/ServerListView.axaml b/Nebula.Launcher/Controls/ServerListView.axaml new file mode 100644 index 0000000..ec2de2c --- /dev/null +++ b/Nebula.Launcher/Controls/ServerListView.axaml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/Nebula.Launcher/Controls/ServerListView.axaml.cs b/Nebula.Launcher/Controls/ServerListView.axaml.cs new file mode 100644 index 0000000..fd11a68 --- /dev/null +++ b/Nebula.Launcher/Controls/ServerListView.axaml.cs @@ -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; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/LauncherConVar.cs b/Nebula.Launcher/LauncherConVar.cs index f10a8e4..756ad56 100644 --- a/Nebula.Launcher/LauncherConVar.cs +++ b/Nebula.Launcher/LauncherConVar.cs @@ -1,3 +1,4 @@ +using Nebula.Launcher.Models; using Nebula.Launcher.ViewModels.Pages; using Nebula.Shared.Services; @@ -22,6 +23,11 @@ public static class LauncherConVar "https://auth.fallback.spacestation14.com/" ]) ]); + + public static readonly ConVar Hub = ConVarBuilder.Build("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 CurrentLang = ConVarBuilder.Build("launcher.language", "en-US"); public static readonly ConVar ILSpyUrl = ConVarBuilder.Build("decompiler.url", diff --git a/Nebula.Launcher/Models/ListItemTemplate.cs b/Nebula.Launcher/Models/ListItemTemplate.cs new file mode 100644 index 0000000..71646da --- /dev/null +++ b/Nebula.Launcher/Models/ListItemTemplate.cs @@ -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); \ No newline at end of file diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index 9ab546c..b677a12 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -42,6 +42,10 @@ AddFavoriteView.axaml Code + + ServerListView.axaml + Code + @@ -69,4 +73,8 @@ + + + + diff --git a/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs b/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs new file mode 100644 index 0000000..8146b69 --- /dev/null +++ b/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs @@ -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 _serverLists = []; + + public bool IsLoaded { get; private set; } + public Action? OnLoaded { get; set; } + public Action? Dirty { get; set; } + public IEnumerable GetServers() + { + return _serverLists; + } + + public IEnumerable 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 GetFavoriteEntries() + { + return ConfigurationService.GetConfigValue(LauncherConVar.Favorites)?.ToList() ?? []; + } + + private void Initialise(){} + private void InitialiseInDesignMode(){} +} \ No newline at end of file diff --git a/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs b/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs new file mode 100644 index 0000000..c4ef744 --- /dev/null +++ b/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs @@ -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 _servers = []; + private readonly List _errors = []; + + public HubServerListProvider With(string hubUrl) + { + HubUrl = hubUrl; + return this; + } + + public IEnumerable GetServers() + { + return _servers; + } + + public IEnumerable 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>(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(){} +} \ No newline at end of file diff --git a/Nebula.Launcher/ServerListProviders/IServerListProvider.cs b/Nebula.Launcher/ServerListProviders/IServerListProvider.cs new file mode 100644 index 0000000..13d6ccb --- /dev/null +++ b/Nebula.Launcher/ServerListProviders/IServerListProvider.cs @@ -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 GetServers(); + public IEnumerable GetErrors(); + + public void LoadServerList(); +} + +public interface IServerListDirtyInvoker +{ + public Action? Dirty { get; set; } +} \ No newline at end of file diff --git a/Nebula.Launcher/ServerListProviders/TestServerList.cs b/Nebula.Launcher/ServerListProviders/TestServerList.cs new file mode 100644 index 0000000..5bc0e88 --- /dev/null +++ b/Nebula.Launcher/ServerListProviders/TestServerList.cs @@ -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 GetServers() + { + return [new ServerEntryModelView(),new ServerEntryModelView()]; + } + + public IEnumerable GetErrors() + { + return [new Exception("On no!")]; + } + + public void LoadServerList() + { + + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/ContentView/ContentViewBase.cs b/Nebula.Launcher/ViewModels/ContentView/ContentViewBase.cs deleted file mode 100644 index f6f24b1..0000000 --- a/Nebula.Launcher/ViewModels/ContentView/ContentViewBase.cs +++ /dev/null @@ -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() - { - } -} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/ContentView/DecompilerContentView.cs b/Nebula.Launcher/ViewModels/ContentView/DecompilerContentView.cs deleted file mode 100644 index e5471de..0000000 --- a/Nebula.Launcher/ViewModels/ContentView/DecompilerContentView.cs +++ /dev/null @@ -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() - { - } -} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index df733f8..b8ee4f8 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -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 _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 _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() where T : ViewModelBase, IViewModelPage + public T RequirePage() where T : ViewModelBase { if (CurrentPage is T vam) return vam; var page = ViewHelperService.GetViewModel(); - 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; } diff --git a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs index 9d99ccf..b2f3cc4 100644 --- a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs @@ -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, diff --git a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs index 1fa4c9d..9979e16 100644 --- a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs @@ -119,7 +119,7 @@ public partial class StringConfigurationViewModel : ViewModelBase , IConfigurati private string _oldText = string.Empty; - public ConfigContext Context { get; set; } + public required ConfigContext Context { get; set; } public void InitializeConfig() { ConfigName = Context.ConVar.Name; diff --git a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs index 62007c7..cbbbc1a 100644 --- a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs @@ -1,397 +1,462 @@ 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 _root = new(); - - private readonly List _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 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)); + 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(); 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 CreateEntry(string serverUrl) + public void OnGoEnter() { - 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) + 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()!; + 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(); + 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() { + var root = ViewHelperService.GetViewModel(); + 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 _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 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(); + 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"; + 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 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() @@ -404,17 +469,23 @@ public struct ContentPath Pathes = pathes; } - public ContentPath(string path) + public ContentPath(string? path) { Pathes = string.IsNullOrEmpty(path) ? new List() - : 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 +510,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(Pathes)); @@ -450,19 +529,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 +public sealed class ContentComparer : IComparer { - 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); } } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Pages/IViewModelPage.cs b/Nebula.Launcher/ViewModels/Pages/IViewModelPage.cs deleted file mode 100644 index f2f473f..0000000 --- a/Nebula.Launcher/ViewModels/Pages/IViewModelPage.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Nebula.Launcher.ViewModels.Pages; - -public interface IViewModelPage -{ - public void OnPageOpen(object? args); -} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs b/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs deleted file mode 100644 index 42bce27..0000000 --- a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs +++ /dev/null @@ -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 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(); - } -} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs similarity index 55% rename from Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs rename to Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs index 54d23ae..8ade626 100644 --- a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs @@ -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 Servers { get; }= new(); - public ObservableCollection 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 Items { get; private set; } + [ObservableProperty] private ServerListTabTemplate _selectedItem; + + [GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; set; } + + private Dictionary _viewCache = []; - private List UnsortedServers { get; } = new(); //Design think protected override void InitialiseInDesignMode() { - ServerViewContainer = new ServerViewContainer(this, ViewHelperService); - HubErrors.Add(new Exception("UVI")); + Items = new ObservableCollection([ + 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(); + foreach (var record in ConfigurationService.GetConfigValue(LauncherConVar.Hub) ?? []) + { + tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService()!.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(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(); - 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 _entries = new(); public ICollection Items => _entries.Values; @@ -177,16 +146,12 @@ public class ServerViewContainer( return entry; } - entry = viewHelperService.GetViewModel().WithData(url, serverStatus); + entry = _viewHelperService.GetViewModel().WithData(url, serverStatus); _entries.Add(url.ToString(), entry); } - entry.OnFavoriteToggle += () => - { - if (entry.IsFavorite) serverListViewModel.RemoveFavorite(entry); - else serverListViewModel.AddFavorite(entry); - }; + Console.WriteLine("LENGTH OF PENIS IS: " + _entries.Count); return entry; } diff --git a/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs b/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs index e1824e7..566e527 100644 --- a/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs @@ -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) diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index f1275ce..d0c5328 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -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,7 @@ 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!; public ServerStatus Status { get; private set; } = new( @@ -116,8 +120,14 @@ public partial class ServerEntryModelView : ViewModelBase CurrLog = ViewHelperService.GetViewModel(); } - public void ProcessFilter(ServerFilter serverFilter) + public void ProcessFilter(ServerFilter? serverFilter) { + if (serverFilter == null) + { + IsVisible = true; + return; + } + IsVisible = serverFilter.IsMatch(Status.Name, Tags); } @@ -136,9 +146,16 @@ public partial class ServerEntryModelView : ViewModelBase SetStatus(serverStatus); else FetchStatus(); + + IsFavorite = GetFavoriteEntries().Contains(Address.ToString()); return this; } + + private List GetFavoriteEntries() + { + return ConfigurationService.GetConfigValue(LauncherConVar.Favorites)?.ToList() ?? []; + } private async void FetchStatus() { @@ -157,12 +174,16 @@ public partial class ServerEntryModelView : ViewModelBase public void OpenContentViewer() { - MainViewModel.RequirePage().Go(Address.ToString(), new ContentPath()); + MainViewModel.RequirePage().Go(Address, ContentPath.Empty); } public void ToggleFavorites() { - OnFavoriteToggle?.Invoke(); + IsFavorite = !IsFavorite; + if(IsFavorite) + FavoriteServerListProvider.AddFavorite(this); + else + FavoriteServerListProvider.RemoveFavorite(this); } public void RunInstance() @@ -188,7 +209,7 @@ 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 { @@ -372,4 +393,9 @@ public class LinkGoCommand : ICommand } public event EventHandler? CanExecuteChanged; +} + +public interface IFilterConsumer +{ + public void ProcessFilter(ServerFilter? serverFilter); } \ No newline at end of file diff --git a/Nebula.Launcher/Views/FileContentEntryView.axaml b/Nebula.Launcher/Views/FileContentEntryView.axaml new file mode 100644 index 0000000..5c49963 --- /dev/null +++ b/Nebula.Launcher/Views/FileContentEntryView.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + diff --git a/Nebula.Launcher/Views/FileContentEntryView.axaml.cs b/Nebula.Launcher/Views/FileContentEntryView.axaml.cs new file mode 100644 index 0000000..aeace0a --- /dev/null +++ b/Nebula.Launcher/Views/FileContentEntryView.axaml.cs @@ -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; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Views/MainView.axaml b/Nebula.Launcher/Views/MainView.axaml index 2d0b758..bbe49a1 100644 --- a/Nebula.Launcher/Views/MainView.axaml +++ b/Nebula.Launcher/Views/MainView.axaml @@ -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"> @@ -63,7 +64,7 @@ Padding="0" SelectedItem="{Binding SelectedListItem}"> - + - - - - - - - - + + + \ No newline at end of file diff --git a/Nebula.Launcher/Views/Pages/ServerListView.axaml b/Nebula.Launcher/Views/Pages/ServerOverviewView.axaml similarity index 62% rename from Nebula.Launcher/Views/Pages/ServerListView.axaml rename to Nebula.Launcher/Views/Pages/ServerOverviewView.axaml index 7d8700e..5b640e5 100644 --- a/Nebula.Launcher/Views/Pages/ServerListView.axaml +++ b/Nebula.Launcher/Views/Pages/ServerOverviewView.axaml @@ -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"> - + - - - - - - - + RowDefinitions="40,*,40"> - + + + + + + + + + + + + + + + + + + Grid.Row="2" /> -