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.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 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? 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 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 LocalChange { get; } public HierarchicalTreeDataGridSource FileTree { get; } public FlatTreeDataGridSource Commits { get; } public ObservableCollection LocalChangeSetRaw { get; } = new(); public ObservableCollection 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(LocalChangeSetRaw) { Columns = { new CheckBoxColumn( string.Empty, n => n.Included, (n, v) => n.Included = v), new HierarchicalExpanderColumn( new TextColumn( "Name", n => Path.GetFileName(n.FullPath)), n => n.Contents), new TextColumn( "Change", n => n.Contents != null ? String.Empty : n.Type.ToString()), new TextColumn( "File Type", n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)), new TextColumn( "Size", n => 0), new TextColumn( "ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null), } }; Commits = new FlatTreeDataGridSource(Repository.Commits.Select(CommitTransitNode.FromCommit)) { Columns = { new TextColumn( string.Empty, n => n.CommitId == LocalDatabase.CurrentCommit.ToString() ? "*" : String.Empty), new TextColumn( "Message", x => x.Message), new TextColumn( "Author", x => x.Author), new TextColumn( "Time", x => x.CommitAt!.Value), new TextColumn( "Id", x => x.CommitId.Substring(0, 13)), } }; FileTree = new HierarchicalTreeDataGridSource(CurrentCommitFileTreeRaw) { Columns = { new HierarchicalExpanderColumn( new TextColumn( "Name", n => Path.GetFileName(n.FullPath)), n => n.Contents), new TextColumn( "File Type", n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)), new TextColumn( "Size", n => 0), new TextColumn( "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 store, IEnumerable 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(n.Type), new WorkspaceFile { WorkPath = n.FullPath, ModifyTime = n.ModifiedTime!.Value })); } } } private Task> CalculateFileTreeOfChangesNodeAsync(IEnumerable changesNode) { return Task.Run(() => { // Generate a map of all folders var folderMap = new Dictionary(); var nodes = new List(); 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; } }); } 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(); 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 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 (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 (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; } }