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.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/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/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/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/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HomeView.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/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/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/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/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/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/ServerConnectView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/ServerConnectionView.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> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1" /> <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.Desktop" Version="11.2.1" />
<PackageReference Include="Avalonia.Fonts.Inter" 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.--> <!--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" Version="1.10.0" />
<PackageReference Include="Irihi.Ursa.ReactiveUIExtension" Version="1.0.1" /> <PackageReference Include="Irihi.Ursa.ReactiveUIExtension" Version="1.0.1" />
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="1.10.0" /> <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.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" 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"> <PackageReference Include="ReactiveUI.SourceGenerators" Version="2.1.27">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -36,10 +39,21 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Flawless.Communication\Flawless.Communication.csproj" /> <UpToDateCheckInput Remove="Views\Templates\WithBackButtonLayout.axaml" />
</ItemGroup> </ItemGroup>
<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> </ItemGroup>
</Project> </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;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flawless.Client.Remote; using Flawless.Client.Remote;
using ReactiveUI; using ReactiveUI;
@ -6,15 +9,24 @@ using Refit;
namespace Flawless.Client.Service; 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; public IObservable<bool> IsLoggedIn => _isLoggedIn;
@ -49,12 +61,11 @@ public class Api
_token.Value = null; _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 ? HttpMessageHandlerFactory = () => new ApiAuthenticationHandler(new HttpClientHandler())
Task.FromResult<string>($"Bearer {_token}") : Task.FromResult(string.Empty)
}; };
var tempGateway = RestService.For<IFlawlessServer>(host, setting); 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 Refit;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable annotations #nullable enable annotations
@ -19,178 +18,188 @@ namespace Flawless.Client.Remote
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/delete/{username}")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/enable/{username}")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/disable/{username}")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/reset_password")] [Post("/api/admin/user/reset_password")]
Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default); Task ResetPassword([Body] ResetPasswordRequest body);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/auth/status")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/register")] [Post("/api/auth/register")]
Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default); Task Register([Body] RegisterRequest body);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/login")] [Post("/api/auth/login")]
Task<TokenInfo> Login([Body] LoginRequest body, CancellationToken cancellationToken = default); Task<TokenInfo> Login([Body] LoginRequest body);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/auth/refresh")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/logout_all")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/renew_password")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/")] [Get("/")]
Task Index(CancellationToken cancellationToken = default); Task Index();
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_repo")] [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> /// <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")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/archive_repo")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")] [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> /// <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")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/update_user")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_user")] [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> /// <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")] [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> /// <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")] [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> /// <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")] [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> /// <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")] [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> /// <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")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/lock_file")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unlock_file")] [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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Multipart] [Multipart]
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/create_commit")] [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> /// <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")] [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> /// <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")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_info")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_email")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/user/update_phone")] [Post("/api/user/update_phone")]
Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default); Task UpdatePhone([Body] UserContactModifyResponse body);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/get_info")] [Get("/api/user/get_info")]
Task<UserInfoResponse> GetInfo([Query] string username, CancellationToken cancellationToken = default); Task<UserInfoResponse> GetInfo([Query] string username);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")] [Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/query_info")] [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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/user/delete")] [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))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class LoginRequest 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))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RegisterRequest 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))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
public enum RepositoryRole public enum RepositoryRole
{ {
@ -314,6 +424,9 @@ namespace Flawless.Client.Remote
public partial class ServerStatusResponse public partial class ServerStatusResponse
{ {
[JsonPropertyName("friendlyName")]
public string FriendlyName { get; set; }
[JsonPropertyName("allowPublicRegister")] [JsonPropertyName("allowPublicRegister")]
public bool AllowPublicRegister { get; set; } 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))")] [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")] [JsonPropertyName("result")]
public int Offset { get; set; } public ICollection<UserInfoResponse> Result { get; set; }
[JsonPropertyName("length")]
public int Length { get; set; }
[JsonPropertyName("total")]
public int? Total { get; set; }
[JsonPropertyName("data")]
public ICollection<UserInfoResponse> Data { get; set; }
} }
@ -460,6 +564,43 @@ namespace Flawless.Client.Remote
public string ContentType { get; private set; } 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 Avalonia.ReactiveUI;
using Flawless.Client.Models; using Flawless.Client.Models;
using Flawless.Client.Service; using Flawless.Client.Service;
using Flawless.Communication.Response; using Flawless.Client.ViewModels.ModalBox;
using Flawless.Communication.Shared; using Flawless.Client.Views.ModalBox;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.SourceGenerators; using ReactiveUI.SourceGenerators;
using Ursa.Controls;
namespace Flawless.Client.ViewModels; namespace Flawless.Client.ViewModels;
@ -19,31 +20,50 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
public IScreen HostScreen { get; } public IScreen HostScreen { get; }
public ObservableCollection<RepositoryHomePageModel> Repositories { get; } = new(new[] [Reactive] private RepositoryModel? _selectedRepository;
{
new RepositoryHomePageModel(
"cardidi", "test1", "Abc", false, true, ""),
});
[Reactive] private RepositoryHomePageModel? _selectedRepository;
[Reactive] private string _serverFriendlyName; [Reactive] private string _serverFriendlyName;
public RepositoryService RepoSrc => RepositoryService.C;
public HomeViewModel(IScreen hostScreen) public HomeViewModel(IScreen hostScreen)
{ {
HostScreen = hostScreen; HostScreen = hostScreen;
Api.Current.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance) Api.C.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance)
.Subscribe(v => ServerFriendlyName = v ?? "Unknown Server"); .Subscribe(v => ServerFriendlyName = v ?? "Unknown Server");
} }
[ReactiveCommand] [ReactiveCommand]
private async Task RefreshRepositoriesAsync() private async Task RefreshRepositoriesAsync()
{ {
if (await RepoSrc.UpdateRepositoriesFromServerAsync())
{
}
} }
[ReactiveCommand] [ReactiveCommand]
private async Task CreateRepositoryAsync() 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] [ReactiveCommand]
@ -51,8 +71,25 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
{ {
} }
[ReactiveCommand]
private async Task DownloadRepositoryAsync()
{
}
[ReactiveCommand] [ReactiveCommand]
private async Task DeleteRepositoryAsync() 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> 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) public LoginPageViewModel(IScreen hostScreen)
{ {
@ -50,7 +50,8 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
{ {
try try
{ {
await Api.Current.LoginAsync(Username, Password); await Api.C.LoginAsync(Username, Password);
await RepositoryService.C.UpdateRepositoriesFromServerAsync();
} }
catch (ApiException ex) catch (ApiException ex)
{ {

View File

@ -13,9 +13,9 @@ public partial class MainWindowViewModel : ViewModelBase, IScreen
public MainWindowViewModel() public MainWindowViewModel()
{ {
#pragma warning disable VSTHRD110 #pragma warning disable VSTHRD110
Api.Current.IsLoggedIn.Where(x => x) Api.C.IsLoggedIn.Where(x => x)
.SubscribeOnUIThread(_ => Router.Navigate.Execute(new HomeViewModel(this))); .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))); .SubscribeOnUIThread(_ => Router.Navigate.Execute(new ServerConnectViewModel(this)));
#pragma warning restore VSTHRD110 #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 try
{ {
await Api.Current.Gateway.Register(new RegisterRequest await Api.C.Gateway.Register(new RegisterRequest
{ {
Email = _email, Email = _email,
Username = _username, 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) 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] [ReactiveCommand]
private async ValueTask OpenRepoPageAsync() 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) public ServerConnectViewModel(IScreen hostScreen)

View File

@ -9,25 +9,20 @@ using Refit;
namespace Flawless.Client.ViewModels; 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] private string _host = "http://localhost:5256/";
[Reactive(SetModifier = AccessModifier.Protected)] private string? _issue; [Reactive(SetModifier = AccessModifier.Protected)] private string? _issue;
public IObservable<bool> CanSetHost { get; } public IObservable<bool> CanSetHost { get; }
public ServerSetupPageViewModel(IScreen hostScreen) public ServerSetupPageViewModel(IScreen hostScreen) : base(hostScreen)
{ {
HostScreen = hostScreen;
// Must clear this gateway // 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)); CanSetHost = this.WhenAnyValue(x => x.Host, s => !string.IsNullOrWhiteSpace(s));
} }
@ -39,7 +34,7 @@ public partial class ServerSetupPageViewModel : ViewModelBase, IRoutableViewMode
try try
{ {
Issue = string.Empty; Issue = string.Empty;
await Api.Current.SetGatewayAsync(Host); await Api.C.SetGatewayAsync(Host);
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen)); HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
} }
catch (ApiException ex) 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; namespace Flawless.Client.ViewModels;
public abstract class ViewModelBase : ReactiveObject {} 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"> x:Class="Flawless.Client.Views.HomeView">
<DockPanel Margin="50"> <DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top"> <Grid RowDefinitions="Auto, 18, Auto" ColumnDefinitions="*, Auto" DockPanel.Dock="Top">
<Label Content="{Binding ServerFriendlyName, StringFormat='Server {0}'}" FontSize="18" FontWeight="400"></Label> <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> <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"> <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" <u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"
Command="{Binding RefreshRepositoriesCommand}"/> Command="{Binding RefreshRepositoriesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Create" <u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Create"
@ -22,23 +31,38 @@
<u:DisableContainer IsEnabled="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}"> <u:DisableContainer IsEnabled="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconFolderOpen}" Content="Open" <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}"/> Command="{Binding OpenRepositoryCommand}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconDelete}" Content="Delete" <u:IconButton Classes="Danger" Icon="{StaticResource SemiIconDelete}" Content="Delete"
IsEnabled="{Binding SelectedRepository.IsOwner}" IsEnabled="{Binding SelectedRepository.IsDownloaded}"
Command="{Binding DeleteRepositoryCommand}"/> Command="{Binding DeleteRepositoryCommand}"/>
</StackPanel> </StackPanel>
</u:DisableContainer> </u:DisableContainer>
</StackPanel> </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> </StackPanel>
<Rectangle DockPanel.Dock="Top" Height="20"/> <Grid IsVisible="{Binding RepoSrc.Repositories.Count}" ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
<Grid ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" AllowAutoHide="True"> <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}" SelectedItem="{Binding SelectedRepository, Mode=TwoWay}"
Margin="0, 0, 8, 0"> Margin="0, 0, 8, 0">
<u:SelectionList.ItemTemplate> <u:SelectionList.ItemTemplate>
<DataTemplate> <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> </DataTemplate>
</u:SelectionList.ItemTemplate> </u:SelectionList.ItemTemplate>
</u:SelectionList> </u:SelectionList>
@ -47,9 +71,8 @@
<StackPanel VerticalAlignment="Stretch" Spacing="20"> <StackPanel VerticalAlignment="Stretch" Spacing="20">
<Label FontWeight="400" FontSize="24" <Label FontWeight="400" FontSize="24"
Content="{Binding SelectedRepository.Name, FallbackValue='Select a Repository'}"/> Content="{Binding SelectedRepository.Name, FallbackValue='Select a Repository'}"/>
<StackPanel Spacing="10" IsVisible="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}"> <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"> Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconArchive}"/> <PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconArchive}"/>
<Label Content="Archived"/> <Label Content="Archived"/>
@ -58,12 +81,11 @@
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconUser}"/> <PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconUser}"/>
<Label Content="{Binding SelectedRepository.OwnerName, FallbackValue='Owner'}"/> <Label Content="{Binding SelectedRepository.OwnerName, FallbackValue='Owner'}"/>
</StackPanel> </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}"> <ScrollViewer IsVisible="{Binding SelectedRepository}">
<StackPanel Orientation="Vertical" Spacing="8">
<Label Content="{Binding SelectedRepository.Description, FallbackValue='Description as below.'}"/> <Label Content="{Binding SelectedRepository.Description, FallbackValue='Description as below.'}"/>
<u:Divider Content="Recent Activities"/>
</StackPanel>
</ScrollViewer> </ScrollViewer>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

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

