diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml index 1ab1d7f..88412e0 100644 --- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml +++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml @@ -8,6 +8,7 @@ + diff --git a/Flawless.Abstract/WorkPath.cs b/Flawless.Abstract/WorkPath.cs index 786b54d..6747007 100644 --- a/Flawless.Abstract/WorkPath.cs +++ b/Flawless.Abstract/WorkPath.cs @@ -106,6 +106,12 @@ public static class WorkPath return sb.ToString(); } + public static string FormatPathDirectorySeparator(string path) + { + return path.Replace(Path.DirectorySeparatorChar, DirectorySeparatorChar) + .Replace(Path.AltDirectorySeparatorChar, DirectorySeparatorChar); + } + /// /// Split work path into path vector. /// diff --git a/Flawless.Client/ErrorGUIHandler.cs b/Flawless.Client/ErrorGUIHandler.cs deleted file mode 100644 index 63125e6..0000000 --- a/Flawless.Client/ErrorGUIHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Avalonia.Controls.Notifications; - -namespace Flawless.Client; - -public static class ErrorGUIHandler -{ - public static void OnError(Exception ex) - { - } -} \ No newline at end of file diff --git a/Flawless.Client/Service/Api.cs b/Flawless.Client/Service/Api.cs index 65a0ecc..e096f92 100644 --- a/Flawless.Client/Service/Api.cs +++ b/Flawless.Client/Service/Api.cs @@ -99,7 +99,7 @@ public class Api : BaseService public bool RequireRefreshToken() { if (_token.Value == null) return true; - if (DateTime.UtcNow.AddMinutes(1) > _token.Value.Expiration) return true; + if (DateTime.UtcNow > _token.Value.Expiration!.Value.UtcDateTime.AddMinutes(-2)) return true; return false; } diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs index 8d383bd..d3c7a42 100644 --- a/Flawless.Client/Service/RepositoryService.cs +++ b/Flawless.Client/Service/RepositoryService.cs @@ -115,6 +115,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return null; } @@ -164,6 +165,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } @@ -226,6 +228,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } @@ -328,6 +331,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); await DeleteFromDiskAsync(repo); return false; @@ -361,6 +365,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } @@ -417,6 +422,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } @@ -435,7 +441,6 @@ public class RepositoryService : BaseService if (manifest == null) return null; // Prepare folders - var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name); var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name); Directory.CreateDirectory(depotsRoot); @@ -464,18 +469,22 @@ public class RepositoryService : BaseService } // Create mapping dictionary - var mappingDict = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!); + var streamMap = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!); foreach (var dl in mainDepotLabel) { - if (mappingDict.ContainsKey(dl.Id)) continue; - var dst = Path.Combine(depotsRoot, dl.Id.ToString()); - mappingDict.Add(dl.Id, new FileStream(dst, FileMode.Create)); + // If this file is not being opened, open it from file system + if (!streamMap.ContainsKey(dl.Id)) + { + var dst = Path.Combine(depotsRoot, dl.Id.ToString()); + streamMap.Add(dl.Id, new FileStream(dst, FileMode.Open, FileAccess.Read, FileShare.Read)); + } } - return new RepositoryFileTreeAccessor(mappingDict, manifest.Value); + return new RepositoryFileTreeAccessor(streamMap, manifest.Value); } catch (Exception e) { + UIHelper.NotifyError(e); if (downloadedDepots != null) foreach (var t in downloadedDepots) { @@ -537,6 +546,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return null; } @@ -561,6 +571,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return null; } @@ -575,6 +586,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } @@ -630,8 +642,15 @@ public class RepositoryService : BaseService if (accessor == null) return null; //todo this is a really fatal issue... if (localDb.RepoAccessor != null) { - try { await localDb.RepoAccessor.DisposeAsync(); } - catch (Exception e) { Console.WriteLine(e); } + try + { + await localDb.RepoAccessor.DisposeAsync(); + } + catch (Exception e) + { + UIHelper.NotifyError(e); + Console.WriteLine(e); + } } // Point to newest state. @@ -644,6 +663,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return null; } @@ -735,6 +755,7 @@ public class RepositoryService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Directory.Delete(repoWs, true); Console.WriteLine(e); return null; diff --git a/Flawless.Client/Service/UserService.cs b/Flawless.Client/Service/UserService.cs index 7ddaeb2..2bc5704 100644 --- a/Flawless.Client/Service/UserService.cs +++ b/Flawless.Client/Service/UserService.cs @@ -50,6 +50,7 @@ public partial class UserService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return null; } @@ -80,6 +81,7 @@ public partial class UserService : BaseService } catch (Exception e) { + UIHelper.NotifyError(e); Console.WriteLine(e); return false; } diff --git a/Flawless.Client/UIHelper.cs b/Flawless.Client/UIHelper.cs new file mode 100644 index 0000000..f51cdd3 --- /dev/null +++ b/Flawless.Client/UIHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Notifications; +using Flawless.Client.ViewModels.ModalBox; +using Flawless.Client.Views.ModalBox; +using Ursa.Controls; +using Notification = Ursa.Controls.Notification; +using WindowNotificationManager = Ursa.Controls.WindowNotificationManager; + +namespace Flawless.Client; + +public static class UIHelper +{ + + private static WindowNotificationManager _notificationManager = null!; + + public static WindowNotificationManager Notify + { + get + { + if (_notificationManager != null) return _notificationManager!; + + var lf = ((IClassicDesktopStyleApplicationLifetime)App.Current.ApplicationLifetime); + if (!WindowNotificationManager.TryGetNotificationManager(lf.MainWindow!, out _notificationManager)) + throw new Exception("Can not get notification manager"); + + _notificationManager!.Position = NotificationPosition.TopCenter; + return _notificationManager!; + } + } + + + public static void NotifyError(Exception ex) + { + try + { + var content = ex.ToString(); + if (content.Length > 100) content = content.Substring(0, 100) + "..."; + var nf = new Notification(ex.GetType().Name, content, NotificationType.Error); + Notify.Show(nf); + } + catch (Exception e) + { + Console.WriteLine("Can not notify error to users: " + e); + } + } + + public static void NotifyError(string title, string content) + { + try + { + var nf = new Notification(title, content, NotificationType.Error); + Notify.Show(nf); + } + catch (Exception e) + { + Console.WriteLine($"Can not notify error to users: {title} - {content}, {e}"); + } + } + + + public static Task SimpleAskAsync(string content, DialogMode mode = DialogMode.None) + { + var opt = new OverlayDialogOptions + { + FullScreen = false, + Buttons = DialogButton.YesNo, + CanResize = false, + CanDragMove = false, + IsCloseButtonVisible = true, + CanLightDismiss = true, + Mode = mode + }; + + var vm = new SimpleMessageDialogViewModel(content); + return OverlayDialog.ShowModal(vm, AppDefaultValues.HostId, opt); + } + + public static Task SimpleAlert(string content, DialogMode mode = DialogMode.Error) + { + var opt = new OverlayDialogOptions + { + FullScreen = false, + Buttons = DialogButton.YesNo, + CanResize = false, + CanDragMove = false, + IsCloseButtonVisible = true, + CanLightDismiss = true, + Mode = mode + }; + + var vm = new SimpleMessageDialogViewModel(content); + return OverlayDialog.ShowModal(vm, AppDefaultValues.HostId, opt); + } +} \ No newline at end of file diff --git a/Flawless.Client/ViewModels/LoginPageViewModel.cs b/Flawless.Client/ViewModels/LoginPageViewModel.cs index 87de465..be693ea 100644 --- a/Flawless.Client/ViewModels/LoginPageViewModel.cs +++ b/Flawless.Client/ViewModels/LoginPageViewModel.cs @@ -20,8 +20,6 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel [Reactive] private string _username = "cardidi"; [Reactive] private string _password = "4453A2b33"; - - [Reactive(SetModifier = AccessModifier.Protected)] private string _issue = String.Empty; public IObservable CanLogin; @@ -56,12 +54,12 @@ public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel catch (ApiException ex) { await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex.Content}"); - Issue = ex.Content ?? String.Empty; + UIHelper.NotifyError(ex); } catch (Exception ex) { await Console.Error.WriteLineAsync($"Login as '{Username}' Failed: {ex}"); - Issue = ex.Message; + UIHelper.NotifyError(ex); } Console.WriteLine($"Login as '{Username}' success!"); diff --git a/Flawless.Client/ViewModels/RegisterPageViewModel.cs b/Flawless.Client/ViewModels/RegisterPageViewModel.cs index d351ea9..bc1fa87 100644 --- a/Flawless.Client/ViewModels/RegisterPageViewModel.cs +++ b/Flawless.Client/ViewModels/RegisterPageViewModel.cs @@ -23,8 +23,6 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel [Reactive] private string _password; - [Reactive(SetModifier = AccessModifier.Protected)] private string _issue; - public RegisterPageViewModel(IScreen hostScreen) { HostScreen = hostScreen; @@ -49,12 +47,12 @@ public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel catch (ApiException ex) { await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex.Content}"); - Issue = ex.Content ?? String.Empty; + UIHelper.NotifyError(ex); } catch (Exception ex) { await Console.Error.WriteLineAsync($"Register as '{Username}' Failed: {ex}"); - Issue = ex.Message; + UIHelper.NotifyError(ex); } Console.WriteLine($"Register as '{Username}' success!"); diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs index a1b38cd..4b29758 100644 --- a/Flawless.Client/ViewModels/RepositoryViewModel.cs +++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs @@ -10,6 +10,7 @@ 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; @@ -106,13 +107,14 @@ public partial class RepositoryViewModel : RoutableViewModelBase 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; @@ -181,10 +183,49 @@ public partial class RepositoryViewModel : RoutableViewModelBase } }; - DetectLocalChangesAsyncCommand.Execute(); + 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(); + } + 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) @@ -201,21 +242,72 @@ public partial class RepositoryViewModel : RoutableViewModelBase } } + 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; + } + }); + } + [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 (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] @@ -242,37 +334,28 @@ public partial class RepositoryViewModel : RoutableViewModelBase [ReactiveCommand] private async ValueTask DetectLocalChangesAsync() { - var ns = await Task.Run(() => + var ns = await Task.Run(async () => { 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)); - } + 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; + } } \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs b/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs index dad59b1..0759661 100644 --- a/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs +++ b/Flawless.Client/ViewModels/ServerSetupPageViewModel.cs @@ -14,8 +14,6 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase [Reactive] private string _host = "http://localhost:5256/"; - [Reactive(SetModifier = AccessModifier.Protected)] private string? _issue; - public IObservable CanSetHost { get; } public ServerSetupPageViewModel(IScreen hostScreen) : base(hostScreen) @@ -33,19 +31,18 @@ public partial class ServerSetupPageViewModel : RoutableViewModelBase { try { - Issue = string.Empty; await Api.C.SetGatewayAsync(Host); HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen)); } catch (ApiException ex) { await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString()); - Issue = ex.Content; + UIHelper.NotifyError(ex); } catch (Exception ex) { await Console.Error.WriteLineAsync("Can not connect to server: " + ex.ToString()); - Issue = ex.Message; + UIHelper.NotifyError(ex); } } diff --git a/Flawless.Client/Views/HelloSetup/LoginPageView.axaml b/Flawless.Client/Views/HelloSetup/LoginPageView.axaml index a75a8a0..54f45f6 100644 --- a/Flawless.Client/Views/HelloSetup/LoginPageView.axaml +++ b/Flawless.Client/Views/HelloSetup/LoginPageView.axaml @@ -20,6 +20,5 @@