mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb066b53f1 | ||
|
|
5ea242af9a |
@@ -10,11 +10,10 @@ public sealed class ComponentLibraryCollapseStateTests
|
|||||||
public void CreateExpanded_InitializesExpandedStateAndHidesChip()
|
public void CreateExpanded_InitializesExpandedStateAndHidesChip()
|
||||||
{
|
{
|
||||||
var margin = new Thickness(24, 24, 24, 100);
|
var margin = new Thickness(24, 24, 24, 100);
|
||||||
var state = ComponentLibraryCollapseState.CreateExpanded(margin, 0.75);
|
var state = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||||
|
|
||||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, state.VisualState);
|
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, state.VisualState);
|
||||||
Assert.Equal(margin, state.ExpandedMargin);
|
Assert.Equal(margin, state.ExpandedMargin);
|
||||||
Assert.Equal(0.75, state.ExpandedOpacity, 3);
|
|
||||||
Assert.False(state.IsChipVisible);
|
Assert.False(state.IsChipVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ public sealed class ComponentLibraryCollapseStateTests
|
|||||||
public void WithVisualState_PreservesStableExpandedSnapshotAcrossTransitions()
|
public void WithVisualState_PreservesStableExpandedSnapshotAcrossTransitions()
|
||||||
{
|
{
|
||||||
var margin = new Thickness(20, 18, 20, 96);
|
var margin = new Thickness(20, 18, 20, 96);
|
||||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 1);
|
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||||
|
|
||||||
var collapsing = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
|
var collapsing = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
|
||||||
var collapsed = collapsing.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
|
var collapsed = collapsing.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
|
||||||
@@ -36,24 +35,19 @@ public sealed class ComponentLibraryCollapseStateTests
|
|||||||
Assert.Equal(margin, collapsed.ExpandedMargin);
|
Assert.Equal(margin, collapsed.ExpandedMargin);
|
||||||
Assert.Equal(margin, restoring.ExpandedMargin);
|
Assert.Equal(margin, restoring.ExpandedMargin);
|
||||||
|
|
||||||
Assert.Equal(1, collapsing.ExpandedOpacity, 3);
|
|
||||||
Assert.Equal(1, collapsed.ExpandedOpacity, 3);
|
|
||||||
Assert.Equal(1, restoring.ExpandedOpacity, 3);
|
|
||||||
|
|
||||||
Assert.True(collapsing.IsChipVisible);
|
Assert.True(collapsing.IsChipVisible);
|
||||||
Assert.True(collapsed.IsChipVisible);
|
Assert.True(collapsed.IsChipVisible);
|
||||||
Assert.False(restoring.IsChipVisible);
|
Assert.False(restoring.IsChipVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateExpanded_ProducesRestorableSnapshotEvenWhenOriginalOpacityIsLow()
|
public void CreateExpanded_DoesNotCaptureTransientOpacityAsRestorableState()
|
||||||
{
|
{
|
||||||
var margin = new Thickness(18, 22, 18, 88);
|
var margin = new Thickness(18, 22, 18, 88);
|
||||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 0.15);
|
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||||
var restored = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
|
var restored = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
|
||||||
|
|
||||||
Assert.Equal(margin, restored.ExpandedMargin);
|
Assert.Equal(margin, restored.ExpandedMargin);
|
||||||
Assert.Equal(0.15, restored.ExpandedOpacity, 3);
|
|
||||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, restored.VisualState);
|
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, restored.VisualState);
|
||||||
Assert.False(restored.IsChipVisible);
|
Assert.False(restored.IsChipVisible);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using LanMountainDesktop.Services;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class ComponentPreviewImageServiceTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task QueueGenerationAsync_ExecutesWorkSeriallyAcrossKeys()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var executionOrder = new List<string>();
|
|
||||||
var activeCount = 0;
|
|
||||||
var maxActiveCount = 0;
|
|
||||||
|
|
||||||
Task<ComponentPreviewImageEntry> Queue(string componentTypeId)
|
|
||||||
{
|
|
||||||
var key = ComponentPreviewKey.ForComponentType(componentTypeId, widthCells: 2, heightCells: 2);
|
|
||||||
return service.QueueGenerationAsync(
|
|
||||||
key,
|
|
||||||
visualSignature: $"sig:{componentTypeId}",
|
|
||||||
async _ =>
|
|
||||||
{
|
|
||||||
var activeNow = Interlocked.Increment(ref activeCount);
|
|
||||||
maxActiveCount = Math.Max(maxActiveCount, activeNow);
|
|
||||||
lock (executionOrder)
|
|
||||||
{
|
|
||||||
executionOrder.Add(componentTypeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(40);
|
|
||||||
Interlocked.Decrement(ref activeCount);
|
|
||||||
return CreateImage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var first = Queue("Clock");
|
|
||||||
var second = Queue("Weather");
|
|
||||||
var third = Queue("Calendar");
|
|
||||||
|
|
||||||
await Task.WhenAll(first, second, third);
|
|
||||||
|
|
||||||
Assert.Equal(1, maxActiveCount);
|
|
||||||
Assert.Equal(["Clock", "Weather", "Calendar"], executionOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task QueueGenerationAsync_DeduplicatesConcurrentRequestsForSameKey()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var generationCount = 0;
|
|
||||||
var bitmap = CreateImage();
|
|
||||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
|
|
||||||
Task<IImage?> Generation(CancellationToken _)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref generationCount);
|
|
||||||
return completion.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
var first = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
|
||||||
var second = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
|
||||||
|
|
||||||
Assert.Same(first, second);
|
|
||||||
|
|
||||||
completion.SetResult(bitmap);
|
|
||||||
var entry = await first;
|
|
||||||
|
|
||||||
Assert.Equal(1, generationCount);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Ready, entry.State);
|
|
||||||
Assert.Same(bitmap, entry.Bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Invalidate_ResetsSingleKeyToPending()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var image = CreateDisposableImage();
|
|
||||||
var stored = service.Store(key, image, "clock-sig");
|
|
||||||
var previousRevision = stored.Revision;
|
|
||||||
|
|
||||||
var result = service.Invalidate(key);
|
|
||||||
|
|
||||||
Assert.True(result);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Pending, stored.State);
|
|
||||||
Assert.Null(stored.Bitmap);
|
|
||||||
Assert.True(image.IsDisposed);
|
|
||||||
Assert.True(stored.Revision > previousRevision);
|
|
||||||
Assert.Equal("clock-sig", stored.VisualSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RemovePlacementPreviews_RemovesOnlyMatchingPlacementEntries()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
|
|
||||||
var removedClock = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2);
|
|
||||||
var removedWeather = ComponentPreviewKey.ForPlacementInstance("Weather", "desk-1", widthCells: 4, heightCells: 2);
|
|
||||||
var keptPlacement = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-2", widthCells: 2, heightCells: 2);
|
|
||||||
var keptType = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var removedClockImage = CreateDisposableImage();
|
|
||||||
var removedWeatherImage = CreateDisposableImage();
|
|
||||||
var keptPlacementImage = CreateDisposableImage();
|
|
||||||
var keptTypeImage = CreateDisposableImage();
|
|
||||||
|
|
||||||
service.Store(removedClock, removedClockImage, "sig-a");
|
|
||||||
service.Store(removedWeather, removedWeatherImage, "sig-b");
|
|
||||||
service.Store(keptPlacement, keptPlacementImage, "sig-c");
|
|
||||||
service.Store(keptType, keptTypeImage, "sig-d");
|
|
||||||
|
|
||||||
var removedCount = service.RemovePlacementPreviews("desk-1");
|
|
||||||
|
|
||||||
Assert.Equal(2, removedCount);
|
|
||||||
Assert.False(service.TryGetEntry(removedClock, out _));
|
|
||||||
Assert.False(service.TryGetEntry(removedWeather, out _));
|
|
||||||
Assert.True(service.TryGetEntry(keptPlacement, out _));
|
|
||||||
Assert.True(service.TryGetEntry(keptType, out _));
|
|
||||||
Assert.True(removedClockImage.IsDisposed);
|
|
||||||
Assert.True(removedWeatherImage.IsDisposed);
|
|
||||||
Assert.False(keptPlacementImage.IsDisposed);
|
|
||||||
Assert.False(keptTypeImage.IsDisposed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidateVisualSignature_InvalidatesEveryMatchingEntry()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
const string matchingSignature = "shared-sig";
|
|
||||||
const string otherSignature = "other-sig";
|
|
||||||
|
|
||||||
var first = service.Store(
|
|
||||||
ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2),
|
|
||||||
CreateImage(),
|
|
||||||
matchingSignature);
|
|
||||||
var second = service.Store(
|
|
||||||
ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2),
|
|
||||||
CreateImage(),
|
|
||||||
matchingSignature);
|
|
||||||
var third = service.Store(
|
|
||||||
ComponentPreviewKey.ForComponentType("Weather", widthCells: 2, heightCells: 1),
|
|
||||||
CreateImage(),
|
|
||||||
otherSignature);
|
|
||||||
|
|
||||||
var invalidatedCount = service.InvalidateVisualSignature(matchingSignature);
|
|
||||||
|
|
||||||
Assert.Equal(2, invalidatedCount);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Pending, first.State);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Pending, second.State);
|
|
||||||
Assert.Null(first.Bitmap);
|
|
||||||
Assert.Null(second.Bitmap);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Ready, third.State);
|
|
||||||
Assert.NotNull(third.Bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Store_ReplacingBitmap_DisposesPreviousBitmap_WhenInstanceChanges()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var first = CreateDisposableImage();
|
|
||||||
var second = CreateDisposableImage();
|
|
||||||
|
|
||||||
service.Store(key, first, "sig-a");
|
|
||||||
service.Store(key, second, "sig-b");
|
|
||||||
|
|
||||||
Assert.True(first.IsDisposed);
|
|
||||||
Assert.False(second.IsDisposed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Store_ReplacingBitmap_DoesNotDispose_WhenSameInstanceReused()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var image = CreateDisposableImage();
|
|
||||||
|
|
||||||
service.Store(key, image, "sig-a");
|
|
||||||
service.Store(key, image, "sig-b");
|
|
||||||
|
|
||||||
Assert.False(image.IsDisposed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void StoreFailure_DisposesExistingBitmap()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var image = CreateDisposableImage();
|
|
||||||
|
|
||||||
service.Store(key, image, "sig-a");
|
|
||||||
var entry = service.StoreFailure(key, "sig-a", "failed");
|
|
||||||
|
|
||||||
Assert.True(image.IsDisposed);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Failed, entry.State);
|
|
||||||
Assert.Null(entry.Bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task QueueGenerationAsync_DisposesStaleGeneratedBitmap_WhenEntryWasInvalidated()
|
|
||||||
{
|
|
||||||
var service = new ComponentPreviewImageService();
|
|
||||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
|
||||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
var stale = CreateDisposableImage();
|
|
||||||
|
|
||||||
var generationTask = service.QueueGenerationAsync(key, "sig-a", _ => completion.Task);
|
|
||||||
_ = service.Invalidate(key);
|
|
||||||
completion.SetResult(stale);
|
|
||||||
var entry = await generationTask;
|
|
||||||
|
|
||||||
Assert.True(stale.IsDisposed);
|
|
||||||
Assert.Equal(ComponentPreviewImageState.Pending, entry.State);
|
|
||||||
Assert.Null(entry.Bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IImage CreateImage() => new TestImage();
|
|
||||||
private static DisposableTestImage CreateDisposableImage() => new();
|
|
||||||
|
|
||||||
private sealed class TestImage : IImage
|
|
||||||
{
|
|
||||||
public Size Size => new(1, 1);
|
|
||||||
|
|
||||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
|
||||||
{
|
|
||||||
_ = context;
|
|
||||||
_ = sourceRect;
|
|
||||||
_ = destRect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class DisposableTestImage : IImage, IDisposable
|
|
||||||
{
|
|
||||||
public Size Size => new(1, 1);
|
|
||||||
|
|
||||||
public bool IsDisposed { get; private set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
IsDisposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
|
||||||
{
|
|
||||||
_ = context;
|
|
||||||
_ = sourceRect;
|
|
||||||
_ = destRect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs
Normal file
135
LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using LanMountainDesktop.Views.Components;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Tests;
|
||||||
|
|
||||||
|
public sealed class DesktopComponentRenderModeTests
|
||||||
|
{
|
||||||
|
private const string ComponentId = "RenderModeProbe";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DescriptorCreateControl_DefaultsToLiveRenderMode()
|
||||||
|
{
|
||||||
|
var descriptor = CreateDescriptor();
|
||||||
|
var control = (ProbeControl)descriptor.CreateControl(
|
||||||
|
cellSize: 64,
|
||||||
|
CreateTimeZoneService(),
|
||||||
|
CreateWeatherInfoService(),
|
||||||
|
new RecommendationDataService(),
|
||||||
|
new CalculatorDataService(),
|
||||||
|
CreateSettingsFacade(),
|
||||||
|
placementId: "desktop-placement");
|
||||||
|
|
||||||
|
Assert.Equal(DesktopComponentRenderMode.Live, control.RuntimeContext?.RenderMode);
|
||||||
|
Assert.Equal("desktop-placement", control.RuntimeContext?.PlacementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DescriptorCreateControl_CanCreateLibraryPreviewRenderModeWithoutPlacement()
|
||||||
|
{
|
||||||
|
var descriptor = CreateDescriptor();
|
||||||
|
var control = (ProbeControl)descriptor.CreateControl(
|
||||||
|
cellSize: 64,
|
||||||
|
CreateTimeZoneService(),
|
||||||
|
CreateWeatherInfoService(),
|
||||||
|
new RecommendationDataService(),
|
||||||
|
new CalculatorDataService(),
|
||||||
|
CreateSettingsFacade(),
|
||||||
|
placementId: null,
|
||||||
|
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||||
|
|
||||||
|
Assert.Equal(DesktopComponentRenderMode.LibraryPreview, control.RuntimeContext?.RenderMode);
|
||||||
|
Assert.Null(control.RuntimeContext?.PlacementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComponentLibraryService_CreatesLibraryPreviewRenderMode()
|
||||||
|
{
|
||||||
|
var service = new ComponentLibraryService(
|
||||||
|
CreateComponentRegistry(),
|
||||||
|
CreateRuntimeRegistry());
|
||||||
|
|
||||||
|
var created = service.TryCreateControl(
|
||||||
|
ComponentId,
|
||||||
|
new ComponentLibraryCreateContext(
|
||||||
|
64,
|
||||||
|
CreateTimeZoneService(),
|
||||||
|
CreateWeatherInfoService(),
|
||||||
|
new RecommendationDataService(),
|
||||||
|
new CalculatorDataService(),
|
||||||
|
CreateSettingsFacade(),
|
||||||
|
PlacementId: null,
|
||||||
|
RenderMode: DesktopComponentRenderMode.LibraryPreview),
|
||||||
|
out var control,
|
||||||
|
out var exception);
|
||||||
|
|
||||||
|
Assert.True(created, exception?.ToString());
|
||||||
|
var probe = Assert.IsType<ProbeControl>(control);
|
||||||
|
Assert.Equal(DesktopComponentRenderMode.LibraryPreview, probe.RuntimeContext?.RenderMode);
|
||||||
|
Assert.Null(probe.RuntimeContext?.PlacementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DesktopComponentRuntimeDescriptor CreateDescriptor()
|
||||||
|
{
|
||||||
|
Assert.True(CreateRuntimeRegistry().TryGetDescriptor(ComponentId, out var descriptor));
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DesktopComponentRuntimeRegistry CreateRuntimeRegistry()
|
||||||
|
{
|
||||||
|
return new DesktopComponentRuntimeRegistry(
|
||||||
|
CreateComponentRegistry(),
|
||||||
|
[
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
ComponentId,
|
||||||
|
displayNameLocalizationKey: null,
|
||||||
|
_ => new ProbeControl(),
|
||||||
|
cornerRadiusResolver: (System.Func<double, double>?)null)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ComponentRegistry CreateComponentRegistry()
|
||||||
|
{
|
||||||
|
return new ComponentRegistry(
|
||||||
|
[
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
ComponentId,
|
||||||
|
"Render Mode Probe",
|
||||||
|
"Apps",
|
||||||
|
"Test",
|
||||||
|
MinWidthCells: 1,
|
||||||
|
MinHeightCells: 1,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ISettingsFacadeService CreateSettingsFacade()
|
||||||
|
{
|
||||||
|
return HostSettingsFacadeProvider.GetOrCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeZoneService CreateTimeZoneService()
|
||||||
|
{
|
||||||
|
return CreateSettingsFacade().Region.GetTimeZoneService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IWeatherInfoService CreateWeatherInfoService()
|
||||||
|
{
|
||||||
|
return CreateSettingsFacade().Weather.GetWeatherInfoService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ProbeControl : Control, IComponentRuntimeContextAware
|
||||||
|
{
|
||||||
|
public DesktopComponentRuntimeContext? RuntimeContext { get; private set; }
|
||||||
|
|
||||||
|
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||||
|
{
|
||||||
|
RuntimeContext = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.ComponentSystem;
|
||||||
|
|
||||||
|
internal static class ComponentPreviewRuntimeQuiescer
|
||||||
|
{
|
||||||
|
private static readonly BindingFlags TimerMemberFlags =
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
|
||||||
|
public static void Attach(Control control)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(control);
|
||||||
|
|
||||||
|
control.IsHitTestVisible = false;
|
||||||
|
control.Focusable = false;
|
||||||
|
control.AttachedToVisualTree += (_, _) =>
|
||||||
|
Dispatcher.UIThread.Post(() => Quiesce(control), DispatcherPriority.Background);
|
||||||
|
control.DetachedFromVisualTree += (_, _) => Quiesce(control);
|
||||||
|
Quiesce(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Detach(Control control)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(control);
|
||||||
|
|
||||||
|
Quiesce(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Quiesce(Control control)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(control);
|
||||||
|
|
||||||
|
foreach (var candidate in EnumerateControls(control))
|
||||||
|
{
|
||||||
|
StopDispatcherTimers(candidate);
|
||||||
|
candidate.IsHitTestVisible = false;
|
||||||
|
candidate.Focusable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Control> EnumerateControls(Control root)
|
||||||
|
{
|
||||||
|
yield return root;
|
||||||
|
|
||||||
|
foreach (var descendant in root.GetVisualDescendants().OfType<Control>())
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void StopDispatcherTimers(object target)
|
||||||
|
{
|
||||||
|
var type = target.GetType();
|
||||||
|
foreach (var field in type.GetFields(TimerMemberFlags))
|
||||||
|
{
|
||||||
|
if (typeof(DispatcherTimer).IsAssignableFrom(field.FieldType) &&
|
||||||
|
field.GetValue(target) is DispatcherTimer timer)
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in type.GetProperties(TimerMemberFlags))
|
||||||
|
{
|
||||||
|
if (!property.CanRead ||
|
||||||
|
property.GetIndexParameters().Length != 0 ||
|
||||||
|
!typeof(DispatcherTimer).IsAssignableFrom(property.PropertyType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (property.GetValue(target) is DispatcherTimer timer)
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TargetInvocationException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LanMountainDesktop.ComponentSystem;
|
||||||
|
|
||||||
|
public enum DesktopComponentRenderMode
|
||||||
|
{
|
||||||
|
Live = 0,
|
||||||
|
LibraryPreview = 1
|
||||||
|
}
|
||||||
@@ -13,4 +13,5 @@ public sealed record DesktopComponentRuntimeContext(
|
|||||||
IAppearanceThemeService AppearanceTheme,
|
IAppearanceThemeService AppearanceTheme,
|
||||||
ComponentChromeContext Chrome,
|
ComponentChromeContext Chrome,
|
||||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||||
|
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
{
|
{
|
||||||
private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(150);
|
private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(150);
|
||||||
private static readonly Easing TransitionEasing = new CubicEaseOut();
|
private static readonly Easing TransitionEasing = new CubicEaseOut();
|
||||||
private const double StableOpacityThreshold = 0.01;
|
|
||||||
|
|
||||||
private readonly Border _componentLibraryWindow;
|
private readonly Border _componentLibraryWindow;
|
||||||
private readonly Border _collapsedChipHost;
|
private readonly Border _collapsedChipHost;
|
||||||
@@ -37,9 +36,7 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
_collapsedChipIcon = collapsedChipIcon;
|
_collapsedChipIcon = collapsedChipIcon;
|
||||||
|
|
||||||
EnsureTransforms();
|
EnsureTransforms();
|
||||||
_state = ComponentLibraryCollapseState.CreateExpanded(
|
_state = ComponentLibraryCollapseState.CreateExpanded(_componentLibraryWindow.Margin);
|
||||||
_componentLibraryWindow.Margin,
|
|
||||||
_componentLibraryWindow.Opacity <= 0 ? 1 : _componentLibraryWindow.Opacity);
|
|
||||||
ApplyExpandedSnapshot();
|
ApplyExpandedSnapshot();
|
||||||
_collapsedChipHost.IsVisible = false;
|
_collapsedChipHost.IsVisible = false;
|
||||||
_collapsedChipHost.IsHitTestVisible = false;
|
_collapsedChipHost.IsHitTestVisible = false;
|
||||||
@@ -50,19 +47,16 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
|
|
||||||
public ComponentLibraryCollapseVisualState VisualState => _state.VisualState;
|
public ComponentLibraryCollapseVisualState VisualState => _state.VisualState;
|
||||||
|
|
||||||
public void SyncExpandedState(Thickness margin, double opacity)
|
public void SyncExpandedState(Thickness margin)
|
||||||
{
|
{
|
||||||
var hasStableOpacity = IsStableExpandedOpacity(opacity);
|
|
||||||
var nextExpandedOpacity = hasStableOpacity ? Math.Clamp(opacity, 0, 1) : _state.ExpandedOpacity;
|
|
||||||
_state = _state with
|
_state = _state with
|
||||||
{
|
{
|
||||||
ExpandedMargin = margin,
|
ExpandedMargin = margin
|
||||||
ExpandedOpacity = nextExpandedOpacity
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded or ComponentLibraryCollapseVisualState.Restoring)
|
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded or ComponentLibraryCollapseVisualState.Restoring)
|
||||||
{
|
{
|
||||||
ApplyExpandedSnapshot(applyOpacity: hasStableOpacity);
|
ApplyExpandedSnapshot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +116,7 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
_componentLibraryWindow.Opacity = 1;
|
||||||
_windowTranslate.Y = 0;
|
_windowTranslate.Y = 0;
|
||||||
},
|
},
|
||||||
DispatcherPriority.Background);
|
DispatcherPriority.Background);
|
||||||
@@ -190,14 +184,10 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyExpandedSnapshot(bool applyOpacity = true)
|
private void ApplyExpandedSnapshot()
|
||||||
{
|
{
|
||||||
_componentLibraryWindow.Margin = _state.ExpandedMargin;
|
_componentLibraryWindow.Margin = _state.ExpandedMargin;
|
||||||
if (applyOpacity)
|
_componentLibraryWindow.Opacity = 1;
|
||||||
{
|
|
||||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
_componentLibraryWindow.IsVisible = true;
|
_componentLibraryWindow.IsVisible = true;
|
||||||
_componentLibraryWindow.IsHitTestVisible = true;
|
_componentLibraryWindow.IsHitTestVisible = true;
|
||||||
_windowTranslate.Y = 0;
|
_windowTranslate.Y = 0;
|
||||||
@@ -270,11 +260,4 @@ internal sealed class ComponentLibraryCollapsePresenter
|
|||||||
_componentLibraryWindow.Opacity = 0;
|
_componentLibraryWindow.Opacity = 0;
|
||||||
_windowTranslate.Y = 28;
|
_windowTranslate.Y = 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsStableExpandedOpacity(double opacity)
|
|
||||||
{
|
|
||||||
return !double.IsNaN(opacity) &&
|
|
||||||
!double.IsInfinity(opacity) &&
|
|
||||||
opacity > StableOpacityThreshold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,13 @@ internal enum ComponentLibraryCollapseVisualState
|
|||||||
internal readonly record struct ComponentLibraryCollapseState(
|
internal readonly record struct ComponentLibraryCollapseState(
|
||||||
ComponentLibraryCollapseVisualState VisualState,
|
ComponentLibraryCollapseVisualState VisualState,
|
||||||
Thickness ExpandedMargin,
|
Thickness ExpandedMargin,
|
||||||
double ExpandedOpacity,
|
|
||||||
bool IsChipVisible)
|
bool IsChipVisible)
|
||||||
{
|
{
|
||||||
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin, double expandedOpacity)
|
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin)
|
||||||
{
|
{
|
||||||
return new(
|
return new(
|
||||||
ComponentLibraryCollapseVisualState.Expanded,
|
ComponentLibraryCollapseVisualState.Expanded,
|
||||||
expandedMargin,
|
expandedMargin,
|
||||||
expandedOpacity,
|
|
||||||
IsChipVisible: false);
|
IsChipVisible: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
|
|||||||
context.RecommendationInfoService,
|
context.RecommendationInfoService,
|
||||||
context.CalculatorDataService,
|
context.CalculatorDataService,
|
||||||
context.SettingsFacade,
|
context.SettingsFacade,
|
||||||
context.PlacementId);
|
context.PlacementId,
|
||||||
|
context.RenderMode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Media;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
|
||||||
|
|
||||||
public sealed class ComponentPreviewImageService : IComponentPreviewImageService
|
|
||||||
{
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private readonly Dictionary<ComponentPreviewKey, ComponentPreviewImageEntry> _entries = new(ComponentPreviewKeyComparer.Instance);
|
|
||||||
private readonly Dictionary<ComponentPreviewKey, Task<ComponentPreviewImageEntry>> _inFlightRequests = new(ComponentPreviewKeyComparer.Instance);
|
|
||||||
private Task _queueTail = Task.CompletedTask;
|
|
||||||
|
|
||||||
public ComponentPreviewImageEntry GetOrCreateEntry(ComponentPreviewKey key, string? visualSignature = null)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_entries.TryGetValue(key, out var existing))
|
|
||||||
{
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
var created = new ComponentPreviewImageEntry(key, visualSignature);
|
|
||||||
_entries[key] = created;
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_entries.TryGetValue(key, out var existing))
|
|
||||||
{
|
|
||||||
entry = existing;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyCollection<ComponentPreviewImageEntry> GetEntriesSnapshot()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
return _entries.Values.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ComponentPreviewImageEntry> QueueGenerationAsync(
|
|
||||||
ComponentPreviewKey key,
|
|
||||||
string visualSignature,
|
|
||||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(generationWork);
|
|
||||||
|
|
||||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
var entry = GetOrCreateEntryCore(key);
|
|
||||||
|
|
||||||
if (entry.State == ComponentPreviewImageState.Ready &&
|
|
||||||
entry.Bitmap is not null &&
|
|
||||||
StringComparer.Ordinal.Equals(entry.VisualSignature, normalizedSignature))
|
|
||||||
{
|
|
||||||
return Task.FromResult(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_inFlightRequests.TryGetValue(key, out var inFlight))
|
|
||||||
{
|
|
||||||
return inFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedRevision = entry.BeginGeneration(normalizedSignature);
|
|
||||||
var previousTask = _queueTail;
|
|
||||||
var queuedTask = RunGenerationAsync(
|
|
||||||
previousTask,
|
|
||||||
key,
|
|
||||||
entry,
|
|
||||||
expectedRevision,
|
|
||||||
normalizedSignature,
|
|
||||||
generationWork,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
_inFlightRequests[key] = queuedTask;
|
|
||||||
_queueTail = queuedTask.ContinueWith(
|
|
||||||
static _ => { },
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskContinuationOptions.ExecuteSynchronously,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
return queuedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentPreviewImageEntry Store(ComponentPreviewKey key, IImage bitmap, string visualSignature)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(bitmap);
|
|
||||||
|
|
||||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
var entry = GetOrCreateEntryCore(key);
|
|
||||||
entry.StoreBitmap(bitmap, normalizedSignature);
|
|
||||||
_inFlightRequests.Remove(key);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentPreviewImageEntry StoreFailure(ComponentPreviewKey key, string visualSignature, string? errorMessage = null)
|
|
||||||
{
|
|
||||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
var entry = GetOrCreateEntryCore(key);
|
|
||||||
entry.StoreFailure(normalizedSignature, errorMessage);
|
|
||||||
_inFlightRequests.Remove(key);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invalidate(ComponentPreviewKey key, string? visualSignature = null)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (!_entries.TryGetValue(key, out var entry))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Invalidate(visualSignature);
|
|
||||||
_inFlightRequests.Remove(key);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RemovePlacementPreviews(string placementId)
|
|
||||||
{
|
|
||||||
var normalizedPlacementId = NormalizeRequired(placementId, nameof(placementId));
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
var entriesToRemove = _entries
|
|
||||||
.Where(static pair => pair.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
|
||||||
.Where(pair => StringComparer.OrdinalIgnoreCase.Equals(pair.Key.PlacementId, normalizedPlacementId))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var pair in entriesToRemove)
|
|
||||||
{
|
|
||||||
pair.Value.DisposeBitmap();
|
|
||||||
_entries.Remove(pair.Key);
|
|
||||||
_inFlightRequests.Remove(pair.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entriesToRemove.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int InvalidateVisualSignature(string visualSignature)
|
|
||||||
{
|
|
||||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
var entriesToInvalidate = _entries.Values
|
|
||||||
.Where(entry => StringComparer.Ordinal.Equals(entry.VisualSignature, normalizedSignature))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var entry in entriesToInvalidate)
|
|
||||||
{
|
|
||||||
entry.Invalidate(normalizedSignature);
|
|
||||||
_inFlightRequests.Remove(entry.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entriesToInvalidate.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ComponentPreviewImageEntry> RunGenerationAsync(
|
|
||||||
Task previousTask,
|
|
||||||
ComponentPreviewKey key,
|
|
||||||
ComponentPreviewImageEntry entry,
|
|
||||||
long expectedRevision,
|
|
||||||
string visualSignature,
|
|
||||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await previousTask.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Keep serial queue processing even if previous work faulted.
|
|
||||||
}
|
|
||||||
|
|
||||||
IImage? bitmap;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bitmap = await generationWork(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
entry.TryApplyFailure(expectedRevision, visualSignature, ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (bitmap is null)
|
|
||||||
{
|
|
||||||
entry.TryApplyFailure(expectedRevision, visualSignature, "Preview generation returned no bitmap.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
entry.TryApplyGeneratedBitmap(expectedRevision, bitmap, visualSignature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_inFlightRequests.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewImageEntry GetOrCreateEntryCore(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
if (_entries.TryGetValue(key, out var existing))
|
|
||||||
{
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
var created = new ComponentPreviewImageEntry(key);
|
|
||||||
_entries[key] = created;
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeRequired(string? value, string paramName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Value cannot be null or whitespace.", paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
|
||||||
|
|
||||||
public enum ComponentPreviewKeyKind
|
|
||||||
{
|
|
||||||
ComponentType = 0,
|
|
||||||
PlacementInstance = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly record struct ComponentPreviewKey
|
|
||||||
{
|
|
||||||
private ComponentPreviewKey(
|
|
||||||
ComponentPreviewKeyKind kind,
|
|
||||||
string componentTypeId,
|
|
||||||
string? placementId,
|
|
||||||
int widthCells,
|
|
||||||
int heightCells)
|
|
||||||
{
|
|
||||||
Kind = kind;
|
|
||||||
ComponentTypeId = NormalizeRequired(componentTypeId, nameof(componentTypeId));
|
|
||||||
PlacementId = kind == ComponentPreviewKeyKind.PlacementInstance
|
|
||||||
? NormalizeRequired(placementId, nameof(placementId))
|
|
||||||
: null;
|
|
||||||
WidthCells = NormalizeSpan(widthCells, nameof(widthCells));
|
|
||||||
HeightCells = NormalizeSpan(heightCells, nameof(heightCells));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentPreviewKeyKind Kind { get; }
|
|
||||||
|
|
||||||
public string ComponentTypeId { get; }
|
|
||||||
|
|
||||||
public string? PlacementId { get; }
|
|
||||||
|
|
||||||
public int WidthCells { get; }
|
|
||||||
|
|
||||||
public int HeightCells { get; }
|
|
||||||
|
|
||||||
public static ComponentPreviewKey ForComponentType(string componentTypeId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
return new ComponentPreviewKey(ComponentPreviewKeyKind.ComponentType, componentTypeId, null, widthCells, heightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ComponentPreviewKey ForPlacementInstance(string componentTypeId, string placementId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
return new ComponentPreviewKey(
|
|
||||||
ComponentPreviewKeyKind.PlacementInstance,
|
|
||||||
componentTypeId,
|
|
||||||
placementId,
|
|
||||||
widthCells,
|
|
||||||
heightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return Kind == ComponentPreviewKeyKind.ComponentType
|
|
||||||
? $"Type:{ComponentTypeId}[{WidthCells}x{HeightCells}]"
|
|
||||||
: $"Placement:{ComponentTypeId}@{PlacementId}[{WidthCells}x{HeightCells}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeRequired(string? value, string paramName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Value cannot be null or whitespace.", paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int NormalizeSpan(int value, string paramName)
|
|
||||||
{
|
|
||||||
if (value <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(paramName, value, "Span must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ComponentPreviewImageState
|
|
||||||
{
|
|
||||||
Pending = 0,
|
|
||||||
Ready = 1,
|
|
||||||
Failed = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ComponentPreviewImageEntry : ObservableObject
|
|
||||||
{
|
|
||||||
private IImage? _bitmap;
|
|
||||||
private ComponentPreviewImageState _state = ComponentPreviewImageState.Pending;
|
|
||||||
private string _visualSignature = string.Empty;
|
|
||||||
private string? _errorMessage;
|
|
||||||
private long _revision;
|
|
||||||
private DateTimeOffset _lastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
public ComponentPreviewImageEntry(ComponentPreviewKey key, string? visualSignature = null)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentPreviewKey Key { get; }
|
|
||||||
|
|
||||||
public IImage? Bitmap
|
|
||||||
{
|
|
||||||
get => _bitmap;
|
|
||||||
private set => SetProperty(ref _bitmap, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentPreviewImageState State
|
|
||||||
{
|
|
||||||
get => _state;
|
|
||||||
private set => SetProperty(ref _state, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string VisualSignature
|
|
||||||
{
|
|
||||||
get => _visualSignature;
|
|
||||||
private set => SetProperty(ref _visualSignature, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ErrorMessage
|
|
||||||
{
|
|
||||||
get => _errorMessage;
|
|
||||||
private set => SetProperty(ref _errorMessage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long Revision
|
|
||||||
{
|
|
||||||
get => _revision;
|
|
||||||
private set => SetProperty(ref _revision, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTimeOffset LastUpdatedUtc
|
|
||||||
{
|
|
||||||
get => _lastUpdatedUtc;
|
|
||||||
private set => SetProperty(ref _lastUpdatedUtc, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal long BeginGeneration(string visualSignature)
|
|
||||||
{
|
|
||||||
var normalizedVisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
var nextRevision = Revision + 1;
|
|
||||||
Revision = nextRevision;
|
|
||||||
VisualSignature = normalizedVisualSignature;
|
|
||||||
State = ComponentPreviewImageState.Pending;
|
|
||||||
ReplaceBitmap(null);
|
|
||||||
ErrorMessage = null;
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
return nextRevision;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool TryApplyGeneratedBitmap(long expectedRevision, IImage bitmap, string visualSignature)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(bitmap);
|
|
||||||
|
|
||||||
if (Revision != expectedRevision)
|
|
||||||
{
|
|
||||||
DisposeIfNeeded(bitmap);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
State = ComponentPreviewImageState.Ready;
|
|
||||||
ReplaceBitmap(bitmap);
|
|
||||||
ErrorMessage = null;
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool TryApplyFailure(long expectedRevision, string visualSignature, string? errorMessage)
|
|
||||||
{
|
|
||||||
if (Revision != expectedRevision)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
State = ComponentPreviewImageState.Failed;
|
|
||||||
ReplaceBitmap(null);
|
|
||||||
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? "Unknown preview generation failure." : errorMessage.Trim();
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void StoreBitmap(IImage bitmap, string visualSignature)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(bitmap);
|
|
||||||
|
|
||||||
Revision += 1;
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
State = ComponentPreviewImageState.Ready;
|
|
||||||
ReplaceBitmap(bitmap);
|
|
||||||
ErrorMessage = null;
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void StoreFailure(string visualSignature, string? errorMessage)
|
|
||||||
{
|
|
||||||
Revision += 1;
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
State = ComponentPreviewImageState.Failed;
|
|
||||||
ReplaceBitmap(null);
|
|
||||||
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? "Unknown preview generation failure." : errorMessage.Trim();
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Invalidate(string? visualSignature = null)
|
|
||||||
{
|
|
||||||
Revision += 1;
|
|
||||||
if (visualSignature is not null)
|
|
||||||
{
|
|
||||||
VisualSignature = NormalizeSignature(visualSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
State = ComponentPreviewImageState.Pending;
|
|
||||||
ReplaceBitmap(null);
|
|
||||||
ErrorMessage = null;
|
|
||||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void DisposeBitmap()
|
|
||||||
{
|
|
||||||
ReplaceBitmap(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReplaceBitmap(IImage? bitmap)
|
|
||||||
{
|
|
||||||
var previous = _bitmap;
|
|
||||||
if (ReferenceEquals(previous, bitmap))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap = bitmap;
|
|
||||||
DisposeIfNeeded(previous);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DisposeIfNeeded(IImage? bitmap)
|
|
||||||
{
|
|
||||||
if (bitmap is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeSignature(string? visualSignature)
|
|
||||||
{
|
|
||||||
return visualSignature?.Trim() ?? string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ComponentPreviewKeyComparer : IEqualityComparer<ComponentPreviewKey>
|
|
||||||
{
|
|
||||||
public static ComponentPreviewKeyComparer Instance { get; } = new();
|
|
||||||
|
|
||||||
public bool Equals(ComponentPreviewKey x, ComponentPreviewKey y)
|
|
||||||
{
|
|
||||||
return x.Kind == y.Kind &&
|
|
||||||
StringComparer.OrdinalIgnoreCase.Equals(x.ComponentTypeId, y.ComponentTypeId) &&
|
|
||||||
StringComparer.OrdinalIgnoreCase.Equals(x.PlacementId, y.PlacementId) &&
|
|
||||||
x.WidthCells == y.WidthCells &&
|
|
||||||
x.HeightCells == y.HeightCells;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(ComponentPreviewKey obj)
|
|
||||||
{
|
|
||||||
var hash = new HashCode();
|
|
||||||
hash.Add(obj.Kind);
|
|
||||||
hash.Add(obj.ComponentTypeId, StringComparer.OrdinalIgnoreCase);
|
|
||||||
hash.Add(obj.PlacementId, StringComparer.OrdinalIgnoreCase);
|
|
||||||
hash.Add(obj.WidthCells);
|
|
||||||
hash.Add(obj.HeightCells);
|
|
||||||
return hash.ToHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,8 @@ public sealed record ComponentLibraryCreateContext(
|
|||||||
IRecommendationInfoService RecommendationInfoService,
|
IRecommendationInfoService RecommendationInfoService,
|
||||||
ICalculatorDataService CalculatorDataService,
|
ICalculatorDataService CalculatorDataService,
|
||||||
ISettingsFacadeService SettingsFacade,
|
ISettingsFacadeService SettingsFacade,
|
||||||
string? PlacementId = null);
|
string? PlacementId = null,
|
||||||
|
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||||
|
|
||||||
public interface IComponentLibraryService
|
public interface IComponentLibraryService
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Media;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
|
||||||
|
|
||||||
public interface IComponentPreviewImageService
|
|
||||||
{
|
|
||||||
ComponentPreviewImageEntry GetOrCreateEntry(ComponentPreviewKey key, string? visualSignature = null);
|
|
||||||
|
|
||||||
bool TryGetEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry);
|
|
||||||
|
|
||||||
IReadOnlyCollection<ComponentPreviewImageEntry> GetEntriesSnapshot();
|
|
||||||
|
|
||||||
Task<ComponentPreviewImageEntry> QueueGenerationAsync(
|
|
||||||
ComponentPreviewKey key,
|
|
||||||
string visualSignature,
|
|
||||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
|
||||||
CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
ComponentPreviewImageEntry Store(ComponentPreviewKey key, IImage bitmap, string visualSignature);
|
|
||||||
|
|
||||||
ComponentPreviewImageEntry StoreFailure(ComponentPreviewKey key, string visualSignature, string? errorMessage = null);
|
|
||||||
|
|
||||||
bool Invalidate(ComponentPreviewKey key, string? visualSignature = null);
|
|
||||||
|
|
||||||
int RemovePlacementPreviews(string placementId);
|
|
||||||
|
|
||||||
int InvalidateVisualSignature(string visualSignature);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using Avalonia.Controls;
|
||||||
using LanMountainDesktop.Services;
|
|
||||||
using FluentIcons.Common;
|
using FluentIcons.Common;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
@@ -55,33 +54,20 @@ public sealed class ComponentLibraryCategoryViewModel
|
|||||||
public sealed class ComponentLibraryItemViewModel
|
public sealed class ComponentLibraryItemViewModel
|
||||||
: ObservableObject
|
: ObservableObject
|
||||||
{
|
{
|
||||||
private readonly string _loadingPreviewText;
|
|
||||||
private readonly string _previewUnavailableText;
|
|
||||||
private string _displayName;
|
private string _displayName;
|
||||||
private string? _description;
|
private string? _description;
|
||||||
private ComponentPreviewKey _previewKey;
|
private Control? _previewControl;
|
||||||
private ComponentPreviewImageEntry? _previewImageEntry;
|
|
||||||
private ComponentPreviewImageState _previewState;
|
|
||||||
private string? _previewErrorMessage;
|
|
||||||
private string _previewStatusText;
|
|
||||||
|
|
||||||
public ComponentLibraryItemViewModel(
|
public ComponentLibraryItemViewModel(
|
||||||
string componentId,
|
string componentId,
|
||||||
string displayName,
|
string displayName,
|
||||||
ComponentPreviewKey previewKey,
|
|
||||||
string? description = null,
|
string? description = null,
|
||||||
string loadingPreviewText = "Loading preview...",
|
Control? previewControl = null)
|
||||||
string previewUnavailableText = "Preview unavailable",
|
|
||||||
ComponentPreviewImageEntry? previewImageEntry = null)
|
|
||||||
{
|
{
|
||||||
ComponentId = componentId;
|
ComponentId = componentId;
|
||||||
_displayName = displayName;
|
_displayName = displayName;
|
||||||
_description = description;
|
_description = description;
|
||||||
_previewKey = previewKey;
|
_previewControl = previewControl;
|
||||||
_loadingPreviewText = loadingPreviewText;
|
|
||||||
_previewUnavailableText = previewUnavailableText;
|
|
||||||
_previewStatusText = loadingPreviewText;
|
|
||||||
UpdatePreviewImageEntry(previewImageEntry, raiseEntryChanged: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ComponentId { get; }
|
public string ComponentId { get; }
|
||||||
@@ -98,98 +84,10 @@ public sealed class ComponentLibraryItemViewModel
|
|||||||
set => SetProperty(ref _description, value);
|
set => SetProperty(ref _description, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentPreviewKey PreviewKey
|
public Control? PreviewControl
|
||||||
{
|
{
|
||||||
get => _previewKey;
|
get => _previewControl;
|
||||||
set => SetProperty(ref _previewKey, value);
|
set => SetProperty(ref _previewControl, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentPreviewImageEntry? PreviewImageEntry => _previewImageEntry;
|
|
||||||
|
|
||||||
public object? PreviewBitmap => _previewImageEntry?.Bitmap;
|
|
||||||
|
|
||||||
public ComponentPreviewImageState PreviewState => _previewState;
|
|
||||||
|
|
||||||
public bool IsPreviewPending => _previewState == ComponentPreviewImageState.Pending;
|
|
||||||
|
|
||||||
public bool IsPreviewReady => _previewState == ComponentPreviewImageState.Ready && _previewImageEntry?.Bitmap is not null;
|
|
||||||
|
|
||||||
public bool IsPreviewFailed => _previewState == ComponentPreviewImageState.Failed;
|
|
||||||
|
|
||||||
public string? PreviewErrorMessage => _previewErrorMessage;
|
|
||||||
|
|
||||||
public string PreviewStatusText => _previewStatusText;
|
|
||||||
|
|
||||||
public void UpdatePreviewImageEntry(ComponentPreviewImageEntry? previewImageEntry)
|
|
||||||
{
|
|
||||||
UpdatePreviewImageEntry(previewImageEntry, raiseEntryChanged: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePreviewImageEntry(ComponentPreviewImageEntry? previewImageEntry, bool raiseEntryChanged)
|
|
||||||
{
|
|
||||||
if (raiseEntryChanged && ReferenceEquals(_previewImageEntry, previewImageEntry))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_previewImageEntry is not null)
|
|
||||||
{
|
|
||||||
_previewImageEntry.PropertyChanged -= OnPreviewImageEntryPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
_previewImageEntry = previewImageEntry;
|
|
||||||
_previewState = previewImageEntry?.State ?? ComponentPreviewImageState.Pending;
|
|
||||||
_previewErrorMessage = previewImageEntry?.ErrorMessage;
|
|
||||||
|
|
||||||
_previewStatusText = _previewState switch
|
|
||||||
{
|
|
||||||
ComponentPreviewImageState.Ready => string.Empty,
|
|
||||||
ComponentPreviewImageState.Failed => string.IsNullOrWhiteSpace(_previewErrorMessage)
|
|
||||||
? _previewUnavailableText
|
|
||||||
: _previewErrorMessage!,
|
|
||||||
_ => _loadingPreviewText
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_previewImageEntry is not null)
|
|
||||||
{
|
|
||||||
_previewImageEntry.PropertyChanged += OnPreviewImageEntryPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
RaisePreviewDependentProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPreviewImageEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
_ = sender;
|
|
||||||
if (string.IsNullOrWhiteSpace(e.PropertyName) ||
|
|
||||||
e.PropertyName is nameof(ComponentPreviewImageEntry.Bitmap) or
|
|
||||||
nameof(ComponentPreviewImageEntry.State) or
|
|
||||||
nameof(ComponentPreviewImageEntry.ErrorMessage))
|
|
||||||
{
|
|
||||||
_previewState = _previewImageEntry?.State ?? ComponentPreviewImageState.Pending;
|
|
||||||
_previewErrorMessage = _previewImageEntry?.ErrorMessage;
|
|
||||||
_previewStatusText = _previewState switch
|
|
||||||
{
|
|
||||||
ComponentPreviewImageState.Ready => string.Empty,
|
|
||||||
ComponentPreviewImageState.Failed => string.IsNullOrWhiteSpace(_previewErrorMessage)
|
|
||||||
? _previewUnavailableText
|
|
||||||
: _previewErrorMessage!,
|
|
||||||
_ => _loadingPreviewText
|
|
||||||
};
|
|
||||||
|
|
||||||
RaisePreviewDependentProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RaisePreviewDependentProperties()
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(PreviewImageEntry));
|
|
||||||
OnPropertyChanged(nameof(PreviewBitmap));
|
|
||||||
OnPropertyChanged(nameof(PreviewState));
|
|
||||||
OnPropertyChanged(nameof(IsPreviewPending));
|
|
||||||
OnPropertyChanged(nameof(IsPreviewReady));
|
|
||||||
OnPropertyChanged(nameof(IsPreviewFailed));
|
|
||||||
OnPropertyChanged(nameof(PreviewErrorMessage));
|
|
||||||
OnPropertyChanged(nameof(PreviewStatusText));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,48 +99,11 @@
|
|||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
Padding="8">
|
Padding="8">
|
||||||
<Grid>
|
<ContentControl Content="{Binding PreviewControl}"
|
||||||
<Image Source="{Binding PreviewBitmap}"
|
HorizontalAlignment="Center"
|
||||||
Stretch="Uniform"
|
VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Stretch"
|
IsHitTestVisible="False"
|
||||||
VerticalAlignment="Stretch"
|
Focusable="False" />
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
|
||||||
IsVisible="{Binding IsPreviewReady}" />
|
|
||||||
|
|
||||||
<Border IsVisible="{Binding IsPreviewPending}"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
|
||||||
<StackPanel HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="8">
|
|
||||||
<ProgressBar Width="96"
|
|
||||||
IsIndeterminate="True" />
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontSize="12"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
|
||||||
Text="{Binding PreviewStatusText}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border IsVisible="{Binding IsPreviewFailed}"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
|
||||||
<StackPanel HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="8">
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
|
||||||
Text="{Binding PreviewStatusText}" />
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
|
||||||
Text="{Binding PreviewErrorMessage}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using FluentIcons.Common;
|
using FluentIcons.Common;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
@@ -14,10 +15,6 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
private IComponentLibraryService? _componentLibraryService;
|
private IComponentLibraryService? _componentLibraryService;
|
||||||
private Func<double, ComponentLibraryCreateContext>? _createContextFactory;
|
private Func<double, ComponentLibraryCreateContext>? _createContextFactory;
|
||||||
private Func<string, string, string>? _localize;
|
private Func<string, string, string>? _localize;
|
||||||
private Func<ComponentLibraryComponentEntry, ComponentPreviewKey>? _previewKeyResolver;
|
|
||||||
private Func<ComponentPreviewKey, ComponentPreviewImageEntry?>? _previewEntryResolver;
|
|
||||||
private Action<ComponentPreviewKey>? _warmPreviewRequested;
|
|
||||||
private Action<ComponentPreviewKey>? _renderPreviewRequested;
|
|
||||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||||
|
|
||||||
public ComponentLibraryWindow()
|
public ComponentLibraryWindow()
|
||||||
@@ -29,20 +26,12 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
public ComponentLibraryWindow(
|
public ComponentLibraryWindow(
|
||||||
IComponentLibraryService componentLibraryService,
|
IComponentLibraryService componentLibraryService,
|
||||||
Func<double, ComponentLibraryCreateContext> createContextFactory,
|
Func<double, ComponentLibraryCreateContext> createContextFactory,
|
||||||
Func<string, string, string> localize,
|
Func<string, string, string> localize)
|
||||||
Func<ComponentLibraryComponentEntry, ComponentPreviewKey>? previewKeyResolver = null,
|
|
||||||
Func<ComponentPreviewKey, ComponentPreviewImageEntry?>? previewEntryResolver = null,
|
|
||||||
Action<ComponentPreviewKey>? warmPreviewRequested = null,
|
|
||||||
Action<ComponentPreviewKey>? renderPreviewRequested = null)
|
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
_componentLibraryService = componentLibraryService ?? throw new ArgumentNullException(nameof(componentLibraryService));
|
_componentLibraryService = componentLibraryService ?? throw new ArgumentNullException(nameof(componentLibraryService));
|
||||||
_createContextFactory = createContextFactory ?? throw new ArgumentNullException(nameof(createContextFactory));
|
_createContextFactory = createContextFactory ?? throw new ArgumentNullException(nameof(createContextFactory));
|
||||||
_localize = localize ?? throw new ArgumentNullException(nameof(localize));
|
_localize = localize ?? throw new ArgumentNullException(nameof(localize));
|
||||||
_previewKeyResolver = previewKeyResolver;
|
|
||||||
_previewEntryResolver = previewEntryResolver;
|
|
||||||
_warmPreviewRequested = warmPreviewRequested;
|
|
||||||
_renderPreviewRequested = renderPreviewRequested;
|
|
||||||
Reload();
|
Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +45,7 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
_viewModel.Title = _localize("component_library.title", "Widgets");
|
_viewModel.Title = _localize("component_library.title", "Widgets");
|
||||||
|
DisposePreviewControls(_viewModel.Categories.SelectMany(static category => category.Components));
|
||||||
_viewModel.Categories.Clear();
|
_viewModel.Categories.Clear();
|
||||||
_viewModel.Components.Clear();
|
_viewModel.Components.Clear();
|
||||||
|
|
||||||
@@ -88,24 +78,12 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
var displayName = string.IsNullOrWhiteSpace(entry.DisplayNameLocalizationKey)
|
var displayName = string.IsNullOrWhiteSpace(entry.DisplayNameLocalizationKey)
|
||||||
? entry.DisplayName
|
? entry.DisplayName
|
||||||
: _localize?.Invoke(entry.DisplayNameLocalizationKey, entry.DisplayName) ?? entry.DisplayName;
|
: _localize?.Invoke(entry.DisplayNameLocalizationKey, entry.DisplayName) ?? entry.DisplayName;
|
||||||
var previewKey = ResolvePreviewKey(entry);
|
var previewControl = CreateStaticPreviewControl(entry);
|
||||||
var previewEntry = _previewEntryResolver?.Invoke(previewKey);
|
return new ComponentLibraryItemViewModel(
|
||||||
var item = new ComponentLibraryItemViewModel(
|
|
||||||
entry.ComponentId,
|
entry.ComponentId,
|
||||||
displayName,
|
displayName,
|
||||||
previewKey,
|
|
||||||
description: null,
|
description: null,
|
||||||
_localize?.Invoke("component_library.preview.loading", "Loading preview...") ?? "Loading preview...",
|
previewControl);
|
||||||
_localize?.Invoke("component_library.preview.unavailable", "Preview unavailable") ?? "Preview unavailable",
|
|
||||||
previewEntry);
|
|
||||||
|
|
||||||
if (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending)
|
|
||||||
{
|
|
||||||
_warmPreviewRequested?.Invoke(previewKey);
|
|
||||||
_renderPreviewRequested?.Invoke(previewKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
@@ -124,7 +102,7 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
_viewModel.Components.Add(component);
|
_viewModel.Components.Add(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestPreviewWarmup(selectedCategory.Components);
|
ComponentPreviewRuntimeQuiescer.Quiesce(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
||||||
@@ -147,48 +125,54 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
Hide();
|
Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePreviewImage(ComponentPreviewImageEntry previewImageEntry)
|
private Control? CreateStaticPreviewControl(ComponentLibraryComponentEntry entry)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(previewImageEntry);
|
if (_componentLibraryService is null || _createContextFactory is null)
|
||||||
|
|
||||||
foreach (var category in _viewModel.Categories)
|
|
||||||
{
|
{
|
||||||
foreach (var component in category.Components)
|
return null;
|
||||||
{
|
|
||||||
if (component.PreviewKey.Equals(previewImageEntry.Key))
|
|
||||||
{
|
|
||||||
component.UpdatePreviewImageEntry(previewImageEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cellSize = ResolvePreviewCellSize(entry);
|
||||||
|
var context = _createContextFactory(cellSize) with
|
||||||
|
{
|
||||||
|
PlacementId = null,
|
||||||
|
RenderMode = DesktopComponentRenderMode.LibraryPreview
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_componentLibraryService.TryCreateControl(entry.ComponentId, context, out var control, out _))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control is not null)
|
||||||
|
{
|
||||||
|
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComponentPreviewKey ResolvePreviewKey(ComponentLibraryComponentEntry entry)
|
private static double ResolvePreviewCellSize(ComponentLibraryComponentEntry entry)
|
||||||
{
|
{
|
||||||
if (_previewKeyResolver is not null)
|
var maxWidth = 180d;
|
||||||
{
|
var maxHeight = 120d;
|
||||||
return _previewKeyResolver(entry);
|
return Math.Clamp(
|
||||||
}
|
Math.Min(
|
||||||
|
maxWidth / Math.Max(1, entry.MinWidthCells),
|
||||||
return ComponentPreviewKey.ForComponentType(entry.ComponentId, entry.MinWidthCells, entry.MinHeightCells);
|
maxHeight / Math.Max(1, entry.MinHeightCells)),
|
||||||
|
24d,
|
||||||
|
72d);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RequestPreviewWarmup(IEnumerable<ComponentLibraryItemViewModel> components)
|
private static void DisposePreviewControls(IEnumerable<ComponentLibraryItemViewModel> components)
|
||||||
{
|
{
|
||||||
if (_warmPreviewRequested is null && _renderPreviewRequested is null)
|
foreach (var control in components.Select(static component => component.PreviewControl).OfType<Control>())
|
||||||
{
|
{
|
||||||
return;
|
ComponentPreviewRuntimeQuiescer.Detach(control);
|
||||||
}
|
if (control is IDisposable disposable)
|
||||||
|
|
||||||
foreach (var component in components)
|
|
||||||
{
|
|
||||||
if (!component.IsPreviewPending)
|
|
||||||
{
|
{
|
||||||
continue;
|
disposable.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_warmPreviewRequested?.Invoke(component.PreviewKey);
|
|
||||||
_renderPreviewRequested?.Invoke(component.PreviewKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ public sealed record DesktopComponentControlFactoryContext(
|
|||||||
ISettingsService SettingsService,
|
ISettingsService SettingsService,
|
||||||
IComponentInstanceSettingsStore ComponentSettingsStore,
|
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||||
string? PlacementId = null);
|
string? PlacementId = null,
|
||||||
|
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||||
|
|
||||||
public sealed class DesktopComponentRuntimeRegistration
|
public sealed class DesktopComponentRuntimeRegistration
|
||||||
{
|
{
|
||||||
@@ -115,7 +116,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
IRecommendationInfoService recommendationInfoService,
|
IRecommendationInfoService recommendationInfoService,
|
||||||
ICalculatorDataService calculatorDataService,
|
ICalculatorDataService calculatorDataService,
|
||||||
ISettingsFacadeService settingsFacade,
|
ISettingsFacadeService settingsFacade,
|
||||||
string? placementId = null)
|
string? placementId = null,
|
||||||
|
DesktopComponentRenderMode renderMode = DesktopComponentRenderMode.Live)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(settingsFacade);
|
ArgumentNullException.ThrowIfNull(settingsFacade);
|
||||||
|
|
||||||
@@ -141,7 +143,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
settingsService,
|
settingsService,
|
||||||
componentSettingsStore,
|
componentSettingsStore,
|
||||||
componentAccessor,
|
componentAccessor,
|
||||||
placementId));
|
placementId,
|
||||||
|
renderMode));
|
||||||
var runtimeContext = new DesktopComponentRuntimeContext(
|
var runtimeContext = new DesktopComponentRuntimeContext(
|
||||||
Definition.Id,
|
Definition.Id,
|
||||||
placementId,
|
placementId,
|
||||||
@@ -150,7 +153,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
appearanceTheme,
|
appearanceTheme,
|
||||||
chromeContext,
|
chromeContext,
|
||||||
componentAccessor,
|
componentAccessor,
|
||||||
componentSettingsStore);
|
componentSettingsStore,
|
||||||
|
renderMode);
|
||||||
|
|
||||||
ApplySettingsDependencies(control, settingsService, componentSettingsStore);
|
ApplySettingsDependencies(control, settingsService, componentSettingsStore);
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
|
||||||
xmlns:fi="using:FluentIcons.Avalonia"
|
xmlns:fi="using:FluentIcons.Avalonia"
|
||||||
xmlns:converters="using:Avalonia.Data.Converters"
|
xmlns:converters="using:Avalonia.Data.Converters"
|
||||||
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
|
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
|
||||||
x:DataType="vm:ComponentLibraryWindowViewModel">
|
x:DataType="vm:ComponentLibraryWindowViewModel">
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<!-- 闂備礁鎲$敮鎺懳涘☉姘仏妞ゆ劧绠戠粈鍡樹繆閵堝懎顏ラ柍褜鍓欓崯顖炲Φ閸曨厽鍠嗛柛鏇ㄥ幖椤ュ酣鎮?- 闂傚倷绶¢崜鐔奉焽瑜旈獮?Fluent NavigationView 濠碉紕鍋涢鍛偓娑掓櫊閹?-->
|
|
||||||
<Style Selector="ListBoxItem.category-item">
|
<Style Selector="ListBoxItem.category-item">
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
<Setter Property="Margin" Value="0,2"/>
|
<Setter Property="Margin" Value="0,2"/>
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}"/>
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}"/>
|
||||||
<Setter Property="Transitions">
|
|
||||||
<Transitions>
|
|
||||||
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00"/>
|
|
||||||
</Transitions>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ListBoxItem.category-item:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="ListBoxItem.category-item:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
|
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
|
||||||
@@ -26,18 +18,6 @@
|
|||||||
<Style Selector="ListBoxItem.category-item:selected /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="ListBoxItem.category-item:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}"/>
|
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}"/>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ListBoxItem.category-item:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- 闂備礁鎲$敮鎺懳涘☉姘仏妞ゆ劧绲绘禍婊堟煟閻斿搫顣肩紒鍌氱墦閺屸€愁吋閸涱喗鎮欓梺纭呮腹閸楀啿顕i鍕倞鐟滃繘骞?-->
|
|
||||||
<Style Selector="ListBoxItem.category-item fi|FluentIcon.category-icon">
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBoxItem.category-item:selected fi|FluentIcon.category-icon">
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="ListBoxItem.category-item TextBlock.category-text">
|
<Style Selector="ListBoxItem.category-item TextBlock.category-text">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -47,14 +27,9 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*"
|
<Grid ColumnDefinitions="Auto,*">
|
||||||
ColumnSpacing="0"
|
<Border Width="280" Background="Transparent">
|
||||||
Margin="0">
|
|
||||||
<!-- 闁诲骸缍婂鑽ょ磽濮樿泛鐤鹃柛鎾茶閸嬫挻鎷呴崘顭戞闂佺硶鏅涢幊妯虹暦?- 闂備礁鎲$敮鎺懳涘☉姘仏妞ゆ劧绠戠粈鍡樹繆閵堝懎顏ラ柍?+ 闂佸湱鍘ч悺銊ッ洪悢鐓庣??闂備礁鎼悮顐﹀磿閸欏鐝舵慨妞诲亾鐎殿喗鎸冲鍫曞箣椤撶啿鏌ょ紓鍌氬€风粈浣衡偓姘间簻閳? -->
|
|
||||||
<Border Width="280"
|
|
||||||
Background="Transparent">
|
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
<!-- 闂備礁鎲$敮鎺懳涘☉姘仏妞ゆ劧绠戠粈鍡樹繆閵堝懎顏ラ柍?-->
|
|
||||||
<ListBox x:Name="CategoryListBox"
|
<ListBox x:Name="CategoryListBox"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
@@ -64,13 +39,10 @@
|
|||||||
ItemsSource="{Binding Categories}">
|
ItemsSource="{Binding Categories}">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
||||||
<Grid ColumnDefinitions="Auto,*"
|
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" Margin="12,10">
|
||||||
ColumnSpacing="12"
|
|
||||||
Margin="12,10">
|
|
||||||
<fi:FluentIcon Icon="{Binding Icon}"
|
<fi:FluentIcon Icon="{Binding Icon}"
|
||||||
IconVariant="Regular"
|
IconVariant="Regular"
|
||||||
FontSize="18"
|
FontSize="18"/>
|
||||||
Classes="category-icon"/>
|
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -81,9 +53,7 @@
|
|||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<!-- 闂佸湱鍘ч悺銊ッ洪悢鐓庣??闂備礁鎼悮顐﹀磿閸欏鐝舵慨妞诲亾鐎殿喗鎸冲鍫曞箣椤撶啿鏌ょ紓鍌氬€风粈浣衡偓姘间簻閳? - 闂備線娼荤拹鐔煎礉鐏炲墽鈻曢煫鍥ㄦ⒒閻熷湱鎲稿澶樻晪闂侇剙绉甸崵瀣亜韫囨挸顏╅柣蹇旂懇楠炴牜鈧稒蓱缁€瀣煕?-->
|
<StackPanel Grid.Row="1" Margin="12,8,8,12">
|
||||||
<StackPanel Grid.Row="1"
|
|
||||||
Margin="12,8,8,12">
|
|
||||||
<Border Height="1"
|
<Border Height="1"
|
||||||
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
Opacity="0.4"
|
Opacity="0.4"
|
||||||
@@ -100,35 +70,26 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 闂備礁鎲¢悷銉╁储閺嶎厼鐤鹃柛顐f礀缁€鍐煕濞戝崬寮鹃柛鐔锋喘閺屾盯寮介浣碘偓鍐磼濡も偓閼活垶顢欒箛娑欐櫆闁圭瀛╅悵鐑芥⒑濮瑰洤濡奸悗姘煎墴瀹曡鎯旈妸锔规寗闂佸搫鍟崐绋库枔?-->
|
|
||||||
<Border Grid.Column="1"
|
<Border Grid.Column="1"
|
||||||
Width="1"
|
Width="1"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
Opacity="0.5"/>
|
Opacity="0.5"/>
|
||||||
|
|
||||||
<!-- 缂傚倸鍊风粈浣衡偓姘间簻閳诲酣濮€閳藉懐鐭楅梺鍛婃处閸n喖顭囬弮鍫熺厱?(闂備礁鎲¢悷銉╁储閺嶎厼鐤? -->
|
|
||||||
<ScrollViewer Grid.Column="1"
|
<ScrollViewer Grid.Column="1"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
<StackPanel Margin="16,8,12,8"
|
<StackPanel Margin="16,8,12,8">
|
||||||
Spacing="0">
|
|
||||||
|
|
||||||
<!-- 闂備礁鎼悧鍡浰囬悽绋跨劦妞ゆ巻鍋撴い锔诲櫍閹虫瑩骞嬮悩鐢碉紲闂佸憡娲︽禍婵嬵敃娴犲鐓涢柛鎰╁妼椤h櫕绻涢崼鐔风伌鐎殿喕鍗冲畷婊嗩槹濞?-->
|
|
||||||
<Panel IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
|
<Panel IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
|
||||||
|
|
||||||
<!-- 缂傚倸鍊风粈浣衡偓姘间簻閳诲酣濮€椤厽鍕冮梺鍝勬川婵増绂掑☉銏♀拻闁割偅绋戦悘顏呯節?- 闂備礁鎼悧鍡浰囨潏鈹惧亾濮樼厧骞樼紒顔规櫇閳ь剨缍嗛崢濂稿礈瑜版帗鐓涢柛婊€绀侀悘銉ヮ熆閻熷府韬柡浣哥Ф娴狅箓鎳栭埡鍐╁枦缂傚倷鐒﹂崝鏍€冮崨鑸汗婵炴垯鍨洪崵鍕倶閻愰潧浜鹃柣婵愬灣閹叉悂鎳滈鈧悘顏堟煕閵婏附鐨戝ù鐙呯畵瀹曟帒顭ㄩ崼銏犵闂備礁鎲$敮鎺懳涘☉銏犵柧?-->
|
|
||||||
<Border Classes="surface-translucent-panel"
|
<Border Classes="surface-translucent-panel"
|
||||||
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel Spacing="16">
|
<StackPanel Spacing="16">
|
||||||
<!-- 缂傚倸鍊风粈浣衡偓姘间簻閳诲酣濮€閵堝懎鍞ㄩ梺鎼炲労閸擄箓寮?-->
|
|
||||||
<TextBlock FontSize="28"
|
<TextBlock FontSize="28"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
Text="{Binding SelectedComponent.DisplayName}"/>
|
Text="{Binding SelectedComponent.DisplayName}"/>
|
||||||
|
|
||||||
<!-- 闂備焦鎮堕崕閬嶅箹椤愶附鍋╅柣鎰靛墮缁剁偟鎲稿澶嬪剭妞ゆ帒瀚崕宥夋煕閺囥劌鐏遍柡鍡樻礋閹嘲鈻庤箛鏇烆暫閻庤娲熸禍鍫曞箖?-->
|
|
||||||
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
@@ -136,59 +97,14 @@
|
|||||||
Width="420"
|
Width="420"
|
||||||
Height="300"
|
Height="300"
|
||||||
HorizontalAlignment="Center">
|
HorizontalAlignment="Center">
|
||||||
<Grid Margin="16">
|
<ContentControl x:Name="SelectedComponentPreviewHost"
|
||||||
<!-- 濠碘槅鍋呭妯尖偓姘煎灦閿濈偛顓兼径濠勫€為梺鍛婃寙閸愮偓姣?-->
|
Margin="16"
|
||||||
<Image Source="{Binding SelectedComponent.PreviewBitmap}"
|
HorizontalAlignment="Center"
|
||||||
Stretch="Uniform"
|
VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Center"
|
IsHitTestVisible="False"
|
||||||
VerticalAlignment="Center"
|
Focusable="False"/>
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
|
||||||
IsVisible="{Binding SelectedComponent.IsPreviewReady}"/>
|
|
||||||
|
|
||||||
<!-- 闂備礁鎲″缁樻叏閹灐褰掑炊閵娧€鏋栧銈嗘尵婵鐟ч梻?-->
|
|
||||||
<Border IsVisible="{Binding SelectedComponent.IsPreviewPending}"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
|
||||||
<StackPanel HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="12">
|
|
||||||
<ProgressBar Width="120"
|
|
||||||
IsIndeterminate="True"/>
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontSize="14"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
|
||||||
Text="{Binding SelectedComponent.PreviewStatusText}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 濠电姰鍨洪崕鑲╁垝閸撗勫枂闁挎洖鍊归崑鎰版煠閸濄儺鏆柛?-->
|
|
||||||
<Border IsVisible="{Binding SelectedComponent.IsPreviewFailed}"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
|
||||||
<StackPanel HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="8">
|
|
||||||
<fi:FluentIcon Icon="ImageOff"
|
|
||||||
IconVariant="Regular"
|
|
||||||
FontSize="48"
|
|
||||||
Opacity="0.5"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
|
||||||
Text="{Binding SelectedComponent.PreviewStatusText}"/>
|
|
||||||
<TextBlock HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
|
||||||
Text="{Binding SelectedComponent.PreviewErrorMessage}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- "婵犵數鍎戠紞鈧い鏇嗗嫭鍙忛柣鎰悁閻掑﹪鐓崶銊︾闁活厼顑呴湁?闂備礁婀遍…鍫ニ囬悽绋跨?- 闂備線娼荤拹鐔煎礉閹存繍鐒藉ù鍏兼綑缁狙囨煕椤垵鏋涢柡浣哥埣閹﹢鎮欓崣澶婃闂佺厧鐏氶崹鍧楀极瀹ュ洣娌柣鎾崇岸閺嬪繘姊哄ú缁樺▏闁告柨顑囬埀顒勬涧閺堫剟鏁嶉幇顑╃喖宕崟顓犵暢闂佽崵濮撮鍛村疮閾忣偆鐝?-->
|
|
||||||
<Button HorizontalAlignment="Center"
|
<Button HorizontalAlignment="Center"
|
||||||
Classes="accent"
|
Classes="accent"
|
||||||
Padding="24,10"
|
Padding="24,10"
|
||||||
@@ -203,12 +119,12 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- 缂傚倷绀侀惌浣糕枍閿濆棙鍙忛柟闂寸缁?-->
|
|
||||||
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
|
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
MinHeight="400">
|
MinHeight="400">
|
||||||
<StackPanel Spacing="16" HorizontalAlignment="Center"
|
<StackPanel Spacing="16"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<fi:FluentIcon Icon="Apps"
|
<fi:FluentIcon Icon="Apps"
|
||||||
IconVariant="Regular"
|
IconVariant="Regular"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using LanMountainDesktop.Services;
|
|||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
using LanMountainDesktop.Views.Components;
|
using LanMountainDesktop.Views.Components;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
@@ -19,18 +18,19 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
{
|
{
|
||||||
public event EventHandler<string>? AddComponentRequested;
|
public event EventHandler<string>? AddComponentRequested;
|
||||||
|
|
||||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
private static readonly LocalizationService LocalizationService = new();
|
||||||
private List<DesktopComponentDefinition> _allDefinitions = new();
|
|
||||||
|
|
||||||
private ComponentRegistry? _componentRegistry;
|
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||||
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
|
||||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||||
private readonly IWeatherInfoService _weatherDataService;
|
private readonly IWeatherInfoService _weatherDataService;
|
||||||
private readonly TimeZoneService _timeZoneService;
|
private readonly TimeZoneService _timeZoneService;
|
||||||
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
|
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
|
||||||
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
||||||
|
|
||||||
private static readonly LocalizationService _localizationService = new();
|
private List<DesktopComponentDefinition> _allDefinitions = new();
|
||||||
|
private ComponentRegistry? _componentRegistry;
|
||||||
|
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
||||||
|
private Control? _selectedPreviewControl;
|
||||||
|
|
||||||
public FusedDesktopComponentLibraryControl()
|
public FusedDesktopComponentLibraryControl()
|
||||||
{
|
{
|
||||||
@@ -43,10 +43,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
LoadRegistry();
|
LoadRegistry();
|
||||||
LoadCategories();
|
LoadCategories();
|
||||||
|
|
||||||
// 为 ListBoxItem 添加 category-item 样式类
|
|
||||||
CategoryListBox.ContainerPrepared += OnCategoryListBoxContainerPrepared;
|
CategoryListBox.ContainerPrepared += OnCategoryListBoxContainerPrepared;
|
||||||
|
|
||||||
// 默认选择第一个分类
|
|
||||||
if (_viewModel.Categories.Count > 0)
|
if (_viewModel.Categories.Count > 0)
|
||||||
{
|
{
|
||||||
CategoryListBox.SelectedIndex = 0;
|
CategoryListBox.SelectedIndex = 0;
|
||||||
@@ -55,6 +52,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
|
|
||||||
private void OnCategoryListBoxContainerPrepared(object? sender, ContainerPreparedEventArgs e)
|
private void OnCategoryListBoxContainerPrepared(object? sender, ContainerPreparedEventArgs e)
|
||||||
{
|
{
|
||||||
|
_ = sender;
|
||||||
if (e.Container is ListBoxItem listBoxItem)
|
if (e.Container is ListBoxItem listBoxItem)
|
||||||
{
|
{
|
||||||
listBoxItem.Classes.Add("category-item");
|
listBoxItem.Classes.Add("category-item");
|
||||||
@@ -71,7 +69,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
_settingsFacade);
|
_settingsFacade);
|
||||||
|
|
||||||
_allDefinitions = _componentRegistry.GetAll()
|
_allDefinitions = _componentRegistry.GetAll()
|
||||||
.Where(d => d.AllowDesktopPlacement)
|
.Where(static definition => definition.AllowDesktopPlacement)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +78,6 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
_viewModel.Categories.Clear();
|
_viewModel.Categories.Clear();
|
||||||
|
|
||||||
var languageCode = _settingsFacade.Region.Get().LanguageCode;
|
var languageCode = _settingsFacade.Region.Get().LanguageCode;
|
||||||
|
|
||||||
// 添加"全部组件"分类
|
|
||||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||||
"all",
|
"all",
|
||||||
L(languageCode, "component_category.all", "All"),
|
L(languageCode, "component_category.all", "All"),
|
||||||
@@ -89,32 +85,26 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
Array.Empty<ComponentLibraryItemViewModel>()));
|
Array.Empty<ComponentLibraryItemViewModel>()));
|
||||||
|
|
||||||
var usedCategories = _allDefinitions
|
var usedCategories = _allDefinitions
|
||||||
.Select(d => d.Category)
|
.Select(static definition => definition.Category)
|
||||||
.Distinct()
|
.Where(static category => !string.IsNullOrWhiteSpace(category))
|
||||||
.Where(c => !string.IsNullOrEmpty(c));
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var cat in usedCategories)
|
foreach (var category in usedCategories)
|
||||||
{
|
{
|
||||||
var icon = ResolveCategoryIcon(cat);
|
|
||||||
var title = GetLocalizedCategoryTitle(languageCode, cat);
|
|
||||||
|
|
||||||
var categoryComponents = _allDefinitions
|
var categoryComponents = _allDefinitions
|
||||||
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
|
.Where(definition => string.Equals(definition.Category, category, StringComparison.OrdinalIgnoreCase))
|
||||||
.OrderBy(d => d.DisplayName)
|
.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(d => CreateComponentItem(d))
|
.Select(CreateComponentItem)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||||
cat,
|
category,
|
||||||
title,
|
GetLocalizedCategoryTitle(languageCode, category),
|
||||||
icon,
|
ResolveCategoryIcon(category),
|
||||||
categoryComponents));
|
categoryComponents));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分类图标映射 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
|
|
||||||
/// </summary>
|
|
||||||
private static Symbol ResolveCategoryIcon(string categoryId)
|
private static Symbol ResolveCategoryIcon(string categoryId)
|
||||||
{
|
{
|
||||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return Symbol.Clock;
|
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return Symbol.Clock;
|
||||||
@@ -129,9 +119,6 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
return Symbol.Apps;
|
return Symbol.Apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分类本地化标题 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
|
|
||||||
/// </summary>
|
|
||||||
private string GetLocalizedCategoryTitle(string languageCode, string categoryId)
|
private string GetLocalizedCategoryTitle(string languageCode, string categoryId)
|
||||||
{
|
{
|
||||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.clock", "Clock");
|
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.clock", "Clock");
|
||||||
@@ -148,101 +135,123 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
|
|
||||||
private string L(string languageCode, string key, string fallback)
|
private string L(string languageCode, string key, string fallback)
|
||||||
{
|
{
|
||||||
return _localizationService.GetString(languageCode, key, fallback);
|
return LocalizationService.GetString(languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
|
private static ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
|
||||||
{
|
{
|
||||||
var previewKey = ComponentPreviewKey.ForComponentType(
|
return new ComponentLibraryItemViewModel(definition.Id, definition.DisplayName);
|
||||||
definition.Id,
|
|
||||||
definition.MinWidthCells,
|
|
||||||
definition.MinHeightCells);
|
|
||||||
|
|
||||||
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
|
||||||
ComponentPreviewImageEntry? previewEntry = null;
|
|
||||||
|
|
||||||
if (mainWindow is not null)
|
|
||||||
{
|
|
||||||
previewEntry = mainWindow.GetPreviewEntry(previewKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = new ComponentLibraryItemViewModel(
|
|
||||||
definition.Id,
|
|
||||||
definition.DisplayName,
|
|
||||||
previewKey,
|
|
||||||
description: null,
|
|
||||||
"正在加载预览...",
|
|
||||||
"预览不可用",
|
|
||||||
previewEntry);
|
|
||||||
|
|
||||||
if (mainWindow is not null && (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending))
|
|
||||||
{
|
|
||||||
mainWindow.RequestDetachedLibraryPreview(previewKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
|
|
||||||
{
|
|
||||||
foreach (var category in _viewModel.Categories)
|
|
||||||
{
|
|
||||||
foreach (var component in category.Components)
|
|
||||||
{
|
|
||||||
if (component.PreviewKey.Equals(entry.Key))
|
|
||||||
{
|
|
||||||
component.UpdatePreviewImageEntry(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
UpdateSelectedComponent();
|
UpdateSelectedComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSelectedComponent()
|
private void UpdateSelectedComponent()
|
||||||
{
|
{
|
||||||
var selectedCategory = CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel;
|
if (CategoryListBox.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
|
||||||
if (selectedCategory is null)
|
|
||||||
{
|
{
|
||||||
_viewModel.SelectedComponent = null;
|
_viewModel.SelectedComponent = null;
|
||||||
|
SetSelectedPreviewControl(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取该分类下的组件列表
|
var filtered = selectedCategory.Id == "all"
|
||||||
IEnumerable<DesktopComponentDefinition> filtered;
|
? _allDefinitions.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||||
if (selectedCategory.Id == "all")
|
: _allDefinitions
|
||||||
{
|
.Where(definition => string.Equals(definition.Category, selectedCategory.Id, StringComparison.OrdinalIgnoreCase))
|
||||||
filtered = _allDefinitions.OrderBy(d => d.DisplayName);
|
.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
filtered = _allDefinitions
|
|
||||||
.Where(d => string.Equals(d.Category, selectedCategory.Id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.OrderBy(d => d.DisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择该分类下的第一个组件作为默认选中
|
|
||||||
var firstComponent = filtered.FirstOrDefault();
|
var firstComponent = filtered.FirstOrDefault();
|
||||||
if (firstComponent is not null)
|
if (firstComponent is null)
|
||||||
{
|
|
||||||
// 查找或创建对应的 ViewModel
|
|
||||||
var existingComponent = selectedCategory.Components.FirstOrDefault(c => c.ComponentId == firstComponent.Id);
|
|
||||||
if (existingComponent is not null)
|
|
||||||
{
|
|
||||||
_viewModel.SelectedComponent = existingComponent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_viewModel.SelectedComponent = CreateComponentItem(firstComponent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_viewModel.SelectedComponent = null;
|
_viewModel.SelectedComponent = null;
|
||||||
|
SetSelectedPreviewControl(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_viewModel.SelectedComponent = selectedCategory.Components.FirstOrDefault(component => component.ComponentId == firstComponent.Id)
|
||||||
|
?? CreateComponentItem(firstComponent);
|
||||||
|
SetSelectedPreviewControl(CreateStaticPreviewControl(firstComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control? CreateStaticPreviewControl(DesktopComponentDefinition definition)
|
||||||
|
{
|
||||||
|
if (_componentRuntimeRegistry is null ||
|
||||||
|
!_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var control = descriptor.CreateControl(
|
||||||
|
ResolvePreviewCellSize(definition),
|
||||||
|
_timeZoneService,
|
||||||
|
_weatherDataService,
|
||||||
|
_recommendationInfoService,
|
||||||
|
_calculatorDataService,
|
||||||
|
_settingsFacade,
|
||||||
|
placementId: null,
|
||||||
|
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||||
|
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
"ComponentLibrary",
|
||||||
|
$"Failed to create static fused preview for component '{definition.Id}'.",
|
||||||
|
ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ResolvePreviewCellSize(DesktopComponentDefinition definition)
|
||||||
|
{
|
||||||
|
const double maxWidth = 360d;
|
||||||
|
const double maxHeight = 240d;
|
||||||
|
return Math.Clamp(
|
||||||
|
Math.Min(
|
||||||
|
maxWidth / Math.Max(1, definition.MinWidthCells),
|
||||||
|
maxHeight / Math.Max(1, definition.MinHeightCells)),
|
||||||
|
32d,
|
||||||
|
96d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetSelectedPreviewControl(Control? control)
|
||||||
|
{
|
||||||
|
DisposeSelectedPreviewControl();
|
||||||
|
_selectedPreviewControl = control;
|
||||||
|
if (SelectedComponentPreviewHost is not null)
|
||||||
|
{
|
||||||
|
SelectedComponentPreviewHost.Content = control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeSelectedPreviewControl()
|
||||||
|
{
|
||||||
|
if (_selectedPreviewControl is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentPreviewRuntimeQuiescer.Detach(_selectedPreviewControl);
|
||||||
|
if (_selectedPreviewControl is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedPreviewControl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
DisposeSelectedPreviewControl();
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
||||||
@@ -255,15 +264,11 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
|
|
||||||
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
|
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// 打开设置窗口并导航到插件目录页面
|
|
||||||
if (Application.Current is App app)
|
if (Application.Current is App app)
|
||||||
{
|
{
|
||||||
app.OpenIndependentSettingsModule("FusedDesktopComponentLibrary", "plugin-catalog");
|
app.OpenIndependentSettingsModule("FusedDesktopComponentLibrary", "plugin-catalog");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所在窗口
|
this.FindAncestorOfType<Window>()?.Close();
|
||||||
var window = this.FindAncestorOfType<Window>();
|
|
||||||
var componentLibraryWindow = this.FindAncestorOfType<Window>();
|
|
||||||
componentLibraryWindow?.Close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,4 @@ public partial class FusedDesktopComponentLibraryWindow : Window
|
|||||||
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
||||||
mainWindow?.UnregisterFusedLibraryWindow(this);
|
mainWindow?.UnregisterFusedLibraryWindow(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
|
|
||||||
{
|
|
||||||
LibraryControl.UpdatePreviewImage(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Layout;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -18,412 +11,117 @@ namespace LanMountainDesktop.Views;
|
|||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private const double PreviewRenderCellSizeMin = 42;
|
|
||||||
private const double PreviewRenderCellSizeMax = 112;
|
|
||||||
|
|
||||||
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
|
|
||||||
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
|
|
||||||
private bool _componentLibraryPreviewWarmupStarted;
|
|
||||||
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
|
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
|
||||||
|
|
||||||
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
|
|
||||||
|
|
||||||
private void EnsureComponentLibraryPreviewWarmup()
|
private void EnsureComponentLibraryPreviewWarmup()
|
||||||
{
|
{
|
||||||
if (_componentLibraryCategories.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeCategoryId = _componentLibraryActiveCategoryId ??
|
|
||||||
_componentLibraryCategories[Math.Clamp(_componentLibraryCategoryIndex, 0, _componentLibraryCategories.Count - 1)].Id;
|
|
||||||
if (!_componentLibraryPreviewWarmupStarted)
|
|
||||||
{
|
|
||||||
_componentLibraryPreviewWarmupStarted = true;
|
|
||||||
_ = WarmComponentLibraryPreviewsSeriallyAsync(activeCategoryId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeCategory = _componentLibraryCategories.FirstOrDefault(category =>
|
|
||||||
string.Equals(category.Id, activeCategoryId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (activeCategory is not null)
|
|
||||||
{
|
|
||||||
_ = WarmComponentLibraryCategoryPreviewsAsync(activeCategory);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WarmComponentLibraryPreviewsSeriallyAsync(string activeCategoryId)
|
private Control CreateStaticComponentLibraryPreview(
|
||||||
{
|
string componentId,
|
||||||
var prioritized = _componentLibraryCategories
|
double cellSize,
|
||||||
.OrderBy(category => string.Equals(category.Id, activeCategoryId, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
double previewWidth,
|
||||||
.ToList();
|
double previewHeight)
|
||||||
|
|
||||||
foreach (var category in prioritized)
|
|
||||||
{
|
|
||||||
await WarmComponentLibraryCategoryPreviewsAsync(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WarmComponentLibraryCategoryPreviewsAsync(ComponentLibraryCategory category)
|
|
||||||
{
|
|
||||||
foreach (var component in category.Components)
|
|
||||||
{
|
|
||||||
var span = NormalizeComponentCellSpan(
|
|
||||||
component.ComponentId,
|
|
||||||
(component.MinWidthCells, component.MinHeightCells));
|
|
||||||
await EnsureComponentTypePreviewImageAsync(component.ComponentId, span.WidthCells, span.HeightCells);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IImage?> EnsureComponentTypePreviewImageAsync(string componentId, int widthCells, int heightCells)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(componentId))
|
if (string.IsNullOrWhiteSpace(componentId))
|
||||||
{
|
{
|
||||||
return null;
|
return CreateStaticComponentPreviewFallback(previewWidth, previewHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
var context = new ComponentLibraryCreateContext(
|
||||||
var cached = ResolvePreviewImageFromService(key);
|
cellSize,
|
||||||
if (cached is not null)
|
_timeZoneService,
|
||||||
{
|
_weatherDataService,
|
||||||
ApplyPreviewEntryToEmbeddedVisuals(key);
|
_recommendationInfoService,
|
||||||
return cached;
|
_calculatorDataService,
|
||||||
}
|
_settingsFacade,
|
||||||
|
PlacementId: null,
|
||||||
|
RenderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||||
|
|
||||||
var entry = await QueuePreviewGenerationAsync(
|
if (!_componentLibraryService.TryCreateControl(componentId, context, out var control, out var exception) ||
|
||||||
key,
|
control is null)
|
||||||
pageIndex: null,
|
|
||||||
action: "ComponentTypePreview",
|
|
||||||
forceRefresh: false);
|
|
||||||
return entry.Bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IImage?> RefreshPlacementPreviewImageAsync(DesktopComponentPlacementSnapshot? placement, bool forceRefresh)
|
|
||||||
{
|
|
||||||
if (placement is null ||
|
|
||||||
string.IsNullOrWhiteSpace(placement.ComponentId) ||
|
|
||||||
string.IsNullOrWhiteSpace(placement.PlacementId))
|
|
||||||
{
|
{
|
||||||
return null;
|
if (exception is not null)
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsPlacementPresent(placement.PlacementId))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot = ClonePlacementSnapshot(placement);
|
|
||||||
var key = CreatePlacementPreviewKey(
|
|
||||||
snapshot.ComponentId,
|
|
||||||
snapshot.PlacementId,
|
|
||||||
snapshot.WidthCells,
|
|
||||||
snapshot.HeightCells);
|
|
||||||
if (!forceRefresh)
|
|
||||||
{
|
|
||||||
var cached = ResolvePreviewImageFromService(key);
|
|
||||||
if (cached is not null)
|
|
||||||
{
|
{
|
||||||
return cached;
|
AppLogger.Warn(
|
||||||
|
"ComponentLibrary",
|
||||||
|
$"Failed to create static preview for component '{componentId}'.",
|
||||||
|
exception);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
return CreateStaticComponentPreviewFallback(previewWidth, previewHeight);
|
||||||
{
|
|
||||||
_componentPreviewImageService.RemovePlacementPreviews(snapshot.PlacementId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = await QueuePreviewGenerationAsync(
|
control.Width = previewWidth;
|
||||||
key,
|
control.Height = previewHeight;
|
||||||
snapshot.PageIndex,
|
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||||
action: "PlacementPreview",
|
return control;
|
||||||
forceRefresh: false);
|
|
||||||
if (!IsPlacementPresent(snapshot.PlacementId))
|
|
||||||
{
|
|
||||||
RemovePlacementPreviewImage(snapshot.PlacementId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.Bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ComponentPreviewImageEntry> QueuePreviewGenerationAsync(
|
private Control CreateStaticComponentPreviewFallback(double previewWidth, double previewHeight)
|
||||||
ComponentPreviewKey key,
|
|
||||||
int? pageIndex,
|
|
||||||
string action,
|
|
||||||
bool forceRefresh,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
var renderCellSize = ResolvePreviewRenderCellSize(key.WidthCells, key.HeightCells);
|
return new Border
|
||||||
var visualSignature = BuildPreviewVisualSignature(key, renderCellSize);
|
|
||||||
if (forceRefresh)
|
|
||||||
{
|
|
||||||
_componentPreviewImageService.Invalidate(key, visualSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
var entry = await _componentPreviewImageService.QueueGenerationAsync(
|
|
||||||
key,
|
|
||||||
visualSignature,
|
|
||||||
async ct =>
|
|
||||||
{
|
|
||||||
_ = ct;
|
|
||||||
if (key.Kind == ComponentPreviewKeyKind.PlacementInstance &&
|
|
||||||
!IsPlacementPresent(key.PlacementId))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bitmap = await CapturePreviewImageAsync(
|
|
||||||
key.ComponentTypeId,
|
|
||||||
key.PlacementId,
|
|
||||||
pageIndex,
|
|
||||||
key.WidthCells,
|
|
||||||
key.HeightCells,
|
|
||||||
renderCellSize,
|
|
||||||
action);
|
|
||||||
if (key.Kind == ComponentPreviewKeyKind.PlacementInstance &&
|
|
||||||
!IsPlacementPresent(key.PlacementId))
|
|
||||||
{
|
|
||||||
DisposeImageIfNeeded(bitmap);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
NotifyPreviewEntryUpdated(entry);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IImage?> CapturePreviewImageAsync(
|
|
||||||
string componentId,
|
|
||||||
string? placementId,
|
|
||||||
int? pageIndex,
|
|
||||||
int widthCells,
|
|
||||||
int heightCells,
|
|
||||||
double renderCellSize,
|
|
||||||
string action)
|
|
||||||
{
|
|
||||||
if (ComponentPreviewStagingHost is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var safeWidthCells = Math.Max(1, widthCells);
|
|
||||||
var safeHeightCells = Math.Max(1, heightCells);
|
|
||||||
var safeCellSize = Math.Clamp(renderCellSize, PreviewRenderCellSizeMin, PreviewRenderCellSizeMax);
|
|
||||||
var previewWidth = safeWidthCells * safeCellSize;
|
|
||||||
var previewHeight = safeHeightCells * safeCellSize;
|
|
||||||
|
|
||||||
var previewControl = CreateDesktopComponentControl(
|
|
||||||
componentId,
|
|
||||||
safeCellSize,
|
|
||||||
placementId,
|
|
||||||
pageIndex,
|
|
||||||
action);
|
|
||||||
if (previewControl is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
previewControl.IsHitTestVisible = false;
|
|
||||||
previewControl.Focusable = false;
|
|
||||||
|
|
||||||
var stage = new Border
|
|
||||||
{
|
{
|
||||||
Width = previewWidth,
|
Width = previewWidth,
|
||||||
Height = previewHeight,
|
Height = previewHeight,
|
||||||
Background = Brushes.Transparent,
|
Background = GetThemeBrush("AdaptiveCardBackgroundBrush"),
|
||||||
ClipToBounds = true,
|
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
||||||
Child = previewControl
|
BorderThickness = new Avalonia.Thickness(1),
|
||||||
|
CornerRadius = new Avalonia.CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)),
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = L("component_library.preview_unavailable", "Preview unavailable"),
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Canvas.SetLeft(stage, -20000);
|
private static void DisposeStaticComponentLibraryPreviews(IEnumerable<Control> roots)
|
||||||
Canvas.SetTop(stage, -20000);
|
{
|
||||||
ComponentPreviewStagingHost.Children.Add(stage);
|
foreach (var control in roots.SelectMany(EnumerateControls))
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
stage.Measure(new Size(previewWidth, previewHeight));
|
ComponentPreviewRuntimeQuiescer.Detach(control);
|
||||||
stage.Arrange(new Rect(0, 0, previewWidth, previewHeight));
|
if (control is IDisposable disposable)
|
||||||
stage.UpdateLayout();
|
|
||||||
await WaitForPreviewRenderPassAsync();
|
|
||||||
|
|
||||||
var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
|
|
||||||
var pixelSize = new PixelSize(
|
|
||||||
Math.Max(1, (int)Math.Ceiling(previewWidth * renderScale)),
|
|
||||||
Math.Max(1, (int)Math.Ceiling(previewHeight * renderScale)));
|
|
||||||
var bitmap = new RenderTargetBitmap(pixelSize, new Vector(96 * renderScale, 96 * renderScale));
|
|
||||||
bitmap.Render(stage);
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
|
||||||
{
|
|
||||||
AppLogger.Warn(
|
|
||||||
"ComponentPreview",
|
|
||||||
$"Action={action}; ComponentId={componentId}; PlacementId={placementId ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false",
|
|
||||||
ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ComponentPreviewStagingHost.Children.Remove(stage);
|
|
||||||
ClearTimeZoneServiceBindings(stage);
|
|
||||||
if (previewControl is IDisposable disposableControl)
|
|
||||||
{
|
{
|
||||||
disposableControl.Dispose();
|
disposable.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WaitForPreviewRenderPassAsync()
|
private static IEnumerable<Control> EnumerateControls(Control root)
|
||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(static () => { }, DispatcherPriority.Background);
|
yield return root;
|
||||||
await Dispatcher.UIThread.InvokeAsync(static () => { }, DispatcherPriority.Render);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double ResolvePreviewRenderCellSize(int widthCells, int heightCells)
|
if (root is Panel panel)
|
||||||
{
|
|
||||||
var baseCellSize = _currentDesktopCellSize > 0
|
|
||||||
? _currentDesktopCellSize * 1.10
|
|
||||||
: 74;
|
|
||||||
var densityBoost = Math.Max(widthCells, heightCells) >= 4 ? 8 : 0;
|
|
||||||
return Math.Clamp(baseCellSize + densityBoost, PreviewRenderCellSizeMin, PreviewRenderCellSizeMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildPreviewVisualSignature(ComponentPreviewKey key, double renderCellSize)
|
|
||||||
{
|
|
||||||
var appearance = _appearanceThemeService.GetCurrent();
|
|
||||||
var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
|
|
||||||
return string.Create(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
$"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.CornerRadiusStyle}|Accent={FormatSignatureColor(appearance.AccentColor)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewKey CreateComponentTypePreviewKey(string componentId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
var span = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
|
||||||
return ComponentPreviewKey.ForComponentType(componentId, span.WidthCells, span.HeightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewKey CreatePlacementPreviewKey(string componentId, string placementId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
var span = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
|
||||||
return ComponentPreviewKey.ForPlacementInstance(componentId, placementId, span.WidthCells, span.HeightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPlacementPresent(string? placementId)
|
|
||||||
{
|
|
||||||
return !string.IsNullOrWhiteSpace(placementId) &&
|
|
||||||
_desktopComponentPlacements.Any(candidate =>
|
|
||||||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildCurrentVisualSignature(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
var renderCellSize = ResolvePreviewRenderCellSize(key.WidthCells, key.HeightCells);
|
|
||||||
return BuildPreviewVisualSignature(key, renderCellSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetReusablePreviewEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry)
|
|
||||||
{
|
|
||||||
if (!_componentPreviewImageService.TryGetEntry(key, out entry) ||
|
|
||||||
entry is null ||
|
|
||||||
entry.State != ComponentPreviewImageState.Ready ||
|
|
||||||
entry.Bitmap is null)
|
|
||||||
{
|
{
|
||||||
entry = null;
|
foreach (var child in panel.Children.OfType<Control>())
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedSignature = BuildCurrentVisualSignature(key);
|
|
||||||
if (!string.Equals(entry.VisualSignature, expectedSignature, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
entry = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IImage? ResolvePreviewImageFromService(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
if (!TryGetReusablePreviewEntry(key, out var entry) || entry is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.Bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewImageEntry? ResolvePreviewEntry(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
if (!_componentPreviewImageService.TryGetEntry(key, out var entry) || entry is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.State != ComponentPreviewImageState.Ready)
|
|
||||||
{
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryGetReusablePreviewEntry(key, out var reusable) ? reusable : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IImage? ResolveComponentTypePreviewImage(string componentId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
var key = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
|
||||||
return ResolvePreviewImageFromService(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IImage? ResolveDesktopEditPreviewImage(string componentId, string? placementId, int widthCells, int heightCells)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(placementId))
|
|
||||||
{
|
|
||||||
var placementKey = CreatePlacementPreviewKey(componentId, placementId, widthCells, heightCells);
|
|
||||||
var placementImage = ResolvePreviewImageFromService(placementKey);
|
|
||||||
if (placementImage is not null)
|
|
||||||
{
|
{
|
||||||
return placementImage;
|
foreach (var descendant in EnumerateControls(child))
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var componentTypeKey = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
if (root is ContentControl { Content: Control content })
|
||||||
return ResolvePreviewImageFromService(componentTypeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int WidthCells, int HeightCells) ResolveOverlayPreviewSpan(
|
|
||||||
string componentId,
|
|
||||||
string? placementId,
|
|
||||||
int? widthCells,
|
|
||||||
int? heightCells)
|
|
||||||
{
|
|
||||||
if (widthCells is > 0 && heightCells is > 0)
|
|
||||||
{
|
{
|
||||||
return NormalizeComponentCellSpan(componentId, (widthCells.Value, heightCells.Value));
|
foreach (var descendant in EnumerateControls(content))
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
if (root is Decorator { Child: Control decoratorChild })
|
||||||
TryGetDesktopPlacementById(placementId, out var placement))
|
|
||||||
{
|
{
|
||||||
return NormalizeComponentCellSpan(componentId, (placement.WidthCells, placement.HeightCells));
|
foreach (var descendant in EnumerateControls(decoratorChild))
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_desktopEditSession.ComponentId) &&
|
|
||||||
string.Equals(_desktopEditSession.ComponentId, componentId, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
_desktopEditSession.WidthCells > 0 &&
|
|
||||||
_desktopEditSession.HeightCells > 0)
|
|
||||||
{
|
|
||||||
return NormalizeComponentCellSpan(componentId, (_desktopEditSession.WidthCells, _desktopEditSession.HeightCells));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor))
|
|
||||||
{
|
|
||||||
return NormalizeComponentCellSpan(
|
|
||||||
componentId,
|
|
||||||
(descriptor.Definition.MinWidthCells, descriptor.Definition.MinHeightCells));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (1, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyDesktopEditOverlayPreviewImage(
|
private void ApplyDesktopEditOverlayPreviewImage(
|
||||||
@@ -432,9 +130,12 @@ public partial class MainWindow : Window
|
|||||||
int? widthCells = null,
|
int? widthCells = null,
|
||||||
int? heightCells = null)
|
int? heightCells = null)
|
||||||
{
|
{
|
||||||
var span = ResolveOverlayPreviewSpan(componentId, placementId, widthCells, heightCells);
|
_ = componentId;
|
||||||
|
_ = placementId;
|
||||||
|
_ = widthCells;
|
||||||
|
_ = heightCells;
|
||||||
EnsureDesktopEditOverlayPresenter();
|
EnsureDesktopEditOverlayPresenter();
|
||||||
_desktopEditOverlayPresenter?.SetPreviewImage(ResolveDesktopEditPreviewImage(componentId, placementId, span.WidthCells, span.HeightCells));
|
_desktopEditOverlayPresenter?.SetPreviewImage(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrimeDesktopEditPreviewImage(
|
private void PrimeDesktopEditPreviewImage(
|
||||||
@@ -444,164 +145,28 @@ public partial class MainWindow : Window
|
|||||||
int widthCells,
|
int widthCells,
|
||||||
int heightCells)
|
int heightCells)
|
||||||
{
|
{
|
||||||
|
_ = componentId;
|
||||||
|
_ = placementId;
|
||||||
_ = pageIndex;
|
_ = pageIndex;
|
||||||
var normalized = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
_ = widthCells;
|
||||||
_ = EnsureComponentTypePreviewImageAsync(componentId, normalized.WidthCells, normalized.HeightCells);
|
_ = heightCells;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
|
||||||
TryGetDesktopPlacementById(placementId, out var placement))
|
|
||||||
{
|
|
||||||
_ = RefreshPlacementPreviewImageAsync(placement, forceRefresh: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueuePlacementPreviewRefresh(DesktopComponentPlacementSnapshot? placement)
|
private void QueuePlacementPreviewRefresh(DesktopComponentPlacementSnapshot? placement)
|
||||||
{
|
{
|
||||||
_ = RefreshPlacementPreviewImageAsync(placement, forceRefresh: true);
|
_ = placement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemovePlacementPreviewImage(string? placementId)
|
private void RemovePlacementPreviewImage(string? placementId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(placementId))
|
_ = placementId;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_componentPreviewImageService.RemovePlacementPreviews(placementId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemovePlacementPreviewImages(IEnumerable<DesktopComponentPlacementSnapshot> placements)
|
private void RemovePlacementPreviewImages(IEnumerable<DesktopComponentPlacementSnapshot> placements)
|
||||||
{
|
{
|
||||||
foreach (var placementId in placements
|
_ = placements;
|
||||||
.Select(placement => placement.PlacementId)
|
|
||||||
.Where(static id => !string.IsNullOrWhiteSpace(id))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
RemovePlacementPreviewImage(placementId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterComponentLibraryPreviewVisual(ComponentPreviewKey key, Image image, Control fallback)
|
|
||||||
{
|
|
||||||
if (!_componentLibraryPreviewVisualTargets.TryGetValue(key, out var visuals))
|
|
||||||
{
|
|
||||||
visuals = [];
|
|
||||||
_componentLibraryPreviewVisualTargets[key] = visuals;
|
|
||||||
}
|
|
||||||
|
|
||||||
visuals.Add(new ComponentLibraryPreviewVisualTarget(image, fallback));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearComponentLibraryPreviewVisualTargets()
|
|
||||||
{
|
|
||||||
_componentLibraryPreviewVisualTargets.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyPreviewEntryToEmbeddedVisuals(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
if (!_componentLibraryPreviewVisualTargets.TryGetValue(key, out var targets))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var previewImage = ResolvePreviewImageFromService(key);
|
|
||||||
foreach (var target in targets)
|
|
||||||
{
|
|
||||||
target.Image.Source = previewImage;
|
|
||||||
target.Image.IsVisible = previewImage is not null;
|
|
||||||
target.Fallback.IsVisible = previewImage is null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NotifyPreviewEntryUpdated(ComponentPreviewImageEntry entry)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
|
|
||||||
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
|
|
||||||
_fusedLibraryWindow?.UpdatePreviewImage(entry);
|
|
||||||
|
|
||||||
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
|
||||||
{
|
|
||||||
RefreshDesktopEditOverlayPreviewIfActive(entry.Key.ComponentTypeId, entry.Key.PlacementId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RefreshDesktopEditOverlayPreviewIfActive(entry.Key.ComponentTypeId, placementId: null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DispatcherPriority.Background);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DisposeImageIfNeeded(IImage? image)
|
|
||||||
{
|
|
||||||
if (image is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatSignatureColor(Color color)
|
|
||||||
{
|
|
||||||
return string.Create(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
$"{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshDesktopEditOverlayPreviewIfActive(string componentId, string? placementId)
|
|
||||||
{
|
|
||||||
if (_desktopEditOverlayPresenter is null ||
|
|
||||||
(!_desktopEditSession.IsActive && !_isDesktopEditCommitPending) ||
|
|
||||||
string.IsNullOrWhiteSpace(_desktopEditSession.ComponentId) ||
|
|
||||||
!string.Equals(_desktopEditSession.ComponentId, componentId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
|
||||||
!string.Equals(_desktopEditSession.PlacementId, placementId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyDesktopEditOverlayPreviewImage(
|
|
||||||
_desktopEditSession.ComponentId,
|
|
||||||
_desktopEditSession.PlacementId,
|
|
||||||
_desktopEditSession.WidthCells,
|
|
||||||
_desktopEditSession.HeightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewKey ResolveDetachedLibraryPreviewKey(ComponentLibraryComponentEntry entry)
|
|
||||||
{
|
|
||||||
return CreateComponentTypePreviewKey(entry.ComponentId, entry.MinWidthCells, entry.MinHeightCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentPreviewImageEntry? ResolveDetachedLibraryPreviewEntry(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
return ResolvePreviewEntry(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RequestDetachedLibraryPreviewWarm(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
_ = QueuePreviewGenerationAsync(
|
|
||||||
key,
|
|
||||||
pageIndex: null,
|
|
||||||
action: "DetachedLibraryWarm",
|
|
||||||
forceRefresh: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RequestDetachedLibraryPreviewRender(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
_ = QueuePreviewGenerationAsync(
|
|
||||||
key,
|
|
||||||
pageIndex: null,
|
|
||||||
action: "DetachedLibraryRender",
|
|
||||||
forceRefresh: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FusedDesktop 支持
|
|
||||||
|
|
||||||
public void RegisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
|
public void RegisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
|
||||||
{
|
{
|
||||||
_fusedLibraryWindow = window;
|
_fusedLibraryWindow = window;
|
||||||
@@ -614,15 +179,4 @@ public partial class MainWindow : Window
|
|||||||
_fusedLibraryWindow = null;
|
_fusedLibraryWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComponentPreviewImageEntry? GetPreviewEntry(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
return ResolvePreviewEntry(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RequestDetachedLibraryPreview(ComponentPreviewKey key)
|
|
||||||
{
|
|
||||||
RequestDetachedLibraryPreviewWarm(key);
|
|
||||||
RequestDetachedLibraryPreviewRender(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1480,13 +1480,11 @@ public partial class MainWindow : Window
|
|||||||
_weatherDataService,
|
_weatherDataService,
|
||||||
_recommendationInfoService,
|
_recommendationInfoService,
|
||||||
_calculatorDataService,
|
_calculatorDataService,
|
||||||
_settingsFacade);
|
_settingsFacade,
|
||||||
|
PlacementId: null,
|
||||||
|
RenderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||||
},
|
},
|
||||||
L,
|
L);
|
||||||
previewKeyResolver: ResolveDetachedLibraryPreviewKey,
|
|
||||||
previewEntryResolver: ResolveDetachedLibraryPreviewEntry,
|
|
||||||
warmPreviewRequested: RequestDetachedLibraryPreviewWarm,
|
|
||||||
renderPreviewRequested: RequestDetachedLibraryPreviewRender);
|
|
||||||
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
|
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
|
||||||
window.Closed += OnDetachedComponentLibraryClosed;
|
window.Closed += OnDetachedComponentLibraryClosed;
|
||||||
return window;
|
return window;
|
||||||
@@ -3620,7 +3618,6 @@ public partial class MainWindow : Window
|
|||||||
var category = _componentLibraryCategories[_componentLibraryCategoryIndex];
|
var category = _componentLibraryCategories[_componentLibraryCategoryIndex];
|
||||||
_componentLibraryActiveCategoryId = category.Id;
|
_componentLibraryActiveCategoryId = category.Id;
|
||||||
_componentLibraryComponentIndex = 0;
|
_componentLibraryComponentIndex = 0;
|
||||||
_ = WarmComponentLibraryCategoryPreviewsAsync(category);
|
|
||||||
BuildComponentLibraryComponentPages(category);
|
BuildComponentLibraryComponentPages(category);
|
||||||
ShowComponentLibraryComponentsView();
|
ShowComponentLibraryComponentsView();
|
||||||
}
|
}
|
||||||
@@ -3638,10 +3635,10 @@ public partial class MainWindow : Window
|
|||||||
var componentCount = _componentLibraryActiveComponents.Count;
|
var componentCount = _componentLibraryActiveComponents.Count;
|
||||||
|
|
||||||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||||||
|
DisposeStaticComponentLibraryPreviews(ComponentLibraryComponentPagesContainer.Children.OfType<Control>());
|
||||||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||||||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||||||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||||||
ClearComponentLibraryPreviewVisualTargets();
|
|
||||||
if (componentCount == 0)
|
if (componentCount == 0)
|
||||||
{
|
{
|
||||||
_componentLibraryComponentIndex = 0;
|
_componentLibraryComponentIndex = 0;
|
||||||
@@ -3715,51 +3712,22 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
||||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||||
var previewKey = CreateComponentTypePreviewKey(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
var previewControl = CreateStaticComponentLibraryPreview(
|
||||||
var cachedPreviewImage = ResolveComponentTypePreviewImage(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
component.ComponentId,
|
||||||
|
previewCellSize,
|
||||||
|
previewWidth,
|
||||||
|
previewHeight);
|
||||||
|
|
||||||
var previewImage = new Image
|
var previewSurface = new Border
|
||||||
{
|
{
|
||||||
Width = previewWidth,
|
Width = previewWidth,
|
||||||
Height = previewHeight,
|
Height = previewHeight,
|
||||||
Stretch = Stretch.Uniform,
|
Background = Brushes.Transparent,
|
||||||
Source = cachedPreviewImage,
|
ClipToBounds = false,
|
||||||
IsVisible = cachedPreviewImage is not null,
|
Child = previewControl,
|
||||||
IsHitTestVisible = false
|
IsHitTestVisible = false
|
||||||
};
|
};
|
||||||
|
|
||||||
var previewFallback = new Border
|
|
||||||
{
|
|
||||||
Width = previewWidth,
|
|
||||||
Height = previewHeight,
|
|
||||||
Background = GetThemeBrush("AdaptiveCardBackgroundBrush"),
|
|
||||||
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)),
|
|
||||||
IsVisible = cachedPreviewImage is null,
|
|
||||||
Child = new TextBlock
|
|
||||||
{
|
|
||||||
Text = L("component_library.preview_loading", "Preparing preview"),
|
|
||||||
FontSize = 11,
|
|
||||||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center
|
|
||||||
}
|
|
||||||
};
|
|
||||||
RegisterComponentLibraryPreviewVisual(previewKey, previewImage, previewFallback);
|
|
||||||
|
|
||||||
var previewSurface = new Grid
|
|
||||||
{
|
|
||||||
Width = previewWidth,
|
|
||||||
Height = previewHeight,
|
|
||||||
IsHitTestVisible = false,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
previewImage,
|
|
||||||
previewFallback
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var previewBorder = new Border
|
var previewBorder = new Border
|
||||||
{
|
{
|
||||||
Width = previewWidth,
|
Width = previewWidth,
|
||||||
@@ -3807,15 +3775,6 @@ public partial class MainWindow : Window
|
|||||||
Grid.SetRow(page, 0);
|
Grid.SetRow(page, 0);
|
||||||
Grid.SetColumn(page, i);
|
Grid.SetColumn(page, i);
|
||||||
ComponentLibraryComponentPagesContainer.Children.Add(page);
|
ComponentLibraryComponentPagesContainer.Children.Add(page);
|
||||||
|
|
||||||
if (cachedPreviewImage is null)
|
|
||||||
{
|
|
||||||
_ = EnsureComponentTypePreviewImageAsync(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ApplyPreviewEntryToEmbeddedVisuals(previewKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
|
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
|
||||||
@@ -3837,10 +3796,10 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||||||
|
DisposeStaticComponentLibraryPreviews(ComponentLibraryComponentPagesContainer.Children.OfType<Control>());
|
||||||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||||||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||||||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||||||
ClearComponentLibraryPreviewVisualTargets();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
|
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_componentLibraryCollapsePresenter.SyncExpandedState(ComponentLibraryWindow.Margin, ComponentLibraryWindow.Opacity);
|
_componentLibraryCollapsePresenter.SyncExpandedState(ComponentLibraryWindow.Margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CollapseComponentLibraryForDesktopEdit(string? title)
|
private void CollapseComponentLibraryForDesktopEdit(string? title)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public partial class MainWindow : Window
|
|||||||
private long _desktopSwipeLastTimestamp;
|
private long _desktopSwipeLastTimestamp;
|
||||||
private double _desktopSwipeVelocityX;
|
private double _desktopSwipeVelocityX;
|
||||||
private double _desktopSwipeBaseOffset;
|
private double _desktopSwipeBaseOffset;
|
||||||
|
private int? _desktopSwipePointerId;
|
||||||
private bool _desktopPageContextInitialized;
|
private bool _desktopPageContextInitialized;
|
||||||
private bool _desktopPageContextEditMode;
|
private bool _desktopPageContextEditMode;
|
||||||
private int _desktopPageContextActiveMask;
|
private int _desktopPageContextActiveMask;
|
||||||
@@ -515,6 +516,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if (isThreeFinger || isRightDrag)
|
if (isThreeFinger || isRightDrag)
|
||||||
{
|
{
|
||||||
|
ClearDesktopPageContextSettle(refreshContext: false);
|
||||||
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁ら梻鍌欑劍鐎笛呯矙閹烘挾鈹嶆繛宸簼閸婂鏌ㄩ弮鍥撳ù婧垮€濋弻娑㈠Ψ閿濆懎顬堝銈忕稻閻擄繝寮婚敓鐘查唶婵犲灚鍔栨缂傚倷绶¢崰鏍矓閻㈢數鐭夐柟鐑橆殔鐎氬鏌涢…鎴濅簻闁衡偓椤撶喓绠鹃悗娑欘焽閻鎮介娑辨疁閽樼喖鏌涘☉娆愮稇闁藉啰鍠栭弻鏇熷緞濡櫣浠紓浣插亾濠㈣埖鍔栭悡鐔兼煃鏉炴媽鍏岄柟鐣屽█閹粙顢涘☉娆戠▏濡炪倖娲╃紞渚€宕洪埀顒併亜閹哄秶鍔嶉柛娆忕箻閹鏁愭惔鈥茬敖闂佽鐏氶崝鎴﹀蓟? ClearDesktopPageContextSettle(refreshContext: false);
|
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁ら梻鍌欑劍鐎笛呯矙閹烘挾鈹嶆繛宸簼閸婂鏌ㄩ弮鍥撳ù婧垮€濋弻娑㈠Ψ閿濆懎顬堝銈忕稻閻擄繝寮婚敓鐘查唶婵犲灚鍔栨缂傚倷绶¢崰鏍矓閻㈢數鐭夐柟鐑橆殔鐎氬鏌涢…鎴濅簻闁衡偓椤撶喓绠鹃悗娑欘焽閻鎮介娑辨疁閽樼喖鏌涘☉娆愮稇闁藉啰鍠栭弻鏇熷緞濡櫣浠紓浣插亾濠㈣埖鍔栭悡鐔兼煃鏉炴媽鍏岄柟鐣屽█閹粙顢涘☉娆戠▏濡炪倖娲╃紞渚€宕洪埀顒併亜閹哄秶鍔嶉柛娆忕箻閹鏁愭惔鈥茬敖闂佽鐏氶崝鎴﹀蓟? ClearDesktopPageContextSettle(refreshContext: false);
|
||||||
_isThreeFingerOrRightDragSwipeActive = true;
|
_isThreeFingerOrRightDragSwipeActive = true;
|
||||||
_isDesktopSwipeActive = true;
|
_isDesktopSwipeActive = true;
|
||||||
@@ -525,6 +527,8 @@ public partial class MainWindow : Window
|
|||||||
_desktopSwipeVelocityX = 0;
|
_desktopSwipeVelocityX = 0;
|
||||||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||||||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||||||
|
_desktopSwipePointerId = pointerId;
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
// 闂傚倷绀侀幖顐ょ矓閺夋嚚娲煛閸滀焦鏅╅梺鎼炲劘閸斿酣銆呴弻銉﹀€甸柨婵嗗€瑰▍鍡樸亜閹邦喗娅曢柍褜鍓涢幊鎾诲箟闄囬妵鎰板礃椤斻垹娲崺锟犲川椤旈棿鍝楅梻浣虹《濡插懘宕㈤崜褏鐭嗗鑸靛姈閳锋帡鏌涢幇鈺佸缂佺嫏鍕╀簻闁圭儤鎸鹃妴鎺旂磼鏉堛劌娴€规洜鍠栭、鏃堝椽娴i晲缂撻梻鍌欑閹诧紕鎹㈤崒婊呯煋閻庡灚鐡曟慨? e.Handled = true;
|
// 闂傚倷绀侀幖顐ょ矓閺夋嚚娲煛閸滀焦鏅╅梺鎼炲劘閸斿酣銆呴弻銉﹀€甸柨婵嗗€瑰▍鍡樸亜閹邦喗娅曢柍褜鍓涢幊鎾诲箟闄囬妵鎰板礃椤斻垹娲崺锟犲川椤旈棿鍝楅梻浣虹《濡插懘宕㈤崜褏鐭嗗鑸靛姈閳锋帡鏌涢幇鈺佸缂佺嫏鍕╀簻闁圭儤鎸鹃妴鎺旂磼鏉堛劌娴€规洜鍠栭、鏃堝椽娴i晲缂撻梻鍌欑閹诧紕鎹㈤崒婊呯煋閻庡灚鐡曟慨? e.Handled = true;
|
||||||
return;
|
return;
|
||||||
@@ -532,6 +536,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 闂傚倷绀侀幉锟犫€﹂崶顒€绐楅柟閭﹀墾閼板灝銆掑锝呬壕閻庤娲╃换婵嗩嚕閹绢喗鍋勫瀣閳诲本绻濋悽闈浶㈤柨鏇樺劦瀹曞綊宕归锝呭伎闂佸啿鎼幊蹇涙倿婵犳碍鐓涢柛鏇ㄥ亞缁犳娊鎮? if (IsInteractivePointerSource(e.Source))
|
// 闂傚倷绀侀幉锟犫€﹂崶顒€绐楅柟閭﹀墾閼板灝銆掑锝呬壕閻庤娲╃换婵嗩嚕閹绢喗鍋勫瀣閳诲本绻濋悽闈浶㈤柨鏇樺劦瀹曞綊宕归锝呭伎闂佸啿鎼幊蹇涙倿婵犳碍鐓涢柛鏇ㄥ亞缁犳娊鎮? if (IsInteractivePointerSource(e.Source))
|
||||||
|
if (IsInteractivePointerSource(e.Source))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -555,6 +560,13 @@ public partial class MainWindow : Window
|
|||||||
_desktopSwipeVelocityX = 0;
|
_desktopSwipeVelocityX = 0;
|
||||||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||||||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||||||
|
_desktopSwipePointerId = pointerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDesktopSwipePointer(IPointer? pointer)
|
||||||
|
{
|
||||||
|
return !_desktopSwipePointerId.HasValue ||
|
||||||
|
pointer is not null && pointer.Id == _desktopSwipePointerId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInteractivePointerSource(object? source)
|
private static bool IsInteractivePointerSource(object? source)
|
||||||
@@ -736,6 +748,11 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void OnDesktopPagesPointerMoved(object? sender, PointerEventArgs e)
|
private void OnDesktopPagesPointerMoved(object? sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_isDesktopSwipeActive && !IsDesktopSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_isDesktopSwipeActive || !TryGetPointerPositionInDesktopViewport(e, out var pointerInViewport))
|
if (!_isDesktopSwipeActive || !TryGetPointerPositionInDesktopViewport(e, out var pointerInViewport))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -797,6 +814,11 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
var pointerId = e.Pointer?.Id ?? 0;
|
var pointerId = e.Pointer?.Id ?? 0;
|
||||||
_activePointerIds.Remove(pointerId);
|
_activePointerIds.Remove(pointerId);
|
||||||
|
|
||||||
|
if (_isDesktopSwipeActive && !IsDesktopSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (EndDesktopSwipeInteraction(e.Pointer))
|
if (EndDesktopSwipeInteraction(e.Pointer))
|
||||||
{
|
{
|
||||||
@@ -808,7 +830,17 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
var pointerId = e.Pointer?.Id ?? 0;
|
var pointerId = e.Pointer?.Id ?? 0;
|
||||||
_activePointerIds.Remove(pointerId);
|
_activePointerIds.Remove(pointerId);
|
||||||
|
|
||||||
|
if (!_isDesktopSwipeActive || !IsDesktopSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Pointer?.Captured == DesktopPagesViewport)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
EndDesktopSwipeInteraction(e.Pointer);
|
EndDesktopSwipeInteraction(e.Pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,6 +861,7 @@ public partial class MainWindow : Window
|
|||||||
_isDesktopSwipeDirectionLocked = false;
|
_isDesktopSwipeDirectionLocked = false;
|
||||||
_isThreeFingerOrRightDragSwipeActive = false;
|
_isThreeFingerOrRightDragSwipeActive = false;
|
||||||
_activePointerIds.Clear();
|
_activePointerIds.Clear();
|
||||||
|
_desktopSwipePointerId = null;
|
||||||
_desktopSwipeVelocityX = 0;
|
_desktopSwipeVelocityX = 0;
|
||||||
_desktopSwipeLastTimestamp = 0;
|
_desktopSwipeLastTimestamp = 0;
|
||||||
if (wasDirectionLocked)
|
if (wasDirectionLocked)
|
||||||
@@ -851,6 +884,7 @@ public partial class MainWindow : Window
|
|||||||
_isDesktopSwipeDirectionLocked = false;
|
_isDesktopSwipeDirectionLocked = false;
|
||||||
_isThreeFingerOrRightDragSwipeActive = false;
|
_isThreeFingerOrRightDragSwipeActive = false;
|
||||||
_activePointerIds.Clear();
|
_activePointerIds.Clear();
|
||||||
|
_desktopSwipePointerId = null;
|
||||||
|
|
||||||
if (pointer?.Captured == DesktopPagesViewport)
|
if (pointer?.Captured == DesktopPagesViewport)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -225,14 +225,6 @@
|
|||||||
<Canvas x:Name="DesktopEditDragLayer"
|
<Canvas x:Name="DesktopEditDragLayer"
|
||||||
IsHitTestVisible="False" />
|
IsHitTestVisible="False" />
|
||||||
|
|
||||||
<Canvas x:Name="ComponentPreviewStagingHost"
|
|
||||||
Width="1"
|
|
||||||
Height="1"
|
|
||||||
Opacity="0"
|
|
||||||
ClipToBounds="True"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
IsHitTestVisible="False" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -627,7 +619,7 @@
|
|||||||
<Border x:Name="ComponentLibraryWindow"
|
<Border x:Name="ComponentLibraryWindow"
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Opacity="0"
|
Opacity="0"
|
||||||
Classes="surface-translucent-strong"
|
Background="Transparent"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Width="620"
|
Width="620"
|
||||||
@@ -636,8 +628,6 @@
|
|||||||
Height="320"
|
Height="320"
|
||||||
MinHeight="260"
|
MinHeight="260"
|
||||||
Margin="24,24,24,100"
|
Margin="24,24,24,100"
|
||||||
CornerRadius="36"
|
|
||||||
Padding="14"
|
|
||||||
PointerPressed="OnComponentLibraryWindowPointerPressed"
|
PointerPressed="OnComponentLibraryWindowPointerPressed"
|
||||||
PointerMoved="OnComponentLibraryWindowPointerMoved"
|
PointerMoved="OnComponentLibraryWindowPointerMoved"
|
||||||
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
||||||
@@ -647,142 +637,146 @@
|
|||||||
</Transitions>
|
</Transitions>
|
||||||
</Border.Transitions>
|
</Border.Transitions>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*"
|
<Border Classes="surface-translucent-strong"
|
||||||
RowSpacing="10">
|
CornerRadius="36"
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
Padding="14">
|
||||||
<TextBlock x:Name="ComponentLibraryTitleTextBlock"
|
<Grid RowDefinitions="Auto,*"
|
||||||
VerticalAlignment="Center"
|
RowSpacing="10">
|
||||||
FontSize="16"
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
FontWeight="SemiBold"
|
<TextBlock x:Name="ComponentLibraryTitleTextBlock"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
VerticalAlignment="Center"
|
||||||
Text="Widgets" />
|
FontSize="16"
|
||||||
<Button x:Name="CloseComponentLibraryButton"
|
FontWeight="SemiBold"
|
||||||
Grid.Column="1"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
Padding="8"
|
Text="Widgets" />
|
||||||
Width="32"
|
<Button x:Name="CloseComponentLibraryButton"
|
||||||
Height="32"
|
Grid.Column="1"
|
||||||
Background="Transparent"
|
Padding="8"
|
||||||
BorderThickness="0"
|
Width="32"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Height="32"
|
||||||
Click="OnCloseComponentLibraryClick">
|
Background="Transparent"
|
||||||
<fi:SymbolIcon Classes="icon-s"
|
BorderThickness="0"
|
||||||
Symbol="Dismiss"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
IconVariant="Regular" />
|
Click="OnCloseComponentLibraryClick">
|
||||||
</Button>
|
<fi:SymbolIcon Classes="icon-s"
|
||||||
</Grid>
|
Symbol="Dismiss"
|
||||||
|
IconVariant="Regular" />
|
||||||
<Border Grid.Row="1"
|
</Button>
|
||||||
Classes="surface-translucent-panel"
|
|
||||||
CornerRadius="12"
|
|
||||||
Padding="14">
|
|
||||||
<Grid>
|
|
||||||
<!-- Category picker (outer) -->
|
|
||||||
<Grid x:Name="ComponentLibraryCategoriesView">
|
|
||||||
<Grid RowDefinitions="*">
|
|
||||||
<Border x:Name="ComponentLibraryCategoryViewport"
|
|
||||||
Background="Transparent"
|
|
||||||
ClipToBounds="True">
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
|
||||||
<Grid x:Name="ComponentLibraryCategoryPagesHost"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Top">
|
|
||||||
<Grid x:Name="ComponentLibraryCategoryPagesContainer" />
|
|
||||||
</Grid>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<TextBlock x:Name="ComponentLibraryEmptyTextBlock"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
|
||||||
Text="No components." />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Component picker (inner) -->
|
|
||||||
<Grid x:Name="ComponentLibraryComponentsView"
|
|
||||||
IsVisible="False"
|
|
||||||
RowDefinitions="Auto,*"
|
|
||||||
RowSpacing="10">
|
|
||||||
<Button x:Name="ComponentLibraryBackButton"
|
|
||||||
Grid.Row="0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Padding="8,6"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderThickness="0"
|
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
|
||||||
Click="OnComponentLibraryBackClick">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<fi:SymbolIcon Classes="icon-s" Symbol="ArrowLeft" IconVariant="Regular" />
|
|
||||||
<TextBlock x:Name="ComponentLibraryBackTextBlock"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="Back" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
|
||||||
ColumnDefinitions="Auto,*,Auto"
|
|
||||||
ColumnSpacing="8">
|
|
||||||
<Button x:Name="ComponentLibraryPrevComponentButton"
|
|
||||||
Grid.Column="0"
|
|
||||||
Width="36"
|
|
||||||
Height="36"
|
|
||||||
Padding="0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
CornerRadius="18"
|
|
||||||
Click="OnComponentLibraryPrevComponentClick"
|
|
||||||
IsVisible="False">
|
|
||||||
<fi:SymbolIcon Symbol="ChevronLeft"
|
|
||||||
IconVariant="Regular" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Border x:Name="ComponentLibraryComponentViewport"
|
|
||||||
Grid.Column="1"
|
|
||||||
Background="Transparent"
|
|
||||||
ClipToBounds="True"
|
|
||||||
PointerPressed="OnComponentLibraryComponentViewportPointerPressed"
|
|
||||||
PointerMoved="OnComponentLibraryComponentViewportPointerMoved"
|
|
||||||
PointerReleased="OnComponentLibraryComponentViewportPointerReleased"
|
|
||||||
PointerCaptureLost="OnComponentLibraryComponentViewportPointerCaptureLost">
|
|
||||||
<Grid>
|
|
||||||
<Grid x:Name="ComponentLibraryComponentPagesHost"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Top">
|
|
||||||
<Grid.RenderTransform>
|
|
||||||
<TranslateTransform>
|
|
||||||
<TranslateTransform.Transitions>
|
|
||||||
<Transitions>
|
|
||||||
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
|
||||||
</Transitions>
|
|
||||||
</TranslateTransform.Transitions>
|
|
||||||
</TranslateTransform>
|
|
||||||
</Grid.RenderTransform>
|
|
||||||
|
|
||||||
<Grid x:Name="ComponentLibraryComponentPagesContainer" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Button x:Name="ComponentLibraryNextComponentButton"
|
|
||||||
Grid.Column="2"
|
|
||||||
Width="36"
|
|
||||||
Height="36"
|
|
||||||
Padding="0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
CornerRadius="18"
|
|
||||||
Click="OnComponentLibraryNextComponentClick"
|
|
||||||
IsVisible="False">
|
|
||||||
<fi:SymbolIcon Symbol="ChevronRight"
|
|
||||||
IconVariant="Regular" />
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
|
||||||
</Grid>
|
<Border Grid.Row="1"
|
||||||
|
Classes="surface-translucent-panel"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="14">
|
||||||
|
<Grid>
|
||||||
|
<!-- Category picker (outer) -->
|
||||||
|
<Grid x:Name="ComponentLibraryCategoriesView">
|
||||||
|
<Grid RowDefinitions="*">
|
||||||
|
<Border x:Name="ComponentLibraryCategoryViewport"
|
||||||
|
Background="Transparent"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<Grid x:Name="ComponentLibraryCategoryPagesHost"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<Grid x:Name="ComponentLibraryCategoryPagesContainer" />
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock x:Name="ComponentLibraryEmptyTextBlock"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||||
|
Text="No components." />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Component picker (inner) -->
|
||||||
|
<Grid x:Name="ComponentLibraryComponentsView"
|
||||||
|
IsVisible="False"
|
||||||
|
RowDefinitions="Auto,*"
|
||||||
|
RowSpacing="10">
|
||||||
|
<Button x:Name="ComponentLibraryBackButton"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Padding="8,6"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
|
Click="OnComponentLibraryBackClick">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<fi:SymbolIcon Classes="icon-s" Symbol="ArrowLeft" IconVariant="Regular" />
|
||||||
|
<TextBlock x:Name="ComponentLibraryBackTextBlock"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Back" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
|
ColumnSpacing="8">
|
||||||
|
<Button x:Name="ComponentLibraryPrevComponentButton"
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="36"
|
||||||
|
Height="36"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
CornerRadius="18"
|
||||||
|
Click="OnComponentLibraryPrevComponentClick"
|
||||||
|
IsVisible="False">
|
||||||
|
<fi:SymbolIcon Symbol="ChevronLeft"
|
||||||
|
IconVariant="Regular" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Border x:Name="ComponentLibraryComponentViewport"
|
||||||
|
Grid.Column="1"
|
||||||
|
Background="Transparent"
|
||||||
|
ClipToBounds="True"
|
||||||
|
PointerPressed="OnComponentLibraryComponentViewportPointerPressed"
|
||||||
|
PointerMoved="OnComponentLibraryComponentViewportPointerMoved"
|
||||||
|
PointerReleased="OnComponentLibraryComponentViewportPointerReleased"
|
||||||
|
PointerCaptureLost="OnComponentLibraryComponentViewportPointerCaptureLost">
|
||||||
|
<Grid>
|
||||||
|
<Grid x:Name="ComponentLibraryComponentPagesHost"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<TranslateTransform>
|
||||||
|
<TranslateTransform.Transitions>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
|
</Transitions>
|
||||||
|
</TranslateTransform.Transitions>
|
||||||
|
</TranslateTransform>
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
|
||||||
|
<Grid x:Name="ComponentLibraryComponentPagesContainer" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button x:Name="ComponentLibraryNextComponentButton"
|
||||||
|
Grid.Column="2"
|
||||||
|
Width="36"
|
||||||
|
Height="36"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
CornerRadius="18"
|
||||||
|
Click="OnComponentLibraryNextComponentClick"
|
||||||
|
IsVisible="False">
|
||||||
|
<fi:SymbolIcon Symbol="ChevronRight"
|
||||||
|
IconVariant="Regular" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border x:Name="ComponentLibraryCollapsedChipHost"
|
<Border x:Name="ComponentLibraryCollapsedChipHost"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
private Point _swipeLastPoint;
|
private Point _swipeLastPoint;
|
||||||
private double _swipeVelocityX;
|
private double _swipeVelocityX;
|
||||||
private long _swipeLastTimestamp;
|
private long _swipeLastTimestamp;
|
||||||
|
private int? _swipePointerId;
|
||||||
|
|
||||||
// 三指/右键拖动状态
|
// 三指/右键拖动状态
|
||||||
private bool _isThreeFingerOrRightDragSwipeActive;
|
private bool _isThreeFingerOrRightDragSwipeActive;
|
||||||
@@ -624,6 +625,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
_swipeLastPoint = pointerPos;
|
_swipeLastPoint = pointerPos;
|
||||||
_swipeVelocityX = 0;
|
_swipeVelocityX = 0;
|
||||||
_swipeLastTimestamp = Stopwatch.GetTimestamp();
|
_swipeLastTimestamp = Stopwatch.GetTimestamp();
|
||||||
|
_swipePointerId = pointerId;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -634,6 +636,12 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
|
|
||||||
protected override void OnPointerMoved(PointerEventArgs e)
|
protected override void OnPointerMoved(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_isSwipeActive && !IsSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
base.OnPointerMoved(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_isSwipeActive)
|
if (!_isSwipeActive)
|
||||||
{
|
{
|
||||||
base.OnPointerMoved(e);
|
base.OnPointerMoved(e);
|
||||||
@@ -686,6 +694,12 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
{
|
{
|
||||||
var pointerId = e.Pointer?.Id ?? 0;
|
var pointerId = e.Pointer?.Id ?? 0;
|
||||||
_activePointerIds.Remove(pointerId);
|
_activePointerIds.Remove(pointerId);
|
||||||
|
|
||||||
|
if (_isSwipeActive && !IsSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
base.OnPointerReleased(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_isSwipeActive)
|
if (_isSwipeActive)
|
||||||
{
|
{
|
||||||
@@ -703,7 +717,19 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
{
|
{
|
||||||
var pointerId = e.Pointer?.Id ?? 0;
|
var pointerId = e.Pointer?.Id ?? 0;
|
||||||
_activePointerIds.Remove(pointerId);
|
_activePointerIds.Remove(pointerId);
|
||||||
|
|
||||||
|
if (_isSwipeActive && !IsSwipePointer(e.Pointer))
|
||||||
|
{
|
||||||
|
base.OnPointerCaptureLost(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isSwipeActive && e.Pointer?.Captured == this)
|
||||||
|
{
|
||||||
|
base.OnPointerCaptureLost(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_isSwipeActive)
|
if (_isSwipeActive)
|
||||||
{
|
{
|
||||||
EndSwipeInteraction(e.Pointer);
|
EndSwipeInteraction(e.Pointer);
|
||||||
@@ -725,6 +751,12 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSwipePointer(IPointer? pointer)
|
||||||
|
{
|
||||||
|
return !_swipePointerId.HasValue ||
|
||||||
|
pointer is not null && pointer.Id == _swipePointerId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateSwipeVelocity(Point currentPoint)
|
private void UpdateSwipeVelocity(Point currentPoint)
|
||||||
{
|
{
|
||||||
@@ -754,6 +786,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
_isSwipeDirectionLocked = false;
|
_isSwipeDirectionLocked = false;
|
||||||
_isThreeFingerOrRightDragSwipeActive = false;
|
_isThreeFingerOrRightDragSwipeActive = false;
|
||||||
_activePointerIds.Clear();
|
_activePointerIds.Clear();
|
||||||
|
_swipePointerId = null;
|
||||||
_swipeVelocityX = 0;
|
_swipeVelocityX = 0;
|
||||||
_swipeLastTimestamp = 0;
|
_swipeLastTimestamp = 0;
|
||||||
}
|
}
|
||||||
@@ -769,6 +802,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
_isSwipeDirectionLocked = false;
|
_isSwipeDirectionLocked = false;
|
||||||
_isThreeFingerOrRightDragSwipeActive = false;
|
_isThreeFingerOrRightDragSwipeActive = false;
|
||||||
_activePointerIds.Clear();
|
_activePointerIds.Clear();
|
||||||
|
_swipePointerId = null;
|
||||||
|
|
||||||
if (pointer?.Captured == this)
|
if (pointer?.Captured == this)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user