1
0

Compare commits

...

3 Commits

66 changed files with 1125 additions and 277 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/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/HelloView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HomeView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
@ -13,7 +15,15 @@
<entry key="Flawless.Client/Views/MainView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ModalBox/CreateRepositoryDialog.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/RepoCommitPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoFileTreePageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepoWorkspacePageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryPage/RepositoryDashboardPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/RepositoryView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ServerConnectView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ServerConnectionView.axaml" value="Flawless.Client/Flawless.Client.csproj" />

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/SuppressUninitializedWarningFix/Enabled/@EntryValue">False</s:Boolean></wpf:ResourceDictionary>

View File

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

View File

@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1" />
<PackageReference Include="Avalonia.Controls.TreeDataGrid" Version="11.1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@ -25,8 +26,10 @@
<PackageReference Include="Irihi.Ursa" Version="1.10.0" />
<PackageReference Include="Irihi.Ursa.ReactiveUIExtension" Version="1.0.1" />
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="1.10.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ReactiveUI.SourceGenerators" Version="2.1.27">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -36,10 +39,21 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flawless.Communication\Flawless.Communication.csproj" />
<UpToDateCheckInput Remove="Views\Templates\WithBackButtonLayout.axaml" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\Templates\WithBackButtonLayout.axaml" />
<Compile Update="Views\HelloSetup\LoginPageView.axaml.cs">
<DependentUpon>LoginPageView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\HelloSetup\RegisterPageView.axaml.cs">
<DependentUpon>RegisterPageView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\HelloSetup\ServerSetupPageView.axaml.cs">
<DependentUpon>ServerSetupPageView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,6 @@
namespace Flawless.Client.Models;
public record CommitModel : ReactiveRecordModel
{
}

View File

@ -0,0 +1,7 @@
using ReactiveUI;
namespace Flawless.Client.Models;
public abstract class ReactiveModel : ReactiveObject {}
public abstract record ReactiveRecordModel : ReactiveRecord {}

View File

@ -1,27 +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,
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,
r.LatestCommitId.ToString().Substring(0, 6));
}
}

View File

@ -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<Member> Members { get; } = new();
public ObservableCollection<Commit> Commits { get; } = new();
public ObservableCollection<Depot> Depots { get; } = new();
public ObservableCollection<Lock> 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<Guid> Dependencies { get; } = new();
}
}

View File

@ -0,0 +1,6 @@
namespace Flawless.Client.Models;
public record UserModel : ReactiveRecordModel
{
}

View File

@ -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<Api>
{
#region Instance
class ApiAuthenticationHandler : DelegatingHandler
{
public ApiAuthenticationHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{
}
private static Api? _instance;
protected override Task<HttpResponseMessage> 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);
}
public static Api Current => _instance ??= new Api();
#endregion
}
public IObservable<bool> 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<string>($"Bearer {_token}") : Task.FromResult(string.Empty)
HttpMessageHandlerFactory = () => new ApiAuthenticationHandler(new HttpClientHandler())
};
var tempGateway = RestService.For<IFlawlessServer>(host, setting);

View File

@ -0,0 +1,10 @@
namespace Flawless.Client.Service;
public class BaseService<TService> where TService : class, new()
{
private static TService? _currentService;
public static TService Current => _currentService ??= new TService();
public static TService C => Current;
}

View File

