From 1fad0005bddd7e169b32dd24d31a2cc0d9fdb110 Mon Sep 17 00:00:00 2001 From: lincube Date: Wed, 4 Mar 2026 12:25:07 +0800 Subject: [PATCH] 0.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 天气组件又在优化调整。调整了噪音监测组件。 --- .github/workflows/build.yml | 4 +- .github/workflows/code-quality.yml | 5 +- .github/workflows/release.yml | 12 +- .../Components/StudyNoiseCurveWidget.axaml | 24 +- .../Components/StudyNoiseCurveWidget.axaml.cs | 259 +++++++++++++++--- 5 files changed, 251 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 574c587..2f346f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,10 +31,10 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore - run: dotnet restore + run: dotnet restore ${{ env.Solution_Name }} - name: Build - run: dotnet build --no-restore -c ${{ matrix.configuration }} -v minimal + run: dotnet build ${{ env.Solution_Name }} --no-restore -c ${{ matrix.configuration }} -v minimal - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 0b20c63..ed193f7 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -8,6 +8,7 @@ on: env: DOTNET_VERSION: '10.0.x' + Solution_Name: LanMontainDesktop.sln jobs: analyze: @@ -31,10 +32,10 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore - run: dotnet restore + run: dotnet restore ${{ env.Solution_Name }} - name: Build - run: dotnet build -c Release --no-restore -v minimal + run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal - name: Check formatting run: dotnet format --verify-no-changes --verbosity diagnostic || true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 237c01a..a1a43e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,10 +62,10 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore - run: dotnet restore + run: dotnet restore ${{ env.Solution_Name }} - name: Build - run: dotnet build -c Release --no-restore -v minimal + run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal - name: Publish run: | @@ -127,10 +127,10 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore - run: dotnet restore + run: dotnet restore ${{ env.Solution_Name }} - name: Build - run: dotnet build -c Release --no-restore -v minimal + run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal - name: Publish run: | @@ -183,10 +183,10 @@ jobs: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore - run: dotnet restore + run: dotnet restore ${{ env.Solution_Name }} - name: Build - run: dotnet build -c Release --no-restore -v minimal + run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal - name: Publish run: | diff --git a/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml b/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml index f434506..028ca4b 100644 --- a/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml +++ b/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml @@ -17,12 +17,22 @@ - + + + diff --git a/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs b/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs index 55e07af..ba1d18f 100644 --- a/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs +++ b/LanMontainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using Avalonia; using Avalonia.Controls; @@ -6,11 +7,27 @@ using Avalonia.Media; using Avalonia.Threading; using LanMontainDesktop.Models; using LanMontainDesktop.Services; +using LanMontainDesktop.Theme; namespace LanMontainDesktop.Views.Components; public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget { + private const double NormalTextMinContrast = 4.5; + private const double LargeTextMinContrast = 3.0; + + private static readonly Color[] LightToneCandidates = + { + Color.Parse("#FFFFFFFF"), + Color.Parse("#FFF8FCFF"), + Color.Parse("#FFF0F7FF"), + Color.Parse("#FFE8F3FF"), + Color.Parse("#FFE0EEFF") + }; + + private static readonly Color DarkSubstrate = Color.Parse("#FF0B1220"); + private static readonly Color LightSubstrate = Color.Parse("#FFF1F5FA"); + private readonly object _snapshotSync = new(); private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault(); private readonly AppSettingsService _settingsService = new(); @@ -29,6 +46,14 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge private bool _isSubscribed; private int _framesSinceCompaction; + private enum StatusVisualKind + { + Default = 0, + Quiet = 1, + Noisy = 2, + Error = 3 + } + public StudyNoiseCurveWidget() { InitializeComponent(); @@ -41,6 +66,8 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge ReloadLanguageCode(); ApplyCellSize(_currentCellSize); ApplyDefaultXAxisLabels(); + ApplyTypographyByBackground(ResolvePanelBackgroundColor()); + ApplyStatusBadgeStyle(StatusVisualKind.Default, ResolvePanelBackgroundColor()); } public void ApplyCellSize(double cellSize) @@ -53,10 +80,16 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge Math.Clamp(14 * scale, 8, 22), Math.Clamp(10 * scale, 6, 16)); - StatusTextBlock.FontSize = Math.Clamp(16 * scale, 11, 30); - RealtimeValueTextBlock.FontSize = Math.Clamp(18 * scale, 11, 34); + StatusTextBlock.FontSize = Math.Clamp(16 * scale, 12, 30); + RealtimeValueTextBlock.FontSize = Math.Clamp(18 * scale, 12, 34); - var axisFontSize = Math.Clamp(10 * scale, 8.5, 18); + StatusBadgeBorder.Padding = new Thickness( + Math.Clamp(8 * scale, 4, 11), + Math.Clamp(3 * scale, 2, 6)); + StatusBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 5, 12)); + StatusBadgeBorder.BorderThickness = new Thickness(Math.Clamp(1 * scale, 0.8, 1.5)); + + var axisFontSize = Math.Clamp(10 * scale, 9.5, 18); YTopTextBlock.FontSize = axisFontSize; YUpperTextBlock.FontSize = axisFontSize; YMiddleTextBlock.FontSize = axisFontSize; @@ -111,6 +144,8 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge private void OnSizeChanged(object? sender, SizeChangedEventArgs e) { ApplyCellSize(_currentCellSize); + var panelColor = ResolvePanelBackgroundColor(); + ApplyTypographyByBackground(panelColor); } private void OnStudySnapshotUpdated(object? sender, StudyAnalyticsSnapshotChangedEventArgs e) @@ -165,8 +200,12 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge private void ApplySnapshot(StudyAnalyticsSnapshot snapshot) { + var panelColor = ResolvePanelBackgroundColor(); + ApplyTypographyByBackground(panelColor); + + var statusKind = ResolveStatusVisualKind(snapshot); StatusTextBlock.Text = ResolveStatusText(snapshot); - StatusTextBlock.Foreground = ResolveStatusBrush(snapshot); + ApplyStatusBadgeStyle(statusKind, panelColor); if (snapshot.LatestRealtimePoint is { } latestPoint) { @@ -184,6 +223,186 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge UpdateXAxisLabels(snapshot); } + private void ApplyTypographyByBackground(Color panelColor) + { + var samples = BuildPanelBackgroundSamples(panelColor); + var primaryBrush = CreateAdaptiveLightBrush(samples, LargeTextMinContrast, preferredAlpha: 0xF6); + var secondaryBrush = CreateAdaptiveLightBrush(samples, NormalTextMinContrast, preferredAlpha: 0xDF); + + RealtimeValueTextBlock.Foreground = primaryBrush; + YTopTextBlock.Foreground = secondaryBrush; + YUpperTextBlock.Foreground = secondaryBrush; + YMiddleTextBlock.Foreground = secondaryBrush; + YLowerTextBlock.Foreground = secondaryBrush; + YBottomTextBlock.Foreground = secondaryBrush; + XLeftTextBlock.Foreground = secondaryBrush; + XCenterTextBlock.Foreground = secondaryBrush; + XRightTextBlock.Foreground = secondaryBrush; + } + + private void ApplyStatusBadgeStyle(StatusVisualKind kind, Color panelColor) + { + var badgeBaseColor = kind switch + { + StatusVisualKind.Quiet => Color.Parse("#FF0F6B49"), + StatusVisualKind.Noisy => Color.Parse("#FF805018"), + StatusVisualKind.Error => Color.Parse("#FF8D2A3A"), + _ => Color.Parse("#FF213547") + }; + + var panelLuminance = RelativeLuminance(ToOpaqueAgainst(panelColor, DarkSubstrate)); + var badgeAlpha = panelLuminance > 0.58 + ? (byte)0xE6 + : panelLuminance > 0.46 + ? (byte)0xDB + : (byte)0xCC; + + var badgeColor = Color.FromArgb(badgeAlpha, badgeBaseColor.R, badgeBaseColor.G, badgeBaseColor.B); + var badgeComposite = ToOpaqueAgainst(badgeColor, ToOpaqueAgainst(panelColor, DarkSubstrate)); + + StatusBadgeBorder.Background = new SolidColorBrush(badgeColor); + StatusBadgeBorder.BorderBrush = new SolidColorBrush(Color.FromArgb(0x96, 0xFF, 0xFF, 0xFF)); + StatusTextBlock.Foreground = CreateAdaptiveLightBrush(new[] { badgeComposite }, NormalTextMinContrast, preferredAlpha: 0xFF); + } + + private static StatusVisualKind ResolveStatusVisualKind(StudyAnalyticsSnapshot snapshot) + { + if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported || + snapshot.State == StudyAnalyticsRuntimeState.Error || + snapshot.StreamStatus == NoiseStreamStatus.Error) + { + return StatusVisualKind.Error; + } + + if (snapshot.StreamStatus == NoiseStreamStatus.Noisy) + { + return StatusVisualKind.Noisy; + } + + if (snapshot.State == StudyAnalyticsRuntimeState.Running && snapshot.StreamStatus == NoiseStreamStatus.Quiet) + { + return StatusVisualKind.Quiet; + } + + return StatusVisualKind.Default; + } + + private Color ResolvePanelBackgroundColor() + { + if (RootBorder.Background is ISolidColorBrush solidBackground) + { + return solidBackground.Color; + } + + if (TryGetResource("AdaptiveGlassStrongBackgroundBrush", null, out var resource) && + resource is ISolidColorBrush solidBrush) + { + return solidBrush.Color; + } + + return Color.Parse("#FF1E293B"); + } + + private static IReadOnlyList BuildPanelBackgroundSamples(Color panelColor) + { + var opaqueOnDark = ToOpaqueAgainst(panelColor, DarkSubstrate); + var opaqueOnLight = ToOpaqueAgainst(panelColor, LightSubstrate); + + return new[] + { + opaqueOnDark, + opaqueOnLight, + ColorMath.Blend(opaqueOnDark, DarkSubstrate, 0.28), + ColorMath.Blend(opaqueOnDark, Color.Parse("#FFFFFFFF"), 0.16), + ColorMath.Blend(opaqueOnLight, Color.Parse("#FFFFFFFF"), 0.08), + ColorMath.Blend(opaqueOnLight, DarkSubstrate, 0.18) + }; + } + + private static SolidColorBrush CreateAdaptiveLightBrush( + IReadOnlyList backgroundSamples, + double minContrast, + byte preferredAlpha) + { + var alphaCandidates = new byte[] + { + preferredAlpha, + (byte)Math.Clamp(preferredAlpha + 20, 0, 255), + 0xFF + }; + + for (var alphaIndex = 0; alphaIndex < alphaCandidates.Length; alphaIndex++) + { + var alpha = alphaCandidates[alphaIndex]; + for (var toneIndex = 0; toneIndex < LightToneCandidates.Length; toneIndex++) + { + var tone = LightToneCandidates[toneIndex]; + var candidate = Color.FromArgb(alpha, tone.R, tone.G, tone.B); + if (MinContrastRatio(candidate, backgroundSamples) >= minContrast) + { + return new SolidColorBrush(candidate); + } + } + } + + return new SolidColorBrush(Color.Parse("#FFFFFFFF")); + } + + private static double MinContrastRatio(Color foreground, IReadOnlyList backgrounds) + { + if (backgrounds.Count == 0) + { + return 21; + } + + var minimum = double.MaxValue; + for (var i = 0; i < backgrounds.Count; i++) + { + var background = backgrounds[i]; + var visibleForeground = foreground.A >= 0xFF + ? Color.FromArgb(0xFF, foreground.R, foreground.G, foreground.B) + : ToOpaqueAgainst(foreground, background); + + var ratio = ColorMath.ContrastRatio(visibleForeground, background); + if (ratio < minimum) + { + minimum = ratio; + } + } + + return minimum; + } + + private static Color ToOpaqueAgainst(Color foreground, Color background) + { + if (foreground.A >= 0xFF) + { + return Color.FromArgb(0xFF, foreground.R, foreground.G, foreground.B); + } + + var alpha = foreground.A / 255d; + var red = (byte)Math.Round((foreground.R * alpha) + (background.R * (1 - alpha))); + var green = (byte)Math.Round((foreground.G * alpha) + (background.G * (1 - alpha))); + var blue = (byte)Math.Round((foreground.B * alpha) + (background.B * (1 - alpha))); + return Color.FromArgb(0xFF, red, green, blue); + } + + private static double RelativeLuminance(Color color) + { + static double ToLinear(byte channel) + { + var c = channel / 255d; + return c <= 0.03928 + ? c / 12.92 + : Math.Pow((c + 0.055) / 1.055, 2.4); + } + + var r = ToLinear(color.R); + var g = ToLinear(color.G); + var b = ToLinear(color.B); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + private void UpdateXAxisLabels(StudyAnalyticsSnapshot snapshot) { var buffer = snapshot.RealtimeBuffer; @@ -249,28 +468,6 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge return L("study.environment.status.initializing", "初始化中"); } - private IBrush ResolveStatusBrush(StudyAnalyticsSnapshot snapshot) - { - if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported || - snapshot.State == StudyAnalyticsRuntimeState.Error || - snapshot.StreamStatus == NoiseStreamStatus.Error) - { - return new SolidColorBrush(Color.Parse("#FFFF9D9D")); - } - - if (snapshot.StreamStatus == NoiseStreamStatus.Noisy) - { - return new SolidColorBrush(Color.Parse("#FFFFD791")); - } - - if (snapshot.State == StudyAnalyticsRuntimeState.Running && snapshot.StreamStatus == NoiseStreamStatus.Quiet) - { - return new SolidColorBrush(Color.Parse("#FFCBFFE8")); - } - - return TryResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FF1E293B"); - } - private void ReloadLanguageCode() { var snapshot = _settingsService.Load(); @@ -281,14 +478,4 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge { return _localizationService.GetString(_languageCode, key, fallback); } - - private IBrush TryResolveThemeBrush(string resourceKey, string fallbackHex) - { - if (TryGetResource(resourceKey, null, out var resource) && resource is IBrush brush) - { - return brush; - } - - return new SolidColorBrush(Color.Parse(fallbackHex)); - } }