1
0

feat: Add features created by server.

This commit is contained in:
Ca2didi 2025-04-21 02:47:18 +08:00
parent e88a483025
commit e9a986058d
26 changed files with 680 additions and 45 deletions

View File

@ -6,6 +6,8 @@
<entry key="Flawless.Client.Avanonia/Views/MainWindow.axaml" value="Flawless.Client.Avanonia/Flawless.Client.Avanonia.csproj" />
<entry key="Flawless.Client/App.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Theme/ToggleSwitch.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloSetup/FirsetSetipViewModel.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloSetup/FirstSetupView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloSetup/LoginPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloSetup/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
@ -24,7 +26,9 @@
<entry key="Flawless.Client/Views/ModalBox/EditRepositoryMemberDialogueView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/MergeDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/PasswordChangeDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/UserCreateDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RegisterView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/IssueEditDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />

View File

@ -31,9 +31,11 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIReactiveObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5a_003F1516290d_003FIReactiveObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALoggerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1b4c09e663de4a9ea6963d356da7095e13a00_003Ffa_003F8ad0b278_003FLoggerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALogLevel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd0319a0b2a6743ffb27031fdd18a267a1f000_003F81_003Fe3a61b51_003FLogLevel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5c_003F228dd86b_003FReactiveObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaView_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003F3c_003F7b186404_003FReactiveUrsaView_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaView_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003Fde_003F4660a705_003FReactiveUrsaView_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReactiveUrsaWindow_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01a3316df1f14aff8404556f39e0d9e52200_003F1f_003F9c292772_003FReactiveUrsaWindow_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARefitSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa2b3a0b86cd053ed984c171eb448b551b9a2a0c89c5a68191996cffac5d85d2b_003FRefitSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AScopedLoggerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003Fb3_003Fdcbe23f1_003FScopedLoggerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

View File

@ -8,4 +8,14 @@ public partial class AppSettingModel : ReactiveModel
{
[Reactive, NonSerialized]
private string _repositoryPath = AppDefaultValues.DefaultRepositoryDirectory;
[Reactive, NonSerialized] private bool
_refreshWorkspaceOnOpen = true,
_refreshWorkspaceOnFilesystemChanges = true;
[Reactive, NonSerialized] private string _diffTool;
[Reactive, NonSerialized] private string _fileManagerTool;
}

View File

@ -30,4 +30,6 @@ public partial class UserModel : ReactiveModel
[Reactive] private string _phoneNumber;
[Reactive] private DateTime _joinDate;
[Reactive] private bool _isAdmin;
}

View File

@ -32,7 +32,7 @@ public class Api : BaseService<Api>
public IObservable<string?> ServerUrl => _serverUrl;
public IObservable<ServerStatusResponse?> Status => _status;
public IReactiveProperty<ServerStatusResponse?> Status => _status;
public IObservable<TokenInfo?> Token => _token;

View File

@ -818,7 +818,7 @@ namespace Flawless.Client.Remote
public string NickName { get; set; }
[JsonPropertyName("gender")]
public UserSex Gender { get; set; }
public UserSex? Gender { get; set; }
[JsonPropertyName("bio")]
public string Bio { get; set; }

View File

@ -15,8 +15,19 @@ public class SettingService : BaseService<SettingService>
public static string SettingFilePath { get; } =
Path.Combine(AppDefaultValues.ProgramDataDirectory, "settings.json");
public AppSettingModel AppSetting { get; } = new AppSettingModel();
public AppSettingModel AppSetting { get; private set; } = new AppSettingModel();
public ValueTask ResetAsync()
{
AppSetting.RepositoryPath = AppDefaultValues.DefaultRepositoryDirectory;
AppSetting.RefreshWorkspaceOnOpen = true;
AppSetting.RefreshWorkspaceOnFilesystemChanges = true;
AppSetting.DiffTool = "";
AppSetting.FileManagerTool = "";
return WriteToDiskAsync();
}
public async ValueTask WriteToDiskAsync()
{
var stream = File.Exists(SettingFilePath) ? File.OpenWrite(SettingFilePath) : File.Create(SettingFilePath);

View File

@ -86,7 +86,7 @@ public static class UIHelper
}
}
public static void NotifyError(string title, string content)
public static void NotifyError(string content, string title = "Error")
{
try
{
@ -146,4 +146,17 @@ public static class UIHelper
var vm = new SimpleMessageDialogViewModel(content);
return OverlayDialog.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
}
public static void NotifySuccess(string content, string title = "Success")
{
try
{
var nf = new Notification(title, content, NotificationType.Success);
Notify.Show(nf);
}
catch (Exception e)
{
Console.WriteLine($"Can not notify success to users: {title} - {content}, {e}");
}
}
}