View File

@ -5,12 +5,14 @@
xmlns:u="https://irihi.tech/ursa" xmlns:u="https://irihi.tech/ursa"
xmlns:semi="https://irihi.tech/semi" xmlns:semi="https://irihi.tech/semi"
xmlns:vm="using:Flawless.Client.ViewModels" xmlns:vm="using:Flawless.Client.ViewModels"
x:DataType="vm:SettingViewModel"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768" mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
x:Class="Flawless.Client.Views.SettingView"> x:Class="Flawless.Client.Views.SettingView">
<DockPanel Margin="50"> <DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="20"> <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"/> <Label FontWeight="400" FontSize="28" Content="Settings"/>
</StackPanel> </StackPanel>
<TabControl TabStripPlacement="Left" Margin="0 20"> <TabControl TabStripPlacement="Left" Margin="0 20">
@ -18,10 +20,14 @@
</TabItem> </TabItem>
<TabItem Header="Local Storage"> <TabItem Header="Local Storage">
</TabItem> </TabItem>
<TabItem Header="Server Configuration">
</TabItem>
<TabItem Header="Server Users"> <TabItem Header="Server Users">
</TabItem> </TabItem>
<TabItem Header="Server Info"> <TabItem Header="Server Info">
</TabItem> </TabItem>
<TabItem Header="Client Info">
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

