diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user index 8bece1a237ef6b008da694102f6315c2fbec6d97..a4c636f05f3e18e362d327eec1ae5108a0198098 100644 --- a/Blacklight.sln.DotSettings.user +++ b/Blacklight.sln.DotSettings.user @@ -16,5 +16,9 @@ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALowerCaseStringEnumConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e29f6bc726b4d68bc914cbabf2575377600_003F27_003F3006d2c3_003FLowerCaseStringEnumConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetworkInformation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b1b048ea0e94b28ad7cbac8ef6bc4c26e00_003Fb4_003Fff405095_003FNetworkInformation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProportionalDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F91c655b1795150937d9264ac1431c463ed5e5e8ad0286ad17e16f16175d94_003FProportionalDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2854ce6d56c18d0d837d3a3ef9c4f2c7c77691fa3528c8394986ac7ce7719_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeDataTemplate_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe9a5203b1cebd5ba4f745e6a4b3dd651886fe711fe1ad32dec118143c4fbd0_003FTreeDataTemplate_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeDataTemplate_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe9a5203b1cebd5ba4f745e6a4b3dd651886fe711fe1ad32dec118143c4fbd0_003FTreeDataTemplate_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeView_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F49b572a64c2c02980ffe97ff1673549ed9bdfbf9738ec66def83611762e85c_003FTreeView_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> </wpf:ResourceDictionary> \ No newline at end of file diff --git a/Blacklight/App.axaml b/Blacklight/App.axaml index 2cf90ee42b75875f01df58ca1f6859980bbc9296..d91b9e4df8dc6f44c2f41197659178002029b0e8 100644 --- a/Blacklight/App.axaml +++ b/Blacklight/App.axaml @@ -7,6 +7,7 @@ xmlns:core="clr-namespace:Dock.Model.Core;assembly=Dock.Model" xmlns:documents="clr-namespace:Blacklight.ViewModels.Documents" xmlns:util="clr-namespace:Blacklight.Util" + xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" RequestedThemeVariant="Default"> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <Application.Resources> @@ -61,6 +62,7 @@ <Application.Styles> <FluentTheme /> <DockFluentTheme /> + <dialogHostAvalonia:DialogHostStyles /> <Style Selector="DockControl"> <Setter Property="(ControlRecyclingDataTemplate.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" /> </Style> diff --git a/Blacklight/Blacklight.csproj b/Blacklight/Blacklight.csproj index a0cadd8e5e27383aa4b56174ba361c94fdf46a21..d7c3af54b36de8b14a2b4bc9b85a1dba1b1e49c3 100644 --- a/Blacklight/Blacklight.csproj +++ b/Blacklight/Blacklight.csproj @@ -22,6 +22,7 @@ <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" /> <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.2.0.9" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> + <PackageReference Include="DialogHost.Avalonia" Version="0.9.2" /> <PackageReference Include="Dock.Avalonia" Version="11.2.0" /> <PackageReference Include="Dock.Model.Mvvm" Version="11.2.0.1" /> <PackageReference Include="Markdig" Version="0.40.0" /> @@ -45,4 +46,9 @@ <ItemGroup> <ProjectReference Include="..\Lightquark.NET\Lightquark.NET.csproj" /> </ItemGroup> + + + <ItemGroup> + <UpToDateCheckInput Remove="Views\Windows\CreateChannelView.axaml" /> + </ItemGroup> </Project> diff --git a/Blacklight/ViewModels/ClientViewModel.cs b/Blacklight/ViewModels/ClientViewModel.cs index d852c107ad8cea6befe80b2e0d8a02846c5e1ecb..b1d4d229de762a2cd8efd1cf825389683db03c39 100644 --- a/Blacklight/ViewModels/ClientViewModel.cs +++ b/Blacklight/ViewModels/ClientViewModel.cs @@ -62,6 +62,7 @@ public class ClientViewModel : ViewModelBase public void TestCommand() { Log.Information("Current items in QuarkList: {ItemCount}", Client.QuarkList.Count); + _ = Client.UpdateResourceTree(); } public void LogoutNow() diff --git a/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs b/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..b181e04683e3f83e16cbeda133579696edf0eb53 --- /dev/null +++ b/Blacklight/ViewModels/Dialogs/CreateChannelViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using DialogHostAvalonia; +using Lightquark.NET; +using Lightquark.NET.Objects; +using Lightquark.NET.Objects.Reply; +using Microsoft.Extensions.DependencyInjection; +using Serilog; + +namespace Blacklight.ViewModels.Dialogs; + +public class CreateChannelViewModel : ViewModelBase +{ + private Quark _meow; + private string _errorMessage = ""; + private string? _channelName; + private string? _channelDescription; + public ICommand CreateChannelCommand => new RelayCommand(CreateChannel); + + private async void CreateChannel() + { + try + { + if (ChannelName == null) + { + ErrorMessage = "Name is required"; + return; + } + if (ChannelName.Length is < 1 or > 64) + { + ErrorMessage = "Name should be between 1 and 64 characters"; + return; + } + + var client = App.Services!.GetRequiredService<Client>(); + var res = await client.CreateChannel(Quark.Id, ChannelName, ChannelDescription); + if (res.Request.Success) + { + DialogHost.GetDialogSession("DialogHost")?.Close(false); + return; + } + + ErrorMessage = res.Response.Message; + } + catch (Exception ex) + { + Log.Error(ex, "Failed channel creation"); + ErrorMessage = "Failed to create channel."; + } + } + + public Quark Quark + { + get => _meow; + set => SetProperty(ref _meow, value); + } + + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public string? ChannelName + { + get => _channelName; + set => SetProperty(ref _channelName, value); + } + + public string? ChannelDescription + { + get => _channelDescription; + set => SetProperty(ref _channelDescription, value); + } +} + diff --git a/Blacklight/ViewModels/DockFactory.cs b/Blacklight/ViewModels/DockFactory.cs index 414f3124cfbe01608e75482eb5f31d93e8115fdd..ec3a31137fff4054e7a9780c7814cae6078af01d 100644 --- a/Blacklight/ViewModels/DockFactory.cs +++ b/Blacklight/ViewModels/DockFactory.cs @@ -11,6 +11,7 @@ using Dock.Model.Core; using Dock.Model.Mvvm; using Dock.Model.Mvvm.Controls; using Lightquark.NET.Objects; +using MongoDB.Bson; namespace Blacklight.ViewModels; @@ -166,6 +167,21 @@ public class DockContext parent.ActiveDockable = document; } + // TODO + // public void RemoveDocument(ObjectId channelId) + // { + // var docks = GetDocumentDocks(_mainLayoutDock); + // foreach (var dockWithParent in docks) + // { + // if (dockWithParent.DocumentDock.VisibleDockables == null) throw new Exception("wtf? _documentDock.VisibleDockables is null"); + // var toRemove = dockWithParent.DocumentDock.VisibleDockables.OfType<DocumentViewModel>().Where(d => d.Channel.Id == channelId); + // foreach (var removable in toRemove) + // { + // dockWithParent.DocumentDock.VisibleDockables.Remove(removable); + // } + // } + // } + public void RemoveDocument(DocumentViewModel document) { var docks = GetDocumentDocks(_mainLayoutDock); diff --git a/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs b/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs index 59890ce769fd6787828d34c87785e6c04b9a5e64..11998447dcb10c927951df1b329147fcb61e3527 100644 --- a/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs +++ b/Blacklight/ViewModels/Tools/ResourceExplorerViewModel.cs @@ -1,5 +1,13 @@ using System; +using System.Diagnostics; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Media; +using Blacklight.ViewModels.Dialogs; +using DialogHostAvalonia; using Dock.Model.Mvvm.Controls; +using Lightquark.NET; +using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using Serilog; @@ -15,14 +23,18 @@ public class ResourceExplorerViewModel : Tool return; } Log.Information("Cats go meow {QuarkId}", quarkId); - + var client = App.Services!.GetRequiredService<Client>(); + DialogHost.Show(new CreateChannelViewModel + { + Quark = client.Quarks.First(q => q.Id == quarkId) + }); } // ReSharper disable once UnusedMember.Global public bool CanCreateChannel(object obj) { - return obj is ObjectId; + return obj is ObjectId; // TODO: Check permission } public void LeaveQuark(object obj) @@ -32,7 +44,7 @@ public class ResourceExplorerViewModel : Tool // ReSharper disable once UnusedMember.Global public bool CanLeaveQuark(object obj) { - return true; + return false; } public void EditQuark(object obj) { @@ -41,15 +53,63 @@ public class ResourceExplorerViewModel : Tool // ReSharper disable once UnusedMember.Global public bool CanEditQuark(object obj) { - return true; + return false; } public void DeleteQuark(object obj) { throw new NotImplementedException(); } // ReSharper disable once UnusedMember.Global - public bool CanDeletQuark(object obj) + public bool CanDeleteQuark(object obj) + { + return false; + } + public void EditChannel(object obj) + { + throw new NotImplementedException(); + } + // ReSharper disable once UnusedMember.Global + public bool CanEditChannel(object obj) + { + return false; + } + public async void DeleteChannel(object obj) + { + try + { + if (obj is not ObjectId channelId) + { + Log.Information("obj is {Obj}", obj.GetType().FullName); + return; + } + var client = App.Services!.GetRequiredService<Client>(); + var res = await client.DeleteChannel(channelId); + if (res.Request.Success) return; + + // App.DockContext.RemoveDocument(channelId); TODO + + await DialogHost.Show(new TextBlock + { + Text = $"Failed to delete channel: {res.Response.Message}", + Foreground = Brushes.Red, + FontWeight = FontWeight.Bold + }); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to delete channel"); + + await DialogHost.Show(new TextBlock + { + Text = "Failed to delete channel", + Foreground = Brushes.Red, + FontWeight = FontWeight.Bold + }); + } + } + // ReSharper disable once UnusedMember.Global + public bool CanDeleteChannel(object obj) { - return true; + return obj is ObjectId; // TODO: Check permission } } \ No newline at end of file diff --git a/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs b/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs deleted file mode 100644 index 436893ad2badf75b1597dc1859f57b66f3676d19..0000000000000000000000000000000000000000 --- a/Blacklight/ViewModels/Windows/CreateChannelViewModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Blacklight.ViewModels.Windows; - -public class CreateChannelViewModel : ViewModelBase -{ - -} \ No newline at end of file diff --git a/Blacklight/Views/Dialogs/CreateChannelView.axaml b/Blacklight/Views/Dialogs/CreateChannelView.axaml new file mode 100644 index 0000000000000000000000000000000000000000..d886551e1828799f10b9f9e41a1f5a5337cf426f --- /dev/null +++ b/Blacklight/Views/Dialogs/CreateChannelView.axaml @@ -0,0 +1,65 @@ +<UserControl xmlns="https://github.com/avaloniaui" + 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:dialogs="clr-namespace:Blacklight.ViewModels.Dialogs" + xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Blacklight.Views.Dialogs.CreateChannelView" + x:DataType="dialogs:CreateChannelViewModel"> + <Grid> + <Grid.RowDefinitions> + *,*,*,*,* + </Grid.RowDefinitions> + <Grid.ColumnDefinitions> + Auto,*,Auto + </Grid.ColumnDefinitions> + + <TextBlock Grid.Row="0" + Grid.Column="0" + Grid.ColumnSpan="3" + FontWeight="Bold" HorizontalAlignment="Center" Text="{Binding Quark.Name, StringFormat=Create channel in {0}}" /> + + <Grid Margin="0,4" Grid.Row="1" + Grid.Column="0" + Grid.ColumnSpan="3"> + <Grid.ColumnDefinitions> + Auto,*,Auto + </Grid.ColumnDefinitions> + <TextBlock VerticalAlignment="Center" Grid.Column="0" Margin="0, 0, 10, 0">Name:</TextBlock> + <TextBox MinWidth="196" MaxLength="64" Grid.Column="2" Watermark="Name" Text="{Binding ChannelName}"/> + </Grid> + + <Grid Margin="0,4" Grid.Row="2" + Grid.Column="0" + Grid.ColumnSpan="3"> + <Grid.ColumnDefinitions> + Auto,*,Auto + </Grid.ColumnDefinitions> + <TextBlock VerticalAlignment="Center" Grid.Column="0" Margin="0, 0, 10, 0">Description:</TextBlock> + <TextBox MinWidth="196" MaxLength="512" Grid.Column="2" Watermark="Description (optional)" Text="{Binding ChannelDescription}"/> + </Grid> + + <TextBlock MaxWidth="256" + Grid.Row="3" + Grid.Column="0" + Foreground="Red" + FontWeight="Bold" + TextWrapping="Wrap" + HorizontalAlignment="Center" + Grid.ColumnSpan="3" + Text="{Binding ErrorMessage}"/> + + <Grid Margin="0,4" Grid.Row="4" + Grid.Column="0" + Grid.ColumnSpan="3" + HorizontalAlignment="Stretch"> + <Grid.ColumnDefinitions> + Auto,*,Auto + </Grid.ColumnDefinitions> + <Button Grid.Column="0" Command="{Binding CreateChannelCommand}">Create</Button> + <Button Grid.Column="2" + Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHostAvalonia:DialogHost}, Path=CloseDialogCommand}">Cancel</Button> + </Grid> + </Grid> +</UserControl> diff --git a/Blacklight/Views/Windows/CreateChannelView.axaml.cs b/Blacklight/Views/Dialogs/CreateChannelView.axaml.cs similarity index 62% rename from Blacklight/Views/Windows/CreateChannelView.axaml.cs rename to Blacklight/Views/Dialogs/CreateChannelView.axaml.cs index f24ee5675eb3ece6f819f3decb4ab84e70eb4899..d8fe34c80f23c46b57e6c23a440493f8487f21b8 100644 --- a/Blacklight/Views/Windows/CreateChannelView.axaml.cs +++ b/Blacklight/Views/Dialogs/CreateChannelView.axaml.cs @@ -2,9 +2,9 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Blacklight.Views.Windows; +namespace Blacklight.Views.Dialogs; -public partial class CreateChannelView : Window +public partial class CreateChannelView : UserControl { public CreateChannelView() { diff --git a/Blacklight/Views/MainWindow.axaml b/Blacklight/Views/MainWindow.axaml index 6b12f563ea5e99e9079331d649fa3f6b27dfb341..894fb5402cc3048b8909550a9db58a84b0afb84b 100644 --- a/Blacklight/Views/MainWindow.axaml +++ b/Blacklight/Views/MainWindow.axaml @@ -6,7 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:login="clr-namespace:Blacklight.Views.Login" - xmlns:util="clr-namespace:Blacklight.Util" + xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="700" x:Class="Blacklight.Views.MainWindow" x:DataType="vm:MainWindowViewModel" @@ -34,18 +34,24 @@ <views:ClientView /> </DataTemplate> </Window.DataTemplates> - - <Panel> + + <dialogHostAvalonia:DialogHost + CloseOnClickAway="True" + BlurBackground="True" + BlurBackgroundRadius="5" + Identifier="DialogHost"> <Panel> <Panel> - <TransitioningContentControl Content="{Binding CurrentViewModel}"> - <TransitioningContentControl.PageTransition> - <CrossFade Duration="0.500" /> - </TransitioningContentControl.PageTransition> - </TransitioningContentControl> + <Panel> + <TransitioningContentControl Content="{Binding CurrentViewModel}"> + <TransitioningContentControl.PageTransition> + <CrossFade Duration="0.500" /> + </TransitioningContentControl.PageTransition> + </TransitioningContentControl> + </Panel> </Panel> </Panel> - </Panel> + </dialogHostAvalonia:DialogHost> </Window> \ No newline at end of file diff --git a/Blacklight/Views/Tools/ResourceExplorerView.axaml b/Blacklight/Views/Tools/ResourceExplorerView.axaml index 45bd4d82e7e14fd71303d17bd77492ae52b48006..d35dfa4ecffe5b4cdfee3fe14e479a616f111788 100644 --- a/Blacklight/Views/Tools/ResourceExplorerView.axaml +++ b/Blacklight/Views/Tools/ResourceExplorerView.axaml @@ -11,7 +11,12 @@ x:Class="Blacklight.Views.Tools.ResourceExplorerView"> <Grid ColumnDefinitions="*" RowDefinitions="*" Background="{DynamicResource ResourceExplorerColor}"> <TreeView Grid.Row="0" ItemsSource="{Binding $parent[views:ClientView].((viewModels:ClientViewModel)DataContext).Client.ResourceTree}" SelectionChanged="ItemSelected"> - <TreeView.DataTemplates> + <TreeView.Styles> + <Style Selector="TreeViewItem" x:DataType="objects:QuarkListItem"> + <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/> + </Style> + </TreeView.Styles> + <TreeView.DataTemplates> <util:ResourceExplorerTemplateSelector> <TreeDataTemplate x:Key="QuarkTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch"> @@ -43,6 +48,17 @@ <StackPanel Orientation="Horizontal"> <TextBlock FontWeight="Bold" Foreground="{DynamicResource ChannelHashtagColor}" Margin="0,0,2,0">#</TextBlock> <TextBlock Text="{Binding Channel.Name, FallbackValue='No Channel Information'}"></TextBlock> + + <StackPanel.ContextMenu> + <ContextMenu> + <MenuItem Header="Edit channel" + Command="{Binding $parent[TreeView].((tools:ResourceExplorerViewModel)DataContext).EditChannel}" + CommandParameter="{Binding Channel.Id}"/> + <MenuItem Header="Delete channel" + Command="{Binding $parent[TreeView].((tools:ResourceExplorerViewModel)DataContext).DeleteChannel}" + CommandParameter="{Binding Channel.Id}"/> + </ContextMenu> + </StackPanel.ContextMenu> </StackPanel> </TreeDataTemplate> <TreeDataTemplate x:Key="FolderTemplate" x:DataType="objects:QuarkListItem" ItemsSource="{Binding Children}"> diff --git a/Blacklight/Views/Windows/CreateChannelView.axaml b/Blacklight/Views/Windows/CreateChannelView.axaml deleted file mode 100644 index 91d5363aca908c2cdc81cb85976325fdfd626825..0000000000000000000000000000000000000000 --- a/Blacklight/Views/Windows/CreateChannelView.axaml +++ /dev/null @@ -1,23 +0,0 @@ -<Window xmlns="https://github.com/avaloniaui" - 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" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Blacklight.Views.Windows.CreateChannelView" - WindowStartupLocation="CenterScreen" - Title="CreateChannelView"> - <Panel> - <ExperimentalAcrylicBorder IsHitTestVisible="False"> - <ExperimentalAcrylicBorder.Material> - <ExperimentalAcrylicMaterial - BackgroundSource="Digger" - TintColor="Black" - TintOpacity="1" - MaterialOpacity="0.65" /> - </ExperimentalAcrylicBorder.Material> - </ExperimentalAcrylicBorder> - <TextBlock> - Welcome to Avalonia! - </TextBlock> - </Panel> -</Window> \ No newline at end of file diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs index 86990912885fee07f71c046bf69ba47495fb51e5..01bb2545c4827fa412be92eba232ed9a5ac84c63 100644 --- a/Lightquark.NET/Client.cs +++ b/Lightquark.NET/Client.cs @@ -73,7 +73,7 @@ public partial class Client : ObservableObject { Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore, - Converters = new List<JsonConverter> { new AttachmentCreationConverter(), new StatusCreationConverter(), new ChannelCreationConverter() }, + Converters = new List<JsonConverter> { new AttachmentCreationConverter(), new StatusCreationConverter() }, ContractResolver = new CamelCasePropertyNamesContractResolver() }; var mapperConfig = new MapperConfiguration(cfg => diff --git a/Lightquark.NET/ClientMethods/Channel.cs b/Lightquark.NET/ClientMethods/Channel.cs index 25ab170a3023b22905e9aab76da68b40f7be28f5..9268f2cdc879bcd3465a6c408caac332125d9acc 100644 --- a/Lightquark.NET/ClientMethods/Channel.cs +++ b/Lightquark.NET/ClientMethods/Channel.cs @@ -1,21 +1,89 @@ -using Lightquark.NET.Objects; +using Avalonia.Threading; +using Lightquark.NET.Objects; +using Lightquark.NET.Objects.Reply; +using MongoDB.Bson; +using Serilog; namespace Lightquark.NET; public partial class Client { - private void AddOrUpdateChannel(Channel channel) + private async void AddOrUpdateChannel(Channel channel) { - var existingChannel = Channels.FirstOrDefault(q => q.Id == channel.Id); - if (existingChannel != null) + try { - // Update - var index = Channels.IndexOf(existingChannel); - Channels[index] = _mapper.Map(channel, existingChannel); + var existingChannel = Channels.FirstOrDefault(q => q.Id == channel.Id); + if (existingChannel != null) + { + // Update + var index = Channels.IndexOf(existingChannel); + channel = _mapper.Map(channel, existingChannel); + Channels[index] = channel; + } + else + { + Channels.Add(channel); + } + + var parentQuark = Quarks.FirstOrDefault(q => q.Id == channel.QuarkId); + if (parentQuark != null) + { + Log.Information("Channel {ChannelId} ({ChannelName}) has parent quark found in cache: {QuarkId} ({QuarkName})", + channel.Id, + channel.Name, + parentQuark.Id, + parentQuark.Name); + var c = parentQuark.Channels.ToList(); + c.Add(channel); + parentQuark.Channels = c.ToArray(); + } + else + { + Log.Warning("Channel {ChannelId} ({ChannelName}) has no parent quark in cache: {QuarkId}", channel.Id, channel.Name, channel.QuarkId); + } } - else + catch (Exception ex) { - Channels.Add(channel); + Log.Error(ex, "Failed to AddOrUpdateChannel"); } } + + private void RemoveChannel(ObjectId channelId) + { + var existingChannel = Channels.FirstOrDefault(c => c.Id == channelId); + if (existingChannel == null) return; + Channels.Remove(existingChannel); + var parentQuark = Quarks.FirstOrDefault(q => q.Channels.Contains(existingChannel)); + if (parentQuark == null) return; + var channels = parentQuark.Channels.ToList(); + parentQuark.Channels = channels.Where(c => c.Id != channelId).ToArray(); + } + + public async Task<Reply<ChannelCreateReplyResponse>> CreateChannel(ObjectId quarkId, string name, string? description) + { + var res = await CallRpc<ChannelCreateReplyResponse>("POST", $"{Version}/channel", new + { + quark = quarkId.ToString(), + name, + description + }); + + return res; + } + + public async Task<Reply<ChannelDeleteReplyResponse>> DeleteChannel(ObjectId channelId) + { + var res = await CallRpc<ChannelDeleteReplyResponse>("DELETE", $"{Version}/channel/{channelId}", null); + return res; + } +} + +public record ChannelCreateReplyResponse : BaseReplyResponse +{ + +} + +public record ChannelDeleteReplyResponse : BaseReplyResponse +{ + } \ No newline at end of file diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs index a3e95ded3a7adda8e73acc877c8746ea18d09a05..8125502155a429c52cb42a3a5e72c38369cfc35f 100644 --- a/Lightquark.NET/ClientMethods/Gateway.cs +++ b/Lightquark.NET/ClientMethods/Gateway.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Avalonia.Threading; using Lightquark.NET.Objects; using Lightquark.NET.Objects.Reply; using Lightquark.NET.Util; @@ -120,6 +121,19 @@ public partial class Client var selectionMessage = ParseGateway<SelectionGatewayMessage>(msg.Text!); GatekeeperSelection(selectionMessage); break; + case "channelCreate": + case "channelUpdate": + var channelCreateMessage = ParseGateway<ChannelCreateGatewayMessage>(msg.Text!); + channelCreateMessage.Channel.QuarkId = channelCreateMessage.Quark.Id; + channelCreateMessage.Channel.Quark = null; + AddOrUpdateChannel(channelCreateMessage.Channel); + _ = Dispatcher.UIThread.Invoke(UpdateResourceTree); + break; + case "channelDelete": + var channelDeleteMessage = ParseGateway<ChannelCreateGatewayMessage>(msg.Text!); + RemoveChannel(channelDeleteMessage.Channel.Id); + _ = Dispatcher.UIThread.Invoke(UpdateResourceTree); + break; } } catch (Exception ex) @@ -244,6 +258,14 @@ public record MessageCreateGatewayMessage : BaseGatewayMessage public required ObjectId ChannelId { get; init; } } +public record ChannelCreateGatewayMessage : BaseGatewayMessage +{ + [JsonProperty("channel")] + public required Channel Channel { get; init; } + [JsonProperty("quark")] + public required Quark Quark { get; init; } +} + public record GatewayBusEvent { public required BaseGatewayMessage BaseMessage { get; init; } diff --git a/Lightquark.NET/ClientMethods/Quark.cs b/Lightquark.NET/ClientMethods/Quark.cs index c834c384eacb10d68f84997aa720d127876570d6..5722be5a61943a99b9bbf7a719cf67c21d8ff15f 100644 --- a/Lightquark.NET/ClientMethods/Quark.cs +++ b/Lightquark.NET/ClientMethods/Quark.cs @@ -1,5 +1,7 @@ using System.Collections.ObjectModel; +using System.Diagnostics; using Avalonia.Media.Imaging; +using Avalonia.Threading; using Lightquark.NET.Assets; using Lightquark.NET.Objects; using Lightquark.NET.Objects.Reply; @@ -104,7 +106,7 @@ public partial class Client } await SaveQuarkList(); - await UpdateResourceTree(); + _ = Dispatcher.UIThread.Invoke(UpdateResourceTree); } private Func<QuarkListItem, bool> IdFinder(ObjectId id) @@ -149,8 +151,13 @@ public partial class Client }); } - private async Task UpdateResourceTree() + private int _g; + private readonly SemaphoreSlim _g2 = new(1); + public async Task UpdateResourceTree() { + Log.Information("[{G}] UpdateResourceTree called", _g++); + await _g2.WaitAsync(); + Log.Information("[{G}] UpdateResourceTree obtained semaphore", _g); var quarkList = QuarkList.ToList(); foreach (var quarkListItem in quarkList.ToList()) { @@ -174,6 +181,7 @@ public partial class Client var populatedQuark = await PopulateQuarkListQuark(quarkListItem); if (populatedQuark != null) { + populatedQuark.IsExpanded = quarkListItem.IsExpanded; quarkList[index] = populatedQuark; } } @@ -185,19 +193,22 @@ public partial class Client { ResourceTree.Add(quarkListItem); } + + _g2.Release(); + Log.Information("[{G}] UpdateResourceTree ends, {A} items", _g, ResourceTree.Count); } private async Task<QuarkListItem?> PopulateQuarkListQuark(QuarkListItem quarkListItem) { if (quarkListItem.Type != QuarkListItemType.Quark) throw new Exception("Quark list parse failure? Non-quark passed to PopulateQuarkListQuark"); - Log.Information("Quark {@Quark}", quarkListItem); + // Log.Information("Quark {@Quark}", quarkListItem); if (quarkListItem.QuarkId == null || quarkListItem.QuarkId == ObjectId.Empty) throw new Exception("Quark list parse failure? Empty or null quark id"); var quark = Quarks.FirstOrDefault(q => q.Id == quarkListItem.QuarkId); if (quark == null) { - Log.Information("Missing Quark in cache, getting it from API"); + // Log.Information("Missing Quark in cache, getting it from API"); var rpcQuark = await CallRpc<QuarkReplyResponse>("GET", $"/{Version}/quark/{quarkListItem.QuarkId}", null); if (!rpcQuark.Request.Success) return quarkListItem; AddOrUpdateQuark(rpcQuark.Response.Quark!); @@ -206,11 +217,12 @@ public partial class Client quarkListItem.Quark = quark; quarkListItem.Children = []; - Log.Information("Quark has {Count} channels", quark.Channels?.Length); + // Log.Information("Quark has {Count} channels", quark.Channels?.Length); foreach (var channel in quark.Channels ?? []) { - Log.Information("Channel {Ch}", channel); - AddOrUpdateChannel((Channel)channel); + // Log.Information("Channel {Ch}", channel); + channel.QuarkId = quark.Id; + AddOrUpdateChannel(channel); var cacheChannel = Channels.FirstOrDefault(c => c.Id == channel.Id); quarkListItem.Children.Add(new QuarkListItem { diff --git a/Lightquark.NET/Objects/Channel.cs b/Lightquark.NET/Objects/Channel.cs index 9385ec0653a198172c333489819e4c65e012ec3c..663a1cf1b1e91726f605cfc133d345a23c5d4093 100644 --- a/Lightquark.NET/Objects/Channel.cs +++ b/Lightquark.NET/Objects/Channel.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; namespace Lightquark.NET.Objects; -public class Channel : ObservableObject, IChannel +public class Channel : ObservableObject { private ObjectId _id; private string _name; @@ -14,6 +14,7 @@ public class Channel : ObservableObject, IChannel private int? _index; private ObjectId _quarkId; private Quark[]? _virtualQuarks; + private Quark? _quark; [JsonProperty("_id")] [JsonConverter(typeof(ObjectIdConverter))] @@ -51,18 +52,15 @@ public class Channel : ObservableObject, IChannel set => SetProperty(ref _quarkId, value); } - // Virtuals - [JsonIgnore] - public Quark[]? VirtualQuarks + [JsonProperty("quark")] + public Quark? Quark { - get => _virtualQuarks; + get => _quark; set { - if (SetProperty(ref _virtualQuarks, value)) OnPropertyChanged(nameof(Quark)); + SetProperty(ref _quark, value); + if (value != null) QuarkId = value.Id; } } - - [JsonProperty("quark")] - public IQuark? Quark => VirtualQuarks?.First(); } \ No newline at end of file diff --git a/Lightquark.NET/Objects/Quark.cs b/Lightquark.NET/Objects/Quark.cs index 451574cc0c2021f0e8a225612934c2fc3ffd7695..1e6b137fb51d8016526df0d524fb8a9ac2a8abe0 100644 --- a/Lightquark.NET/Objects/Quark.cs +++ b/Lightquark.NET/Objects/Quark.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace Lightquark.NET.Objects; -public class Quark : ObservableObject, IQuark +public class Quark : ObservableObject { [JsonIgnore] private Bitmap? _icon; @@ -15,6 +15,8 @@ public class Quark : ObservableObject, IQuark [JsonIgnore] public string? FetchedIcon = null; + private Channel[] _channels; + [JsonIgnore] public Bitmap? Icon { @@ -41,8 +43,12 @@ public class Quark : ObservableObject, IQuark public required ObjectId[] Owners { get; set; } [JsonProperty("channels")] - public IChannel[] Channels { get; set; } - + public Channel[] Channels + { + get => _channels; + set => _channels = value.DistinctBy(c => c.Id).ToArray(); + } + [JsonProperty("inviteEnabled")] public bool InviteEnabled { get; set; } = true; diff --git a/Lightquark.NET/Objects/QuarkListItem.cs b/Lightquark.NET/Objects/QuarkListItem.cs index 5608dc436bf265d7b970661d66b8ecbbde93529b..46c691b085e1a4ff0b73ca28c09c0fe101a58f0d 100644 --- a/Lightquark.NET/Objects/QuarkListItem.cs +++ b/Lightquark.NET/Objects/QuarkListItem.cs @@ -1,13 +1,21 @@ using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; using Lightquark.NET.Util.Converters; using Lightquark.Types.Mongo; using MongoDB.Bson; using Newtonsoft.Json; +using Serilog; namespace Lightquark.NET.Objects; -public class QuarkListItem +public class QuarkListItem : ObservableObject { + private bool _isExpanded; + private string? _folderName; + private ObservableCollection<QuarkListItem>? _children; + private Channel? _channel; + private Quark? _quark; + [JsonProperty("type")] [JsonConverter(typeof(LowerCaseStringEnumConverter))] public required QuarkListItemType Type { get; init; } @@ -15,18 +23,41 @@ public class QuarkListItem [JsonProperty("quarkId")] [JsonConverter(typeof(ObjectIdConverter))] public ObjectId? QuarkId { get; init; } - + [JsonIgnore] - public Quark? Quark { get; set; } - + public Quark? Quark + { + get => _quark; + set => SetProperty(ref _quark, value); + } + [JsonIgnore] - public Channel? Channel { get; set; } - + public Channel? Channel + { + get => _channel; + set => SetProperty(ref _channel, value); + } + [JsonProperty("children")] - public ObservableCollection<QuarkListItem>? Children { get; set; } + public ObservableCollection<QuarkListItem>? Children + { + get => _children; + set => SetProperty(ref _children, value); + } [JsonProperty("name")] - public string? FolderName { get; set; } + public string? FolderName + { + get => _folderName; + set => SetProperty(ref _folderName, value); + } + + [JsonIgnore] + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } } public enum QuarkListItemType diff --git a/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs b/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs deleted file mode 100644 index 67c89994a7931e0a18440accfa4ee84a7fc31e31..0000000000000000000000000000000000000000 --- a/Lightquark.NET/Util/Converters/ChannelCreationConverter.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Lightquark.NET.Objects; -using Lightquark.Types.Mongo; -using Newtonsoft.Json.Converters; - -namespace Lightquark.NET.Util.Converters; - -public class ChannelCreationConverter : CustomCreationConverter<IChannel> -{ - public override IChannel Create(Type objectType) - { - return new Channel(); - } -} \ No newline at end of file