1
0
2025-05-21 11:20:11 +08:00

832 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
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.Remote;
using Flawless.Client.Service;
using Flawless.Client.ViewModels.ModalBox;
using Flawless.Client.Views.ModalBox;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Ursa.Controls;
using WorkspaceFile = Flawless.Core.Modal.WorkspaceFile;
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;
[Reactive] private long _size;
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,
Size = -1
};
}
public static LocalChangesNode FromWorkspaceFile(LocalChangesNode? parent, ChangeInfo file)
{
return new LocalChangesNode
{
Type = file.Type.ToString(),
FullPath = file.File.WorkPath,
ModifiedTime = file.File.ModifyTime,
Parent = parent,
Size = (long) file.File.Size
};
}
}
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 class DepotStatsInfo
{
public string Id { get; set; }
public long Size { get; set; }
}
public RepositoryModel Repository { get; }
public RepositoryLocalDatabaseModel LocalDatabase { get; }
public HierarchicalTreeDataGridSource<LocalChangesNode> LocalChange { get; }
public HierarchicalTreeDataGridSource<LocalChangesNode> FileTree { get; }
public FlatTreeDataGridSource<CommitTransitNode> Commits { get; }
public FlatTreeDataGridSource<DepotStatsInfo> Depots { get; }
public ObservableCollectionExtended<LocalChangesNode> LocalChangeSetRaw { get; } = new();
public ObservableCollectionExtended<LocalChangesNode> CurrentCommitFileTreeRaw { get; } = new();
public ObservableCollectionExtended<CommitTransitNode> CommitsRaw { get; } = new();
public ObservableCollection<DepotStatsInfo> DepotsStats { get; } = new();
public UserModel User { get; }
[Reactive] private bool _autoDetectChanges = true;
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole, _showWebHook;
[Reactive] private ISeries[] _byDay = [new ColumnSeries<DateTimePoint>()];
public ICartesianAxis[] XAxesByDay { get; set; } = [
new DateTimeAxis(TimeSpan.FromDays(1), date => date.ToString("MMMM dd"))
];
private string _wsRoot;
public RepositoryViewModel(RepositoryModel repo, IScreen hostScreen) : base(hostScreen)
{
Repository = repo;
LocalDatabase = RepositoryService.C.GetRepositoryLocalDatabase(repo);
User = UserService.C.GetUserInfoAsync(Api.C.Username.Value!)!;
_wsRoot = PathUtility.GetWorkspacePath(Api.C.Username.Value!, Repository.OwnerName, Repository.Name);
// 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, string>(
"Size",
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
Commits = new FlatTreeDataGridSource<CommitTransitNode>(CommitsRaw)
{
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, string>(
"Size",
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
new TextColumn<LocalChangesNode, DateTime?>(
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
}
};
Depots = new FlatTreeDataGridSource<DepotStatsInfo>(DepotsStats)
{
Columns =
{
new TextColumn<DepotStatsInfo, string>(
"Id",
n => n.Id),
new TextColumn<DepotStatsInfo, string>(
"Size",
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
}
};
_ = StartupTasksAsync();
}
private async Task StartupTasksAsync()
{
await RefreshRepositoryRoleInfoAsync();
await RefreshRepositoryIssuesAsync();
await RefreshStatisticDataAsync();
await RefreshWebhooksAsync();
await DetectLocalChangesAsync();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
private async ValueTask RendererFileTreeAsync()
{
if (LocalDatabase.RepoAccessor == null) return;
var accessor = LocalDatabase.RepoAccessor;
var nodes = await CalculateFileTreeOfChangesNodeAsync(accessor.Select(
f => new ChangeInfo(ChangeInfoType.Add, f)));
CurrentCommitFileTreeRaw.Clear();
CurrentCommitFileTreeRaw.AddRange(nodes);
}
private void SyncCommitsFromRepository()
{
CommitsRaw.Clear();
CommitsRaw.AddRange(Repository.Commits.Select(CommitTransitNode.FromCommit));
}
private void CollectChanges(List<ChangeInfo> store, IEnumerable<LocalChangesNode> changesNode)
{
foreach (var n in changesNode)
{
if (n.Contents != null) CollectChanges(store, n.Contents);
else if (n.Included)
{
store.Add(new ChangeInfo(Enum.Parse<ChangeInfoType>(n.Type), new WorkspaceFile
{
WorkPath = n.FullPath,
ModifyTime = n.ModifiedTime!.Value
}));
}
}
}
private Task<List<LocalChangesNode>> CalculateFileTreeOfChangesNodeAsync(IEnumerable<ChangeInfo> 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 AddWebhookAsync()
{
if (!IsOwnerRole) return;
var vm = new WebhookEditDialogViewModel();
var result = await OverlayDialog.ShowModal<WebhookEditDialogView, WebhookEditDialogViewModel>(
vm, AppDefaultValues.HostId, UIHelper.DefaultOverlayDialogOptionsYesNo());
if (result == DialogResult.Yes)
{
using var l = UIHelper.MakeLoading("Create Webhook...");
await RepositoryService.C.AddWebhookAsync(Repository, vm.Url, vm.Secret, vm.EventType);
}
}
[ReactiveCommand]
private async Task DeleteWebhookAsync(RepositoryModel.Webhook webhook)
{
if (!IsOwnerRole) return;
var confirm = await UIHelper.SimpleAskAsync($"Do you sure to delete webhook with url at {webhook.TargetUrl} ?");
if (confirm == DialogResult.Yes)
{
using var l = UIHelper.MakeLoading("Delete webhook...");
await RepositoryService.C.DeleteWebhookAsync(Repository, webhook.Id);
}
}
[ReactiveCommand]
public async Task ToggleWebhookAsync(RepositoryModel.Webhook webhook)
{
if (!IsOwnerRole) return;
using var l = UIHelper.MakeLoading("Update Webhook...");
var success = await RepositoryService.C.ToggleWebhookAsync(
Repository,
webhook.Id,
!webhook.Active);
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitKeepAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Keep) to Commit {sel.CommitId}");
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Keep);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitSoftAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Soft) to Commit {sel.CommitId}");
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count != 0)
{
var result = await UIHelper.SimpleAskAsync(
"There are commits that has not been committed. Do you wish discard them at all?",
DialogMode.Warning);
if (result == DialogResult.No) return;
}
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Soft);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task RevertFileTreeToSelectedCommitHardAsync()
{
var sel = Commits.RowSelection?.SelectedItem;
if (sel != null)
{
using var l = UIHelper.MakeLoading($"Reset (Hard) to Commit {sel.CommitId}");
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var result = await UIHelper.SimpleAskAsync(
"All files will being matched with this commit. Do you wish to execute it?",
DialogMode.Warning);
if (result == DialogResult.No) return;
var kid = Repository.Commits.First(x => x.CommitId.ToString() == sel.CommitId).CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C
.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Hard);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
}
[ReactiveCommand]
private async Task CreateIssueAsync()
{
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
var vm = new IssueEditDialogViewModel();
var r = await OverlayDialog.ShowModal<IssueDetailEditView, IssueEditDialogViewModel>(vm, AppDefaultValues.HostId, opt);
if (r == DialogResult.Yes)
{
using var l = UIHelper.MakeLoading("Sending issue...");
var tags = vm.Tag?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var issueId = await RepositoryService.C.CreateIssueAsync(Repository, vm.Title, vm.Description, tags);
if (issueId == null) return;
await HostScreen.Router.Navigate.Execute(new IssueDetailViewModel(HostScreen, Repository, issueId.Value));
}
}
[ReactiveCommand]
private async Task OpenIssueAsync(RepositoryModel.Issue issue)
{
using var l = UIHelper.MakeLoading("Config issue...");
await HostScreen.Router.Navigate.Execute(new IssueDetailViewModel(HostScreen, Repository, issue.Id));
}
[ReactiveCommand]
private async Task CloseIssueAsync(RepositoryModel.Issue issue)
{
using var l = UIHelper.MakeLoading("Config issue...");
await RepositoryService.C.CloseIssueAsync(Repository, issue.Id);
}
[ReactiveCommand]
private async Task ReopenIssueAsync(RepositoryModel.Issue issue)
{
using var l = UIHelper.MakeLoading("Config issue...");
await RepositoryService.C.ReopenIssueAsync(Repository, issue.Id);
}
[ReactiveCommand]
private async Task PullLatestRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Pulling latest changes...");
var mayUpdate = await RepositoryService.C.IsPointedToCommitNotPeekResultFromServerAsync(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;
if (!await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository)) return;
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
var kid = Repository.Commits.MaxBy(k => k.CommittedOn)!.CommitId;
var changeDict = changes.ToImmutableDictionary(x => x.File.WorkPath, x => x.File);
await RepositoryService.C.SetPointerAndMergeDepotsWithLocalFromRemoteAsync(
Repository, kid, changeDict, RepositoryResetMethod.Keep);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[ReactiveCommand]
private async Task CommitSelectedChangesAsync()
{
var changes = new List<ChangeInfo>();
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.CommitWorkspaceAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return;
// LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
LocalDatabase.CommitMessage = string.Empty;
await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[ReactiveCommand]
private async Task RevertSelectedChangesAsync()
{
var changes = new List<ChangeInfo>();
CollectChanges(changes, LocalChangeSetRaw);
if (changes.Count == 0)
{
await UIHelper.SimpleAlert("You haven't choose any changes yet!");
return;
}
using var l = UIHelper.MakeLoading("Reverting changes...");
RepositoryService.C.RevertChangesToBaseline(Repository, changes);
await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync();
SyncCommitsFromRepository();
}
[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 CloseRepositoryAsync()
{
using var l = UIHelper.MakeLoading("Save changes...");
await RepositoryService.C.CloseRepositoryAsync(Repository);
await HostScreen.Router.NavigateBack.Execute();
}
[ReactiveCommand]
private async Task RefreshWebhooksAsync()
{
ShowWebHook = IsDeveloperRole && (Api.C.Status.Value?.AllowWebHook ?? false);
if (!ShowWebHook) return;
using var l = UIHelper.MakeLoading("Refresh webhooks...");
await RepositoryService.C.UpdateWebhooksFromServerAsync(Repository);
}
[ReactiveCommand]
private async ValueTask RefreshRepositoryRoleInfoAsync()
{
using var l = UIHelper.MakeLoading("Refreshing member info...");
await RepositoryService.C.UpdateMembersFromServerAsync(Repository);
UpdatePermissionOfRepository();
}
[ReactiveCommand]
private async ValueTask RefreshRepositoryIssuesAsync()
{
if (!IsReporterRole) return;
using var l = UIHelper.MakeLoading("Refreshing issues...");
await RepositoryService.C.UpdateIssuesListFromServerAsync(Repository);
}
[ReactiveCommand]
private async ValueTask DeleteMemberFromServerAsync(RepositoryModel.Member member)
{
if (!IsOwnerRole)
{
UIHelper.NotifyError("Only repository owner can edit this!", "Permission issue");
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("Only repository owner can edit this!", "Permission issue");
return;
}
var style = UIHelper.DefaultOverlayDialogOptionsYesNo();
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("Invalid role level!", "Permission issue");
return;
}
if (vm.SafeRole == member.Role)
{
UIHelper.NotifyError("No modification yet.", "Modification issue");
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("Only repository owner can edit this!", "Permission issue");
return;
}
var style = UIHelper.DefaultOverlayDialogOptionsYesNo();
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("Invalid role level!", "Permission issue");
return;
}
vm.Username = vm.Username.Trim();
if (string.IsNullOrEmpty(vm.Username) || vm.Username.Length < 3)
{
UIHelper.NotifyError("Not a valid username!", "Parameter error");
return;
}
using var l = UIHelper.MakeLoading("Add member...");
await RepositoryService.C.AddMemberFromServerAsync(Repository, vm.Username, vm.SafeRole);
}
}
[ReactiveCommand]
private async ValueTask DetectLocalChangesAsync()
{
if (!IsDeveloperRole) return;
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 OpenFolder()
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() {
FileName = PathUtility.GetWorkspacePath(Api.C.Username.Value!, Repository.OwnerName, Repository.Name),
UseShellExecute = true,
Verb = "open"
});
}
[ReactiveCommand]
private void DeselectAllChanges()
{
foreach (var n in LocalChangeSetRaw)
n.Included = false;
}
[ReactiveCommand]
private async Task RefreshStatisticDataAsync()
{
if (!IsOwnerRole) return;
try
{
var api = Api.C;
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
{
api.ClearGateway();
return;
}
var rsp = await api.Gateway.Stats(Repository.OwnerName, Repository.Name);
DepotsStats.Clear();
foreach (var dp in rsp.Depots)
DepotsStats.Add(new DepotStatsInfo{ Id = dp.DepotName, Size = dp.DepotSize});
ByDay = [
new ColumnSeries<DateTimePoint>
{
Values = rsp.CommitByDay.Select(k => new DateTimePoint(k.Day.Date, k.Count)).ToList(),
}
];
}
catch (Exception e)
{
UIHelper.NotifyError(e);
return;
}
}
}