diff --git a/Nebula.Launcher/Controls/FilterBox.cs b/Nebula.Launcher/Controls/FilterBox.cs new file mode 100644 index 0000000..dbf11da --- /dev/null +++ b/Nebula.Launcher/Controls/FilterBox.cs @@ -0,0 +1,72 @@ +using System; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Nebula.Launcher.Controls; + +public class FilterBox : UserControl +{ + public static readonly StyledProperty FilterCommandProperty = + AvaloniaProperty.Register(nameof(FilterCommand)); + + public ICommand FilterCommand + { + get => GetValue(FilterCommandProperty); + set => SetValue(FilterCommandProperty, value); + } + + public Action? OnFilterChanged {get; set;} + + public string? FilterBoxName { + set => filterName.Text = value; + get => filterName.Text; + } + + private StackPanel filterPanel; + private TextBox filterName = new TextBox(); + + public FilterBox() + { + filterPanel = new StackPanel() + { + Orientation = Orientation.Horizontal, + Spacing = 5, + }; + + Content = filterPanel; + } + + public void AddFilter(string name, string tag) + { + var checkBox = new CheckBox(); + checkBox.Content = new TextBlock() + { + Text = name, + }; + + + + checkBox.IsCheckedChanged += (_, _) => + { + var args = new FilterBoxChangedEventArgs(tag, checkBox.IsChecked ?? false); + OnFilterChanged?.Invoke(args); + FilterCommand?.Execute(args); + }; + + filterPanel.Children.Add(checkBox); + } +} + +public sealed class FilterBoxChangedEventArgs : EventArgs +{ + public FilterBoxChangedEventArgs(string name, bool @checked) + { + Tag = name; + Checked = @checked; + } + + public string Tag {get; private set;} + public bool Checked {get; private set;} +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index 36ff3ac..e3e7109 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -39,6 +39,7 @@ public partial class MainViewModel : ViewModelBase [GenerateProperty] private DebugService DebugService { get; } = default!; [GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!; + [GenerateProperty] private ContentService ContentService { get; } = default!; [GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!; [GenerateProperty] private FileService FileService { get; } = default!; @@ -46,9 +47,8 @@ public partial class MainViewModel : ViewModelBase protected override void InitialiseInDesignMode() { - CurrentPage = ViewHelperService.GetViewModel(); Items = new ObservableCollection(_templates); - SelectedListItem = Items.First(vm => vm.ModelType == typeof(AccountInfoViewModel)); + RequirePage(); } protected override void Initialise() @@ -67,7 +67,7 @@ public partial class MainViewModel : ViewModelBase loadingHandler.LoadingName = "Migration task, please wait..."; loadingHandler.IsCancellable = false; - if (!FileService.CheckMigration(loadingHandler)) + if (!ContentService.CheckMigration(loadingHandler)) return; OnPopupRequired(loadingHandler); @@ -77,10 +77,36 @@ public partial class MainViewModel : ViewModelBase { if (value is null) return; - if (!ViewHelperService.TryGetViewModel(value.ModelType, out var vmb) || vmb is not IViewModelPage viewModelPage) return; + if (!ViewHelperService.TryGetViewModel(value.ModelType, out var vmb)) return; - viewModelPage.OnPageOpen(value.args); - CurrentPage = vmb; + OpenPage(vmb, value.args); + } + + public T RequirePage() where T : ViewModelBase, IViewModelPage + { + if (CurrentPage is T vam) return vam; + + var page = ViewHelperService.GetViewModel(); + OpenPage(page, null); + return page; + } + + private void OpenPage(ViewModelBase obj, object? args) + { + var tabItems = Items.Where(vm => vm.ModelType == obj.GetType()); + + var listItemTemplates = tabItems as ListItemTemplate[] ?? tabItems.ToArray(); + if (listItemTemplates.Length != 0) + { + SelectedListItem = listItemTemplates.First(); + } + + if (obj is IViewModelPage page) + { + page.OnPageOpen(args); + } + + CurrentPage = obj; } public void PopupMessage(PopupViewModelBase viewModelBase) diff --git a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs index 8d201fe..ec6f532 100644 --- a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs @@ -146,7 +146,14 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase , IViewModel private void FillRoot(IEnumerable infos) { - foreach (var info in infos) _root.Add(new ContentEntry(this, info.StatusData.Name, info.Address, info.Address, default!)); + 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) @@ -384,6 +391,11 @@ public struct ContentPath { public List Pathes { get; } + public ContentPath() + { + Pathes = []; + } + public ContentPath(List pathes) { Pathes = pathes; diff --git a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs b/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs index b075a00..42bce27 100644 --- a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs +++ b/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.Favorite.cs @@ -31,6 +31,8 @@ public partial class ServerListViewModel s.IsFavorite = true; FavoriteServers.Add(s); } + + ApplyFilter(); } public void AddFavorite(ServerEntryModelView entryModelView) diff --git a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs index 033a359..54d23ae 100644 --- a/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ServerListViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using Nebula.Launcher.Controls; using Nebula.Launcher.Services; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views; @@ -27,6 +28,7 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage public ObservableCollection Servers { get; }= new(); public ObservableCollection HubErrors { get; } = new(); + public readonly ServerFilter CurrentFilter = new ServerFilter(); public Action? OnSearchChange; [GenerateProperty] private HubService HubService { get; } @@ -38,8 +40,6 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage private ServerViewContainer ServerViewContainer { get; set; } private List UnsortedServers { get; } = new(); - - private List _filters = new(); //Design think protected override void InitialiseInDesignMode() @@ -63,23 +63,22 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage if (!HubService.IsUpdating) UpdateServerEntries(); UpdateFavoriteEntries(); } - - public void OnFilterChanged(string name, bool active) + + public void ApplyFilter() { - DebugService.Debug($"OnFilterChanged: {name} {active}"); - if(active) - _filters.Add(name); - else - _filters.Remove(name); - - if(IsFavoriteMode) + foreach (var entry in ServerViewContainer.Items) { - UpdateFavoriteEntries(); + entry.ProcessFilter(CurrentFilter); } + } + + public void OnFilterChanged(FilterBoxChangedEventArgs args) + { + if (args.Checked) + CurrentFilter.Tags.Add(args.Tag); else - { - UpdateServerEntries(); - } + CurrentFilter.Tags.Remove(args.Tag); + ApplyFilter(); } private void HubServerLoadingError(Exception obj) @@ -96,26 +95,19 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage Task.Run(() => { UnsortedServers.Sort(new ServerComparer()); - foreach (var info in UnsortedServers.Where(CheckServerThink)) + foreach (var info in UnsortedServers) { var view = ServerViewContainer.Get(info.Address.ToRobustUrl(), info.StatusData); Servers.Add(view); } + ApplyFilter(); }); } private void OnChangeSearch() { - if(string.IsNullOrEmpty(SearchText)) return; - - if(IsFavoriteMode) - { - UpdateFavoriteEntries(); - } - else - { - UpdateServerEntries(); - } + CurrentFilter.SearchText = SearchText; + ApplyFilter(); } private void HubServerChangedEventArgs(HubServerChangedEventArgs obj) @@ -134,18 +126,6 @@ public partial class ServerListViewModel : ViewModelBase, IViewModelPage } } - private bool CheckServerThink(ServerHubInfo hubInfo) - { - var isNameEqual = string.IsNullOrEmpty(SearchText) || hubInfo.StatusData.Name.ToLower().Contains(SearchText.ToLower()); - - if (_filters.Count == 0) return isNameEqual; - if(_filters.Select(t=>t.Replace('_',':').Replace("ERPYes","18+")).Any(t=>hubInfo.StatusData.Tags.Contains(t) || hubInfo.InferredTags.Contains(t))) - return isNameEqual; - - - return false; - } - public void FilterRequired() { IsFilterVisible = !IsFilterVisible; @@ -178,6 +158,8 @@ public class ServerViewContainer( ) { private readonly Dictionary _entries = new(); + + public ICollection Items => _entries.Values; public void Clear() { @@ -240,4 +222,31 @@ public class ServerComparer : IComparer, IComparer, { return Compare(x.Item2, y.Item2); } +} + +public sealed class ServerFilter +{ + public string SearchText { get; set; } = ""; + public HashSet Tags { get; } = new(); + public bool IsMatchByName(string name) + { + if (string.IsNullOrWhiteSpace(SearchText)) + return true; + + return name.Contains(SearchText, StringComparison.OrdinalIgnoreCase); + } + + public bool IsMatchByTags(IEnumerable itemTags) + { + if (Tags.Count == 0) + return true; + + var itemTagSet = new HashSet(itemTags); + return Tags.All(tag => itemTagSet.Contains(tag)); + } + + public bool IsMatch(string name, IEnumerable itemTags) + { + return IsMatchByName(name) && IsMatchByTags(itemTags); + } } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index f9541d8..3ab1a2e 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -2,13 +2,16 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Input; +using Avalonia.Controls; using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.Services; +using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views; using Nebula.Shared.Models; @@ -17,15 +20,25 @@ using Nebula.Shared.Utils; namespace Nebula.Launcher.ViewModels; -[ViewModelRegister(typeof(ServerEntryView), isSingleton: false)] +[ViewModelRegister(typeof(ServerEntryView), false)] [ConstructGenerator] public partial class ServerEntryModelView : ViewModelBase { + [ObservableProperty] private string _description = "Fetching info..."; + [ObservableProperty] private bool _expandInfo; + [ObservableProperty] private bool _isFavorite; + [ObservableProperty] private bool _isVisible; + + private string _lastError = ""; private Process? _p; - public RobustUrl Address { get; private set; } - public Action? OnFavoriteToggle; - + + private ServerInfo? _serverInfo; + [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 CancellationService CancellationService { get; } = default!; @@ -34,53 +47,27 @@ public partial class ServerEntryModelView : ViewModelBase [GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!; [GenerateProperty] private ViewHelperService ViewHelperService { get; } = default!; [GenerateProperty] private RestService RestService { get; } = default!; + [GenerateProperty] private MainViewModel MainViewModel { get; } = default!; - [ObservableProperty] private string _description = "Fetching info..."; - [ObservableProperty] private bool _expandInfo = false; - [ObservableProperty] private bool _tagDataVisible = false; - [ObservableProperty] private bool _isFavorite = false; - - public ServerStatus Status { get; private set; } = - new ServerStatus( - "Fetching data...", - $"Loading...", [], - "", - -1, - -1, - -1, + public ServerStatus Status { get; private set; } = + new( + "Fetching data...", + "Loading...", [], + "", + -1, + -1, + -1, false, DateTime.Now, -1 ); - + public ObservableCollection Links { get; } = new(); public bool RunVisible => Process == null; - private ServerInfo? _serverInfo = null; - - private string _lastError = ""; - - public async Task GetServerInfo() - { - if (_serverInfo == null) - { - try - { - _serverInfo = await RestService.GetAsync(Address.InfoUri, CancellationService.Token); - } - catch (Exception e) - { - Description = e.Message; - DebugService.Error(e); - } - } - - return _serverInfo; - } - public ObservableCollection Tags { get; } = []; - public ICommand OnLinkGo { get; }= new LinkGoCommand(); + public ICommand OnLinkGo { get; } = new LinkGoCommand(); private Process? Process { @@ -92,10 +79,26 @@ public partial class ServerEntryModelView : ViewModelBase } } + public async Task GetServerInfo() + { + if (_serverInfo == null) + try + { + _serverInfo = await RestService.GetAsync(Address.InfoUri, CancellationService.Token); + } + catch (Exception e) + { + Description = e.Message; + DebugService.Error(e); + } + + return _serverInfo; + } + protected override void InitialiseInDesignMode() { Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!"; - Links.Add(new ServerLink("Discord","discord","https://cinka.ru")); + Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru")); Status = new ServerStatus("Ameba", "Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ", ["rp:hrp", "18+"], @@ -109,14 +112,16 @@ public partial class ServerEntryModelView : ViewModelBase CurrLog = ViewHelperService.GetViewModel(); } + public void ProcessFilter(ServerFilter serverFilter) + { + IsVisible = serverFilter.IsMatch(Status.Name, Tags); + } + public void SetStatus(ServerStatus serverStatus) { Status = serverStatus; Tags.Clear(); - foreach (var tag in Status.Tags) - { - Tags.Add(tag); - } + foreach (var tag in Status.Tags) Tags.Add(tag); OnPropertyChanged(nameof(Status)); } @@ -124,13 +129,9 @@ public partial class ServerEntryModelView : ViewModelBase { Address = url; if (serverStatus is not null) - { SetStatus(serverStatus); - } else - { FetchStatus(); - } return this; } @@ -149,8 +150,13 @@ public partial class ServerEntryModelView : ViewModelBase -1); } } - - + + public void OpenContentViewer() + { + MainViewModel.RequirePage().Go(Address.ToString(), new ContentPath()); + } + + public void ToggleFavorites() { OnFavoriteToggle?.Invoke(); @@ -179,8 +185,8 @@ public partial class ServerEntryModelView : ViewModelBase await RunnerService.PrepareRun(buildInfo, loadingContext, CancellationService.Token); - var path = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); - + var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + Process = Process.Start(new ProcessStartInfo { FileName = "dotnet.exe", @@ -236,7 +242,7 @@ public partial class ServerEntryModelView : ViewModelBase DebugService.Log("PROCESS EXIT WITH CODE " + Process.ExitCode); - if(Process.ExitCode != 0) + if (Process.ExitCode != 0) PopupMessageService.Popup($"Game exit with code {Process.ExitCode}.\nReason: {_lastError}"); Process.Dispose(); @@ -261,7 +267,7 @@ public partial class ServerEntryModelView : ViewModelBase CurrLog.Append(e.Data); } } - + public void ReadLog() { PopupMessageService.Popup(CurrLog); @@ -275,25 +281,16 @@ public partial class ServerEntryModelView : ViewModelBase public async void ExpandInfoRequired() { ExpandInfo = !ExpandInfo; - if (Avalonia.Controls.Design.IsDesignMode) - { - return; - } - + if (Design.IsDesignMode) return; + var info = await GetServerInfo(); - if (info == null) - { - return; - } - + if (info == null) return; + Description = info.Desc; Links.Clear(); - if(info.Links is null) return; - foreach (var link in info.Links) - { - Links.Add(link); - } + if (info.Links is null) return; + foreach (var link in info.Links) Links.Add(link); } private static string FindDotnetPath() @@ -353,6 +350,7 @@ public class LinkGoCommand : ICommand { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } + public bool CanExecute(object? parameter) { return true; @@ -360,7 +358,7 @@ public class LinkGoCommand : ICommand public void Execute(object? parameter) { - if(parameter is not string str) return; + if (parameter is not string str) return; Helper.SafeOpenBrowser(str); } diff --git a/Nebula.Launcher/Views/MainView.axaml b/Nebula.Launcher/Views/MainView.axaml index a1e9813..3088fc9 100644 --- a/Nebula.Launcher/Views/MainView.axaml +++ b/Nebula.Launcher/Views/MainView.axaml @@ -120,7 +120,7 @@ https://cinka.ru/nebula-launcher/ - v0.05-a + v0.08-a diff --git a/Nebula.Launcher/Views/Pages/ServerListView.axaml b/Nebula.Launcher/Views/Pages/ServerListView.axaml index 76c2f35..7d8700e 100644 --- a/Nebula.Launcher/Views/Pages/ServerListView.axaml +++ b/Nebula.Launcher/Views/Pages/ServerListView.axaml @@ -8,7 +8,8 @@ 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:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Nebula.Launcher.Controls"> @@ -22,7 +23,7 @@ Grid.RowSpan="2" Margin="5,0,0,10" Padding="0,0,10,0"> - + - - - - - - - - - - - - - + + diff --git a/Nebula.Launcher/Views/Pages/ServerListView.axaml.cs b/Nebula.Launcher/Views/Pages/ServerListView.axaml.cs index 921f8bd..817b004 100644 --- a/Nebula.Launcher/Views/Pages/ServerListView.axaml.cs +++ b/Nebula.Launcher/Views/Pages/ServerListView.axaml.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using Avalonia.Controls; -using Avalonia.Interactivity; using ServerListViewModel = Nebula.Launcher.ViewModels.Pages.ServerListViewModel; namespace Nebula.Launcher.Views.Pages; @@ -12,6 +9,15 @@ public partial class ServerListView : UserControl public ServerListView() { InitializeComponent(); + + EssentialFilters.AddFilter("Non RP", "rp:none"); + EssentialFilters.AddFilter("Low RP", "rp:low"); + EssentialFilters.AddFilter("Medium RP", "rp:med"); + EssentialFilters.AddFilter("Hard RP", "rp:high"); + EssentialFilters.AddFilter("18+", "18+"); + + LanguageFilters.AddFilter("RU","lang:ru"); + LanguageFilters.AddFilter("EN","lang:en"); } // This constructor is used when the view is created via dependency injection @@ -26,11 +32,4 @@ public partial class ServerListView : UserControl var context = (ServerListViewModel?)DataContext; context?.OnSearchChange?.Invoke(); } - - private void Button_OnClick(object? sender, RoutedEventArgs e) - { - var send = sender as CheckBox; - var context = (ServerListViewModel?)DataContext; - context?.OnFilterChanged(send.Name, send.IsChecked.Value); - } } \ No newline at end of file diff --git a/Nebula.Launcher/Views/ServerEntryView.axaml b/Nebula.Launcher/Views/ServerEntryView.axaml index 9c934cf..0bacb55 100644 --- a/Nebula.Launcher/Views/ServerEntryView.axaml +++ b/Nebula.Launcher/Views/ServerEntryView.axaml @@ -5,7 +5,6 @@ x:Class="Nebula.Launcher.Views.ServerEntryView" x:DataType="viewModels:ServerEntryModelView" xmlns="https://github.com/avaloniaui" - xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:converters="clr-namespace:Nebula.Launcher.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -13,7 +12,8 @@ xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels" xmlns:views="clr-namespace:Nebula.Launcher.Views" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + IsVisible="{Binding IsVisible}"> @@ -234,6 +234,10 @@ IsVisible="{Binding ExpandInfo}" Margin="5,5,0,0" Spacing="5"> +