1
0

361 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.Core.Modal;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using ChangeType = Flawless.Client.Service.LocalFileTreeAccessor.ChangeType;
namespace Flawless.Client.ViewModels;
public class LocalChangesNode
{
public required string FullPath { get; set; }
public required string Type { get; set; }
public DateTime? ModifiedTime { get; set; }
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;
_actualIncluded = value;
}
}
private bool _actualIncluded;
public ObservableCollection<LocalChangesNode>? Contents { get; set; }
public static LocalChangesNode FromFolder(string folderPath)
{
return new LocalChangesNode
{
Type = "Folder",
FullPath = folderPath,
Contents = new()
};
}
public static LocalChangesNode FromWorkspaceFile(LocalFileTreeAccessor.ChangeRecord file)
{
return new LocalChangesNode
{
Type = file.Type.ToString(),
FullPath = file.File.WorkPath,
ModifiedTime = file.File.ModifyTime
};
}
}
public class CommitTransitNode
{
public required string Guid { get; set; }
public required string Author { get; set; }
public required string Message { get; set; }
public required DateTime? CommitAt { 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
{
Guid = cm.CommitId.ToString(),
Author = cm.Author,
CommitAt = cm.CommittedOn.ToLocalTime(),
Message = msg,
};
}
}
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 repository permission change watcher
RefreshRepositoryRoleInfo();
Repository.Members.ObserveCollectionChanges().Subscribe(_ => RefreshRepositoryRoleInfo());
// Setup local change set
LocalChange = new HierarchicalTreeDataGridSource<LocalChangesNode>(LocalChangeSetRaw)
{
Columns =
{
new CheckBoxColumn<LocalChangesNode>(
string.Empty, n => n.Included, (n, v) => n.Included = v),
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.Guid == LocalDatabase.CurrentCommit.ToString() ? "*" : 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.Guid.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();
}
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 n = LocalChangesNode.FromWorkspaceFile(file);
var parentNode = AddParentToMap(file.File.WorkPath);
if (parentNode == null) nodes.Add(n);
else parentNode.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;
// 生成当前文件夹,并先找到这个文件夹的直接文件夹
node = LocalChangesNode.FromFolder(path);
var parent = AddParentToMap(path);
folderMap.Add(path, node);
if (parent != null) parent.Contents!.Add(node);
else nodes.Add(node);
// 将新建的文件夹告知调用方
return node;
}
});
}
[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;
}
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 CloseRepositoryAsync()
{
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[ReactiveCommand]
private void RefreshRepositoryRoleInfo()
{
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 ValueTask DetectLocalChangesAsync()
{
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;
}
}