diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
index f4aa09b..1ab1d7f 100644
--- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
+++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml
@@ -21,6 +21,7 @@
+
diff --git a/Flawless.Client/App.axaml b/Flawless.Client/App.axaml
index 7268800..4ff221c 100644
--- a/Flawless.Client/App.axaml
+++ b/Flawless.Client/App.axaml
@@ -8,5 +8,6 @@
+
\ No newline at end of file
diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj
index 8b03c2c..eb15b74 100644
--- a/Flawless.Client/Flawless.Client.csproj
+++ b/Flawless.Client/Flawless.Client.csproj
@@ -36,6 +36,7 @@
+
@@ -66,4 +67,8 @@
+
+
+
+
diff --git a/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs b/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
index 3349125..eb249cf 100644
--- a/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
+++ b/Flawless.Client/Models/RepositoryLocalDatabaseModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
+using System.Text.Json.Serialization;
using Flawless.Client.Service;
using ReactiveUI.SourceGenerators;
@@ -8,14 +9,17 @@ namespace Flawless.Client.Models;
[Serializable]
public partial class RepositoryLocalDatabaseModel : ReactiveModel
{
- public required RepositoryModel RootModal { get; init; }
+ [JsonIgnore]
+ public required RepositoryModel RootModal { get; set; }
- public required LocalFileTreeAccessor LocalAccessor { get; init; }
+ [JsonIgnore]
+ public required LocalFileTreeAccessor LocalAccessor { get; set; }
+ [JsonIgnore]
public RepositoryFileTreeAccessor? RepoAccessor { get; set; }
- public ObservableCollection CurrentLockedFiles { get; } = new();
-
- [NonSerialized]
[Reactive] private Guid? _currentCommit;
+
+ [Reactive] private string? _commitMessage;
+
}
\ No newline at end of file
diff --git a/Flawless.Client/Models/RepositoryModel.cs b/Flawless.Client/Models/RepositoryModel.cs
index c4e2671..4aa14cd 100644
--- a/Flawless.Client/Models/RepositoryModel.cs
+++ b/Flawless.Client/Models/RepositoryModel.cs
@@ -34,7 +34,7 @@ public partial class RepositoryModel : ReactiveModel
public ObservableCollection Locks { get; } = new();
- public enum RepositoryRole
+ public enum RepositoryRole : byte
{
Guest = 0,
Reporter = 1,
diff --git a/Flawless.Client/Models/UserModel.cs b/Flawless.Client/Models/UserModel.cs
index 731443f..b3c9644 100644
--- a/Flawless.Client/Models/UserModel.cs
+++ b/Flawless.Client/Models/UserModel.cs
@@ -1,6 +1,33 @@
-namespace Flawless.Client.Models;
+using System;
+using ReactiveUI.SourceGenerators;
-public record UserModel : ReactiveRecordModel
+namespace Flawless.Client.Models;
+
+public partial class UserModel : ReactiveModel
{
+ public enum SexType : byte
+ {
+ Unset = 0,
+ Male = 1,
+ Female = 2,
+ WalmartPlasticBag = 3
+ }
+ [Reactive] private string _username;
+
+ [Reactive] private string _nickname;
+
+ [Reactive] private string _bio;
+
+ [Reactive] private SexType _sex;
+
+ [Reactive] private bool _emailIsVisible;
+
+ [Reactive] private bool _canEdit;
+
+ [Reactive] private string _email;
+
+ [Reactive] private string _phoneNumber;
+
+ [Reactive] private DateTime _joinDate;
}
\ No newline at end of file
diff --git a/Flawless.Client/Service/Api.cs b/Flawless.Client/Service/Api.cs
index f716a17..65a0ecc 100644
--- a/Flawless.Client/Service/Api.cs
+++ b/Flawless.Client/Service/Api.cs
@@ -36,6 +36,8 @@ public class Api : BaseService
public IObservable Token => _token;
+ public IReactiveProperty Username => _username;
+
private readonly ReactiveProperty _isLoggedIn = new(false);
private readonly ReactiveProperty _serverUrl = new(string.Empty);
@@ -43,6 +45,8 @@ public class Api : BaseService
private readonly ReactiveProperty _status = new(null);
private readonly ReactiveProperty _token = new(null);
+
+ private readonly ReactiveProperty _username = new(string.Empty);
#region GatewayConfig
@@ -55,6 +59,7 @@ public class Api : BaseService
public void ClearGateway()
{
_gateway = null;
+ _username.Value = null;
_isLoggedIn.Value = false;
_status.Value = null;
_serverUrl.Value = null;
@@ -82,7 +87,9 @@ public class Api : BaseService
Password = password
});
+ _username.Value = username;
_isLoggedIn.Value = true;
+ await UserService.C.DownloadUserInfoAsync(username);
}
#endregion
diff --git a/Flawless.Client/Service/BaseService.cs b/Flawless.Client/Service/BaseService.cs
index 14a1aba..382fd2e 100644
--- a/Flawless.Client/Service/BaseService.cs
+++ b/Flawless.Client/Service/BaseService.cs
@@ -1,4 +1,6 @@
-namespace Flawless.Client.Service;
+using Flawless.Client.ViewModels;
+
+namespace Flawless.Client.Service;
public class BaseService where TService : class, new()
{
diff --git a/Flawless.Client/Service/LocalFileTreeAccessor.cs b/Flawless.Client/Service/LocalFileTreeAccessor.cs
index b283141..81e1748 100644
--- a/Flawless.Client/Service/LocalFileTreeAccessor.cs
+++ b/Flawless.Client/Service/LocalFileTreeAccessor.cs
@@ -11,6 +11,28 @@ namespace Flawless.Client.Service;
public class LocalFileTreeAccessor
{
+
+ public enum ChangeType
+ {
+ Folder = 0,
+ Add,
+ Remove,
+ Modify
+ }
+
+ public struct ChangeRecord
+ {
+ public ChangeType Type { get; }
+
+ public WorkspaceFile File { get; }
+
+ public ChangeRecord(ChangeType type, WorkspaceFile file)
+ {
+ Type = type;
+ File = file;
+ }
+ }
+
private static readonly string[] IgnoredDirectories =
{
AppDefaultValues.RepoLocalStorageManagerFolder
@@ -22,11 +44,7 @@ public class LocalFileTreeAccessor
private IReadOnlyDictionary _baseline;
- private readonly Dictionary _newFiles = new();
-
- private readonly Dictionary _deleteFiles = new();
-
- private readonly Dictionary _modifyFiles = new();
+ private Dictionary _difference;
private object _optLock = new();
@@ -34,16 +52,12 @@ public class LocalFileTreeAccessor
public IReadOnlyDictionary BaselineFiles => _baseline;
- public IReadOnlyDictionary NewFiles => _newFiles;
-
- public IReadOnlyDictionary DeletedFiles => _deleteFiles;
-
- public IReadOnlyDictionary ModifiedFiles => _modifyFiles;
-
+ public IReadOnlyDictionary Changes => _difference;
public LocalFileTreeAccessor(RepositoryModel repo, IEnumerable baselines)
{
_repo = repo;
+ _difference = new Dictionary();
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
_rootDirectory = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
}
@@ -53,9 +67,7 @@ public class LocalFileTreeAccessor
lock (_optLock)
{
_baseline = baselines.ToImmutableDictionary(b => b.WorkPath);
- _newFiles.Clear();
- _deleteFiles.Clear();
- _modifyFiles.Clear();
+ _difference.Clear();
RefreshInternal();
}
@@ -65,9 +77,7 @@ public class LocalFileTreeAccessor
{
lock (_optLock)
{
- _newFiles.Clear();
- _deleteFiles.Clear();
- _modifyFiles.Clear();
+ _difference.Clear();
RefreshInternal();
}
@@ -91,8 +101,8 @@ public class LocalFileTreeAccessor
var news = currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
var removed = _baseline.Values.Where(v => !currentFiles.ContainsKey(v.WorkPath));
- foreach (var f in changes) _modifyFiles.Add(f.WorkPath, f);
- foreach (var f in news) _newFiles.Add(f.WorkPath, f);
- foreach (var f in removed) _deleteFiles.Add(f.WorkPath, f);
+ foreach (var f in changes) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Modify, f));
+ foreach (var f in news) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Add, f));
+ foreach (var f in removed) _difference.Add(f.WorkPath, new ChangeRecord(ChangeType.Remove, f));
}
}
\ No newline at end of file
diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs
index 762c259..9aff3a5 100644
--- a/Flawless.Client/Service/RepositoryService.cs
+++ b/Flawless.Client/Service/RepositoryService.cs
@@ -65,8 +65,11 @@ public class RepositoryService : BaseService
{
// Use existed target
using var readFs = new StreamReader(new FileStream(dbPath, FileMode.Open));
- localRepo = JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
- as RepositoryLocalDatabaseModel; // todo add broken test.
+ localRepo = (JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
+ as RepositoryLocalDatabaseModel)!; // todo add broken test.
+
+ localRepo.RootModal = repo;
+ localRepo.LocalAccessor = new LocalFileTreeAccessor(repo, []);
}
else
{
@@ -572,7 +575,8 @@ public class RepositoryService : BaseService
Console.WriteLine(e);
return false;
}
-
+
+ repo.IsDownloaded = false;
return true;
}
}
\ No newline at end of file
diff --git a/Flawless.Client/Service/UserService.cs b/Flawless.Client/Service/UserService.cs
new file mode 100644
index 0000000..7ddaeb2
--- /dev/null
+++ b/Flawless.Client/Service/UserService.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Flawless.Client.Models;
+using ReactiveUI.SourceGenerators;
+
+namespace Flawless.Client.Service;
+
+public partial class UserService : BaseService
+{
+ private Dictionary _cachedUsers = new();
+
+ public bool IsUserCached(string username)
+ => _cachedUsers.ContainsKey(username);
+
+ public UserModel? GetUserInfoAsync(string username)
+ => _cachedUsers.ContainsKey(username) ? _cachedUsers[username] : null;
+
+ public async ValueTask GetOrDownloadUserInfoAsync(string username)
+ {
+ if (_cachedUsers.TryGetValue(username, out var userModel)) return userModel;
+ return await DownloadUserInfoAsync(username);
+ }
+
+ public async ValueTask DownloadUserInfoAsync(string username)
+ {
+ var api = Api.C;
+ UserModel user;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return null;
+ }
+
+ var info = await api.Gateway.GetInfo(username);
+ user = new UserModel();
+
+ user.Username = info.Username;
+ user.Nickname = info.NickName;
+ user.Bio = info.Bio;
+ user.Sex = (UserModel.SexType) info.Gender;
+ user.EmailIsVisible = info.PublicEmail ?? false;
+ user.CanEdit = info.Authorized;
+ user.PhoneNumber = info.Phone;
+ user.Email = info.Email;
+
+ _cachedUsers.Add(username, user);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return null;
+ }
+
+ return user;
+ }
+
+ public async ValueTask RefreshUserInfoAsync(UserModel user)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ var info = await api.Gateway.GetInfo(user.Username);
+
+ user.Nickname = info.NickName;
+ user.Bio = info.Bio;
+ user.Sex = (UserModel.SexType) info.Gender;
+ user.EmailIsVisible = info.PublicEmail ?? false;
+ user.CanEdit = info.Authorized;
+ user.PhoneNumber = info.Phone;
+ user.Email = info.Email;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return false;
+ }
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs
index c2a244f..84bd57e 100644
--- a/Flawless.Client/ViewModels/RepositoryViewModel.cs
+++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs
@@ -1,25 +1,191 @@
-using System.Reactive.Linq;
+using System;
+using System.Collections.Generic;
+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.Client.Models;
using Flawless.Client.Service;
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 partial class RepositoryViewModel : RoutableViewModelBase
{
public RepositoryModel Repository { get; }
+
+ public RepositoryLocalDatabaseModel LocalDatabase { get; }
+
+ public HierarchicalTreeDataGridSource LocalChange { get; }
+
+ public ObservableCollection LocalChangeSetRaw { get; } = new();
+
+ public UserModel User { get; }
+
+ [Reactive] private bool _autoDetectChanges = true;
- [ReactiveCommand]
- private async Task GoBackAsync()
- {
- await RepositoryService.C.CloseRepositoryAsync(Repository);
- await HostScreen.Router.NavigateBack.Execute();
- }
+ [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
+ LocalChangeSetRaw.Add(new LocalChangesNode
+ {
+ Type = "Add",
+ FullPath = "test.md",
+ ModifiedTime = DateTime.Now,
+ });
+ LocalChange = new HierarchicalTreeDataGridSource(LocalChangeSetRaw)
+ {
+ Columns =
+ {
+ new CheckBoxColumn(
+ string.Empty, n => n.Included, (n, v) => n.Included = v),
+
+ new TextColumn(
+ "Change",
+ n => n.Contents != null ? String.Empty : n.Type),
+
+ 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),
+ }
+ };
+
+ // Do refresh when entered
+ // 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);
}
}
\ No newline at end of file
diff --git a/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml b/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
index 132d5bb..58bc3e6 100644
--- a/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
+++ b/Flawless.Client/Views/ModalBox/CreateRepositoryDialogView.axaml
@@ -19,7 +19,7 @@
-
+
diff --git a/Flawless.Client/Views/RepositoryPage/RepoDashboardPageView.axaml b/Flawless.Client/Views/RepositoryPage/RepoDashboardPageView.axaml
index a5bb643..da30e13 100644
--- a/Flawless.Client/Views/RepositoryPage/RepoDashboardPageView.axaml
+++ b/Flawless.Client/Views/RepositoryPage/RepoDashboardPageView.axaml
@@ -10,20 +10,22 @@
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
diff --git a/Flawless.Client/Views/RepositoryPage/RepoWorkspacePageView.axaml b/Flawless.Client/Views/RepositoryPage/RepoWorkspacePageView.axaml
index 8236857..0211cb7 100644
--- a/Flawless.Client/Views/RepositoryPage/RepoWorkspacePageView.axaml
+++ b/Flawless.Client/Views/RepositoryPage/RepoWorkspacePageView.axaml
@@ -2,51 +2,33 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Flawless.Client.ViewModels"
x:DataType="vm:RepositoryViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flawless.Client.Views.RepositoryPage.RepoWorkspacePageView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
diff --git a/Flawless.Client/Views/RepositoryView.axaml b/Flawless.Client/Views/RepositoryView.axaml
index 050e673..6e256a6 100644
--- a/Flawless.Client/Views/RepositoryView.axaml
+++ b/Flawless.Client/Views/RepositoryView.axaml
@@ -16,7 +16,7 @@
Command="{Binding GoBackCommand}" Content="All Repositories"/>
-
+
@@ -24,16 +24,16 @@
-
+
-
+
-
+
@@ -42,34 +42,34 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+