View File

@ -0,0 +1,60 @@
using System;
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 FirstSetupViewModel : 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;
public FirstSetupViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
}
[ReactiveCommand]
private async Task SetupAsync()
{
try
{
using var l = UIHelper.MakeLoading("Setup...");
await Api.C.Gateway.FirstSetup(new FirstSetupRequest()
{
AdminEmail = Email,
AdminUsername = Username,
AdminPassword = Password
});
await Api.C.LoginAsync(Username, Password);
Api.C.Status.Value!.RequireInitialization = false;
}
catch (ApiException ex)
{
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}");
UIHelper.NotifyError(ex);
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}");
UIHelper.NotifyError(ex);
}
Console.WriteLine($"Register as '{Username}' success!");
}
}

View File

@ -37,7 +37,7 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId, false))
{
if (quitIfFailed) await NavigateBackAsync();
UIHelper.NotifyError("Operation failed", "Can not load issue...");
UIHelper.NotifyError("Can not load issue...", "Operation failed");
return;
}
@ -80,7 +80,7 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
using var _ = UIHelper.MakeLoading("Update issue...");
if (string.IsNullOrWhiteSpace(NewComment))
{
UIHelper.NotifyError("No input", "No comment has been written!");
UIHelper.NotifyError("No comment has been written!", "No input");
}
else
{

View File

@ -17,9 +17,9 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
public IScreen HostScreen { get; }
[Reactive] private string _username = "cardidi";
[Reactive] private string _username;
[Reactive] private string _password = "4453A2b33";
[Reactive] private string _password;
public IObservable<bool> CanLogin;

View File

@ -0,0 +1,20 @@
using System;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels.ModalBox;
public partial class PasswordChangeDialogViewModel : ReactiveObject
{
[Reactive] public string OldPassword { get; set; } = string.Empty;
[Reactive] public string NewPassword { get; set; } = string.Empty;
[Reactive] public string ConfirmPassword { get; set; } = string.Empty;
public bool Validate()
{
return !string.IsNullOrEmpty(NewPassword)
&& NewPassword == ConfirmPassword
&& !string.IsNullOrEmpty(OldPassword);
}
}

View File

@ -0,0 +1,19 @@
using System;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels.ModalBox;
public partial class UserCreateDialogViewModel : ReactiveObject
{
[Reactive] public string Username { get; set; } = string.Empty;
[Reactive] public string Password { get; set; } = string.Empty;
[Reactive] public string Email { get; set; } = string.Empty;
public bool Validate()
{
return !string.IsNullOrWhiteSpace(Username)
&& !string.IsNullOrWhiteSpace(Password)
&& !string.IsNullOrWhiteSpace(Email);
}
}

View File

@ -580,7 +580,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
return;
}
@ -599,7 +599,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
return;
}
@ -615,13 +615,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
UIHelper.NotifyError("Invalid role level!", "Permission issue");
return;
}
if (vm.SafeRole == member.Role)
{
UIHelper.NotifyError("Modification issue", "No modification yet.");
UIHelper.NotifyError("No modification yet.", "Modification issue");
return;
}
@ -636,7 +636,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
return;
}
@ -649,14 +649,14 @@ public partial class RepositoryViewModel : RoutableViewModelBase
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
UIHelper.NotifyError("Invalid role level!", "Permission issue");
return;
}
vm.Username = vm.Username.Trim();
if (string.IsNullOrEmpty(vm.Username) || vm.Username.Length < 3)
{
UIHelper.NotifyError("Parameter error", "Not a valid username!");
UIHelper.NotifyError("Not a valid username!", "Parameter error");
return;
}

View File

