diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml index 111cdb0..77363d6 100644 --- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml +++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml @@ -5,9 +5,17 @@ + + + + + + + + diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user index 90d91d6..4ad8656 100644 --- a/Flawless-Version-Control.sln.DotSettings.user +++ b/Flawless-Version-Control.sln.DotSettings.user @@ -17,14 +17,22 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + <AssemblyExplorer> + <Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa\1.10.0\lib\net8.0\Ursa.dll" /> + <Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa.themes.semi\1.10.0\lib\netstandard2.0\Ursa.Themes.Semi.dll" /> +</AssemblyExplorer> <SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> <TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId> diff --git a/Flawless.Client/ApiHelper.cs b/Flawless.Client/ApiHelper.cs deleted file mode 100644 index 765f383..0000000 --- a/Flawless.Client/ApiHelper.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using Flawless.Client.Remote; -using Refit; - -namespace Flawless.Client; - -public static class ApiHelper -{ - private static IFlawlessServer? _gateway; - - public static void ClearGateway() - { - Status = null; - ServerUrl = null; - _gateway = null; - Token = null; - } - - public static async Task SetGatewayAsync(string host) - { - var setting = new RefitSettings - { - AuthorizationHeaderValueGetter = (req, ct) => ApiHelper.Token != null ? - Task.FromResult($"Bearer {ApiHelper.Token}") : Task.FromResult(string.Empty) - }; - - var tempGateway = RestService.For(host, setting); - Status = await tempGateway.Status(); - ServerUrl = host; - _gateway = tempGateway; - } - - public static string? ServerUrl { get; private set; } - - public static ServerStatusResponse? Status { get; private set; } - - public static TokenInfo? Token { get; set; } - - public static bool IsGatewayReady => _gateway != null; - public static IFlawlessServer Gateway => _gateway ?? throw new InvalidProgramException("Not set gateway yet!"); - - - public static bool RequireRefreshToken() - { - if (Token == null) return true; - if (DateTime.UtcNow.AddMinutes(1) > Token.Expiration) return true; - return false; - } - - public static async ValueTask TryRefreshTokenAsync() - { - if (Token == null) return false; - try { Token = await Gateway.Refresh(Token); } - catch (Exception e) - { - await Console.Error.WriteLineAsync(e.ToString()); - return false; - } - - return true; - } -} \ No newline at end of file diff --git a/Flawless.Client/App.axaml b/Flawless.Client/App.axaml index a17c221..6fa1a97 100644 --- a/Flawless.Client/App.axaml +++ b/Flawless.Client/App.axaml @@ -1,15 +1,10 @@ - - - - - - + RequestedThemeVariant="Light"> + - - + + \ No newline at end of file diff --git a/Flawless.Client/App.axaml.cs b/Flawless.Client/App.axaml.cs index 89f3605..502b146 100644 --- a/Flawless.Client/App.axaml.cs +++ b/Flawless.Client/App.axaml.cs @@ -1,8 +1,11 @@ +using System.Reflection; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Flawless.Client.ViewModels; using Flawless.Client.Views; +using ReactiveUI; +using Splat; namespace Flawless.Client; @@ -11,13 +14,14 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); + Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly()); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindowView + desktop.MainWindow = new MainWindowView() { DataContext = new MainWindowViewModel() }; diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj index fdcdf74..78f5a8a 100644 --- a/Flawless.Client/Flawless.Client.csproj +++ b/Flawless.Client/Flawless.Client.csproj @@ -11,13 +11,12 @@ - + - @@ -25,6 +24,9 @@ All + + + @@ -32,6 +34,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Flawless.Client/Program.cs b/Flawless.Client/Program.cs index af0733a..4d42e59 100644 --- a/Flawless.Client/Program.cs +++ b/Flawless.Client/Program.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.ReactiveUI; using System; +using Avalonia.Dialogs; namespace Flawless.Client; @@ -16,6 +17,7 @@ sealed class Program // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseManagedSystemDialogs() .UsePlatformDetect() .WithInterFont() .LogToTrace() diff --git a/Flawless.Client/Service/Api.cs b/Flawless.Client/Service/Api.cs new file mode 100644 index 0000000..ebe81e9 --- /dev/null +++ b/Flawless.Client/Service/Api.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading.Tasks; +using Flawless.Client.Remote; +using ReactiveUI; +using Refit; + +namespace Flawless.Client.Service; + +public class Api +{ + #region Instance + + private static Api? _instance; + + public static Api Current => _instance ??= new Api(); + + #endregion + + public IObservable IsLoggedIn => _isLoggedIn; + + public IObservable ServerUrl => _serverUrl; + + public IObservable Status => _status; + + public IObservable Token => _token; + + private readonly ReactiveProperty _isLoggedIn = new(false); + + private readonly ReactiveProperty _serverUrl = new(string.Empty); + + private readonly ReactiveProperty _status = new(null); + + private readonly ReactiveProperty _token = new(null); + + #region GatewayConfig + + private IFlawlessServer? _gateway; + + public bool IsGatewayReady => _gateway != null; + + public IFlawlessServer Gateway => _gateway ?? throw new InvalidProgramException("Not set gateway yet!"); + + public void ClearGateway() + { + _gateway = null; + _isLoggedIn.Value = false; + _status.Value = null; + _serverUrl.Value = null; + _token.Value = null; + } + + public async Task SetGatewayAsync(string host) + { + var setting = new RefitSettings + { + AuthorizationHeaderValueGetter = (req, ct) => _token.Value != null ? + Task.FromResult($"Bearer {_token}") : Task.FromResult(string.Empty) + }; + + var tempGateway = RestService.For(host, setting); + _status.Value = await tempGateway.Status(); + _serverUrl.Value = host; + _gateway = tempGateway; + } + + public async ValueTask LoginAsync(string username, string password) + { + _token.Value = await _gateway.Login(new LoginRequest + { + Username = username, + Password = password + }); + + _isLoggedIn.Value = true; + } + + #endregion + + #region TokenOperations + + public bool RequireRefreshToken() + { + if (_token.Value == null) return true; + if (DateTime.UtcNow.AddMinutes(1) > _token.Value.Expiration) return true; + return false; + } + + public async ValueTask TryRefreshTokenAsync() + { + if (_token.Value == null) return false; + try { _token.Value = await Gateway.Refresh(_token.Value); } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.ToString()); + return false; + } + + return true; + } + + #endregion +} \ No newline at end of file diff --git a/Flawless.Client/ViewLocator.cs b/Flawless.Client/ViewLocator.cs deleted file mode 100644 index 39a0d91..0000000 --- a/Flawless.Client/ViewLocator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Flawless.Client.ViewModels; - -namespace Flawless.Client; - -public class ViewLocator : IDataTemplate -{ - public Control? Build(object? param) - { - if (param is null) - return null; - - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) - { - return (Control)Activator.CreateInstance(type)!; - } - - return new TextBlock { Text = "Not Found: " + name }; - } - - public bool Match(object? data) - { - return data is ViewModelBase; - } -} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/LoginViewModel.cs b/Flawless.Client/ViewModels/LoginViewModel.cs index c064f5d..cbcdba7 100644 --- a/Flawless.Client/ViewModels/LoginViewModel.cs +++ b/Flawless.Client/ViewModels/LoginViewModel.cs @@ -1,64 +1,56 @@ using System; using System.Reactive; +using System.Reactive.Linq; using System.Threading.Tasks; using Flawless.Client.Remote; +using Flawless.Client.Service; using ReactiveUI; using ReactiveUI.SourceGenerators; using Refit; namespace Flawless.Client.ViewModels; -public partial class LoginViewModel : UserControlViewModelBase +public partial class LoginViewModel : ViewModelBase, IRoutableViewModel { + + public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); + + public IScreen HostScreen { get; } + [Reactive] private string _username = String.Empty; [Reactive] private string _password = String.Empty; - [Reactive] private string _issue = String.Empty; + [Reactive(SetModifier = AccessModifier.Protected)] private string _issue = String.Empty; - private MainWindowViewModel _main; + public IObservable CanLogin; + + public IObservable CanRegister => Api.Current.Status.Select(s => s != null && s.AllowPublicRegister); - public ReactiveCommand LoginCommand { get; } - - public ReactiveCommand RegisterCommand { get; } - - public ReactiveCommand ChooseServerCommand { get; } - - public LoginViewModel(MainWindowViewModel main) + public LoginViewModel(IScreen hostScreen) { - _main = main; - Title = $"Login into '{ApiHelper.ServerUrl}'"; + HostScreen = hostScreen; - var canLogin = this.WhenAnyValue( + CanLogin = this.WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrEmpty(user) && user.Length > 3 && !string.IsNullOrEmpty(pass) && pass.Length >= 6 ); - LoginCommand = ReactiveCommand.CreateFromTask(OnLoginAsync, canLogin); - RegisterCommand = ReactiveCommand.Create(OnRegister); - ChooseServerCommand = ReactiveCommand.Create(OnChooseServer); } - - private void OnChooseServer() + + [ReactiveCommand(CanExecute = nameof(CanRegister))] + private void Register() { - _main.RedirectToServerSetup(); + HostScreen.Router.Navigate.Execute(new RegisterViewModel(HostScreen)); } - private void OnRegister() - { - throw new NotImplementedException(); - } - - private async Task OnLoginAsync() + [ReactiveCommand(CanExecute = nameof(CanLogin))] + private async Task LoginAsync() { try { - ApiHelper.Token = await ApiHelper.Gateway.Login(new LoginRequest - { - Username = _username, - Password = _password - }); + await Api.Current.LoginAsync(Username, Password); } catch (ApiException ex) { @@ -73,4 +65,5 @@ public partial class LoginViewModel : UserControlViewModelBase Console.WriteLine($"Login as '{Username}' success!"); } + } \ No newline at end of file diff --git a/Flawless.Client/ViewModels/MainWindowViewModel.cs b/Flawless.Client/ViewModels/MainWindowViewModel.cs index fc43a40..0efb95d 100644 --- a/Flawless.Client/ViewModels/MainWindowViewModel.cs +++ b/Flawless.Client/ViewModels/MainWindowViewModel.cs @@ -1,25 +1,22 @@ -using ReactiveUI.SourceGenerators; +using System.Reactive; +using System.Reactive.Linq; +using Flawless.Client.Service; +using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace Flawless.Client.ViewModels; -public partial class MainWindowViewModel : ViewModelBase +public partial class MainWindowViewModel : ViewModelBase, IScreen { - [Reactive(SetModifier = AccessModifier.Protected)] - private UserControlViewModelBase _currentView; - + public RoutingState Router { get; } = new RoutingState(); + + public ReactiveCommand GoBackCommand => Router.NavigateBack; + + [Reactive] private bool _requireLogin = true; + public MainWindowViewModel() { - CurrentView = new ServerSetupViewModel(this); - } - - public void RedirectToLogin() - { - CurrentView = new LoginViewModel(this); - } - - public void RedirectToServerSetup() - { - CurrentView = new ServerSetupViewModel(this); + Api.Current.IsLoggedIn.Select(x => !x).BindTo(this, vm => vm.RequireLogin); } } \ No newline at end of file diff --git a/Flawless.Client/ViewModels/RegisterViewModel.cs b/Flawless.Client/ViewModels/RegisterViewModel.cs new file mode 100644 index 0000000..c04a208 --- /dev/null +++ b/Flawless.Client/ViewModels/RegisterViewModel.cs @@ -0,0 +1,62 @@ +using System; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia; +using Flawless.Client.Remote; +using Flawless.Client.Service; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Refit; + +namespace Flawless.Client.ViewModels; + +public partial class RegisterViewModel : ViewModelBase, IRoutableViewModel +{ + + public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); + + public IScreen HostScreen { get; } + + [Reactive] private string _email; + + [Reactive] private string _username; + + [Reactive] private string _password; + + [Reactive(SetModifier = AccessModifier.Protected)] private string _issue; + + public RegisterViewModel(IScreen hostScreen) + { + HostScreen = hostScreen; + } + + + [ReactiveCommand] + private async Task RegisterAsync() + { + try + { + await Api.Current.Gateway.Register(new RegisterRequest + { + Email = _email, + Username = _username, + Password = _password + }); + + + await Api.Current.LoginAsync(Username, Password); + } + catch (ApiException ex) + { + await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}"); + Issue = ex.Content ?? String.Empty; + } + catch (Exception ex) + { + await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}"); + Issue = ex.Message; + } + + Console.WriteLine($"Register as '{Username}' success!"); + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ServerConnectViewModel.cs b/Flawless.Client/ViewModels/ServerConnectViewModel.cs new file mode 100644 index 0000000..7d7b707 --- /dev/null +++ b/Flawless.Client/ViewModels/ServerConnectViewModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; +using Flawless.Client.Service; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Ursa.ReactiveUIExtension; + +namespace Flawless.Client.ViewModels; + +public partial class ServerConnectViewModel : ViewModelBase, IScreen +{ + public RoutingState Router { get; } = new RoutingState(); + + public ReactiveCommand GoBackCommand => Router.NavigateBack; + + [Reactive] + private string _title = String.Empty; + + [ReactiveCommand] + private async ValueTask OpenRepoPageAsync() + { + if (Api.Current.RequireRefreshToken()) await Router.NavigateAndReset.Execute(new ServerSetupViewModel(this)).ToTask(); + } + + public ServerConnectViewModel() + { + Router.CurrentViewModel + .Select(vm => vm?.GetType().Name.Replace("ViewModel", string.Empty) ?? "Hello") + .BindTo(this, vm => vm.Title); + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ServerSetupViewModel.cs b/Flawless.Client/ViewModels/ServerSetupViewModel.cs index 00dafea..f1d14e8 100644 --- a/Flawless.Client/ViewModels/ServerSetupViewModel.cs +++ b/Flawless.Client/ViewModels/ServerSetupViewModel.cs @@ -2,47 +2,45 @@ using System.Reactive; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Flawless.Client.Service; using ReactiveUI; using ReactiveUI.SourceGenerators; using Refit; namespace Flawless.Client.ViewModels; -public partial class ServerSetupViewModel : UserControlViewModelBase +public partial class ServerSetupViewModel : ViewModelBase, IRoutableViewModel { + + public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); + + public IScreen HostScreen { get; } + [Reactive] private string _host = "http://localhost:5256/"; - [Reactive] private string? _issue; + [Reactive(SetModifier = AccessModifier.Protected)] private string? _issue; - private MainWindowViewModel _main; + public IObservable CanSetHost { get; } - public ReactiveCommand SetHostCommand { get; } - - - private static readonly Regex httpRegex = new Regex( - @"^(?i)https?://(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(/[\w\-\.~%!$&'()*+,;=:@/?#]*)?(?-i)$", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - - public ServerSetupViewModel(MainWindowViewModel main) + public ServerSetupViewModel(IScreen hostScreen) { - _main = main; - Title = "Connect to a Flawless Server"; + HostScreen = hostScreen; // Must clear this gateway - if (ApiHelper.IsGatewayReady) ApiHelper.ClearGateway(); - - SetHostCommand = ReactiveCommand.CreateFromTask( - FetchServerDataAsync, - this.WhenAnyValue(x => x.Host, s => !string.IsNullOrWhiteSpace(s))); + if (Api.Current.IsGatewayReady) Api.Current.ClearGateway(); + + CanSetHost = this.WhenAnyValue(x => x.Host, s => !string.IsNullOrWhiteSpace(s)); } - private async Task FetchServerDataAsync() + + [ReactiveCommand] + private async Task SetHostAsync() { try { Issue = string.Empty; - await ApiHelper.SetGatewayAsync(Host); - _main.RedirectToLogin(); + await Api.Current.SetGatewayAsync(Host); + HostScreen.Router.Navigate.Execute(new LoginViewModel(HostScreen)); } catch (ApiException ex) { diff --git a/Flawless.Client/ViewModels/ViewModelBase.cs b/Flawless.Client/ViewModels/ViewModelBase.cs index d95bd26..229260c 100644 --- a/Flawless.Client/ViewModels/ViewModelBase.cs +++ b/Flawless.Client/ViewModels/ViewModelBase.cs @@ -1,15 +1,5 @@ -using Avalonia.Controls; -using ReactiveUI; -using ReactiveUI.SourceGenerators; +using ReactiveUI; namespace Flawless.Client.ViewModels; -public abstract class ViewModelBase : ReactiveObject -{ -} - -public abstract partial class UserControlViewModelBase : ViewModelBase -{ - [Reactive(SetModifier = AccessModifier.Protected)] - private string _title; -} +public abstract class ViewModelBase : ReactiveObject {} diff --git a/Flawless.Client/Views/LoginView.axaml b/Flawless.Client/Views/LoginView.axaml index e968f9d..70efd37 100644 --- a/Flawless.Client/Views/LoginView.axaml +++ b/Flawless.Client/Views/LoginView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:Flawless.Client.ViewModels" - mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400" + mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540" x:Class="Flawless.Client.Views.LoginView" x:DataType="vm:LoginViewModel"> @@ -14,13 +14,11 @@ - - +