@ -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
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/delete/{username}")]
Task Delete(string username, CancellationToken cancellationToken = default);
Task Delete(string username);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/enable/{username}")]
Task Enable(string username, CancellationToken cancellationToken = default);
Task Enable(string username);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/disable/{username}")]
Task Disable(string username, CancellationToken cancellationToken = default);
Task Disable(string username);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/reset_password")]
Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
Task ResetPassword([Body] ResetPasswordRequest body);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/auth/status")]
Task<ServerStatusResponse> Status(CancellationToken cancellationToken = default);
Task<ServerStatusResponse> Status();
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/register")]
Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default);
Task Register([Body] RegisterRequest body);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/login")]
Task<TokenInfo> Login([Body] LoginRequest body, CancellationToken cancellationToken = default);
Task<TokenInfo> Login([Body] LoginRequest body);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/refresh")]
Task<TokenInfo> Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default);
Task<TokenInfo> Refresh([Body] TokenInfo body);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/logout_all")]
Task LogoutAll(CancellationToken cancellationToken = default);
Task LogoutAll();
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/renew_password")]
Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
Task RenewPassword([Body] ResetPasswordRequest body);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/")]
Task Index(CancellationToken cancellationToken = default);
Task Index();
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_repo")]
Task DeleteRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
Task DeleteRepo(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<RepositoryInfoResponse> GetInfo(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/archive_repo")]
Task ArchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
Task ArchiveRepo(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")]
Task UnarchiveRepo(string repositoryName, string userName, CancellationToken cancellationToken = default);
Task UnarchiveRepo(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<RepoUserRoleListingResponse> GetUsers(string repositoryName, string userName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<FileResponse> FetchManifest(string userName, string repositoryName, [Query] string commitId);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<FileResponse> FetchDepot(string userName, string repositoryName, [Query] string depotId);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<RepositoryCommitResponseListingResponse> ListCommit(string userName, string repositoryName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<LockFileInfoListingResponse> ListLockedFiles(string userName, string repositoryName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<GuidPeekResponse> PeekCommit(string userName, string repositoryName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<WorkspaceFile> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId, CancellationToken cancellationToken = default);
Task<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<WorkspaceFile> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<RepositoryInfoResponseListingResponse> RepoList();
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo_create")]
Task RepoCreate([Query] string repositoryName, CancellationToken cancellationToken = default);
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_info")]
Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default);
Task UpdateInfo([Body] UserInfoModifyResponse body);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_email")]
Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
Task UpdateEmail([Body] UserContactModifyResponse body);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_phone")]
Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
Task UpdatePhone([Body] UserContactModifyResponse body);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/get_info")]
Task<UserInfoResponse> GetInfo([Query] string username, CancellationToken cancellationToken = default);
Task<UserInfoResponse> GetInfo([Query] string username);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/query_info")]
Task<UserInfoResponsePagedResponse> QueryInfo([Query] string keyword, [Body] QueryPagesRequest body, CancellationToken cancellationToken = default);
Task<UserInfoResponseListingResponse> QueryInfo([Query] string keyword);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<LockFileInfo> 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<RepoUserRole> 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<RepositoryCommitResponse> 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<RepositoryInfoResponse> 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<UserInfoResponse> Data { get; set; }
[JsonPropertyName("result")]
public ICollection<UserInfoResponse> 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<string, IEnumerable<string>> Headers { get; private set; }
public System.IO.Stream Stream { get; private set; }
public bool IsPartial
{
get { return StatusCode == 206; }
}
public FileResponse(int statusCode, IReadOnlyDictionary<string, IEnumerable<string>> 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();
}
}
}

View File

@ -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<RepositoryService>
{
public ObservableCollection<RepositoryModel> Repositories => _repositories;
private readonly ObservableCollection<RepositoryModel> _repositories = new();
public async ValueTask<bool> 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<bool> 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<bool> 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<bool> 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;
}
}

View File

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

View File

@ -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;
@ -19,31 +20,50 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
public IScreen HostScreen { get; }
public ObservableCollection<RepositoryHomePageModel> Repositories { get; } = new(new[]
{
new RepositoryHomePageModel(
"cardidi", "test1", "Abc", false, 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<CreateRepositoryDialogView, CreateRepositoryDialogViewModel>(form, AppDefaultValues.HostId, opt);
if (mr == DialogResult.OK)
{
await RepoSrc.CreateRepositoryOnServerAsync(form.RepositoryName);
}
}
[ReactiveCommand]
@ -51,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));
}
}

View File

@ -25,7 +25,7 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
public IObservable<bool> CanLogin;
public IObservable<bool> CanRegister => Api.Current.Status.Select(s => s != null && s.AllowPublicRegister);
public IObservable<bool> 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)
{

View File

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

View File

@ -0,0 +1,8 @@
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels.ModalBox;
public partial class CreateRepositoryDialogViewModel : ViewModelBase
{
[Reactive] private string _repositoryName;
}

View File

@ -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)
{

View File

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

View File

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

View File

@ -9,25 +9,20 @@ 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/";
[Reactive(SetModifier = AccessModifier.Protected)] private string? _issue;
public IObservable<bool> 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)

