diff --git a/.github/workflows/build-gallery-windows.yml b/.github/workflows/build-gallery-windows.yml new file mode 100644 index 000000000..821c2fd3a --- /dev/null +++ b/.github/workflows/build-gallery-windows.yml @@ -0,0 +1,23 @@ +name: Build Gallery (Native) + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Native + run: dotnet publish SukiUI.Demo/SukiUI.Demo.csproj -c Release -r win-x64 -o bin/ + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: gallery-native + path: bin diff --git a/SukiUI.Demo/App.axaml b/SukiUI.Demo/App.axaml index 11485037b..ceca5e713 100644 --- a/SukiUI.Demo/App.axaml +++ b/SukiUI.Demo/App.axaml @@ -6,9 +6,6 @@ xmlns:common="clr-namespace:SukiUI.Demo.Common" xmlns:suki="https://github.com/kikipoulet/SukiUI" RequestedThemeVariant="Default"> - - - diff --git a/SukiUI.Demo/App.axaml.cs b/SukiUI.Demo/App.axaml.cs index 810a6a6ce..d0f257f6c 100644 --- a/SukiUI.Demo/App.axaml.cs +++ b/SukiUI.Demo/App.axaml.cs @@ -5,11 +5,19 @@ using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; using SukiUI.Demo.Common; -using SukiUI.Demo.Features; +using SukiUI.Demo.Features.ControlsLibrary; +using SukiUI.Demo.Features.ControlsLibrary.Colors; +using SukiUI.Demo.Features.ControlsLibrary.Dialogs; +using SukiUI.Demo.Features.ControlsLibrary.StackPage; +using SukiUI.Demo.Features.ControlsLibrary.TabControl; +using SukiUI.Demo.Features.ControlsLibrary.Toasts; +using SukiUI.Demo.Features.CustomTheme; +using SukiUI.Demo.Features.Dashboard; +using SukiUI.Demo.Features.Effects; +using SukiUI.Demo.Features.Playground; +using SukiUI.Demo.Features.Splash; +using SukiUI.Demo.Features.Theming; using SukiUI.Demo.Services; -using System; -using System.Linq; -using SukiUI.Controls; using SukiUI.Dialogs; using SukiUI.Toasts; @@ -17,52 +25,76 @@ namespace SukiUI.Demo; public class App : Application { - private IServiceProvider? _provider; - public override void Initialize() { AvaloniaXamlLoader.Load(this); - _provider = ConfigureServices(); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var viewLocator = _provider?.GetRequiredService(); - var mainViewModel = _provider?.GetRequiredService(); - var mainView = _provider?.GetRequiredService(); - mainView.DataContext = mainViewModel; - desktop.MainWindow = mainView; + var services = new ServiceCollection(); + + services.AddSingleton(desktop); + + var views = ConfigureViews(services); + var provider = ConfigureServices(services); + + DataTemplates.Add(new ViewLocator(views)); + + desktop.MainWindow = views.CreateView(provider) as Window; } base.OnFrameworkInitializationCompleted(); } - private static ServiceProvider ConfigureServices() + private static SukiViews ConfigureViews(ServiceCollection services) { - var viewLocator = Current?.DataTemplates.First(x => x is ViewLocator); - var services = new ServiceCollection(); + return new SukiViews() - // Views - services.AddSingleton(); - - // Services - if (viewLocator is not null) - services.AddSingleton(viewLocator); - services.AddSingleton(); + // Add main view + .AddView(services) + + // Add pages + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services) + + // Add additional views + .AddView(services) + .AddView(services) + .AddView(services) + .AddView(services); + } + + private static ServiceProvider ConfigureServices(ServiceCollection services) + { services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - // ViewModels - services.AddSingleton(); - var types = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(s => s.GetTypes()) - .Where(p => !p.IsAbstract && typeof(DemoPageBase).IsAssignableFrom(p)); - foreach (var type in types) - services.AddSingleton(typeof(DemoPageBase), type); - return services.BuildServiceProvider(); } } \ No newline at end of file diff --git a/SukiUI.Demo/Common/SukiViews.cs b/SukiUI.Demo/Common/SukiViews.cs new file mode 100644 index 000000000..b56bbaf8d --- /dev/null +++ b/SukiUI.Demo/Common/SukiViews.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using SukiUI.Demo.Features; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace SukiUI.Demo.Common; + +public class SukiViews +{ + private readonly Dictionary _vmToViewMap = []; + + public SukiViews AddView< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TView, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TViewModel>(ServiceCollection services) + where TView : ContentControl + where TViewModel : ObservableObject + { + var viewType = typeof(TView); + var viewModelType = typeof(TViewModel); + + _vmToViewMap.Add(viewModelType, viewType); + + if (viewModelType.IsAssignableTo(typeof(DemoPageBase))) + { + services.AddSingleton(typeof(DemoPageBase), viewModelType); + } + else + { + services.AddSingleton(viewModelType); + } + + return this; + } + + public bool TryCreateView(IServiceProvider provider, Type viewModelType, [NotNullWhen(true)] out Control? view) + { + var viewModel = provider.GetRequiredService(viewModelType); + + return TryCreateView(viewModel, out view); + } + + public bool TryCreateView(object? viewModel, [NotNullWhen(true)] out Control? view) + { + view = null; + + if (viewModel == null) + { + return false; + } + + var viewModelType = viewModel.GetType(); + + if (_vmToViewMap.TryGetValue(viewModelType, out var viewType)) + { + view = Activator.CreateInstance(viewType) as Control; + + if (view != null) + { + view.DataContext = viewModel; + } + } + + return view != null; + } + + public Control CreateView(IServiceProvider provider) where TViewModel : ObservableObject + { + var viewModelType = typeof(TViewModel); + + if (TryCreateView(provider, viewModelType, out var view)) + { + return view; + } + + throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/SukiUI.Demo/Common/ViewLocator.cs b/SukiUI.Demo/Common/ViewLocator.cs index 4f12def8b..47adb52d7 100644 --- a/SukiUI.Demo/Common/ViewLocator.cs +++ b/SukiUI.Demo/Common/ViewLocator.cs @@ -1,39 +1,37 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; -using System; +using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.Generic; -using System.ComponentModel; namespace SukiUI.Demo.Common; -public class ViewLocator : IDataTemplate +public class ViewLocator(SukiViews views) : IDataTemplate { - private readonly Dictionary _controlCache = new(); + private readonly Dictionary _controlCache = []; - public Control Build(object? data) + public Control Build(object? param) { - if(data is null) - return new TextBlock { Text = "Data is null." }; - - var fullName = data.GetType().FullName; - - if (string.IsNullOrWhiteSpace(fullName)) - return new TextBlock { Text = "Type has no name, or name is empty." }; - - var name = fullName.Replace("ViewModel", "View"); - var type = Type.GetType(name); - if (type is null) - return new TextBlock { Text = $"No View For {name}." }; - - if (!_controlCache.TryGetValue(data, out var res)) + if (param is null) { - res ??= (Control)Activator.CreateInstance(type)!; - _controlCache[data] = res; + return CreateText("Data is null."); } - res.DataContext = data; - return res; + if (_controlCache.TryGetValue(param, out var control)) + { + return control; + } + + if (views.TryCreateView(param, out var view)) + { + _controlCache.Add(param, view); + + return view; + } + + return CreateText($"No View For {param.GetType().Name}."); } - public bool Match(object? data) => data is INotifyPropertyChanged; + public bool Match(object? data) => data is ObservableObject; + + private static TextBlock CreateText(string text) => new TextBlock { Text = text }; } \ No newline at end of file diff --git a/SukiUI.Demo/Converters/StringToControlConverter.cs b/SukiUI.Demo/Converters/StringToControlConverter.cs index 4fc93179a..256b5f523 100644 --- a/SukiUI.Demo/Converters/StringToControlConverter.cs +++ b/SukiUI.Demo/Converters/StringToControlConverter.cs @@ -17,7 +17,15 @@ public sealed class StringToControlConverter : IValueConverter if (string.IsNullOrWhiteSpace(xamlCode)) return null; var previewCode = XamlData.InsertIntoGridControl(xamlCode); - return AvaloniaRuntimeXamlLoader.Parse(previewCode); + + try + { + return AvaloniaRuntimeXamlLoader.Parse(previewCode); + } + catch + { + return null; + } } public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/SukiUI.Demo/Features/ControlsLibrary/IconViewItem.cs b/SukiUI.Demo/Features/ControlsLibrary/IconViewItem.cs new file mode 100644 index 000000000..e441815ca --- /dev/null +++ b/SukiUI.Demo/Features/ControlsLibrary/IconViewItem.cs @@ -0,0 +1,26 @@ +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using SukiUI.Demo.Services; +using SukiUI.Toasts; + +namespace SukiUI.Demo.Features.ControlsLibrary; + +public partial class IconItemViewModel(ClipboardService clipboard, ISukiToastManager toastManager) : ObservableObject +{ + public required string Name { get; init; } + + public required Geometry Geometry { get; init; } + + [RelayCommand] + public void OnClick() + { + clipboard.CopyToClipboard($""); + + toastManager + .CreateSimpleInfoToast() + .WithTitle("Copied To Clipboard") + .WithContent($"Copied the XAML for {Name} to your clipboard.") + .Queue(); + } +} \ No newline at end of file diff --git a/SukiUI.Demo/Features/ControlsLibrary/IconsView.axaml b/SukiUI.Demo/Features/ControlsLibrary/IconsView.axaml index 366ff4092..206b691e1 100644 --- a/SukiUI.Demo/Features/ControlsLibrary/IconsView.axaml +++ b/SukiUI.Demo/Features/ControlsLibrary/IconsView.axaml @@ -38,18 +38,17 @@ - + - + - + Command="{Binding ClickCommand}"> + + Data="{Binding Geometry}" /> diff --git a/SukiUI.Demo/Features/ControlsLibrary/IconsViewModel.cs b/SukiUI.Demo/Features/ControlsLibrary/IconsViewModel.cs index 3d4ff3d26..f30b8a2c6 100644 --- a/SukiUI.Demo/Features/ControlsLibrary/IconsViewModel.cs +++ b/SukiUI.Demo/Features/ControlsLibrary/IconsViewModel.cs @@ -1,40 +1,39 @@ using Avalonia.Media; using Material.Icons; using SukiUI.Content; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using CommunityToolkit.Mvvm.Input; using SukiUI.Demo.Services; using SukiUI.Toasts; +using System.Collections.Generic; +using System.Reflection; namespace SukiUI.Demo.Features.ControlsLibrary; public partial class IconsViewModel : DemoPageBase { - public Dictionary AllIcons { get; } + public IEnumerable Icons { get; } - private readonly ClipboardService _clipboardService; - private readonly ISukiToastManager _toastManager; - public IconsViewModel(ClipboardService clipboardService, ISukiToastManager toastManager) : base("Icons", MaterialIconKind.AlphaICircleOutline, int.MaxValue) { - _clipboardService = clipboardService; - _toastManager = toastManager; - AllIcons = typeof(Icons) - .GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(x => x.FieldType == typeof(StreamGeometry)) - .OrderBy(x => x.Name) - .ToDictionary(x => x.Name, y => (StreamGeometry)y.GetValue(null)!); - } + var fields = typeof(Icons).GetFields(BindingFlags.Public | BindingFlags.Static); - [RelayCommand] - private void OnIconClicked(string iconName) - { - _clipboardService.CopyToClipboard($""); - _toastManager.CreateSimpleInfoToast() - .WithTitle("Copied To Clipboard") - .WithContent($"Copied the XAML for {iconName} to your clipboard.") - .Queue(); + var icons = new List(fields.Length); + + foreach (var field in fields) + { + if (field.GetValue(null) is not Geometry geometry) + { + continue; + } + + var icon = new IconItemViewModel(clipboardService, toastManager) + { + Name = field.Name, + Geometry = geometry + }; + + icons.Add(icon); + } + + Icons = icons; } } \ No newline at end of file diff --git a/SukiUI.Demo/Features/Dashboard/DashboardView.axaml b/SukiUI.Demo/Features/Dashboard/DashboardView.axaml index 68aa6a269..7c36b1858 100644 --- a/SukiUI.Demo/Features/Dashboard/DashboardView.axaml +++ b/SukiUI.Demo/Features/Dashboard/DashboardView.axaml @@ -320,9 +320,15 @@ + ItemsSource="{Binding Invoices}" > + + + + + + + diff --git a/SukiUI.Demo/Features/Playground/PlaygroundView.axaml.cs b/SukiUI.Demo/Features/Playground/PlaygroundView.axaml.cs index 91f29dcee..873cb54a4 100644 --- a/SukiUI.Demo/Features/Playground/PlaygroundView.axaml.cs +++ b/SukiUI.Demo/Features/Playground/PlaygroundView.axaml.cs @@ -31,10 +31,6 @@ public partial class PlaygroundView : UserControl private TextEditor? _textEditor; private GlassCard? _glassPlayground; - - private Button? _renderButton; - - private Button? _clearButton; private PlaygroundViewModel PlaygroundDataContext => (PlaygroundViewModel)DataContext!; @@ -60,11 +56,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _textEditor.TextArea.IndentationStrategy = new CSharpIndentationStrategy(_textEditor.Options); _textEditor.TextArea.RightClickMovesCaret = true; - _renderButton = this.FindControl