@ -33,7 +33,10 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase
{
using var l = UIHelper.MakeLoading("Contacting to server...");
await Api.C.SetGatewayAsync(Host);
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
if (Api.C.Status.Value.RequireInitialization)
HostScreen.Router.Navigate.Execute(new FirstSetupViewModel(HostScreen));
else HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
}
catch (ApiException ex)
{

View File

@ -1,12 +1,372 @@
using System;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Threading;
using DynamicData;
using Flawless.Client.Models;
using Flawless.Client.Remote;
using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Refit;
using Ursa.Controls;
namespace Flawless.Client.ViewModels;
public class SettingViewModel : RoutableViewModelBase
public record Log(DateTime Time, Microsoft.Extensions.Logging.LogLevel Level, string Message)
{
public static Log From(LogEntryResponse response) => new(
response.Timestamp.LocalDateTime,
Enum.Parse<Microsoft.Extensions.Logging.LogLevel>(response.Level),
response.Message);
}
public partial class SettingViewModel : RoutableViewModelBase
{
public ObservableCollection<UserModel> Users { get; } = new();
public ObservableCollection<Log> Logs { get; } = new();
public AppSettingModel SettingModel => SettingService.C.AppSetting;
[Reactive] private UserModel _loginUser;
[Reactive] private string _nickname;
[Reactive] private string _email;
[Reactive] private string _phoneNumber;
[Reactive] private UserModel.SexType _gender;
[Reactive] private string _bio;
[Reactive] private DateTime?
_logSearchFrom = null,
_logSearchTo = null;
[Reactive] private int _page = 1;
[Reactive] private int _pageSize = 50;
[Reactive] private Microsoft.Extensions.Logging.LogLevel _loglevel = Microsoft.Extensions.Logging.LogLevel.Information;
[Reactive] private string _serverBlacklist, _serverWhitelist;
public SettingViewModel(IScreen hostScreen) : base(hostScreen)
{
LoadClientSettings();
}
private async Task LoadServerData()
{
try
{
var sb = new StringBuilder();
ServerBlacklist = sb.AppendJoin(",\n", await Api.C.Gateway.IpWhitelistGet()).ToString();
ServerBlacklist = sb.Clear().AppendJoin(",\n", await Api.C.Gateway.IpBlacklistGet()).ToString();
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
private async Task LoadClientSettings()
{
LoginUser = (await UserService.C.GetOrDownloadUserInfoAsync(Api.C.Username.Value!))!;
Nickname = LoginUser.Nickname;
Email = LoginUser.Email;
PhoneNumber = LoginUser.PhoneNumber;
Gender = LoginUser.Sex;
Bio = LoginUser.Bio;
if (LoginUser.IsAdmin) await LoadServerData();
}
[ReactiveCommand]
private async Task SaveAccountChangesAsync()
{
try
{
await Api.C.Gateway.UpdateInfo(new UserInfoModifyResponse
{
NickName = this.Nickname == LoginUser.Nickname ? null! : this.Nickname,
Gender = this.Gender == LoginUser.Sex ? null : (UserSex) this.Gender,
Bio = this.Bio == LoginUser.Bio ? null! : this.Bio
});
if (Email != LoginUser.Email)
await Api.C.Gateway.UpdateEmail(new UserContactModifyResponse
{ Email = Email, });
if (PhoneNumber != LoginUser.PhoneNumber)
await Api.C.Gateway.UpdatePhone(new UserContactModifyResponse
{ Phone = PhoneNumber, });
UIHelper.NotifySuccess("Saved");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
[ReactiveCommand]
private async Task ChangePasswordAsync()
{
var result = new PasswordChangeDialogViewModel();
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
while (true)
{
var r = await OverlayDialog.ShowModal<PasswordChangeDialogView, PasswordChangeDialogViewModel>(result, AppDefaultValues.HostId, opt);
if (r == DialogResult.No) return;
if (result.Validate()) break;
UIHelper.NotifyError("Please check your input");
}
try
{
await Api.C.Gateway.ResetPassword(new ResetPasswordRequest()
{
Identity = LoginUser.Username,
NewPassword = result.NewPassword,
OldPassword = result.OldPassword
});
UIHelper.NotifySuccess("Password create successfully");
}
catch (ApiException ex)
{
UIHelper.NotifyError(ex);
}
}
[ReactiveCommand]
private async Task DeleteSelfAccountAsync()
{
if (await UIHelper.SimpleAskAsync("This operation can not undo. Do you sure that?") == DialogResult.Yes)
{
try
{
await Api.C.Gateway.Delete(Api.C.Username.Value!);
Process.GetCurrentProcess().Kill();
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
}
[ReactiveCommand]
private async Task SaveClientPreferenceAsync()
{
try
{
await SettingService.C.WriteToDiskAsync();
UIHelper.NotifySuccess("Saved Success!");
}
catch (Exception e)
{
UIHelper.NotifyError(e);
}
}
[ReactiveCommand]
private async Task SaveServerPreferenceAsync()
{
try
{
await Api.C.Gateway.IpWhitelistPost(ServerWhitelist.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
await Api.C.Gateway.IpBlacklistPost(ServerBlacklist.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
UIHelper.NotifySuccess("Saved Success!");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
[ReactiveCommand]
private async Task ResetClientPreferenceAsync()
{
if (await UIHelper.SimpleAskAsync("Do you sure reset settings to default?") == DialogResult.Yes)
{
await SettingService.C.ResetAsync();
}
}
[ReactiveCommand]
private async Task ResetServerPreferenceAsync()
{
if (await UIHelper.SimpleAskAsync("Do you sure reset settings to default?") == DialogResult.Yes)
{
try
{
ServerBlacklist = ServerWhitelist = string.Empty;
await Api.C.Gateway.IpBlacklistPost([]);
await Api.C.Gateway.IpWhitelistPost([]);
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
}
[ReactiveCommand]
private async Task CreateUserAsync()
{
var result = new UserCreateDialogViewModel();
result.Password = GenerateRandomPassword();
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
while (true)
{
var r = await OverlayDialog.ShowModal<UserCreateDialogView, UserCreateDialogViewModel>(result, AppDefaultValues.HostId, opt);
if (r == DialogResult.No) return;
if (result.Validate()) break;
UIHelper.NotifyError("Please check your input");
}
try
{
await Api.C.Gateway.Register(new RegisterRequest()
{
Username = result.Username,
Password = result.Password,
Email = result.Email
});
Users.Add(UserService.C.GetUserInfoAsync(result.Username)!);
UIHelper.NotifySuccess($"User {result.Username} create successfully");
}
catch (ApiException ex)
{
UIHelper.NotifyError(ex);
}
}
[ReactiveCommand]
private async Task DeleteUserAsync(string username)
{
if (await UIHelper.SimpleAskAsync($"Do you sure that wanted to delete user '{username}' permanently?") == DialogResult.Yes)
{
try
{
await Api.C.Gateway.Delete(username);
Users.Remove(Users.First(u => u.Username == username));
UIHelper.NotifySuccess("User deleted");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
}
[ReactiveCommand]
private async Task ForceUpdateUserPasswordAsync(string username)
{
var newPassword = GenerateRandomPassword();
try
{
await Api.C.Gateway.RenewPassword(new ResetPasswordRequest
{
Identity = username,
NewPassword = newPassword
});
await UIHelper.SimpleAlert($"Password has been reset to {newPassword}");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
[ReactiveCommand]
private async Task ActivateUserAsync(string username)
{
await UpdateUserActivateAsync(username, true);
}
[ReactiveCommand]
private async Task InactivateUserAsync(string username)
{
await UpdateUserActivateAsync(username, false);
}
[ReactiveCommand]
private async Task ToSuperUserAsync(string username)
{
await UpdateUserSuperAsync(username, true);
}
[ReactiveCommand]
private async Task ToNormalUserAsync(string username)
{
await UpdateUserSuperAsync(username, false);
}
[ReactiveCommand]
private async Task DownloadServerLogAsync()
{
try
{
var logs = await Api.C.Gateway.Logs(
LogSearchFrom, LogSearchTo, (LogLevel?)Loglevel, Page, PageSize);
Logs.AddRange(logs.Select(Log.From));
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
private async Task UpdateUserActivateAsync(string username, bool active)
{
try
{
if (active) await Api.C.Gateway.Enable(username);
else await Api.C.Gateway.Disable(username);
UIHelper.NotifySuccess($"{username} has already {(active ? "enabled" : "disabled")}.");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
private async Task UpdateUserSuperAsync(string username, bool active)
{
try
{
await Api.C.Gateway.SuperuserPost(username, active);
Users.First(x => x.Username == username).IsAdmin = active;
UIHelper.NotifySuccess($"{username} has already {(active ? "enabled" : "disabled")}.");
}
catch (Exception ex)
{
UIHelper.NotifyError(ex);
}
}
private static string GenerateRandomPassword(int length = 12)
{
const string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*";
var random = new Random();
return new string(Enumerable.Repeat(validChars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
}

View File

@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Flawless.Client.ViewModels"
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540"
x:Class="Flawless.Client.Views.HelloSetup.FirstSetupView"
x:DataType="vm:FirstSetupViewModel">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:RegisterPageViewModel/>
</Design.DataContext>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="10">
<TextBox Watermark="Email" Text="{Binding Email, Mode=TwoWay}"/>
<TextBox Watermark="Username" Text="{Binding Username, Mode=TwoWay}"/>
<TextBox Watermark="Password" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="{DynamicResource Semi}">
<Button Content="Register" Command="{Binding SetupCommand}"/>
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Flawless.Client.ViewModels;
using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views.HelloSetup;
public partial class FirstSetupView : ReactiveUrsaView<FirstSetupViewModel>
{
public FirstSetupView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
x:Class="Flawless.Client.Views.ModalBox.PasswordChangeDialogView"
x:DataType="vm:PasswordChangeDialogViewModel">
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto">
<TextBox Watermark="Old Password"
PasswordChar="*"
Text="{Binding OldPassword}"/>
<TextBox Grid.Row="1" Watermark="New Password"
PasswordChar="*"
Text="{Binding NewPassword}"/>
<TextBox Grid.Row="2" Watermark="Confirm New Password"
PasswordChar="*"
Text="{Binding ConfirmPassword}"/>
</Grid>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Flawless.Client.Views.ModalBox;
public partial class PasswordChangeDialogView : UserControl
{
public PasswordChangeDialogView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
x:Class="Flawless.Client.Views.ModalBox.UserCreateDialogView"
x:DataType="vm:UserCreateDialogViewModel">
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto">
<TextBox Grid.Row="0" Watermark="Username" Text="{Binding Username}"/>
<TextBox Grid.Row="1" Watermark="Password"
PasswordChar="*" Text="{Binding Password}"/>
<TextBox Grid.Row="2" Watermark="Email" Text="{Binding Email}"/>
</Grid>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Flawless.Client.Views.ModalBox;
public partial class UserCreateDialogView : UserControl
{
public UserCreateDialogView()
{
InitializeComponent();
}
}

View File

@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:semi="https://irihi.tech/semi"
xmlns:vm="using:Flawless.Client.ViewModels"
x:DataType="vm:SettingViewModel"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
@ -19,18 +18,20 @@
<TabItem Header="Account">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch" LabelPosition="Top">
<Label Content="You are a server manager!"/>
<TextBox u:FormItem.Label="Username" IsReadOnly="True"/>
<TextBox u:FormItem.Label="Nickname" IsReadOnly="True"/>
<TextBox u:FormItem.Label="Email" IsReadOnly="True"/>
<TextBox u:FormItem.Label="Phone Number" IsReadOnly="True"/>
<ComboBox u:FormItem.Label="Gender">
<ComboBoxItem Content="Unset"/>
<ComboBoxItem Content="Male"/>
<ComboBoxItem Content="Female"/>
<ComboBoxItem Content="Walmart Plastic Bag"/>
</ComboBox>
<TextBox u:FormItem.Label="Bio" Classes="TextArea"/>
<Label IsVisible="{Binding LoginUser.IsAdmin}" Content="You are a server manager!"/>
<TextBox u:FormItem.Label="Username" IsReadOnly="True"
Text="{Binding LoginUser.Username, Mode=OneWay}"/>
<TextBox u:FormItem.Label="Nickname" Text="{Binding Nickname}"/>
<TextBox u:FormItem.Label="Email" Text="{Binding Email}"/>
<TextBox u:FormItem.Label="Phone Number" Text="{Binding PhoneNumber}"/>
<u:EnumSelector u:FormItem.Label="Gender" SelectedValue="{Binding Gender}"/>
<TextBox u:FormItem.Label="Bio" Classes="TextArea" Text="{Binding Bio}"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:IconButton Content="Save" Command="{Binding SaveAccountChangesCommand}"/>
<u:IconButton Content="Change Password" Command="{Binding ChangePasswordCommand}"/>
<u:IconButton Classes="Danger" Content="Delete" Command="{Binding DeleteSelfAccountCommand}"/>
</StackPanel>
</u:Form>
</ScrollViewer>
</TabItem>
@ -42,41 +43,70 @@
</u:FormItem>
<u:FormGroup Header="Workspace Refresh">
<StackPanel Spacing="4">
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace"/>
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed"/>
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Gain Focus"/>
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace"
IsChecked="{Binding SettingModel.RefreshWorkspaceOnOpen}"/>
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed"
IsChecked="{Binding SettingModel.RefreshWorkspaceOnFilesystemChanges}"/>
</StackPanel>
</u:FormGroup>
<u:FormGroup Header="Extern Tools">
<u:PathPicker u:FormItem.Label="Open Folder"/>
<u:PathPicker u:FormItem.Label="Diff Tool"/>
</u:FormGroup>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:IconButton Content="Save" Command="{Binding SaveClientPreferenceCommand}"/>
<u:IconButton Classes="Danger" Content="Reset" Command="{Binding ResetClientPreferenceCommand}"/>
</StackPanel>
</u:Form>
</ScrollViewer>
</TabItem>
<TabItem Header="Server">
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Server">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBox u:FormItem.Label="Server Public Name"/>
<ToggleSwitch u:FormItem.Label="Allow Public Register"/>
<!-- <TextBox u:FormItem.Label="Server Public Name"/> -->
<!-- <ToggleSwitch u:FormItem.Label="Allow Public Register"/> -->
<u:FormGroup>
<TextBox u:FormItem.Label="IP Whitelist" Classes="TextArea"/>
<TextBox u:FormItem.Label="IP Blacklist" Classes="TextArea"/>
<TextBox u:FormItem.Label="IP Whitelist" Classes="TextArea" AcceptsReturn="True"
Text="{Binding ServerWhitelist}"/>
<TextBox u:FormItem.Label="IP Blacklist" Classes="TextArea" AcceptsReturn="True"
Text="{Binding ServerBlacklist}"/>
</u:FormGroup>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:IconButton Content="Save" Command="{Binding SaveServerPreferenceCommand}"/>
<u:IconButton Classes="Danger" Content="Reset"
Command="{Binding ResetServerPreferenceCommand}"/>
</StackPanel>
</u:Form>
</ScrollViewer>
</TabItem>
<TabItem Header="Users">
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Users">
</TabItem>
<TabItem Header="Server Logfile">
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Logfile">
<StackPanel Spacing="8" Orientation="Vertical" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Spacing="4">
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"/>
<DatePicker SelectedDate="{Binding LogSearchFrom}"/>
<DatePicker SelectedDate="{Binding LogSearchTo}"/>
</StackPanel>
<TextBox IsReadOnly="True" Classes="TextArea Bordered" VerticalContentAlignment="Stretch" VerticalAlignment="Stretch"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:EnumSelector SelectedValue="{Binding Loglevel}"/>
<NumericUpDown Value="{Binding PageSize}" Minimum="10" Maximum="100"/>
<NumericUpDown Value="{Binding Page}" Minimum="1"/>
<u:IconButton Icon="{StaticResource SemiIconSearch}"
Command="{Binding DownloadServerLogCommand}"/>
</StackPanel>
<ListBox ItemsSource="{Binding Logs}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto,*,Auto">
<Label Grid.Row="0" Grid.Column="0" FontSize="16" Content="{Binding Level}"/>
<Label Grid.Row="0" Grid.Column="2" FontSize="16" Content="{Binding Time}"/>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding Message}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</TabItem>
<TabItem Header="Server Statitics"></TabItem>
</TabControl>
</DockPanel>
</UserControl>

View File

@ -21,4 +21,6 @@ public record UserInfoResponse
public bool? PublicEmail { get; set; }
public DateTime? CreatedAt { get; set; }
public bool? IsAdmin { get; set; }
}

View File

@ -133,7 +133,7 @@ public class AdminController(
query = query.Where(l => l.Timestamp <= endTime);
// 日志级别过滤
if (level.HasValue)
if (level.HasValue && level.Value != LogLevel.None)
query = query.Where(l => l.LogLevel == level.Value);
// 分页处理

View File

@ -128,6 +128,7 @@ public class UserController(
PublicEmail = authorized ? queryUser.PublicEmail : null,
Email = queryUser.PublicEmail || authorized ? queryUser.Email : null,
Phone = authorized ? queryUser.PhoneNumber : null,
IsAdmin = queryUser.Admin
};
}
}