- tweak: rework FileApi for services

- tweak: rework filter think
- add: content view button
- fix: little fixes in services
This commit is contained in:
2025-05-02 20:06:33 +03:00
parent ef8ee5a8d3
commit f066bb1188
18 changed files with 426 additions and 268 deletions

View File

@@ -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<ICommand> FilterCommandProperty =
AvaloniaProperty.Register<FilterBox, ICommand>(nameof(FilterCommand));
public ICommand FilterCommand
{
get => GetValue(FilterCommandProperty);
set => SetValue(FilterCommandProperty, value);
}
public Action<FilterBoxChangedEventArgs>? 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;}
}

View File

@@ -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<AccountInfoViewModel>();
Items = new ObservableCollection<ListItemTemplate>(_templates);
SelectedListItem = Items.First(vm => vm.ModelType == typeof(AccountInfoViewModel));
RequirePage<AccountInfoViewModel>();
}
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<T>() where T : ViewModelBase, IViewModelPage
{
if (CurrentPage is T vam) return vam;
var page = ViewHelperService.GetViewModel<T>();
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)

View File

@@ -146,7 +146,14 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase , IViewModel
private void FillRoot(IEnumerable<ServerHubInfo> 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<string> Pathes { get; }
public ContentPath()
{
Pathes = [];
}
public ContentPath(List<string> pathes)
{
Pathes = pathes;

View File

@@ -31,6 +31,8 @@ public partial class ServerListViewModel
s.IsFavorite = true;
FavoriteServers.Add(s);
}
ApplyFilter();
}
public void AddFavorite(ServerEntryModelView entryModelView)

View File

@@ -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<ServerEntryModelView> Servers { get; }= new();
public ObservableCollection<Exception> 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<ServerHubInfo> UnsortedServers { get; } = new();
private List<string> _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<string, ServerEntryModelView> _entries = new();
public ICollection<ServerEntryModelView> Items => _entries.Values;
public void Clear()
{
@@ -240,4 +222,31 @@ public class ServerComparer : IComparer<ServerHubInfo>, IComparer<ServerStatus>,
{
return Compare(x.Item2, y.Item2);
}
}
public sealed class ServerFilter
{
public string SearchText { get; set; } = "";
public HashSet<string> Tags { get; } = new();
public bool IsMatchByName(string name)
{
if (string.IsNullOrWhiteSpace(SearchText))
return true;
return name.Contains(SearchText, StringComparison.OrdinalIgnoreCase);
}
public bool IsMatchByTags(IEnumerable<string> itemTags)
{
if (Tags.Count == 0)
return true;
var itemTagSet = new HashSet<string>(itemTags);
return Tags.All(tag => itemTagSet.Contains(tag));
}
public bool IsMatch(string name, IEnumerable<string> itemTags)
{
return IsMatchByName(name) && IsMatchByTags(itemTags);
}
}

View File

@@ -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<ServerLink> Links { get; } = new();
public bool RunVisible => Process == null;
private ServerInfo? _serverInfo = null;
private string _lastError = "";
public async Task<ServerInfo?> GetServerInfo()
{
if (_serverInfo == null)
{
try
{
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
}
catch (Exception e)
{
Description = e.Message;
DebugService.Error(e);
}
}
return _serverInfo;
}
public ObservableCollection<string> 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<ServerInfo?> GetServerInfo()
{
if (_serverInfo == null)
try
{
_serverInfo = await RestService.GetAsync<ServerInfo>(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<LogPopupModelView>();
}
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<ContentBrowserViewModel>().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);
}

View File

@@ -120,7 +120,7 @@
https://cinka.ru/nebula-launcher/
</TextBlock>
</Button>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center">v0.05-a</TextBlock>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center">v0.08-a</TextBlock>
</Panel>
</Label>
</Border>

View File

@@ -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">
<Design.DataContext>
<pages:ServerListViewModel />
@@ -22,7 +23,7 @@
Grid.RowSpan="2"
Margin="5,0,0,10"
Padding="0,0,10,0">
<StackPanel>
<StackPanel Margin="0,0,0,30">
<ItemsControl ItemsSource="{Binding HubErrors}" Margin="10,0,10,0" />
<ItemsControl
IsVisible="{Binding IsFavoriteMode}"
@@ -41,19 +42,8 @@
VerticalAlignment="Bottom"
IsVisible="{Binding IsFilterVisible}">
<StackPanel Orientation="Vertical" Spacing="2" Margin="15">
<StackPanel Orientation="Horizontal" Spacing="15">
<TextBlock Text="Roleplay:" VerticalAlignment="Center" Margin="0,0,15,0"/>
<CheckBox Click="Button_OnClick" Name="rp_none"><TextBlock Text="NonRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_low"><TextBlock Text="LowRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_med"><TextBlock Text="MediumRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="rp_high"><TextBlock Text="HardRP"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="ERPYes"><TextBlock Text="18+"/></CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="15">
<TextBlock Text="Language:" VerticalAlignment="Center" Margin="0,0,4,0"/>
<CheckBox Click="Button_OnClick" Name="lang_ru"><TextBlock Text="RU"/></CheckBox>
<CheckBox Click="Button_OnClick" Name="lang_en"><TextBlock Text="EN"/></CheckBox>
</StackPanel>
<controls:FilterBox Name="EssentialFilters" FilterBoxName="Roleplay" FilterCommand="{Binding OnFilterChanged}"/>
<controls:FilterBox Name="LanguageFilters" FilterBoxName="Language" FilterCommand="{Binding OnFilterChanged}"/>
</StackPanel>
</Border>

View File

@@ -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);
}
}

View File

@@ -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}">
<Design.DataContext>
<viewModels:ServerEntryModelView />
</Design.DataContext>
@@ -234,6 +234,10 @@
IsVisible="{Binding ExpandInfo}"
Margin="5,5,0,0"
Spacing="5">
<Button
Command="{Binding OpenContentViewer}">
<Svg Margin="4" Path="/Assets/svg/folder.svg" />
</Button>
<Button
Command="{Binding StopInstance}"
CornerRadius="10"