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.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? 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 LocalChange { get; } public HierarchicalTreeDataGridSource FileTree { get; } public FlatTreeDataGridSource Commits { get; } public ObservableCollection LocalChangeSetRaw { 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(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.Guid == 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.Guid.Substring(0, 13)), } }; DetectLocalChangesAsyncCommand.Execute(); } 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 })); } } } [ReactiveCommand] private async Task CommitSelectedChangesAsync() { var changes = new List(); CollectChanges(changes, LocalChangeSetRaw); if (changes.Count == 0) return; var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage ?? string.Empty); if (manifest == null) return; LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths); LocalDatabase.CommitMessage = string.Empty; await DetectLocalChangesAsyncCommand.Execute(); } [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(() => { LocalDatabase.LocalAccessor.Refresh(); // Generate a map of all folders var folderMap = new Dictionary(); foreach (var k in LocalDatabase.LocalAccessor.Changes.Keys) AddParentToMap(k); var nodes = new List(); foreach (var file in LocalDatabase.LocalAccessor.Changes.Values) { var directory = Path.GetDirectoryName(file.File.WorkPath); var n = LocalChangesNode.FromWorkspaceFile(file); if (string.IsNullOrEmpty(directory)) nodes.Add(n); else folderMap[directory].Contents!.Add(n); } nodes.AddRange(folderMap.Values); return nodes; void AddParentToMap(string path) { var parent = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(parent) || folderMap.ContainsKey(parent)) return; folderMap.Add(parent, LocalChangesNode.FromFolder(parent)); } }); LocalChangeSetRaw.Clear(); LocalChangeSetRaw.AddRange(ns); } }