View File

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

View File

@ -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<Unit, IRoutableViewModel> GoBackCommand => HostScreen.Router.NavigateBack;
protected RoutableViewModelBase(IScreen hostScreen)
{
HostScreen = hostScreen;
}
}

View File

@ -10,11 +10,20 @@
x:Class="Flawless.Client.Views.HomeView">
<DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top">
<Label Content="{Binding ServerFriendlyName, StringFormat='Server {0}'}" FontSize="18" FontWeight="400"></Label>
<Grid RowDefinitions="Auto, 18, Auto" ColumnDefinitions="*, Auto" DockPanel.Dock="Top">
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<Label Content="{Binding ServerFriendlyName, StringFormat='Server {0}', FallbackValue='Server LocalTest'}" FontSize="18" FontWeight="400"></Label>
<Label Content="Repositories" FontSize="32" FontWeight="600"></Label>
<Rectangle Height="18"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconSetting}"
Command="{Binding OpenGlobalSettingAsyncCommand}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconQuit}"
Command="{Binding QuitLoginCommand}"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"
Command="{Binding RefreshRepositoriesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Create"
@ -22,23 +31,38 @@
<u:DisableContainer IsEnabled="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconFolderOpen}" Content="Open"
IsEnabled="{Binding SelectedRepository.IsDownloaded}"
Command="{Binding OpenRepositoryCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Download"
IsEnabled="{Binding !SelectedRepository.IsDownloaded}"
Command="{Binding OpenRepositoryCommand}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconDelete}" Content="Delete"
IsEnabled="{Binding SelectedRepository.IsOwner}"
IsEnabled="{Binding SelectedRepository.IsDownloaded}"
Command="{Binding DeleteRepositoryCommand}"/>
</StackPanel>
</u:DisableContainer>
</StackPanel>
</Grid>
<Rectangle DockPanel.Dock="Top" Height="18"/>
<StackPanel IsVisible="{Binding !RepoSrc.Repositories.Count}"
VerticalAlignment="Center" Spacing="18">
<PathIcon Data="{StaticResource SemiIconAlertCircle}" HorizontalAlignment="Left" Width="48" Height="48"/>
<Label FontSize="18" Content="Repository is empty, try refresh or create repository."/>
</StackPanel>
<Rectangle DockPanel.Dock="Top" Height="20"/>
<Grid ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
<Grid IsVisible="{Binding RepoSrc.Repositories.Count}" ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" AllowAutoHide="True">
<u:SelectionList ItemsSource="{Binding Repositories, Mode=TwoWay}"
<u:SelectionList ItemsSource="{Binding RepoSrc.Repositories, Mode=TwoWay}"
SelectedItem="{Binding SelectedRepository, Mode=TwoWay}"
Margin="0, 0, 8, 0">
<u:SelectionList.ItemTemplate>
<DataTemplate>
<TextBlock Margin="0, 12" VerticalAlignment="Center" Text="{Binding FullName, Mode=OneWay}"/>
<StackPanel Orientation="Horizontal" Spacing="6" Margin="6 0">
<PathIcon MaxHeight="16" MaxWidth="16" Data="{StaticResource SemiIconDownload}"
IsVisible="{Binding !IsDownloaded}"/>
<PathIcon MaxHeight="16" MaxWidth="16" Data="{StaticResource SemiIconFolder}"
IsVisible="{Binding IsDownloaded}"/>
<TextBlock Margin="0, 12" VerticalAlignment="Center" Text="{Binding StandaloneName, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</u:SelectionList.ItemTemplate>
</u:SelectionList>
@ -47,9 +71,8 @@
<StackPanel VerticalAlignment="Stretch" Spacing="20">
<Label FontWeight="400" FontSize="24"
Content="{Binding SelectedRepository.Name, FallbackValue='Select a Repository'}"/>
<StackPanel Spacing="10" IsVisible="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel IsVisible="{Binding SelectedRepository.IsArchived, FallbackValue=False}"
<StackPanel IsVisible="{Binding SelectedRepository.Archived, FallbackValue=False}"
Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconArchive}"/>
<Label Content="Archived"/>
@ -58,12 +81,11 @@
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconUser}"/>
<Label Content="{Binding SelectedRepository.OwnerName, FallbackValue='Owner'}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconActivity}"/>
<Label Content="{Binding SelectedRepository.LatestCommitId, FallbackValue='No Commit'}"/>
</StackPanel>
<ScrollViewer IsVisible="{Binding SelectedRepository}">
<StackPanel Orientation="Vertical" Spacing="8">
<Label Content="{Binding SelectedRepository.Description, FallbackValue='Description as below.'}"/>
<u:Divider Content="Recent Activities"/>
</StackPanel>
</ScrollViewer>
</StackPanel>
</StackPanel>

