feat: Finish repository fetch functions.
This commit is contained in:
parent
4d24eb80f9
commit
14a4ccd6f6
@ -16,6 +16,8 @@
|
||||
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/MainWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/CreateRepositoryDialog.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RegisterPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RegisterView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
<entry key="Flawless.Client/Views/RepositoryPage/RepoCommitPageView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
|
||||
|
||||
@ -33,13 +33,13 @@
|
||||
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa\1.10.0\lib\net8.0\Ursa.dll" />
|
||||
<Assembly Path="C:\Users\Cardi\.nuget\packages\irihi.ursa.themes.semi\1.10.0\lib\netstandard2.0\Ursa.Themes.Semi.dll" />
|
||||
</AssemblyExplorer></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
@ -6,6 +6,12 @@ namespace Flawless.Client;
|
||||
public static class AppDefaultValues
|
||||
{
|
||||
public const string HostId = "Overlay";
|
||||
|
||||
public const string RepoLocalStorageManagerFolder = ".flawless";
|
||||
|
||||
public const string RepoLocalStorageDatabaseFile = "db.json";
|
||||
|
||||
public const string RepoLocalStorageDepotFolder = "depots";
|
||||
|
||||
public static string ProgramDataDirectory { get; } =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ca2dWorks", "FlawlessClient");
|
||||
|
||||
11
Flawless.Client/ErrorGUIHandler.cs
Normal file
11
Flawless.Client/ErrorGUIHandler.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using Avalonia.Controls.Notifications;
|
||||
|
||||
namespace Flawless.Client;
|
||||
|
||||
public static class ErrorGUIHandler
|
||||
{
|
||||
public static void OnError(Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Refit" Version="8.0.0" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="11.2.1.6" />
|
||||
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -55,5 +56,14 @@
|
||||
<DependentUpon>ServerSetupPageView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\ModalBox\SimpleMessageDialogView.axaml.cs">
|
||||
<DependentUpon>SimpleMessageDialogView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flawless.Abstract\Flawless.Abstract.csproj" />
|
||||
<ProjectReference Include="..\Flawless.Core\Flawless.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
21
Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
Normal file
21
Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Flawless.Client.Service;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.Models;
|
||||
|
||||
[Serializable]
|
||||
public partial class RepositoryLocalDatabaseModel : ReactiveModel
|
||||
{
|
||||
public required RepositoryModel RootModal { get; init; }
|
||||
|
||||
public required LocalFileTreeAccessor LocalAccessor { get; init; }
|
||||
|
||||
public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
|
||||
|
||||
public ObservableCollection<string> CurrentLockedFiles { get; } = new();
|
||||
|
||||
[NonSerialized]
|
||||
[Reactive] private Guid? _currentCommit;
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Flawless.Client;
|
||||
@ -10,4 +13,13 @@ public static class ObserverHelper
|
||||
var dispatcher = Dispatcher.UIThread;
|
||||
observer.Subscribe(val => dispatcher.Invoke(() => onNext(val)));
|
||||
}
|
||||
|
||||
public static void Sort<T>(this ObservableCollection<T> collection, Comparison<T> comparison)
|
||||
{
|
||||
var sortableList = new List<T>(collection);
|
||||
sortableList.Sort(comparison);
|
||||
|
||||
for (int i = 0; i < sortableList.Count; i++) collection.Move(collection.IndexOf(sortableList[i]), i);
|
||||
}
|
||||
|
||||
}
|
||||
24
Flawless.Client/PathUtility.cs
Normal file
24
Flawless.Client/PathUtility.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.IO;
|
||||
using Flawless.Client.Service;
|
||||
|
||||
namespace Flawless.Client;
|
||||
|
||||
public static class PathUtility
|
||||
{
|
||||
public static string GetWorkspacePath(string owner, string repo)
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo);
|
||||
|
||||
|
||||
public static string GetWorkspaceManagerPath(string owner, string repo)
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo, AppDefaultValues.RepoLocalStorageManagerFolder);
|
||||
|
||||
|
||||
public static string GetWorkspaceDbPath(string owner, string repo)
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
|
||||
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDatabaseFile);
|
||||
|
||||
public static string GetWorkspaceDepotCachePath(string owner, string repo)
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, owner, repo,
|
||||
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
|
||||
|
||||
}
|
||||
98
Flawless.Client/Service/LocalFileTreeAccessor.cs
Normal file
98
Flawless.Client/Service/LocalFileTreeAccessor.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Flawless.Abstraction;
|
||||
using Flawless.Client.Models;
|
||||
using Flawless.Core.Modal;
|
||||
|
||||
namespace Flawless.Client.Service;
|
||||
|
||||
public class LocalFileTreeAccessor
|
||||
{
|
||||
private static readonly string[] IgnoredDirectories =
|
||||
{
|
||||
AppDefaultValues.RepoLocalStorageManagerFolder
|
||||
};
|
||||
|
||||
private readonly RepositoryModel _repo;
|
||||
|
||||
private readonly string _rootDirectory;
|
||||
|
||||
private IReadOnlyDictionary<string, WorkspaceFile> _baseline;
|
||||
|
||||
private readonly Dictionary<string, WorkspaceFile> _newFiles = new();
|
||||
|
||||
private readonly Dictionary<string, WorkspaceFile> _deleteFiles = new();
|
||||
|
||||
private readonly Dictionary<string, WorkspaceFile> _modifyFiles = new();
|
||||
|
||||
private object _optLock = new();
|
||||
|
||||
public string WorkingDirectory => _rootDirectory;
|
||||
|
||||
public IReadOnlyDictionary<string, WorkspaceFile> BaselineFiles => _baseline;
|
||||
|
||||
public IReadOnlyDictionary<string, WorkspaceFile> NewFiles => _newFiles;
|
||||
|
||||
public IReadOnlyDictionary<string, WorkspaceFile> DeletedFiles => _deleteFiles;
|
||||
|
||||
public IReadOnlyDictionary<string, WorkspaceFile> ModifiedFiles => _modifyFiles;
|
||||
|
||||
|
||||
public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable<WorkspaceFile> baselines)
|
||||
{
|
||||
_repo = repo;
|
||||
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
||||
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
||||
}
|
||||
|
||||
public void SetBaseline(IEnumerable<WorkspaceFile> baselines)
|
||||
{
|
||||
lock (_optLock)
|
||||
{
|
||||
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
|
||||
_newFiles.Clear();
|
||||
_deleteFiles.Clear();
|
||||
_modifyFiles.Clear();
|
||||
|
||||
RefreshInternal();
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
lock (_optLock)
|
||||
{
|
||||
_newFiles.Clear();
|
||||
_deleteFiles.Clear();
|
||||
_modifyFiles.Clear();
|
||||
|
||||
RefreshInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshInternal()
|
||||
{
|
||||
Dictionary<string, WorkspaceFile> currentFiles = new();
|
||||
foreach (var f in Directory.GetFiles(_rootDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var workPath = WorkPath.FromPlatformPath(f, _rootDirectory);
|
||||
if (!WorkPath.IsPathValid(workPath) || IgnoredDirectories.Any(d => workPath.StartsWith(d)))
|
||||
continue;
|
||||
|
||||
var modifyTime = File.GetLastWriteTimeUtc(f);
|
||||
currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
|
||||
}
|
||||
|
||||
// Find those are changed
|
||||
var changes = currentFiles.Values.Where(v => _baseline.TryGetValue(v.WorkPath, out var fi) && fi.ModifyTime != v.ModifyTime);
|
||||
var news = currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
|
||||
var removed = _baseline.Values.Where(v => !currentFiles.ContainsKey(v.WorkPath));
|
||||
|
||||
foreach (var f in changes) _modifyFiles.Add(f.WorkPath, f);
|
||||
foreach (var f in news) _newFiles.Add(f.WorkPath, f);
|
||||
foreach (var f in removed) _deleteFiles.Add(f.WorkPath, f);
|
||||
}
|
||||
}
|
||||
@ -167,7 +167,7 @@ namespace Flawless.Client.Remote
|
||||
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
|
||||
[Headers("Accept: text/plain, application/json, text/json")]
|
||||
[Post("/api/repo_create")]
|
||||
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName);
|
||||
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName, [Query] string description);
|
||||
|
||||
/// <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>
|
||||
|
||||
132
Flawless.Client/Service/RepositoryFileTreeAccessor.cs
Normal file
132
Flawless.Client/Service/RepositoryFileTreeAccessor.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Core.BinaryDataFormat;
|
||||
using Flawless.Core.Modal;
|
||||
|
||||
namespace Flawless.Client.Service;
|
||||
|
||||
public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable
|
||||
{
|
||||
|
||||
private struct FileReadInfoCache
|
||||
{
|
||||
public readonly Guid DepotId { get; init; }
|
||||
|
||||
public readonly DepotFileInfo FileInfo { get; init; }
|
||||
}
|
||||
|
||||
private readonly Dictionary<Guid, Stream> _mappings;
|
||||
|
||||
private readonly CommitManifest _manifest;
|
||||
|
||||
private readonly Dictionary<Guid, StandardDepotHeaderV1> _headers;
|
||||
|
||||
private readonly Dictionary<string, FileReadInfoCache> _cached;
|
||||
|
||||
private object _readonlyLock = new(); // todo support async method.
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public CommitManifest Manifest => _manifest;
|
||||
|
||||
public bool IsCached { get; private set; }
|
||||
|
||||
public RepositoryFileTreeAccessor(Dictionary<Guid, Stream> currentReadFs, CommitManifest manifest)
|
||||
{
|
||||
IsCached = false;
|
||||
_mappings = currentReadFs;
|
||||
_manifest = manifest;
|
||||
_headers = new Dictionary<Guid, StandardDepotHeaderV1>();
|
||||
_cached = new Dictionary<string, FileReadInfoCache>();
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var mv in _mappings.Values)
|
||||
{
|
||||
try { mv.Dispose(); }
|
||||
catch (Exception e) { Console.WriteLine(e); }
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var mv in _mappings.Values)
|
||||
{
|
||||
try { await mv.DisposeAsync(); }
|
||||
catch (Exception e) { Console.WriteLine(e); }
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateCacheAsync()
|
||||
{
|
||||
DisposeCheck();
|
||||
lock (_readonlyLock) if (IsCached) return;
|
||||
|
||||
var ms = _manifest.FilePaths.ToImmutableSortedDictionary(x => x.WorkPath, x => x.ModifyTime);
|
||||
|
||||
foreach (var (id, stream) in _mappings)
|
||||
{
|
||||
var header = DataTransformer.ExtractStandardDepotHeaderV1(stream);
|
||||
_headers.Add(id, header);
|
||||
await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(stream, header.FileMapSize))
|
||||
{
|
||||
if (ms.TryGetValue(inf.Path, out var targetTime) && targetTime == inf.ModifyTime)
|
||||
{
|
||||
_cached.Add(inf.Path, new FileReadInfoCache { DepotId = id, FileInfo = inf });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ms.Count != _cached.Count)
|
||||
throw new FileNotFoundException("Some of those files are not able to be fount in any depots!");
|
||||
|
||||
lock (_readonlyLock) IsCached = true;
|
||||
}
|
||||
|
||||
public bool TryGetFileInfo(string workPath, out DepotFileInfo info)
|
||||
{
|
||||
DisposeCheck();
|
||||
lock (_readonlyLock) if (!IsCached) throw new InvalidOperationException("Not cached!");
|
||||
|
||||
if (_cached.TryGetValue(workPath, out var r))
|
||||
{
|
||||
info = r.FileInfo;
|
||||
return true;
|
||||
}
|
||||
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryWriteDataIntoStream(string workPath, Stream stream)
|
||||
{
|
||||
DisposeCheck();
|
||||
if (stream == null || !stream.CanWrite) throw new ArgumentException("Stream is not writable!");
|
||||
|
||||
if (_cached.TryGetValue(workPath, out var r))
|
||||
{
|
||||
var baseStream = DataTransformer.ExtractStandardDepotFile(_mappings[r.DepotId], r.FileInfo);
|
||||
baseStream.CopyTo(stream);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DisposeCheck()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException("Accessor has already been disposed");
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,8 +4,11 @@ using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Abstraction;
|
||||
using Flawless.Client.Models;
|
||||
using Flawless.Client.Remote;
|
||||
using Flawless.Core.Modal;
|
||||
using Newtonsoft.Json;
|
||||
using ValueTaskSupplement;
|
||||
|
||||
namespace Flawless.Client.Service;
|
||||
|
||||
@ -13,40 +16,107 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
{
|
||||
public ObservableCollection<RepositoryModel> Repositories => _repositories;
|
||||
|
||||
|
||||
private readonly ObservableCollection<RepositoryModel> _repositories = new();
|
||||
|
||||
public async ValueTask<bool> CreateRepositoryOnServerAsync(string repositoryName)
|
||||
private readonly Dictionary<RepositoryModel, RepositoryLocalDatabaseModel> _localRepoDbModel = new();
|
||||
|
||||
private readonly HashSet<RepositoryModel> _openedRepos = new();
|
||||
|
||||
private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo)
|
||||
{
|
||||
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
||||
if (File.Exists(dbPath)) return false;
|
||||
|
||||
// Get directories
|
||||
var localRepoDb = GetRepositoryLocalDatabase(repo);
|
||||
var folderPath = PathUtility.GetWorkspaceManagerPath(repo.OwnerName, repo.Name);
|
||||
|
||||
// Create initial data.
|
||||
Directory.CreateDirectory(folderPath);
|
||||
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Create));
|
||||
JsonSerializer.CreateDefault().Serialize(writeFs, localRepoDb);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool SaveRepositoryLocalDatabaseChanges(RepositoryModel repo)
|
||||
{
|
||||
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
||||
if (File.Exists(dbPath)) return false;
|
||||
|
||||
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
|
||||
{
|
||||
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Truncate));
|
||||
JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
|
||||
{
|
||||
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
|
||||
{
|
||||
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
||||
if (File.Exists(dbPath))
|
||||
{
|
||||
// Use existed target
|
||||
using var readFs = new StreamReader(new FileStream(dbPath, FileMode.Open));
|
||||
localRepo = JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
|
||||
as RepositoryLocalDatabaseModel; // todo add broken test.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new one.
|
||||
localRepo = new RepositoryLocalDatabaseModel
|
||||
{
|
||||
RootModal = repo,
|
||||
LocalAccessor = new LocalFileTreeAccessor(repo, [])
|
||||
};
|
||||
}
|
||||
|
||||
_localRepoDbModel.Add(repo, localRepo);
|
||||
}
|
||||
|
||||
return localRepo;
|
||||
}
|
||||
|
||||
public async ValueTask<RepositoryModel?> CreateRepositoryOnServerAsync(string repositoryName, string description)
|
||||
{
|
||||
RepositoryModel repo;
|
||||
var api = Api.C;
|
||||
try
|
||||
{
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var r = await api.Gateway.RepoCreate(repositoryName);
|
||||
var r = await api.Gateway.RepoCreate(repositoryName, description);
|
||||
|
||||
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;
|
||||
|
||||
repo = new RepositoryModel
|
||||
{
|
||||
Name = r.RepositoryName,
|
||||
OwnerName = r.OwnerUsername,
|
||||
StandaloneName = RepositoryModel.GetStandaloneName(r.RepositoryName, r.OwnerUsername),
|
||||
Description = r.Description,
|
||||
Archived = r.IsArchived,
|
||||
OwnByCurrentUser = (int) r.Role == (int) RepositoryModel.RepositoryRole.Owner
|
||||
};
|
||||
|
||||
Repositories.Insert(0, repo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
return true;
|
||||
return repo;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> UpdateRepositoriesFromServerAsync()
|
||||
@ -98,18 +168,27 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> UpdateRepositoriesDownloadedFromDiskAsync()
|
||||
public async ValueTask<bool> UpdateRepositoriesDownloadedStatusFromDiskAsync()
|
||||
{
|
||||
foreach (var repo in _repositories)
|
||||
{
|
||||
var fsPath = Path.Combine(SettingService.C.AppSetting.RepositoryPath, repo.OwnerName, repo.Name);
|
||||
repo.IsDownloaded = Directory.Exists(fsPath);
|
||||
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
|
||||
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
|
||||
repo.IsDownloaded = isFolderExists && isDbFileExists;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> UpdateDownloadedStatusFromDiskAsync(RepositoryModel repo)
|
||||
{
|
||||
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
|
||||
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
|
||||
|
||||
return isFolderExists && isDbFileExists;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> UpdateRepositoryMembersFromServerAsync(RepositoryModel repo)
|
||||
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
|
||||
{
|
||||
var api = Api.C;
|
||||
try
|
||||
@ -158,4 +237,344 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsRepositoryOpened(RepositoryModel repo)
|
||||
{
|
||||
return _openedRepos.Any(r => r == repo);
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CloseRepositoryAsync(RepositoryModel repo)
|
||||
{
|
||||
if (_openedRepos.Contains(repo))
|
||||
{
|
||||
var ls = GetRepositoryLocalDatabase(repo);
|
||||
if (ls.RepoAccessor != null) await ls.RepoAccessor!.DisposeAsync();
|
||||
ls.RepoAccessor = null;
|
||||
|
||||
if (!SaveRepositoryLocalDatabaseChanges(repo)) return false;
|
||||
|
||||
_openedRepos.Remove(repo);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
|
||||
{
|
||||
if (!await UpdateDownloadedStatusFromDiskAsync(repo) || repo.IsDownloaded == false) return false;
|
||||
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
||||
var ls = GetRepositoryLocalDatabase(repo);
|
||||
|
||||
if (ls.CurrentCommit != null)
|
||||
{
|
||||
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, ls.CurrentCommit.Value);
|
||||
if (accessor == null) return false;
|
||||
ls.RepoAccessor = accessor;
|
||||
}
|
||||
|
||||
_openedRepos.Add(repo);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CreateNewRepositoryOnStorageAsync(RepositoryModel repo)
|
||||
{
|
||||
// Create basic structures.
|
||||
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
||||
|
||||
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
||||
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
|
||||
if (peekCommit == null) return false; // Should not use this function!
|
||||
|
||||
repo.IsDownloaded = true;
|
||||
_openedRepos.Add(repo);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CloneRepositoryFromRemoteAsync(RepositoryModel repo)
|
||||
{
|
||||
// Create basic structures.
|
||||
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
||||
|
||||
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
||||
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
|
||||
if (peekCommit == null) return false; // Should not use this function!
|
||||
|
||||
// Create basic structures.
|
||||
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
||||
|
||||
// Download base repo info
|
||||
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, peekCommit.CommitId);
|
||||
if (accessor == null)
|
||||
{
|
||||
await DeleteFromDiskAsync(repo);
|
||||
return false;
|
||||
};
|
||||
|
||||
var ls = GetRepositoryLocalDatabase(repo);
|
||||
ls.CurrentCommit = peekCommit.CommitId;
|
||||
ls.RepoAccessor = accessor;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var f in accessor.Manifest.FilePaths)
|
||||
{
|
||||
var pfs = WorkPath.ToPlatformPath(f.WorkPath, ls.LocalAccessor.WorkingDirectory);
|
||||
var directory = Path.GetDirectoryName(pfs);
|
||||
|
||||
// Write into fs
|
||||
if (directory != null) Directory.CreateDirectory(directory);
|
||||
await using var write = File.Create(pfs);
|
||||
accessor.TryWriteDataIntoStream(f.WorkPath, write);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
await DeleteFromDiskAsync(repo);
|
||||
return false;
|
||||
}
|
||||
|
||||
repo.IsDownloaded = true;
|
||||
_openedRepos.Add(repo);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> ShouldUpdateLocalCommitsCacheFromServerAsync(RepositoryModel repo)
|
||||
{
|
||||
var api = Api.C;
|
||||
try
|
||||
{
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return false;
|
||||
}
|
||||
|
||||
var rsp = await api.Gateway.PeekCommit(repo.Name, repo.OwnerName);
|
||||
var emptyRepo = repo.Commits.Count == 0;
|
||||
|
||||
// If they both empty
|
||||
if ((rsp.Result == Guid.Empty) == emptyRepo) return false;
|
||||
|
||||
if (emptyRepo) return true;
|
||||
return rsp.Result == repo.Commits.MaxBy(cm => cm.CommittedOn)!.CommitId;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> UpdateCommitsFromServerAsync(RepositoryModel repo)
|
||||
{
|
||||
var api = Api.C;
|
||||
try
|
||||
{
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return false;
|
||||
}
|
||||
|
||||
var rsp = await api.Gateway.ListCommit(repo.Name, repo.OwnerName);
|
||||
|
||||
// Update existed
|
||||
var dict = rsp.Result.ToDictionary(m => m.Id);
|
||||
for (var i = 0; i < repo.Commits.Count; i++)
|
||||
{
|
||||
var ele = repo.Commits[i];
|
||||
if (!dict.Remove(ele.CommitId, out var cm))
|
||||
{
|
||||
repo.Members.RemoveAt(i);
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
ele.Message = cm.Message;
|
||||
ele.DepotId = cm.MainDepotId;
|
||||
ele.CommittedOn = cm.CommitedOn.UtcDateTime;
|
||||
ele.Author = cm.Author;
|
||||
}
|
||||
|
||||
// Add missing
|
||||
foreach (var cm in dict.Values)
|
||||
{
|
||||
var r = new RepositoryModel.Commit
|
||||
{
|
||||
CommitId = cm.Id,
|
||||
Message = cm.Message,
|
||||
DepotId = cm.MainDepotId,
|
||||
CommittedOn = cm.CommitedOn.UtcDateTime,
|
||||
Author = cm.Author,
|
||||
};
|
||||
|
||||
repo.Commits.Add(r);
|
||||
}
|
||||
|
||||
// Resort them again
|
||||
repo.Commits.Sort((l, r) => r.CommittedOn.CompareTo(l.CommittedOn));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<RepositoryFileTreeAccessor?> DownloadDepotsToGetCommitFileTreeFromServerAsync
|
||||
(RepositoryModel repo, Guid commit, bool storeDownloadedDepots = true)
|
||||
{
|
||||
if (commit == Guid.Empty) return null;
|
||||
|
||||
// Get depots list and manifest
|
||||
var manifest = await DownloadManifestFromServerAsync(repo, commit);
|
||||
if (manifest == null) return null;
|
||||
|
||||
// Prepare folders
|
||||
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
||||
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
// Generate download depots list
|
||||
var mainDepotLabel = manifest.Value.Depot;
|
||||
var willDownload = mainDepotLabel.Where(label =>
|
||||
{
|
||||
var dpPath = Path.Combine(depotsRoot, label.Id.ToString());
|
||||
if (!File.Exists(dpPath)) return true;
|
||||
// todo Needs a way to check if that valid.
|
||||
return false;
|
||||
});
|
||||
|
||||
// Download them
|
||||
var downloadedDepots = await DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync(repo, willDownload);
|
||||
|
||||
try
|
||||
{
|
||||
// Check if anyone download failed
|
||||
if (downloadedDepots == null || downloadedDepots.Any(dl => dl.Item2 == null))
|
||||
throw new Exception("Some depots are not able to be downloaded.");
|
||||
|
||||
if (storeDownloadedDepots)
|
||||
{
|
||||
// Write new downloaded depots into disk
|
||||
var transform = downloadedDepots.Select(dl => (dl.Item1, dl.Item2!));
|
||||
await WriteDownloadedDepotsFromServerToStorageAsync(repo, transform);
|
||||
}
|
||||
|
||||
// Create mapping dictionary
|
||||
var mappingDict = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
|
||||
foreach (var dl in mainDepotLabel)
|
||||
{
|
||||
if (mappingDict.ContainsKey(dl.Id)) continue;
|
||||
var dst = Path.Combine(depotsRoot, dl.Id.ToString());
|
||||
mappingDict.Add(dl.Id, new FileStream(dst, FileMode.Create));
|
||||
}
|
||||
|
||||
return new RepositoryFileTreeAccessor(mappingDict, manifest.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (downloadedDepots != null)
|
||||
foreach (var t in downloadedDepots)
|
||||
{
|
||||
if (t.Item2 == null) continue;
|
||||
try { await t.Item2.DisposeAsync(); }
|
||||
catch (Exception ex) { Console.WriteLine(ex); }
|
||||
}
|
||||
|
||||
Console.WriteLine(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
|
||||
(RepositoryModel repo, IEnumerable<(Guid, Stream)> depots)
|
||||
{
|
||||
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
||||
var tasks = depots.Select(async d =>
|
||||
{
|
||||
var dst = Path.Combine(depotsRoot, d.Item1.ToString());
|
||||
await using var ws = new FileStream(dst, FileMode.Create);
|
||||
await d.Item2.CopyToAsync(ws);
|
||||
d.Item2.Seek(0, SeekOrigin.Begin);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async ValueTask<(Guid, Stream?)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
|
||||
(RepositoryModel repo, IEnumerable<DepotLabel> depotsId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var api = Api.C;
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
}
|
||||
|
||||
return await ValueTaskEx.WhenAll(depotsId.Select(p => DownloadDepotInternalAsync(p.Id, p.Length)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
async ValueTask<(Guid, Stream?)> DownloadDepotInternalAsync(Guid depotId, long length)
|
||||
{
|
||||
using var rsp = await Api.C.Gateway.FetchDepot(repo.OwnerName, repo.Name, depotId.ToString());
|
||||
if (rsp.StatusCode != 200) return (depotId, null);
|
||||
if (rsp.Stream.Length != length) return (depotId, null);
|
||||
|
||||
var memoryStream = new MemoryStream(new byte[rsp.Stream.Length]);
|
||||
await rsp.Stream.CopyToAsync(memoryStream);
|
||||
return (depotId, memoryStream);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<CommitManifest?> DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var api = Api.C;
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
}
|
||||
|
||||
using var manifestResponse = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
|
||||
if (manifestResponse.StatusCode != 200) return null;
|
||||
|
||||
return await System.Text.Json.JsonSerializer.DeserializeAsync<CommitManifest>(manifestResponse.Stream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> DeleteFromDiskAsync(RepositoryModel repo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
||||
if (Directory.Exists(path)) Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.ReactiveUI;
|
||||
@ -22,9 +21,9 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
|
||||
|
||||
[Reactive] private RepositoryModel? _selectedRepository;
|
||||
|
||||
[Reactive] private string _serverFriendlyName;
|
||||
[Reactive] private string? _serverFriendlyName;
|
||||
|
||||
public RepositoryService RepoSrc => RepositoryService.C;
|
||||
public ObservableCollection<RepositoryModel> Repos => RepositoryService.C.Repositories;
|
||||
|
||||
public HomeViewModel(IScreen hostScreen)
|
||||
{
|
||||
@ -36,10 +35,8 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
|
||||
[ReactiveCommand]
|
||||
private async Task RefreshRepositoriesAsync()
|
||||
{
|
||||
if (await RepoSrc.UpdateRepositoriesFromServerAsync())
|
||||
{
|
||||
|
||||
}
|
||||
await RepositoryService.C.UpdateRepositoriesFromServerAsync();
|
||||
await RepositoryService.C.UpdateRepositoriesDownloadedStatusFromDiskAsync();
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
@ -62,34 +59,64 @@ public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
|
||||
|
||||
if (mr == DialogResult.OK)
|
||||
{
|
||||
await RepoSrc.CreateRepositoryOnServerAsync(form.RepositoryName);
|
||||
var repo = await RepositoryService.C.CreateRepositoryOnServerAsync(form.RepositoryName, form.Description);
|
||||
if (repo == null) return;
|
||||
if (!await RepositoryService.C.CreateNewRepositoryOnStorageAsync(repo)) return;
|
||||
|
||||
await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(repo, HostScreen));
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task OpenRepositoryAsync()
|
||||
{
|
||||
if (_selectedRepository == null) return;
|
||||
if (!await RepositoryService.C.OpenRepositoryOnStorageAsync(_selectedRepository)) return;
|
||||
await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(_selectedRepository, HostScreen));
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task DownloadRepositoryAsync()
|
||||
{
|
||||
if (_selectedRepository == null) return;
|
||||
if (!await RepositoryService.C.CloneRepositoryFromRemoteAsync(_selectedRepository)) return;
|
||||
await HostScreen.Router.Navigate.Execute(new RepositoryViewModel(_selectedRepository, HostScreen));
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task DeleteRepositoryAsync()
|
||||
{
|
||||
if (_selectedRepository == null) return;
|
||||
if (await RepositoryService.C.DeleteFromDiskAsync(_selectedRepository))
|
||||
{
|
||||
await RepositoryService.C.UpdateDownloadedStatusFromDiskAsync(_selectedRepository);
|
||||
}
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task QuitLoginAsync()
|
||||
{
|
||||
Api.C.ClearGateway();
|
||||
var opt = new OverlayDialogOptions
|
||||
{
|
||||
FullScreen = false,
|
||||
Buttons = DialogButton.YesNo,
|
||||
CanResize = false,
|
||||
CanDragMove = false,
|
||||
IsCloseButtonVisible = true,
|
||||
CanLightDismiss = true,
|
||||
Mode = DialogMode.Question
|
||||
};
|
||||
|
||||
var vm = new SimpleMessageDialogViewModel("Do you really want to logout?");
|
||||
var mr = await OverlayDialog
|
||||
.ShowModal<SimpleMessageDialogView, SimpleMessageDialogViewModel>(vm, AppDefaultValues.HostId, opt);
|
||||
|
||||
if (mr == DialogResult.Yes) Api.C.ClearGateway();
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
private void OpenGlobalSettingAsync()
|
||||
private async Task OpenGlobalSettingAsync()
|
||||
{
|
||||
HostScreen.Router.Navigate.Execute(new SettingViewModel(HostScreen));
|
||||
await HostScreen.Router.Navigate.Execute(new SettingViewModel(HostScreen));
|
||||
}
|
||||
}
|
||||
@ -5,4 +5,6 @@ namespace Flawless.Client.ViewModels.ModalBox;
|
||||
public partial class CreateRepositoryDialogViewModel : ViewModelBase
|
||||
{
|
||||
[Reactive] private string _repositoryName;
|
||||
|
||||
[Reactive] private string _description;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.ViewModels.ModalBox;
|
||||
|
||||
public partial class SimpleMessageDialogViewModel : ViewModelBase
|
||||
{
|
||||
[Reactive] private string _message;
|
||||
|
||||
public SimpleMessageDialogViewModel(string message)
|
||||
{
|
||||
_message = message;
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,25 @@
|
||||
using Flawless.Client.Models;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Flawless.Client.Models;
|
||||
using Flawless.Client.Service;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace Flawless.Client.ViewModels;
|
||||
|
||||
public class RepositoryViewModel : RoutableViewModelBase
|
||||
public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
{
|
||||
public RepositoryModel Repository { get; }
|
||||
|
||||
public RepositoryService RepoSrv { get; }
|
||||
|
||||
[ReactiveCommand]
|
||||
private async Task GoBackAsync()
|
||||
{
|
||||
await RepositoryService.C.CloseRepositoryAsync(Repository);
|
||||
await HostScreen.Router.NavigateBack.Execute();
|
||||
}
|
||||
|
||||
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
|
||||
{
|
||||
Repository = repo;
|
||||
RepoSrv = RepositoryService.C;
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<u:IconButton Icon="{StaticResource SemiIconSetting}"
|
||||
Command="{Binding OpenGlobalSettingAsyncCommand}"/>
|
||||
Command="{Binding OpenGlobalSettingCommand}"/>
|
||||
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconQuit}"
|
||||
Command="{Binding QuitLoginCommand}"/>
|
||||
</StackPanel>
|
||||
@ -35,7 +35,7 @@
|
||||
Command="{Binding OpenRepositoryCommand}"/>
|
||||
<u:IconButton Icon="{StaticResource SemiIconDownload}" Content="Download"
|
||||
IsEnabled="{Binding !SelectedRepository.IsDownloaded}"
|
||||
Command="{Binding OpenRepositoryCommand}"/>
|
||||
Command="{Binding DownloadRepositoryCommand}"/>
|
||||
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconDelete}" Content="Delete"
|
||||
IsEnabled="{Binding SelectedRepository.IsDownloaded}"
|
||||
Command="{Binding DeleteRepositoryCommand}"/>
|
||||
@ -44,14 +44,14 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Rectangle DockPanel.Dock="Top" Height="18"/>
|
||||
<StackPanel IsVisible="{Binding !RepoSrc.Repositories.Count}"
|
||||
<StackPanel IsVisible="{Binding !Repos.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>
|
||||
<Grid IsVisible="{Binding RepoSrc.Repositories.Count}" ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
|
||||
<Grid IsVisible="{Binding Repos.Count}" ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
|
||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" AllowAutoHide="True">
|
||||
<u:SelectionList ItemsSource="{Binding RepoSrc.Repositories, Mode=TwoWay}"
|
||||
<u:SelectionList ItemsSource="{Binding Repos, Mode=TwoWay}"
|
||||
SelectedItem="{Binding SelectedRepository, Mode=TwoWay}"
|
||||
Margin="0, 0, 8, 0">
|
||||
<u:SelectionList.ItemTemplate>
|
||||
|
||||
@ -12,11 +12,14 @@
|
||||
<u:Form HorizontalAlignment="Stretch" LabelPosition="Top">
|
||||
<u:Form.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*" />
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*, *" />
|
||||
</ItemsPanelTemplate>
|
||||
</u:Form.ItemsPanel>
|
||||
<u:FormItem Label="Name">
|
||||
<u:FormItem Grid.Row="0" Label="Name">
|
||||
<TextBox Text="{Binding RepositoryName}"/>
|
||||
</u:FormItem>
|
||||
<u:FormItem Grid.Row="1" Label="Description">
|
||||
<TextBox MinHeight="40" MaxHeight="80" Text="{Binding Description}"/>
|
||||
</u:FormItem>
|
||||
</u:Form>
|
||||
</UserControl>
|
||||
|
||||
22
Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml
Normal file
22
Flawless.Client/Views/ModalBox/SimpleMessageDialogView.axaml
Normal 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:SimpleMessageDialogViewModel"
|
||||
d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"
|
||||
MinWidth="400"
|
||||
x:Class="Flawless.Client.Views.ModalBox.SimpleMessageDialogView">
|
||||
|
||||
<u:Form HorizontalAlignment="Stretch" LabelPosition="Top">
|
||||
<u:Form.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*" />
|
||||
</ItemsPanelTemplate>
|
||||
</u:Form.ItemsPanel>
|
||||
<u:FormItem>
|
||||
<Label Content="{Binding Message}"/>
|
||||
</u:FormItem>
|
||||
</u:Form>
|
||||
</UserControl>
|
||||
@ -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 SimpleMessageDialogView : ReactiveUrsaView<SimpleMessageDialogViewModel>
|
||||
{
|
||||
public SimpleMessageDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -6,12 +6,14 @@
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||
xmlns:page="using:Flawless.Client.Views.RepositoryPage"
|
||||
x:DataType="vm:RepositoryViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
|
||||
x:Class="Flawless.Client.Views.RepositoryView">
|
||||
|
||||
<DockPanel Margin="50">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
|
||||
<u:IconButton Height="4" Icon="{StaticResource SemiIconArrowLeft}" Content="All Repositories"/>
|
||||
<u:IconButton Height="4" Icon="{StaticResource SemiIconArrowLeft}"
|
||||
Command="{Binding GoBackCommand}" Content="All Repositories"/>
|
||||
<Label FontWeight="400" FontSize="28" Content="Name of Repository"/>
|
||||
</StackPanel>
|
||||
<TabControl TabStripPlacement="Top" Margin="0 20">
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using Flawless.Communication.Shared;
|
||||
using Flawless.Core.Modal;
|
||||
|
||||
namespace Flawless.Communication.Request;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Flawless.Core.Modal;
|
||||
|
||||
[Serializable]
|
||||
public record struct CommitManifest(Guid ManifestId, DepotLabel Depot, WorkspaceFile[] FilePaths);
|
||||
public record struct CommitManifest(Guid ManifestId, DepotLabel[] Depot, WorkspaceFile[] FilePaths);
|
||||
@ -3,4 +3,4 @@
|
||||
namespace Flawless.Core.Modal;
|
||||
|
||||
[Serializable]
|
||||
public record struct DepotLabel(Guid Id, long Length, Guid[] Dependency);
|
||||
public record struct DepotLabel(Guid Id, long Length);
|
||||
@ -405,11 +405,12 @@ public class RepositoryInnieController(
|
||||
var test = new HashSet<WorkspaceFile>(req.WorkspaceSnapshot);
|
||||
var createNewDepot = false;
|
||||
RepositoryDepot mainDepot;
|
||||
DepotLabel depotLabel;
|
||||
List<DepotLabel> depotLabels = new();
|
||||
|
||||
// Judge if receive new depot or use old one...
|
||||
if (req.Depot == null)
|
||||
{
|
||||
// Use existed depots
|
||||
var mainDepotId = Guid.Parse(req.MainDepotId!);
|
||||
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test);
|
||||
|
||||
@ -422,11 +423,13 @@ public class RepositoryInnieController(
|
||||
|
||||
// Set this depot label...
|
||||
mainDepot = preDepot;
|
||||
var deps = mainDepot.Dependencies.Select(d => d.DepotId).ToArray();
|
||||
depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, deps);
|
||||
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
||||
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get a new depot from request - this will flat to direct depot reference to avoid deep search that
|
||||
// raise memory issue.
|
||||
var actualRequiredDepots = new HashSet<Guid>();
|
||||
|
||||
// Read and create a new depot
|
||||
@ -477,9 +480,6 @@ public class RepositoryInnieController(
|
||||
.SelectMany(r => r.Depots)
|
||||
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
||||
.ToArrayAsync());
|
||||
|
||||
depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, actualRequiredDepots.ToArray());
|
||||
rp.Depots.Add(mainDepot);
|
||||
|
||||
// Then write depot into disk
|
||||
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
|
||||
@ -489,6 +489,11 @@ public class RepositoryInnieController(
|
||||
await cacheStream.CopyToAsync(depotStream, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
// Everything alright, so make response
|
||||
depotLabels.Add(new DepotLabel(mainDepot.DepotId, mainDepot.Length));
|
||||
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
||||
rp.Depots.Add(mainDepot);
|
||||
|
||||
createNewDepot = true;
|
||||
}
|
||||
|
||||
@ -498,7 +503,7 @@ public class RepositoryInnieController(
|
||||
var manifest = new CommitManifest
|
||||
{
|
||||
ManifestId = commitId,
|
||||
Depot = depotLabel,
|
||||
Depot = depotLabels.ToArray(),
|
||||
FilePaths = req.WorkspaceSnapshot
|
||||
};
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
|
||||
}
|
||||
|
||||
[HttpPost("repo_create")]
|
||||
public async Task<ActionResult<RepositoryInfoResponse>> CreateRepositoryAsync([FromQuery] string repositoryName)
|
||||
public async Task<ActionResult<RepositoryInfoResponse>> CreateRepositoryAsync([FromQuery] string repositoryName,[FromQuery] string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(repositoryName) || repositoryName.Length <= 3)
|
||||
return BadRequest(new FailedResponse("Repository name is empty or too short!"));
|
||||
@ -51,6 +51,7 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
|
||||
var repo = new Repository()
|
||||
{
|
||||
Name = repositoryName,
|
||||
Description = description,
|
||||
Owner = u,
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user