View File

@ -1,10 +1,12 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Flawless.Client.ViewModels;
using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views; namespace Flawless.Client.Views;
public partial class SettingView : UserControl public partial class SettingView : ReactiveUrsaView<SettingViewModel>
{ {
public SettingView() 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 string OwnerUsername { get; set; }
public required Guid LatestCommitId { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public required bool IsArchived { get; set; } public required bool IsArchived { get; set; }

View File

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

View File

@ -14,38 +14,32 @@ namespace Flawless.Server.Controllers;
[ApiController, Authorize, Route("api")] [ApiController, Authorize, Route("api")]
public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUser> userManager) : ControllerBase public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUser> userManager) : ControllerBase
{ {
[HttpGet("repo_list")] [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 u = (await userManager.GetUserAsync(HttpContext.User))!;
var query = await dbContext.Repositories var query = dbContext.Repositories
.Include(repository => repository.Owner) .Include(repository => repository.Owner)
.Include(repository => repository.Commits) .Include(repository => repository.Commits)
.Include(repository => repository.Members) .Include(repository => repository.Members)
.ThenInclude(repositoryMember => repositoryMember.User) .ThenInclude(repositoryMember => repositoryMember.User)
.Where(rp => rp.Members.Any(m => m.User == u)) .Where(rp => rp.Members.Any(m => m.User == u) || rp.Owner == u)
.Skip(r.Offset) .Select(rp => new RepositoryInfoResponse
.Take(r.Length)
.ToArrayAsync();
return Ok(new PagedResponse<RepositoryInfoResponse>
{
Length = r.Length,
Offset = r.Offset,
Data = query.Select(rp => new RepositoryInfoResponse
{ {
RepositoryName = rp.Name, RepositoryName = rp.Name,
OwnerUsername = rp.Owner.UserName!, OwnerUsername = rp.Owner.UserName!,
LatestCommitId = rp.Commits.OrderByDescending(cm => cm.CommittedOn).FirstOrDefault()?.Id ?? Guid.Empty,
Description = rp.Description, Description = rp.Description,
IsArchived = rp.IsArchived, 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")] [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) if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
return BadRequest(new FailedResponse("Repository name is empty or too short!")); 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)) if (await dbContext.Repositories.AnyAsync(rp => rp.Name == repositoryName && u == rp.Owner))
return BadRequest(new FailedResponse("Repository name has already created!")); return BadRequest(new FailedResponse("Repository name has already created!"));
await dbContext.Repositories.AddAsync(new Repository() var repo = new Repository()
{ {
Name = repositoryName, Name = repositoryName,
Owner = u, Owner = u,
}); };
await dbContext.Repositories.AddAsync(repo);
await dbContext.SaveChangesAsync(); 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")] [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 var payload = await userManager.Users
.Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword))) .Where(u => u.UserName!.Contains(keyword) || (u.NickName != null && u.NickName.Contains(keyword)))
.Skip(r.Offset)
.Take(r.Length)
.Select(u => GetUserInfoInternal(u, null)) .Select(u => GetUserInfoInternal(u, null))
.ToArrayAsync(); .ToArrayAsync();
// Return self as default // Return self as default
return Ok(new PagedResponse<UserInfoResponse> return Ok(new ListingResponse<UserInfoResponse>(payload));
{
Offset = r.Offset,
Length = r.Length,
Data = payload
});
} }
[HttpGet("delete")] [HttpGet("delete")]

View File

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

View File

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

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
namespace Flawless.Server; namespace Flawless.Server;
@ -46,6 +47,26 @@ public static class Program
{ {
opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods opt.DocInclusionPredicate((name, api) => api.HttpMethod != null); // Filter out WebSocket methods
opt.SupportNonNullableReferenceTypes(); 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 == null) throw new SecurityTokenExpiredException("User is not existed.");
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched."); 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 // Extract user info into HttpContext