diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml index 6de3b67..0c9d2dd 100644 --- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml +++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml @@ -6,6 +6,8 @@ + + @@ -13,6 +15,7 @@ + diff --git a/Flawless.Client/AppDefaultValues.cs b/Flawless.Client/AppDefaultValues.cs new file mode 100644 index 0000000..1e6362b --- /dev/null +++ b/Flawless.Client/AppDefaultValues.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace Flawless.Client; + +public static class AppDefaultValues +{ + public const string HostId = "Overlay"; + + public static string ProgramDataDirectory { get; } = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ca2dWorks", "FlawlessClient"); + + public static string DefaultRepositoryDirectory { get; } = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "FlawlessRepositories"); +} \ No newline at end of file diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj index 8c78081..744b21e 100644 --- a/Flawless.Client/Flawless.Client.csproj +++ b/Flawless.Client/Flawless.Client.csproj @@ -29,6 +29,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -38,10 +39,21 @@ - + - + + LoginPageView.axaml + Code + + + RegisterPageView.axaml + Code + + + ServerSetupPageView.axaml + Code + diff --git a/Flawless.Client/Models/AppSettingModel.cs b/Flawless.Client/Models/AppSettingModel.cs new file mode 100644 index 0000000..95a7b4a --- /dev/null +++ b/Flawless.Client/Models/AppSettingModel.cs @@ -0,0 +1,11 @@ +using System; +using ReactiveUI.SourceGenerators; + +namespace Flawless.Client.Models; + +[Serializable] +public partial class AppSettingModel : ReactiveModel +{ + [Reactive, NonSerialized] + private string _repositoryPath = AppDefaultValues.DefaultRepositoryDirectory; +} \ No newline at end of file diff --git a/Flawless.Client/Models/CommitModel.cs b/Flawless.Client/Models/CommitModel.cs new file mode 100644 index 0000000..123cd93 --- /dev/null +++ b/Flawless.Client/Models/CommitModel.cs @@ -0,0 +1,6 @@ +namespace Flawless.Client.Models; + +public record CommitModel : ReactiveRecordModel +{ + +} \ No newline at end of file diff --git a/Flawless.Client/Models/ReactiveModel.cs b/Flawless.Client/Models/ReactiveModel.cs new file mode 100644 index 0000000..6f57667 --- /dev/null +++ b/Flawless.Client/Models/ReactiveModel.cs @@ -0,0 +1,7 @@ +using ReactiveUI; + +namespace Flawless.Client.Models; + +public abstract class ReactiveModel : ReactiveObject {} + +public abstract record ReactiveRecordModel : ReactiveRecord {} \ No newline at end of file diff --git a/Flawless.Client/Models/RepositoryHomePageModel.cs b/Flawless.Client/Models/RepositoryHomePageModel.cs deleted file mode 100644 index 33b4862..0000000 --- a/Flawless.Client/Models/RepositoryHomePageModel.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Flawless.Communication.Response; -using Flawless.Communication.Shared; - -namespace Flawless.Client.Models; - -public record RepositoryHomePageModel( - string OwnerName, - string Name, - string Description, - bool IsArchived, - bool IsOwner, - bool IsLocalAvailable, - string LatestCommitId) -{ - public string FullName { get; } = $"{OwnerName}/{Name}"; - - public static RepositoryHomePageModel FromResponse(RepositoryInfoResponse r) - { - return new RepositoryHomePageModel( - r.OwnerUsername, - r.RepositoryName, - r.Description ?? String.Empty, - r.IsArchived, - r.Role == RepositoryRole.Owner, - true, - r.LatestCommitId.ToString().Substring(0, 6)); - } -} \ No newline at end of file diff --git a/Flawless.Client/Models/RepositoryModel.cs b/Flawless.Client/Models/RepositoryModel.cs new file mode 100644 index 0000000..c4e2671 --- /dev/null +++ b/Flawless.Client/Models/RepositoryModel.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.ObjectModel; +using Flawless.Client.Remote; +using ReactiveUI.SourceGenerators; + +namespace Flawless.Client.Models; + +public partial class RepositoryModel : ReactiveModel +{ + public static string GetStandaloneName(string name, string ownerName) + { + return $"{ownerName}/{name}"; + } + + [Reactive] private string _standaloneName; + + [Reactive] private string _ownerName; + + [Reactive] private string _name; + + [Reactive] private bool _archived; + + [Reactive] private string _description; + + [Reactive] private bool _ownByCurrentUser; + + [Reactive] private bool _isDownloaded; + + public ObservableCollection Members { get; } = new(); + + public ObservableCollection Commits { get; } = new(); + + public ObservableCollection Depots { get; } = new(); + + public ObservableCollection Locks { get; } = new(); + + public enum RepositoryRole + { + Guest = 0, + Reporter = 1, + Developer = 2, + Owner = 3, + } + + public partial class Member : ReactiveModel + { + [Reactive] private string _username; + + [Reactive] private RepositoryRole _role; + } + + public partial class Commit : ReactiveModel + { + [Reactive] private Guid _commitId; + + [Reactive] private string _author; + + [Reactive] private DateTime _committedOn; + + [Reactive] private string _message; + + [Reactive] private Guid _depotId; + } + + public partial class Lock : ReactiveModel + { + [Reactive] private string _path; + + [Reactive] private string _ownerName; + } + + public partial class Depot : ReactiveModel + { + [Reactive] private Guid _depotId; + + [Reactive] private long _length; + + public ObservableCollection Dependencies { get; } = new(); + } +} diff --git a/Flawless.Client/Models/UserModel.cs b/Flawless.Client/Models/UserModel.cs new file mode 100644 index 0000000..731443f --- /dev/null +++ b/Flawless.Client/Models/UserModel.cs @@ -0,0 +1,6 @@ +namespace Flawless.Client.Models; + +public record UserModel : ReactiveRecordModel +{ + +} \ No newline at end of file diff --git a/Flawless.Client/Service/Api.cs b/Flawless.Client/Service/Api.cs index ebe81e9..f716a17 100644 --- a/Flawless.Client/Service/Api.cs +++ b/Flawless.Client/Service/Api.cs @@ -1,4 +1,7 @@ using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Flawless.Client.Remote; using ReactiveUI; @@ -6,15 +9,24 @@ using Refit; namespace Flawless.Client.Service; -public class Api +public class Api : BaseService { - #region Instance + class ApiAuthenticationHandler : DelegatingHandler + { + public ApiAuthenticationHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } - private static Api? _instance; - - public static Api Current => _instance ??= new Api(); + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var tk = Api.C._token.Value; + if (tk != null) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tk.Token); + return base.SendAsync(request, cancellationToken); + } - #endregion + } public IObservable IsLoggedIn => _isLoggedIn; @@ -49,12 +61,11 @@ public class Api _token.Value = null; } - public async Task SetGatewayAsync(string host) + public async ValueTask SetGatewayAsync(string host) { - var setting = new RefitSettings + var setting = new RefitSettings() { - AuthorizationHeaderValueGetter = (req, ct) => _token.Value != null ? - Task.FromResult($"Bearer {_token}") : Task.FromResult(string.Empty) + HttpMessageHandlerFactory = () => new ApiAuthenticationHandler(new HttpClientHandler()) }; var tempGateway = RestService.For(host, setting); diff --git a/Flawless.Client/Service/BaseService.cs b/Flawless.Client/Service/BaseService.cs new file mode 100644 index 0000000..14a1aba --- /dev/null +++ b/Flawless.Client/Service/BaseService.cs @@ -0,0 +1,10 @@ +namespace Flawless.Client.Service; + +public class BaseService where TService : class, new() +{ + private static TService? _currentService; + + public static TService Current => _currentService ??= new TService(); + + public static TService C => Current; +} \ No newline at end of file diff --git a/Flawless.Client/Service/Remote_Generated.cs b/Flawless.Client/Service/Remote_Generated.cs index 62ba23b..3c714c3 100644 --- a/Flawless.Client/Service/Remote_Generated.cs +++ b/Flawless.Client/Service/Remote_Generated.cs @@ -6,7 +6,6 @@ using Refit; using System.Collections.Generic; using System.Text.Json.Serialization; -using System.Threading; using System.Threading.Tasks; #nullable enable annotations @@ -19,178 +18,188 @@ namespace Flawless.Client.Remote /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/admin/user/delete/{username}")] - Task Delete(string username, CancellationToken cancellationToken = default); + Task Delete(string username); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/admin/user/enable/{username}")] - Task Enable(string username, CancellationToken cancellationToken = default); + Task Enable(string username); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/admin/user/disable/{username}")] - Task Disable(string username, CancellationToken cancellationToken = default); + Task Disable(string username); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/admin/user/reset_password")] - Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default); + Task ResetPassword([Body] ResetPasswordRequest body); /// OK /// Thrown when the request returns a non-success status code. [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/auth/status")] - Task Status(CancellationToken cancellationToken = default); + Task Status(); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/auth/register")] - Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default); + Task Register([Body] RegisterRequest body); /// OK /// Thrown when the request returns a non-success status code. [Headers("Accept: text/plain, application/json, text/json")] [Post("/api/auth/login")] - Task Login([Body] LoginRequest body, CancellationToken cancellationToken = default); + Task Login([Body] LoginRequest body); /// OK /// Thrown when the request returns a non-success status code. [Headers("Accept: text/plain, application/json, text/json")] [Post("/api/auth/refresh")] - Task Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default); + Task Refresh([Body] TokenInfo body); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/auth/logout_all")] - Task LogoutAll(CancellationToken cancellationToken = default); + Task LogoutAll(); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/auth/renew_password")] - Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default); + Task RenewPassword([Body] ResetPasswordRequest body); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Get("/")] - Task Index(CancellationToken cancellationToken = default); + Task Index(); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/delete_repo")] - Task DeleteRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + Task DeleteRepo(string repositoryName, string userName); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/get_info")] - Task GetInfo(string repositoryName, string userName, CancellationToken cancellationToken = default); + Task GetInfo(string repositoryName, string userName); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/archive_repo")] - Task ArchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + Task ArchiveRepo(string repositoryName, string userName); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")] - Task UnarchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default); + Task UnarchiveRepo(string repositoryName, string userName); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/get_users")] - Task GetUsers(string repositoryName, string userName, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default); + Task GetUsers(string repositoryName, string userName); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/update_user")] - Task UpdateUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default); + Task UpdateUser(string repositoryName, string userName, [Body] RepoUserRole body); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/delete_user")] - Task DeleteUser(string repositoryName, string userName, [Body] RepoUserRole body, CancellationToken cancellationToken = default); + Task DeleteUser(string repositoryName, string userName, [Body] RepoUserRole body); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/fetch_manifest")] - Task FetchManifest(string userName, string repositoryName, [Query] string commitId, CancellationToken cancellationToken = default); + Task FetchManifest(string userName, string repositoryName, [Query] string commitId); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/fetch_depot")] - Task FetchDepot(string userName, string repositoryName, [Query] string depotId, CancellationToken cancellationToken = default); + Task FetchDepot(string userName, string repositoryName, [Query] string depotId); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/list_commit")] - Task ListCommit(string userName, string repositoryName, CancellationToken cancellationToken = default); + Task ListCommit(string userName, string repositoryName); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/list_locked_files")] - Task ListLockedFiles(string userName, string repositoryName, CancellationToken cancellationToken = default); + Task ListLockedFiles(string userName, string repositoryName); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo/{userName}/{repositoryName}/peek_commit")] - Task PeekCommit(string userName, string repositoryName, CancellationToken cancellationToken = default); + Task PeekCommit(string userName, string repositoryName); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/lock_file")] - Task LockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default); + Task LockFile(string userName, string repositoryName, [Query] string path); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/repo/{userName}/{repositoryName}/unlock_file")] - Task UnlockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default); + Task UnlockFile(string userName, string repositoryName, [Query] string path); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. [Multipart] + [Headers("Accept: text/plain, application/json, text/json")] [Post("/api/repo/{userName}/{repositoryName}/create_commit")] - Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable workspaceSnapshot, IEnumerable requiredDepots, string mainDepotId, CancellationToken cancellationToken = default); + Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable workspaceSnapshot, IEnumerable requiredDepots, string mainDepotId); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/repo_list")] - Task RepoList([Query, AliasAs("Offset")] int offset, [Query, AliasAs("Length")] int length, CancellationToken cancellationToken = default); + Task RepoList(); - /// A that completes when the request is finished. + /// OK /// Thrown when the request returns a non-success status code. + [Headers("Accept: text/plain, application/json, text/json")] [Post("/api/repo_create")] - Task RepoCreate([Query] string repositoryName, CancellationToken cancellationToken = default); + Task RepoCreate([Query] string repositoryName); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/user/update_info")] - Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default); + Task UpdateInfo([Body] UserInfoModifyResponse body); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/user/update_email")] - Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default); + Task UpdateEmail([Body] UserContactModifyResponse body); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Post("/api/user/update_phone")] - Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default); + Task UpdatePhone([Body] UserContactModifyResponse body); /// OK /// Thrown when the request returns a non-success status code. [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/user/get_info")] - Task GetInfo([Query] string username, CancellationToken cancellationToken = default); + Task GetInfo([Query] string username); /// OK /// Thrown when the request returns a non-success status code. [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/user/query_info")] - Task QueryInfo([Query] string keyword, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default); + Task QueryInfo([Query] string keyword); /// A that completes when the request is finished. /// Thrown when the request returns a non-success status code. [Get("/api/user/delete")] - Task Delete(CancellationToken cancellationToken = default); + Task Delete(); } @@ -223,6 +232,48 @@ namespace Flawless.Client.Remote + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CommitSuccessResponse + { + + [JsonPropertyName("committedOn")] + public System.DateTimeOffset CommittedOn { get; set; } + + [JsonPropertyName("commitId")] + public System.Guid CommitId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GuidPeekResponse + { + + [JsonPropertyName("result")] + public System.Guid Result { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LockFileInfo + { + + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonPropertyName("owner")] + public string Owner { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LockFileInfoListingResponse + { + + [JsonPropertyName("result")] + public ICollection Result { get; set; } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class LoginRequest { @@ -237,18 +288,6 @@ namespace Flawless.Client.Remote } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] - public partial class QueryPagesRequest - { - - [JsonPropertyName("offset")] - public int Offset { get; set; } - - [JsonPropertyName("length")] - public int Length { get; set; } - - } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RegisterRequest { @@ -280,6 +319,77 @@ namespace Flawless.Client.Remote } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepoUserRoleListingResponse + { + + [JsonPropertyName("result")] + public ICollection Result { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepositoryCommitResponse + { + + [JsonPropertyName("id")] + public System.Guid Id { get; set; } + + [JsonPropertyName("author")] + public string Author { get; set; } + + [JsonPropertyName("commitedOn")] + public System.DateTimeOffset CommitedOn { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("mainDepotId")] + public System.Guid MainDepotId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepositoryCommitResponseListingResponse + { + + [JsonPropertyName("result")] + public ICollection Result { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepositoryInfoResponse + { + + [JsonPropertyName("repositoryName")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string RepositoryName { get; set; } + + [JsonPropertyName("ownerUsername")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string OwnerUsername { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("isArchived")] + public bool IsArchived { get; set; } + + [JsonPropertyName("role")] + public RepositoryRole Role { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class RepositoryInfoResponseListingResponse + { + + [JsonPropertyName("result")] + public ICollection Result { get; set; } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public enum RepositoryRole { @@ -314,6 +424,9 @@ namespace Flawless.Client.Remote public partial class ServerStatusResponse { + [JsonPropertyName("friendlyName")] + public string FriendlyName { get; set; } + [JsonPropertyName("allowPublicRegister")] public bool AllowPublicRegister { get; set; } @@ -396,20 +509,11 @@ namespace Flawless.Client.Remote } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] - public partial class UserInfoResponsePagedResponse + public partial class UserInfoResponseListingResponse { - [JsonPropertyName("offset")] - public int Offset { get; set; } - - [JsonPropertyName("length")] - public int Length { get; set; } - - [JsonPropertyName("total")] - public int? Total { get; set; } - - [JsonPropertyName("data")] - public ICollection Data { get; set; } + [JsonPropertyName("result")] + public ICollection Result { get; set; } } @@ -460,6 +564,43 @@ namespace Flawless.Client.Remote public string ContentType { get; private set; } } + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class FileResponse : System.IDisposable + { + private System.IDisposable _client; + private System.IDisposable _response; + + public int StatusCode { get; private set; } + + public IReadOnlyDictionary> Headers { get; private set; } + + public System.IO.Stream Stream { get; private set; } + + public bool IsPartial + { + get { return StatusCode == 206; } + } + + public FileResponse(int statusCode, IReadOnlyDictionary> headers, System.IO.Stream stream, System.IDisposable client, System.IDisposable response) + { + StatusCode = statusCode; + Headers = headers; + Stream = stream; + _client = client; + _response = response; + } + + public void Dispose() + { + Stream.Dispose(); + if (_response != null) + _response.Dispose(); + if (_client != null) + _client.Dispose(); + } + } + + } diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs new file mode 100644 index 0000000..f8f57ed --- /dev/null +++ b/Flawless.Client/Service/RepositoryService.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Flawless.Client.Models; +using Flawless.Client.Remote; + +namespace Flawless.Client.Service; + +public class RepositoryService : BaseService +{ + public ObservableCollection Repositories => _repositories; + + + private readonly ObservableCollection _repositories = new(); + + public async ValueTask CreateRepositoryOnServerAsync(string repositoryName) + { + + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + var r = await api.Gateway.RepoCreate(repositoryName); + + var repo = new RepositoryModel(); + repo.Name = r.RepositoryName; + repo.OwnerName = r.OwnerUsername; + repo.StandaloneName = RepositoryModel.GetStandaloneName(r.RepositoryName, r.OwnerUsername); + repo.Description = r.Description; + repo.Archived = r.IsArchived; + repo.OwnByCurrentUser = (int) r.Role == (int) RepositoryModel.RepositoryRole.Owner; + + Repositories.Insert(0, repo); + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + + return true; + } + + public async ValueTask UpdateRepositoriesFromServerAsync() + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + var result = (await api.Gateway.RepoList()).Result; + var dict = result.ToDictionary(rp => RepositoryModel.GetStandaloneName(rp.RepositoryName, rp.OwnerUsername)); + for (var i = 0; i < Repositories.Count; i++) + { + var ele = Repositories[i]; + if (!dict.Remove(ele.StandaloneName, out var role)) + { + Repositories.RemoveAt(i); + i -= 1; + continue; + } + + ele.Archived = ele.Archived; + ele.Description = ele.Description; + } + + foreach (var (repoStandaloneName, rsp) in dict) + { + var repo = new RepositoryModel(); + repo.Name = rsp.RepositoryName; + repo.OwnerName = rsp.OwnerUsername; + repo.StandaloneName = repoStandaloneName; + repo.Description = rsp.Description; + repo.Archived = rsp.IsArchived; + repo.OwnByCurrentUser = (int) rsp.Role == (int) RepositoryModel.RepositoryRole.Owner; + + Repositories.Add(repo); + } + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + + return true; + } + + public async ValueTask UpdateRepositoriesDownloadedFromDiskAsync() + { + foreach (var repo in _repositories) + { + var fsPath = Path.Combine(SettingService.C.AppSetting.RepositoryPath, repo.OwnerName, repo.Name); + repo.IsDownloaded = Directory.Exists(fsPath); + } + + return true; + } + + public async ValueTask UpdateRepositoryMembersFromServerAsync(RepositoryModel repo) + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + var members = await api.Gateway.GetUsers(repo.Name, repo.OwnerName); + + // Update existed + var dict = members.Result.ToDictionary(m => m.Username); + for (var i = 0; i < repo.Members.Count; i++) + { + var ele = repo.Members[i]; + if (!dict.Remove(ele.Username, out var role)) + { + repo.Members.RemoveAt(i); + i -= 1; + continue; + } + + ele.Username = role.Username; + ele.Role = (RepositoryModel.RepositoryRole) role.Role; + } + + // Add missing + foreach (var role in dict.Values) + { + var r = new RepositoryModel.Member + { + Username = role.Username, + Role = (RepositoryModel.RepositoryRole) role.Role + }; + + repo.Members.Add(r); + } + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Flawless.Client/Service/SettingService.cs b/Flawless.Client/Service/SettingService.cs new file mode 100644 index 0000000..e5be8e6 --- /dev/null +++ b/Flawless.Client/Service/SettingService.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Flawless.Client.Models; +using Newtonsoft.Json; +using Path = System.IO.Path; + +namespace Flawless.Client.Service; + +public class SettingService : BaseService +{ + + private readonly JsonSerializer _serializer = JsonSerializer.Create(); + + public static string SettingFilePath { get; } = + Path.Combine(AppDefaultValues.ProgramDataDirectory, "settings.json"); + + public AppSettingModel AppSetting { get; } = new AppSettingModel(); + + public async ValueTask WriteToDiskAsync() + { + var stream = File.Exists(SettingFilePath) ? File.OpenWrite(SettingFilePath) : File.Create(SettingFilePath); + await using var fs = new StreamWriter(stream, Encoding.UTF8); + _serializer.Serialize(fs, AppSetting); + } + + public async ValueTask ReadFromDiskAsync() + { + if (!File.Exists(SettingFilePath)) return; + using var fs = new StreamReader(File.OpenRead(SettingFilePath), Encoding.UTF8); + _serializer.Populate(fs, AppSetting); + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/HomeViewModel.cs b/Flawless.Client/ViewModels/HomeViewModel.cs index e089cd3..1d7ecb9 100644 --- a/Flawless.Client/ViewModels/HomeViewModel.cs +++ b/Flawless.Client/ViewModels/HomeViewModel.cs @@ -6,10 +6,11 @@ using System.Threading.Tasks; using Avalonia.ReactiveUI; using Flawless.Client.Models; using Flawless.Client.Service; -using Flawless.Communication.Response; -using Flawless.Communication.Shared; +using Flawless.Client.ViewModels.ModalBox; +using Flawless.Client.Views.ModalBox; using ReactiveUI; using ReactiveUI.SourceGenerators; +using Ursa.Controls; namespace Flawless.Client.ViewModels; @@ -18,34 +19,51 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); public IScreen HostScreen { get; } - - public ObservableCollection Repositories { get; } = new(new[] - { - new RepositoryHomePageModel( - "cardidi", "test1", "Abc", false, true, false, ""), - new RepositoryHomePageModel( - "cardidi", "RT001", "Abc", false, true, true, ""), - }); - [Reactive] private RepositoryHomePageModel? _selectedRepository; + [Reactive] private RepositoryModel? _selectedRepository; [Reactive] private string _serverFriendlyName; + + public RepositoryService RepoSrc => RepositoryService.C; public HomeViewModel(IScreen hostScreen) { HostScreen = hostScreen; - Api.Current.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance) + Api.C.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance) .Subscribe(v => ServerFriendlyName = v ?? "Unknown Server"); } [ReactiveCommand] private async Task RefreshRepositoriesAsync() { + if (await RepoSrc.UpdateRepositoriesFromServerAsync()) + { + + } } [ReactiveCommand] private async Task CreateRepositoryAsync() { + var form = new CreateRepositoryDialogViewModel(); + var opt = new OverlayDialogOptions + { + FullScreen = false, + Buttons = DialogButton.OK, + CanResize = false, + CanDragMove = false, + IsCloseButtonVisible = true, + CanLightDismiss = true, + Mode = DialogMode.Question + }; + + var mr = await OverlayDialog + .ShowModal(form, AppDefaultValues.HostId, opt); + + if (mr == DialogResult.OK) + { + await RepoSrc.CreateRepositoryOnServerAsync(form.RepositoryName); + } } [ReactiveCommand] @@ -53,8 +71,25 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel { } + [ReactiveCommand] + private async Task DownloadRepositoryAsync() + { + } + [ReactiveCommand] private async Task DeleteRepositoryAsync() { } + + [ReactiveCommand] + private async Task QuitLoginAsync() + { + Api.C.ClearGateway(); + } + + [ReactiveCommand] + private void OpenGlobalSettingAsync() + { + HostScreen.Router.Navigate.Execute(new SettingViewModel(HostScreen)); + } } \ No newline at end of file diff --git a/Flawless.Client/ViewModels/LoginPageViewModel.cs b/Flawless.Client/ViewModels/LoginPageViewModel.cs index 0deccdc..87de465 100644 --- a/Flawless.Client/ViewModels/LoginPageViewModel.cs +++ b/Flawless.Client/ViewModels/LoginPageViewModel.cs @@ -25,7 +25,7 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel public IObservable CanLogin; - public IObservable CanRegister => Api.Current.Status.Select(s => s != null && s.AllowPublicRegister); + public IObservable CanRegister => Api.C.Status.Select(s => s != null && s.AllowPublicRegister); public LoginPageViewModel(IScreen hostScreen) { @@ -50,7 +50,8 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel { try { - await Api.Current.LoginAsync(Username, Password); + await Api.C.LoginAsync(Username, Password); + await RepositoryService.C.UpdateRepositoriesFromServerAsync(); } catch (ApiException ex) { diff --git a/Flawless.Client/ViewModels/MainWindowViewModel.cs b/Flawless.Client/ViewModels/MainWindowViewModel.cs index 1db7b7b..865346f 100644 --- a/Flawless.Client/ViewModels/MainWindowViewModel.cs +++ b/Flawless.Client/ViewModels/MainWindowViewModel.cs @@ -13,9 +13,9 @@ public partial class MainWindowViewModel : ViewModelBase, IScreen public MainWindowViewModel() { #pragma warning disable VSTHRD110 - Api.Current.IsLoggedIn.Where(x => x) + Api.C.IsLoggedIn.Where(x => x) .SubscribeOnUIThread(_ => Router.Navigate.Execute(new HomeViewModel(this))); - Api.Current.IsLoggedIn.Where(x => !x) + Api.C.IsLoggedIn.Where(x => !x) .SubscribeOnUIThread(_ => Router.Navigate.Execute(new ServerConnectViewModel(this))); #pragma warning restore VSTHRD110 } diff --git a/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs b/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs new file mode 100644 index 0000000..411fdcb --- /dev/null +++ b/Flawless.Client/ViewModels/ModalBox/CreateRepositoryDialogViewModel.cs @@ -0,0 +1,8 @@ +using ReactiveUI.SourceGenerators; + +namespace Flawless.Client.ViewModels.ModalBox; + +public partial class CreateRepositoryDialogViewModel : ViewModelBase +{ + [Reactive] private string _repositoryName; +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/RegisterPageViewModel.cs b/Flawless.Client/ViewModels/RegisterPageViewModel.cs index 3845be4..d351ea9 100644 --- a/Flawless.Client/ViewModels/RegisterPageViewModel.cs +++ b/Flawless.Client/ViewModels/RegisterPageViewModel.cs @@ -36,7 +36,7 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel { try { - await Api.Current.Gateway.Register(new RegisterRequest + await Api.C.Gateway.Register(new RegisterRequest { Email = _email, Username = _username, @@ -44,7 +44,7 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel }); - await Api.Current.LoginAsync(Username, Password); + await Api.C.LoginAsync(Username, Password); } catch (ApiException ex) { diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs new file mode 100644 index 0000000..0078e17 --- /dev/null +++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs @@ -0,0 +1,18 @@ +using Flawless.Client.Models; +using Flawless.Client.Service; +using ReactiveUI; + +namespace Flawless.Client.ViewModels; + +public class RepositoryViewModel : RoutableViewModelBase +{ + public RepositoryModel Repository { get; } + + public RepositoryService RepoSrv { get; } + + public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen) + { + Repository = repo; + RepoSrv = RepositoryService.C; + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ServerConnectViewModel.cs b/Flawless.Client/ViewModels/ServerConnectViewModel.cs index 4087f13..6ff8f71 100644 --- a/Flawless.Client/ViewModels/ServerConnectViewModel.cs +++ b/Flawless.Client/ViewModels/ServerConnectViewModel.cs @@ -26,7 +26,7 @@ public partial class ServerConnectViewModel : ViewModelBase, IScreen, IRoutableV [ReactiveCommand] private async ValueTask OpenRepoPageAsync() { - if (Api.Current.RequireRefreshToken()) await Router.NavigateAndReset.Execute(new ServerSetupPageViewModel(this)).ToTask(); + if (Api.C.RequireRefreshToken()) await Router.NavigateAndReset.Execute(new ServerSetupPageViewModel(this)).ToTask(); } public ServerConnectViewModel(IScreen hostScreen) diff --git a/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs b/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs index 9535866..dad59b1 100644 --- a/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs +++ b/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs @@ -9,12 +9,8 @@ using Refit; namespace Flawless.Client.ViewModels; -public partial class ServerSetupPageViewModel : ViewModelBase, IRoutableViewModel +public partial class ServerSetupPageViewModel : RoutableViewModelBase { - - public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); - - public IScreen HostScreen { get; } [Reactive] private string _host = "http://localhost:5256/"; @@ -22,12 +18,11 @@ public partial class ServerSetupPageViewModel : ViewModelBase, IRoutableViewMode public IObservable CanSetHost { get; } - public ServerSetupPageViewModel(IScreen hostScreen) + public ServerSetupPageViewModel(IScreen hostScreen) : base(hostScreen) { - HostScreen = hostScreen; // Must clear this gateway - if (Api.Current.IsGatewayReady) Api.Current.ClearGateway(); + if (Api.C.IsGatewayReady) Api.C.ClearGateway(); CanSetHost = this.WhenAnyValue(x => x.Host, s => !string.IsNullOrWhiteSpace(s)); } @@ -39,7 +34,7 @@ public partial class ServerSetupPageViewModel : ViewModelBase, IRoutableViewMode try { Issue = string.Empty; - await Api.Current.SetGatewayAsync(Host); + await Api.C.SetGatewayAsync(Host); HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen)); } catch (ApiException ex) diff --git a/Flawless.Client/ViewModels/SettingViewModel.cs b/Flawless.Client/ViewModels/SettingViewModel.cs new file mode 100644 index 0000000..fa3b287 --- /dev/null +++ b/Flawless.Client/ViewModels/SettingViewModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Windows.Input; +using ReactiveUI; + +namespace Flawless.Client.ViewModels; + +public class SettingViewModel : RoutableViewModelBase +{ + public SettingViewModel(IScreen hostScreen) : base(hostScreen) + { + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ViewModelBase.cs b/Flawless.Client/ViewModels/ViewModelBase.cs index 229260c..de4f774 100644 --- a/Flawless.Client/ViewModels/ViewModelBase.cs +++ b/Flawless.Client/ViewModels/ViewModelBase.cs @@ -1,5 +1,21 @@ -using ReactiveUI; +using System; +using System.Reactive; +using ReactiveUI; namespace Flawless.Client.ViewModels; public abstract class ViewModelBase : ReactiveObject {} + +public abstract class RoutableViewModelBase : ViewModelBase, IRoutableViewModel +{ + public string? UrlPathSegment { get; } = Guid.NewGuid().ToString(); + + public IScreen HostScreen { get; } + + public ReactiveCommand GoBackCommand => HostScreen.Router.NavigateBack; + + protected RoutableViewModelBase(IScreen hostScreen) + { + HostScreen = hostScreen; + } +} \ No newline at end of file diff --git a/Flawless.Client/Views/LoginPageView.axaml b/Flawless.Client/Views/HelloSetup/LoginPageView.axaml similarity index 100% rename from Flawless.Client/Views/LoginPageView.axaml rename to Flawless.Client/Views/HelloSetup/LoginPageView.axaml diff --git a/Flawless.Client/Views/LoginPageView.axaml.cs b/Flawless.Client/Views/HelloSetup/LoginPageView.axaml.cs similarity index 100% rename from Flawless.Client/Views/LoginPageView.axaml.cs rename to Flawless.Client/Views/HelloSetup/LoginPageView.axaml.cs diff --git a/Flawless.Client/Views/RegisterPageView.axaml b/Flawless.Client/Views/HelloSetup/RegisterPageView.axaml similarity index 100% rename from Flawless.Client/Views/RegisterPageView.axaml rename to Flawless.Client/Views/HelloSetup/RegisterPageView.axaml diff --git a/Flawless.Client/Views/RegisterPageView.axaml.cs b/Flawless.Client/Views/HelloSetup/RegisterPageView.axaml.cs similarity index 100% rename from Flawless.Client/Views/RegisterPageView.axaml.cs rename to Flawless.Client/Views/HelloSetup/RegisterPageView.axaml.cs diff --git a/Flawless.Client/Views/ServerSetupPageView.axaml b/Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml similarity index 100% rename from Flawless.Client/Views/ServerSetupPageView.axaml rename to Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml diff --git a/Flawless.Client/Views/ServerSetupPageView.axaml.cs b/Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml.cs similarity index 100% rename from Flawless.Client/Views/ServerSetupPageView.axaml.cs rename to Flawless.Client/Views/HelloSetup/ServerSetupPageView.axaml.cs diff --git a/Flawless.Client/Views/HomeView.axaml b/Flawless.Client/Views/HomeView.axaml index 789fceb..31b7692 100644 --- a/Flawless.Client/Views/HomeView.axaml +++ b/Flawless.Client/Views/HomeView.axaml @@ -17,8 +17,10 @@ - - + + @@ -29,31 +31,37 @@ - - + + + + + - + IsVisible="{Binding !IsDownloaded}"/> - + IsVisible="{Binding IsDownloaded}"/> + @@ -64,7 +72,7 @@