View File

@ -20,6 +20,6 @@
<Panel>
<rxui:RoutedViewHost Router="{Binding Router}"/>
<ursa:OverlayDialogHost/>
<ursa:OverlayDialogHost HostId="Overlay"/>
</Panel>
</ursa:UrsaWindow>

View File

@ -0,0 +1,22 @@
<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:u="https://irihi.tech/ursa"
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
x:DataType="vm:CreateRepositoryDialogViewModel"
d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"
MinWidth="400"
x:Class="Flawless.Client.Views.ModalBox.CreateRepositoryDialogView">
<u:Form HorizontalAlignment="Stretch" LabelPosition="Top">
<u:Form.ItemsPanel>
<ItemsPanelTemplate>
<Grid ColumnDefinitions="*" RowDefinitions="*" />
</ItemsPanelTemplate>
</u:Form.ItemsPanel>
<u:FormItem Label="Name">
<TextBox Text="{Binding RepositoryName}"/>
</u:FormItem>
</u:Form>
</UserControl>

View File

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

View File

@ -0,0 +1,18 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoCommitPageView">
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0">
</TreeDataGrid>
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
<ScrollViewer>
<StackPanel Spacing="4">
<Label Content="Commit Message"/>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoCommitPageView : UrsaView
{
public RepoCommitPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,29 @@
<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:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoDashboardPageView">
<Grid ColumnDefinitions="2*, *">
<Border Grid.Column="0" Classes="Shadow" Theme="{StaticResource CardBorder}">
<StackPanel>
<Label Content="No Readme File Existed"/>
<!-- <md:MarkdownScrollViewer Markdown="## Hello World!!"/> -->
</StackPanel>
</Border>
<ScrollViewer Grid.Column="1" Margin="36 20">
<StackPanel Orientation="Vertical">
<u:Timeline
HorizontalAlignment="Left"
Mode="Left">
<u:TimelineItem Header="New Commit"/>
<u:TimelineItem/>
<u:TimelineItem/>
<u:TimelineItem/>
</u:Timeline>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoDashboardPageView : UrsaView
{
public RepoDashboardPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,18 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoFileTreePageView">
<Grid ColumnDefinitions="2*, *">
<TreeDataGrid Grid.Column="0">
</TreeDataGrid>
<Border Grid.Column="1" Classes="Shadow" Theme="{StaticResource CardBorder}">
<ScrollViewer>
<StackPanel Spacing="4">
<Label Content="File History"/>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoFileTreePageView : UrsaView
{
public RepoFileTreePageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,10 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoIssuePageView">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Content="Sit down and wait patience."></Label>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoIssuePageView : UrsaView
{
public RepoIssuePageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,10 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Content="Sit down and wait patience."></Label>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoSettingPageView : UrsaView
{
public RepoSettingPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,50 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoWorkspacePageView">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Spacing="6" Margin="8">
<StackPanel Orientation="Horizontal">
<Label Content="Commit Message"/>
</StackPanel>
<TextBox Height="80"/>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
</StackPanel>
<Grid ColumnDefinitions="Auto, *, Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Sync"></Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<SplitButton Content="Commit">
<SplitButton.Flyout>
<MenuFlyout Placement="Top">
<MenuItem Header="Lock"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</StackPanel>
</Grid>
</StackPanel>
<Border Classes="Shadow" Theme="{StaticResource CardBorder}">
<Grid ColumnDefinitions="*, 8, *">
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="8">
<Grid RowDefinitions="Auto, Auto">
<Label Grid.Row="0" FontSize="12" Content="Changes" HorizontalAlignment="Left"/>
<TreeDataGrid Grid.Row="1">
</TreeDataGrid>
</Grid>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Vertical" Spacing="8">
<Grid RowDefinitions="Auto, Auto">
<Label Grid.Row="0" FontSize="12" Content="Ready" HorizontalAlignment="Right"/>
<TreeDataGrid Grid.Row="1">
</TreeDataGrid>
</Grid>
</StackPanel>
</Grid>
</Border>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Flawless.Client.Views.RepositoryPage;
public partial class RepoWorkspacePageView : UrsaView
{
public RepoWorkspacePageView()
{
InitializeComponent();
}
}

View File

@ -5,11 +5,13 @@
xmlns:u="https://irihi.tech/ursa"
xmlns:semi="https://irihi.tech/semi"
xmlns:vm="using:Flawless.Client.ViewModels"
xmlns:page="using:Flawless.Client.Views.RepositoryPage"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
x:Class="Flawless.Client.Views.RepositoryView">
<DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="20">
<u:IconButton Icon="{StaticResource SemiIconArrowLeft}" Content="All Repositories"/>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<u:IconButton Height="4" Icon="{StaticResource SemiIconArrowLeft}" Content="All Repositories"/>
<Label FontWeight="400" FontSize="28" Content="Name of Repository"/>
</StackPanel>
<TabControl TabStripPlacement="Top" Margin="0 20">
@ -20,6 +22,7 @@
<Label Content="Dashboard"/>
</StackPanel>
</TabItem.Header>
<page:RepoDashboardPageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -28,6 +31,7 @@
<Label Content="Workspace"/>
</StackPanel>
</TabItem.Header>
<page:RepoWorkspacePageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -36,6 +40,7 @@
<Label Content="File Tree"/>
</StackPanel>
</TabItem.Header>
<page:RepoFileTreePageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -44,6 +49,7 @@
<Label Content="Commit"/>
</StackPanel>
</TabItem.Header>
<page:RepoCommitPageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -52,6 +58,7 @@
<Label Content="Issue"/>
</StackPanel>
</TabItem.Header>
<page:RepoIssuePageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
<TabItem>
<TabItem.Header>
@ -60,6 +67,7 @@
<Label Content="Setting"/>
</StackPanel>
</TabItem.Header>
<page:RepoSettingPageView Margin="18" DataContext="{Binding $self}"/>
</TabItem>
</TabControl>
</DockPanel>

View File

@ -5,12 +5,14 @@
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"
x:Class="Flawless.Client.Views.SettingView">
<DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="20">
<u:IconButton Icon="{StaticResource SemiIconArrowLeft}" Content="Back"/>
<u:IconButton Height="4" Icon="{StaticResource SemiIconArrowLeft}" Content="Back"
Command="{Binding GoBackCommand}"/>
<Label FontWeight="400" FontSize="28" Content="Settings"/>
</StackPanel>
<TabControl TabStripPlacement="Left" Margin="0 20">
@ -18,10 +20,14 @@
</TabItem>
<TabItem Header="Local Storage">
</TabItem>
<TabItem Header="Server Configuration">
</TabItem>
<TabItem Header="Server Users">
</TabItem>
<TabItem Header="Server Info">
</TabItem>
<TabItem Header="Client Info">
</TabItem>
</TabControl>
</DockPanel>
</UserControl>

View File

@ -1,10 +1,12 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Flawless.Client.ViewModels;
using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views;
public partial class SettingView : UserControl
public partial class SettingView : ReactiveUrsaView<SettingViewModel>
{
public SettingView()
{

View File

@ -1,17 +0,0 @@
namespace Flawless.Communication.Request;
public class QueryPagesRequest<T>
{
public required int Offset { get; init; }
public required int Length { get; init; }
public T? Parameter { get; init; }
}
public class QueryPagesRequest
{
public required int Offset { get; init; }
public required int Length { get; init; }
}

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Response;
public record CommitSuccessResponse(DateTime CommittedOn, Guid CommitId);

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Response;
public record ListingResponse<T>(T[] Result);

View File

@ -1,25 +0,0 @@
namespace Flawless.Communication.Response;
public record PagedResponse<T>
{
public required int Offset { get; init; }
public required int Length { get; init; }
public int? Total { get; init; }
public IEnumerable<T>? Data { get; init; }
}
public record PagedResponse<TData, TMetadata>
{
public required int Offset { get; init; }
public required int Length { get; init; }
public int? Total { get; init; }
public IEnumerable<TData>? Data { get; init; }
public TMetadata? Metadata { get; init; }
}

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Response;
public record PeekResponse<T>(T Result);

View File

@ -0,0 +1,4 @@
namespace Flawless.Communication.Response;
public record RepositoryCommitResponse(
Guid Id, string Author, DateTime CommitedOn, string Message, Guid MainDepotId);

View File

@ -8,8 +8,6 @@ public record RepositoryInfoResponse
public required string OwnerUsername { get; set; }
public required Guid LatestCommitId { get; set; }
public string? Description { get; set; }
public required bool IsArchived { get; set; }

View File

@ -2,5 +2,7 @@
public record ServerStatusResponse
{
public required string? FriendlyName { get; init; }
public required bool AllowPublicRegister { get; set; }
}

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Shared;
public record LockFileInfo(string Path, string Owner);

View File

@ -27,7 +27,8 @@ public class AuthenticationController(
{
return Task.FromResult<ActionResult<ServerStatusResponse>>(Ok(new ServerStatusResponse()
{
AllowPublicRegister = true
AllowPublicRegister = true,
FriendlyName = "Ca2didi Server"
}));
}

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Validations.Rules;
namespace Flawless.Server.Controllers;
@ -48,7 +49,7 @@ public class RepositoryInnieController(
}
[HttpGet("get_info")]
public async Task<IActionResult> IsRepositoryArchiveAsync(string repositoryName)
public async Task<ActionResult<RepositoryInfoResponse>> IsRepositoryArchiveAsync(string repositoryName)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
@ -67,7 +68,6 @@ public class RepositoryInnieController(
{
RepositoryName = rp.Name,
OwnerUsername = rp.Owner.UserName!,
LatestCommitId = rp.Commits.Max(cm => cm.Id),
Description = rp.Description,
IsArchived = rp.IsArchived,
Role = rp.Members.First(m => m.User == u).Role
@ -108,7 +108,7 @@ public class RepositoryInnieController(
}
[HttpGet("get_users")]
public async Task<IActionResult> GetUsersAsync(string repositoryName, QueryPagesRequest r)
public async Task<ActionResult<ListingResponse<RepoUserRole>>> GetUsersAsync(string repositoryName)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
@ -122,17 +122,11 @@ public class RepositoryInnieController(
if (rp == null) return BadRequest(new FailedResponse("Repository not found!"));
return Ok(new PagedResponse<RepoUserRole>
{
Length = r.Length,
Offset = r.Offset,
Total = rp.Members.Count,
Data = rp.Members.Select(pm => new RepoUserRole
return Ok(new ListingResponse<RepoUserRole>(rp.Members.Select(pm => new RepoUserRole
{
Username = pm.User.UserName!,
Role = pm.Role
})
});
}).ToArray()));
}
[HttpPost("update_user")]
@ -230,6 +224,7 @@ public class RepositoryInnieController(
[HttpGet("fetch_manifest")]
[ProducesResponseType<FileStreamResult>(200)]
public async Task<IActionResult> DownloadManifestAsync(string userName, string repositoryName, [FromQuery] string commitId)
{
if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id"));
@ -245,6 +240,7 @@ public class RepositoryInnieController(
[HttpGet("fetch_depot")]
[ProducesResponseType<FileStreamResult>(200)]
public async Task<IActionResult> DownloadDepotAsync(string userName, string repositoryName, [FromQuery] string depotId)
{
if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id"));
@ -260,50 +256,49 @@ public class RepositoryInnieController(
}
[HttpGet("list_commit")]
public async Task<IActionResult> ListCommitsAsync(string userName, string repositoryName)
public async Task<ActionResult<ListingResponse<RepositoryCommitResponse>>> ListCommitsAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
if (grantIssue is not Repository) return (ActionResult) grantIssue;
var commit = await dbContext.Repositories
var commit = dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
.SelectMany(r => r.Commits)
.Include(r => r.Author)
.Include(r => r.Author).Include(repositoryCommit => repositoryCommit.MainDepot)
.OrderByDescending(r => r.CommittedOn)
.ToArrayAsync();
.AsAsyncEnumerable();
return Ok(new
{
Commits = commit
});
List<RepositoryCommitResponse> r = new();
await foreach (var cm in commit)
r.Add(new RepositoryCommitResponse(
cm.Id, cm.Author.UserName!, cm.CommittedOn, cm.Message, cm.MainDepot.DepotId));
return Ok(new ListingResponse<RepositoryCommitResponse>(r.ToArray()));
}
[HttpGet("list_locked_files")]
public async Task<IActionResult> ListLocksAsync(string userName, string repositoryName)
public async Task<ActionResult<ListingResponse<LockFileInfo>>> ListLocksAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
if (grantIssue is not Repository) return (ActionResult) grantIssue;
var lockers = await dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
.SelectMany(r => r.Locked)
.Select(l => new { Path = l.Path, Owner = l.LockDownUser.UserName})
.Select(l => new LockFileInfo(l.Path, l.LockDownUser.UserName))
.ToArrayAsync();
return Ok(new
{
Lockers = lockers
});
return Ok(new ListingResponse<LockFileInfo>(lockers));
}
[HttpGet("peek_commit")]
public async Task<IActionResult> PeekCommitAsync(string userName, string repositoryName)
public async Task<ActionResult<PeekResponse<Guid>>> PeekCommitAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
var commit = await dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
@ -312,10 +307,7 @@ public class RepositoryInnieController(
.OrderByDescending(r => r.CommittedOn)
.FirstOrDefaultAsync();
return Ok(new
{
Commit = commit?.Id.ToString() ?? string.Empty
});
return Ok(new PeekResponse<Guid>(commit?.Id ?? Guid.Empty));
}
@ -394,6 +386,7 @@ public class RepositoryInnieController(
}
[HttpPost("create_commit")]
[ProducesResponseType<CommitSuccessResponse>(200)]
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
@ -538,11 +531,7 @@ public class RepositoryInnieController(
throw;
}
return Ok(new
{
CreatedAt = commit.CommittedOn,
CommitId = commit.Id
});
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id));
}
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)

View File

@ -14,38 +14,32 @@ namespace Flawless.Server.Controllers;
[ApiController, Authorize, Route("api")]
public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUser> userManager) : ControllerBase
{
[HttpGet("repo_list")]
public async Task<IActionResult> ListAllAvailableRepositoriesAsync([FromQuery] QueryPagesRequest r)
public async Task<ActionResult<ListingResponse<RepositoryInfoResponse>>> ListAllAvailableRepositoriesAsync()
{
var u = (await userManager.GetUserAsync(HttpContext.User))!;
var query = await dbContext.Repositories
var query = dbContext.Repositories
.Include(repository => repository.Owner)
.Include(repository => repository.Commits)
.Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User)
.Where(rp => rp.Members.Any(m => m.User == u))
.Skip(r.Offset)
.Take(r.Length)
.ToArrayAsync();
return Ok(new PagedResponse<RepositoryInfoResponse>
{
Length = r.Length,
Offset = r.Offset,
Data = query.Select(rp => new RepositoryInfoResponse
.Where(rp => rp.Members.Any(m => m.User == u) || rp.Owner == u)
.Select(rp => new RepositoryInfoResponse
{
RepositoryName = rp.Name,
OwnerUsername = rp.Owner.UserName!,
LatestCommitId = rp.Commits.OrderByDescending(cm => cm.CommittedOn).FirstOrDefault()?.Id ?? Guid.Empty,
Description = rp.Description,
IsArchived = rp.IsArchived,
Role = rp.Members.First(m => m.User == u).Role
}),
});
Role = rp.Owner == u ? RepositoryRole.Owner : rp.Members.First(m => m.User == u).Role
})
.ToArray();
return Ok(new ListingResponse<RepositoryInfoResponse>(query));
}
[HttpPost("repo_create")]
public async Task<IActionResult> CreateRepositoryAsync([FromQuery] string repositoryName)
public async Task<ActionResult<RepositoryInfoResponse>> CreateRepositoryAsync([FromQuery] string repositoryName)
{
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
@ -54,13 +48,21 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
if (await dbContext.Repositories.AnyAsync(rp => rp.Name == repositoryName && u == rp.Owner))
return BadRequest(new FailedResponse("Repository name has already created!"));
await dbContext.Repositories.AddAsync(new Repository()
var repo = new Repository()
{
Name = repositoryName,
Owner = u,
});
};
await dbContext.Repositories.AddAsync(repo);
await dbContext.SaveChangesAsync();
return Ok();
return Ok(new RepositoryInfoResponse()
{
RepositoryName = repositoryName,
OwnerUsername = u.UserName!,
Description = string.Empty,
IsArchived = false,
Role = RepositoryRole.Owner
});
}
}

View File

@ -94,22 +94,15 @@ public class UserController(
}
[HttpGet("query_info")]
public async Task<ActionResult<PagedResponse<UserInfoResponse>>> GetUserInfoAsync(QueryPagesRequest r, [FromQuery] string keyword)
public async Task<ActionResult<ListingResponse<UserInfoResponse>>> QueryUserInfoAsync([FromQuery] string keyword)
{
var payload = await userManager.Users
.Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword)))
.Skip(r.Offset)
.Take(r.Length)
.Select(u => GetUserInfoInternal(u, null))
.ToArrayAsync();
// Return self as default
return Ok(new PagedResponse<UserInfoResponse>
{
Offset = r.Offset,
Length = r.Length,
Data = payload
});
return Ok(new ListingResponse<UserInfoResponse>(payload));
}
[HttpGet("delete")]

View File

@ -37,4 +37,8 @@
<Compile Remove="Migrations\20250322194407_InitialCreate.Designer.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Exceptions\" />
</ItemGroup>
</Project>

View File

@ -6,7 +6,10 @@ public class ExceptionTransformMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
try { await next(context); }
try
{
await next(context);
}
catch (Exception e)
{
context.Response.StatusCode = 500;

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
namespace Flawless.Server;
@ -46,6 +47,26 @@ public static class Program
{
opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods
opt.SupportNonNullableReferenceTypes();
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme.",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
});
opt.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
}, []
}
});
});
}
@ -164,7 +185,7 @@ public static class Program
if (u == null) throw new SecurityTokenExpiredException("User is not existed.");
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked.");
// if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); //todo Fix lockout prob
}
// Extract user info into HttpContext