1
0

517 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using DynamicData;
using DynamicData.Binding;
using Flawless.Abstraction;
using Flawless.Client.Models;
using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using Flawless.Core.Modal;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Ursa.Controls;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels;
public partial class LocalChangesNode : ReactiveModel
{
[Reactive] private string _fullPath;
[Reactive] private string _type;
[Reactive] private DateTime? _modifiedTime;
[Reactive] private LocalChangesNode? _parent;
public bool Included
{
get
{
if (Contents != null) return Contents.All(c => c.Included);
return _actualIncluded;
}
set
{
if (Contents != null) foreach (var n in Contents) n.Included = value;
this.RaiseAndSetIfChanged(ref _actualIncluded, value, nameof(Included));
_parent?.RaisePropertyChanged(nameof(Included));
}
}
private bool _actualIncluded;
public ObservableCollection<LocalChangesNode>? Contents { get; set; }
public static LocalChangesNode FromFolder(LocalChangesNode? parent, string folderPath)
{
return new LocalChangesNode
{
Type = "Folder",
FullPath = folderPath,
Contents = new(),
Parent = parent
};
}
public static LocalChangesNode FromWorkspaceFile(LocalChangesNode? parent, LocalFileTreeAccessor.ChangeRecord file)
{
return new LocalChangesNode
{
Type = file.Type.ToString(),
FullPath = file.File.WorkPath,
ModifiedTime = file.File.ModifyTime,
Parent = parent
};
}
}
public class CommitTransitNode
{
public required string CommitId { get; set; }
public required string Author { get; set; }
public required string Message { get; set; }
public required DateTime? CommitAt { get; set; }
public required string FullCommitId { get; set; }
public required string FullMessage { get; set; }
public required string MainDepotId { get; set; }
public static CommitTransitNode FromCommit(RepositoryModel.Commit cm)
{
string msg;
if (cm.Message.Length > 28)
{
msg = cm.Message.Substring(0, 28) + "...";
}
else
{
msg = cm.Message;
}
return new CommitTransitNode
{
CommitId = cm.CommitId.ToString(),
Author = cm.Author,
CommitAt = cm.CommittedOn.ToLocalTime(),
Message = msg,
FullCommitId = cm.CommitId.ToString(),
FullMessage = cm.Message,
MainDepotId = cm.DepotId.ToString(),
};
}
}
public partial class RepositoryViewModel : RoutableViewModelBase
{
public RepositoryModel Repository { get; }
public RepositoryLocalDatabaseModel LocalDatabase { get; }
public HierarchicalTreeDataGridSource<LocalChangesNode> LocalChange { get; }
public HierarchicalTreeDataGridSource<LocalChangesNode> FileTree { get; }
public FlatTreeDataGridSource<CommitTransitNode> Commits { get; }
public ObservableCollection<LocalChangesNode> LocalChangeSetRaw { get; } = new();
public ObservableCollection<LocalChangesNode> CurrentCommitFileTreeRaw { get; } = new();
public UserModel User { get; }
[Reactive] private bool _autoDetectChanges = true;
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole;
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
{
Repository = repo;
LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo);
User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!;
// Setup local change set
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
{
Columns =
{
new CheckBoxColumn<LocalChangesNode>(
null, n => n.Included, (n, v) => n.Included = v,
null, new CheckBoxColumnOptions<LocalChangesNode>
{
CanUserResizeColumn = false,
CanUserSortColumn = false,
}),
new HierarchicalExpanderColumn<LocalChangesNode>(
new TextColumn<LocalChangesNode, string>(
"Name",
n => Path.GetFileName(n.FullPath)),
n => n.Contents),
new TextColumn<LocalChangesNode, string>(
"Change",
n => n.Contents != null ? String.Empty : n.Type.ToString()),
new TextColumn<LocalChangesNode, string>(
"File Type",
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
new TextColumn<LocalChangesNode, ulong>(
"Size",
n => 0),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
Commits = new FlatTreeDataGridSource<CommitTransitNode>(Repository.Commits.Select(CommitTransitNode.FromCommit))
{
Columns =
{
new TextColumn<CommitTransitNode, string>(
string.Empty, n => n.CommitId == LocalDatabase.CurrentCommit.ToString() ? "HEAD" : String.Empty),
new TextColumn<CommitTransitNode, string>(
"Message", x => x.Message),
new TextColumn<CommitTransitNode, string>(
"Author", x => x.Author),
new TextColumn<CommitTransitNode, DateTime>(
"Time", x => x.CommitAt!.Value),
new TextColumn<CommitTransitNode, string>(
"Id", x => x.CommitId.Substring(0, 13)),
}
};
FileTree = new HierarchicalTreeDataGridSource<LocalChangesNode>(CurrentCommitFileTreeRaw)
{
Columns =
{
new HierarchicalExpanderColumn<LocalChangesNode>(
new TextColumn<LocalChangesNode, string>(
"Name",
n => Path.GetFileName(n.FullPath)),
n => n.Contents),
new TextColumn<LocalChangesNode, string>(
"File Type",
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
new TextColumn<LocalChangesNode, ulong>(
"Size",
n => 0),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
_ = StartupTasksAsync();
}
private async Task StartupTasksAsync()
{
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
}
private async ValueTask RendererFileTreeAsync()
{
if (LocalDatabase.RepoAccessor == null) return;
var accessor = LocalDatabase.RepoAccessor;
var nodes = await CalculateFileTreeOfChangesNodeAsync(accessor.Select(
f => new LocalFileTreeAccessor.ChangeRecord(ChangeType.Add, f)));
CurrentCommitFileTreeRaw.Clear();
CurrentCommitFileTreeRaw.AddRange(nodes);
}
private void CollectChanges(List<LocalFileTreeAccessor.ChangeRecord> store, IEnumerable<LocalChangesNode> changesNode)
{
foreach (var n in changesNode)
{
if (n.Contents != null) CollectChanges(store, n.Contents);
else if (n.Included)
{
store.Add(new LocalFileTreeAccessor.ChangeRecord(Enum.Parse<ChangeType>(n.Type), new WorkspaceFile
{
WorkPath = n.FullPath,
ModifyTime = n.ModifiedTime!.Value
}));
}
}
}
private Task<List<LocalChangesNode>> CalculateFileTreeOfChangesNodeAsync(IEnumerable<LocalFileTreeAccessor.ChangeRecord> changesNode)
{
return Task.Run(() =>
{
// Generate a map of all folders
var folderMap = new Dictionary<string, LocalChangesNode>();
var nodes = new List<LocalChangesNode>();
foreach (var file in changesNode)
{
var parent = AddParentToMap(file.File.WorkPath);
var n = LocalChangesNode.FromWorkspaceFile(parent, file);
if (parent == null) nodes.Add(n);
else parent.Contents!.Add(n);
}
return nodes;
LocalChangesNode? AddParentToMap(string path)
{
path = WorkPath.FormatPathDirectorySeparator(Path.GetDirectoryName(path) ?? string.Empty);
// 如果为空,则其直接文件夹就是根目录
if (string.IsNullOrEmpty(path)) return null;
// 如果直接文件夹已经存在,则不再生成,直接返回即可
if (folderMap.TryGetValue(path, out var node)) return node;
// 生成当前文件夹,并先找到这个文件夹的直接文件夹
var parent = AddParentToMap(path);
node = LocalChangesNode.FromFolder(parent, path);
folderMap.Add(path, node);
if (parent != null) parent.Contents!.Add(node);
else nodes.Add(node);
// 将新建的文件夹告知调用方
return node;
}
});
}
private void UpdatePermissionOfRepository()
{
var isOwner = Repository.OwnerName == User.Username;
var role = isOwner ?
RepositoryModel.RepositoryRole.Owner :
Repository.Members.First(p => p.Username == User.Username).Role;
if (role >= RepositoryModel.RepositoryRole.Owner) IsOwnerRole = true;
if (role >= RepositoryModel.RepositoryRole.Developer) IsDeveloperRole = true;
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
}
[ReactiveCommand]
private async Task CommitSelectedChangesAsync()
{
var changes = new List<LocalFileTreeAccessor.ChangeRecord>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count == 0)
{
await UIHelper.SimpleAlert("You haven't choose any changes yet!");
return;
}
if (string.IsNullOrWhiteSpace(LocalDatabase.CommitMessage))
{
await UIHelper.SimpleAlert("Commit message can not be empty!");
return;
}
using var l = UIHelper.MakeLoading("Committing changes...");
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return;
LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
LocalDatabase.CommitMessage = string.Empty;
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
}
[ReactiveCommand]
private async Task DeleteRepositoryBothServerAndLocalAsync()
{
var msg = "THIS IS AN UNREVERT ACTION, YOU SHOULD AWARE THAT THIS MAY CAUSE DATA LOSS. DO YOU CONTINUE DELETE THIS REPOSITORY?";
var confirm = await UIHelper.SimpleAskAsync(msg, DialogMode.Error) == DialogResult.Yes;
if (confirm)
{
using var l = UIHelper.MakeLoading("Delete repository...");
if (!await RepositoryService.C.DeleteRepositoryOnServerAsync(Repository)) return;
await HostScreen.Router.NavigateBack.Execute();
await RepositoryService.C.CloseRepositoryAsync(Repository);
await RepositoryService.C.DeleteFromDiskAsync(Repository);
await RepositoryService.C.UpdateRepositoriesFromServerAsync();
await RepositoryService.C.UpdateRepositoriesDownloadedStatusFromDiskAsync();
}
}
[ReactiveCommand]
private async Task PullLatestRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Pulling latest changes...");
var mayUpdate = await RepositoryService.C.IsCurrentPointedToCommitIsNotPeekResultFromServerAsync(Repository);
if (!mayUpdate.HasValue) return;
if (mayUpdate.Value == false)
{
await UIHelper.SimpleAskAsync("Everything is new, no needs to pull.");
return;
}
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var kid = Repository.Commits.MaxBy(k => k.CommittedOn)!.CommitId;
await RepositoryService.C.ResetCommitPointerToTargetAndMergeDepotsIntoRepositoryFromRemoteAsync(Repository, kid);
}
[ReactiveCommand]
private async Task CloseRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Save changes...");
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[ReactiveCommand]
private async ValueTask RefreshRepositoryRoleInfoAsync()
{
using var l = UIHelper.MakeLoading("Refreshing member info...");
await RepositoryService.C.UpdateMembersFromServerAsync(Repository);
UpdatePermissionOfRepository();
}
[ReactiveCommand]
private async ValueTask DeleteMemberFromServerAsync(RepositoryModel.Member member)
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var result = await UIHelper.SimpleAskAsync(
$"Do you really want to delete this member: \n{member.Username} ({member.Role})", DialogMode.Warning);
if (result == DialogResult.Yes)
{
using var l = UIHelper.MakeLoading("Delete member...");
await RepositoryService.C.DeleteMemberFromServerAsync(Repository, member);
}
}
[ReactiveCommand]
private async ValueTask ModifyMemberFromServerAsync(RepositoryModel.Member member)
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var style = UIHelper.DefaultOverlayDialogOptions();
var vm = new EditRepositoryMemberDialogViewModel();
vm.LockUsername = true;
vm.Username = member.Username;
vm.SafeRole = member.Role;
var result = await OverlayDialog.ShowModal<EditRepositoryMemberDialogueView,EditRepositoryMemberDialogViewModel>
(vm, AppDefaultValues.HostId, style);
if (result == DialogResult.Yes)
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
return;
}
if (vm.SafeRole == member.Role)
{
UIHelper.NotifyError("Modification issue", "No modification yet.");
return;
}
using var l = UIHelper.MakeLoading("Modify member...");
member.Role = vm.SafeRole;
await RepositoryService.C.ModifyMemberFromServerAsync(Repository, member);
}
}
[ReactiveCommand]
private async ValueTask AddMemberFromServerAsync()
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Permission issue", "Only repository owner can edit this!");
return;
}
var style = UIHelper.DefaultOverlayDialogOptions();
var vm = new EditRepositoryMemberDialogViewModel();
var result = await OverlayDialog.ShowModal<EditRepositoryMemberDialogueView,EditRepositoryMemberDialogViewModel>
(vm, AppDefaultValues.HostId, style);
if (result == DialogResult.Yes)
{
if (vm.SafeRole == RepositoryModel.RepositoryRole.Owner)
{
UIHelper.NotifyError("Permission issue", "Invalid role level!");
return;
}
vm.Username = vm.Username.Trim();
if (string.IsNullOrEmpty(vm.Username) || vm.Username.Length < 3)
{
UIHelper.NotifyError("Parameter error", "Not a valid username!");
return;
}
using var l = UIHelper.MakeLoading("Add member...");
await RepositoryService.C.AddMemberFromServerAsync(Repository, vm.Username, vm.SafeRole);
}
}
[ReactiveCommand]
private async ValueTask DetectLocalChangesAsync()
{
using var l = UIHelper.MakeLoading("Refreshing local changes...");
var ns = await Task.Run(async () =>
{
LocalDatabase.LocalAccessor.Refresh();
return await CalculateFileTreeOfChangesNodeAsync(LocalDatabase.LocalAccessor.Changes.Values);
});
LocalChangeSetRaw.Clear();
LocalChangeSetRaw.AddRange(ns);
}
[ReactiveCommand]
private void SelectAllChanges()
{
foreach (var n in LocalChangeSetRaw)
n.Included = true;
}
[ReactiveCommand]
private void DeselectAllChanges()
{
foreach (var n in LocalChangeSetRaw)
n.Included = false;
}
}