mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
@@ -4,6 +4,5 @@
|
|||||||
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
|
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
|
||||||
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
|
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
|
||||||
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
|
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
|
||||||
<DefaultItemExcludes>$(DefaultItemExcludes);**\obj_audit\**</DefaultItemExcludes>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Media;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
|
|
||||||
public readonly record struct ComponentAdaptiveTextLayout(
|
|
||||||
double FontSize,
|
|
||||||
FontWeight Weight,
|
|
||||||
int MaxLines,
|
|
||||||
double LineHeight,
|
|
||||||
double OverflowScore,
|
|
||||||
bool FitsCompletely,
|
|
||||||
Size MeasuredSize)
|
|
||||||
{
|
|
||||||
public double MeasuredWidth => MeasuredSize.Width;
|
|
||||||
|
|
||||||
public double MeasuredHeight => MeasuredSize.Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly record struct ComponentBoxLayout(
|
|
||||||
double Width,
|
|
||||||
double Height,
|
|
||||||
Thickness Margin,
|
|
||||||
Thickness Padding)
|
|
||||||
{
|
|
||||||
public double Size => Math.Max(Width, Height);
|
|
||||||
|
|
||||||
public bool IsSquare => Math.Abs(Width - Height) <= 0.001d;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
|
|
||||||
public static class ComponentTypographyLayoutService
|
|
||||||
{
|
|
||||||
public static Size MeasureTextSize(
|
|
||||||
string? text,
|
|
||||||
double fontSize,
|
|
||||||
FontWeight weight,
|
|
||||||
double maxWidth,
|
|
||||||
double lineHeight,
|
|
||||||
FontFamily? fontFamily = null)
|
|
||||||
{
|
|
||||||
var probe = new TextBlock
|
|
||||||
{
|
|
||||||
Text = NormalizeText(text),
|
|
||||||
FontSize = Math.Max(1, fontSize),
|
|
||||||
FontWeight = weight,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
LineHeight = Math.Max(1, lineHeight)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fontFamily is not null)
|
|
||||||
{
|
|
||||||
probe.FontFamily = fontFamily;
|
|
||||||
}
|
|
||||||
|
|
||||||
probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity));
|
|
||||||
return probe.DesiredSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double FitFontSize(
|
|
||||||
string? text,
|
|
||||||
double maxWidth,
|
|
||||||
double maxHeight,
|
|
||||||
int maxLines,
|
|
||||||
double minFontSize,
|
|
||||||
double maxFontSize,
|
|
||||||
FontWeight weight,
|
|
||||||
double lineHeightFactor,
|
|
||||||
FontFamily? fontFamily = null)
|
|
||||||
{
|
|
||||||
var content = NormalizeText(text);
|
|
||||||
var min = Math.Max(6, minFontSize);
|
|
||||||
var max = Math.Max(min, maxFontSize);
|
|
||||||
var low = min;
|
|
||||||
var high = max;
|
|
||||||
var best = min;
|
|
||||||
|
|
||||||
for (var i = 0; i < 18; i++)
|
|
||||||
{
|
|
||||||
var candidate = (low + high) / 2d;
|
|
||||||
var lineHeight = candidate * lineHeightFactor;
|
|
||||||
var size = MeasureTextSize(content, candidate, weight, Math.Max(1, maxWidth), lineHeight, fontFamily);
|
|
||||||
var lineCount = ResolveLineCount(size.Height, lineHeight);
|
|
||||||
var fits = size.Height <= maxHeight + 0.6d && lineCount <= Math.Max(1, maxLines);
|
|
||||||
|
|
||||||
if (fits)
|
|
||||||
{
|
|
||||||
best = candidate;
|
|
||||||
low = candidate;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
high = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ComponentAdaptiveTextLayout FitAdaptiveTextLayout(
|
|
||||||
string? text,
|
|
||||||
double maxWidth,
|
|
||||||
double maxHeight,
|
|
||||||
int minLines,
|
|
||||||
int maxLines,
|
|
||||||
double minFontSize,
|
|
||||||
double maxFontSize,
|
|
||||||
IEnumerable<FontWeight>? weightCandidates = null,
|
|
||||||
double lineHeightFactor = 1.1d,
|
|
||||||
FontFamily? fontFamily = null)
|
|
||||||
{
|
|
||||||
var content = NormalizeText(text);
|
|
||||||
var safeMinLines = Math.Max(1, minLines);
|
|
||||||
var safeMaxLines = Math.Max(safeMinLines, maxLines);
|
|
||||||
var linesByHeight = ResolveMaxLinesByHeight(maxHeight, minFontSize, lineHeightFactor, safeMinLines, safeMaxLines);
|
|
||||||
|
|
||||||
var candidates = weightCandidates?.ToArray();
|
|
||||||
if (candidates is null || candidates.Length == 0)
|
|
||||||
{
|
|
||||||
candidates = new[] { FontWeight.Normal };
|
|
||||||
}
|
|
||||||
|
|
||||||
ComponentAdaptiveTextLayout? best = null;
|
|
||||||
foreach (var weight in candidates)
|
|
||||||
{
|
|
||||||
for (var lineLimit = linesByHeight; lineLimit >= safeMinLines; lineLimit--)
|
|
||||||
{
|
|
||||||
var fontSize = FitFontSize(
|
|
||||||
content,
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
lineLimit,
|
|
||||||
minFontSize,
|
|
||||||
maxFontSize,
|
|
||||||
weight,
|
|
||||||
lineHeightFactor,
|
|
||||||
fontFamily);
|
|
||||||
|
|
||||||
var lineHeight = fontSize * lineHeightFactor;
|
|
||||||
var measuredSize = MeasureTextSize(content, fontSize, weight, Math.Max(1, maxWidth), lineHeight, fontFamily);
|
|
||||||
var measuredLineCount = ResolveLineCount(measuredSize.Height, lineHeight);
|
|
||||||
var overflowLines = Math.Max(0, measuredLineCount - lineLimit);
|
|
||||||
var overflowHeight = Math.Max(0, measuredSize.Height - maxHeight);
|
|
||||||
var overflowScore = overflowLines * 1000d + overflowHeight;
|
|
||||||
var candidate = new ComponentAdaptiveTextLayout(
|
|
||||||
fontSize,
|
|
||||||
weight,
|
|
||||||
lineLimit,
|
|
||||||
lineHeight,
|
|
||||||
overflowScore,
|
|
||||||
overflowLines == 0 && overflowHeight <= 0.6d,
|
|
||||||
measuredSize);
|
|
||||||
|
|
||||||
if (best is null || IsBetterAdaptiveTextCandidate(candidate, best.Value))
|
|
||||||
{
|
|
||||||
best = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (best is not null)
|
|
||||||
{
|
|
||||||
return best.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallbackFontSize = Math.Max(6, minFontSize);
|
|
||||||
return new ComponentAdaptiveTextLayout(
|
|
||||||
fallbackFontSize,
|
|
||||||
FontWeight.Normal,
|
|
||||||
safeMinLines,
|
|
||||||
fallbackFontSize * lineHeightFactor,
|
|
||||||
double.MaxValue,
|
|
||||||
false,
|
|
||||||
MeasureTextSize(content, fallbackFontSize, FontWeight.Normal, Math.Max(1, maxWidth), fallbackFontSize * lineHeightFactor, fontFamily));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ResolveMaxLinesByHeight(
|
|
||||||
double maxHeight,
|
|
||||||
double minFontSize,
|
|
||||||
double lineHeightFactor,
|
|
||||||
int minLines,
|
|
||||||
int maxLines)
|
|
||||||
{
|
|
||||||
var safeMinLines = Math.Max(1, minLines);
|
|
||||||
var safeMaxLines = Math.Max(safeMinLines, maxLines);
|
|
||||||
var lineHeight = Math.Max(1, Math.Max(6, minFontSize) * lineHeightFactor);
|
|
||||||
var maxHeightWithTolerance = Math.Max(1, maxHeight + 0.6d);
|
|
||||||
var linesByHeight = (int)Math.Floor(maxHeightWithTolerance / lineHeight);
|
|
||||||
return Math.Clamp(linesByHeight, safeMinLines, safeMaxLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ResolveLineCount(double measuredHeight, double lineHeight)
|
|
||||||
{
|
|
||||||
return Math.Max(1, (int)Math.Ceiling(measuredHeight / Math.Max(1, lineHeight)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int EstimateDisplayUnits(
|
|
||||||
double availableWidth,
|
|
||||||
double unitWidth,
|
|
||||||
double gapWidth = 0,
|
|
||||||
double reservedWidth = 0,
|
|
||||||
int minUnits = 1,
|
|
||||||
int maxUnits = int.MaxValue)
|
|
||||||
{
|
|
||||||
var safeMinUnits = Math.Max(1, minUnits);
|
|
||||||
var safeMaxUnits = Math.Max(safeMinUnits, maxUnits);
|
|
||||||
var usableWidth = Math.Max(0, availableWidth - reservedWidth);
|
|
||||||
var safeGapWidth = Math.Max(0, gapWidth);
|
|
||||||
var raw = safeGapWidth > 0
|
|
||||||
? (usableWidth + safeGapWidth) / Math.Max(1, unitWidth + safeGapWidth)
|
|
||||||
: usableWidth / Math.Max(1, unitWidth);
|
|
||||||
|
|
||||||
return Math.Clamp((int)Math.Floor(raw), safeMinUnits, safeMaxUnits);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int CountTextDisplayUnits(string? text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var total = 0;
|
|
||||||
foreach (var rune in text.EnumerateRunes())
|
|
||||||
{
|
|
||||||
if (Rune.IsWhiteSpace(rune))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
total += IsCjkRune(rune) ? 2 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ComponentBoxLayout ResolveBadgeBox(
|
|
||||||
double availableWidth,
|
|
||||||
double availableHeight,
|
|
||||||
double preferredSizeScale = 0.42d,
|
|
||||||
double minSize = 10,
|
|
||||||
double maxSize = 24,
|
|
||||||
double insetScale = 0.2d)
|
|
||||||
{
|
|
||||||
var edge = Math.Min(Math.Max(1, availableWidth), Math.Max(1, availableHeight));
|
|
||||||
var size = Math.Clamp(edge * preferredSizeScale, minSize, maxSize);
|
|
||||||
var inset = Math.Clamp(size * insetScale, 0, size * 0.35d);
|
|
||||||
return new ComponentBoxLayout(size, size, new Thickness(0, inset, 0, 0), new Thickness(inset));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ComponentBoxLayout ResolveGlyphBox(
|
|
||||||
double availableWidth,
|
|
||||||
double availableHeight,
|
|
||||||
double preferredSizeScale = 0.50d,
|
|
||||||
double minSize = 12,
|
|
||||||
double maxSize = 28,
|
|
||||||
double insetScale = 0.18d)
|
|
||||||
{
|
|
||||||
var edge = Math.Min(Math.Max(1, availableWidth), Math.Max(1, availableHeight));
|
|
||||||
var size = Math.Clamp(edge * preferredSizeScale, minSize, maxSize);
|
|
||||||
var inset = Math.Clamp(size * insetScale, 0, size * 0.30d);
|
|
||||||
return new ComponentBoxLayout(size, size, new Thickness(inset), new Thickness(inset));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeText(string? text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
return " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join(" ", text.Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsCjkRune(Rune rune)
|
|
||||||
{
|
|
||||||
var value = rune.Value;
|
|
||||||
return (value >= 0x4E00 && value <= 0x9FFF) || // CJK Unified Ideographs
|
|
||||||
(value >= 0x3400 && value <= 0x4DBF) || // CJK Unified Ideographs Extension A
|
|
||||||
(value >= 0x20000 && value <= 0x2A6DF) || // CJK Unified Ideographs Extension B
|
|
||||||
(value >= 0x2A700 && value <= 0x2B73F) || // CJK Unified Ideographs Extension C
|
|
||||||
(value >= 0x2B740 && value <= 0x2B81F) || // CJK Unified Ideographs Extension D
|
|
||||||
(value >= 0x2B820 && value <= 0x2CEAF) || // CJK Unified Ideographs Extension E/F
|
|
||||||
(value >= 0xF900 && value <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
||||||
(value >= 0x2F800 && value <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement
|
|
||||||
(value >= 0x3040 && value <= 0x309F) || // Hiragana
|
|
||||||
(value >= 0x30A0 && value <= 0x30FF) || // Katakana
|
|
||||||
(value >= 0xAC00 && value <= 0xD7AF); // Hangul Syllables
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsBetterAdaptiveTextCandidate(ComponentAdaptiveTextLayout candidate, ComponentAdaptiveTextLayout best)
|
|
||||||
{
|
|
||||||
if (candidate.FitsCompletely && !best.FitsCompletely)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!candidate.FitsCompletely && best.FitsCompletely)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.FitsCompletely && best.FitsCompletely)
|
|
||||||
{
|
|
||||||
if (candidate.FontSize > best.FontSize + 0.12d)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Abs(candidate.FontSize - best.FontSize) <= 0.12d && candidate.MaxLines < best.MaxLines)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.OverflowScore < best.OverflowScore - 0.2d)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2d &&
|
|
||||||
candidate.FontSize > best.FontSize + 0.12d)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2d &&
|
|
||||||
Math.Abs(candidate.FontSize - best.FontSize) <= 0.12d &&
|
|
||||||
candidate.MaxLines > best.MaxLines)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -29,9 +28,7 @@ public sealed record MusicPlaybackState(
|
|||||||
MusicPlaybackStatus PlaybackStatus,
|
MusicPlaybackStatus PlaybackStatus,
|
||||||
bool CanPlayPause,
|
bool CanPlayPause,
|
||||||
bool CanSkipPrevious,
|
bool CanSkipPrevious,
|
||||||
bool CanSkipNext,
|
bool CanSkipNext)
|
||||||
bool CanToggleFavorite,
|
|
||||||
bool IsFavorite)
|
|
||||||
{
|
{
|
||||||
public static MusicPlaybackState Unsupported()
|
public static MusicPlaybackState Unsupported()
|
||||||
{
|
{
|
||||||
@@ -49,9 +46,7 @@ public sealed record MusicPlaybackState(
|
|||||||
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
||||||
CanPlayPause: false,
|
CanPlayPause: false,
|
||||||
CanSkipPrevious: false,
|
CanSkipPrevious: false,
|
||||||
CanSkipNext: false,
|
CanSkipNext: false);
|
||||||
CanToggleFavorite: false,
|
|
||||||
IsFavorite: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MusicPlaybackState NoSession(bool isSupported = true)
|
public static MusicPlaybackState NoSession(bool isSupported = true)
|
||||||
@@ -70,35 +65,7 @@ public sealed record MusicPlaybackState(
|
|||||||
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
||||||
CanPlayPause: false,
|
CanPlayPause: false,
|
||||||
CanSkipPrevious: false,
|
CanSkipPrevious: false,
|
||||||
CanSkipNext: false,
|
CanSkipNext: false);
|
||||||
CanToggleFavorite: false,
|
|
||||||
IsFavorite: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record MusicQueueItem(
|
|
||||||
string Id,
|
|
||||||
string Title,
|
|
||||||
string Artist,
|
|
||||||
string AlbumTitle,
|
|
||||||
byte[]? ThumbnailBytes,
|
|
||||||
TimeSpan Duration,
|
|
||||||
bool IsCurrentItem);
|
|
||||||
|
|
||||||
public sealed record MusicQueueState(
|
|
||||||
bool IsSupported,
|
|
||||||
IReadOnlyList<MusicQueueItem> Items,
|
|
||||||
int CurrentIndex,
|
|
||||||
bool HasMoreItems)
|
|
||||||
{
|
|
||||||
public static MusicQueueState Unsupported()
|
|
||||||
{
|
|
||||||
return new MusicQueueState(false, Array.Empty<MusicQueueItem>(), -1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MusicQueueState Empty()
|
|
||||||
{
|
|
||||||
return new MusicQueueState(true, Array.Empty<MusicQueueItem>(), -1, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,18 +80,6 @@ public interface IMusicControlService
|
|||||||
Task<bool> SkipPreviousAsync(CancellationToken cancellationToken = default);
|
Task<bool> SkipPreviousAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default);
|
Task<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<bool> ToggleFavoriteAsync(CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
Task<MusicQueueState> GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
|
|
||||||
|
|
||||||
event EventHandler<MusicQueueState>? QueueChanged;
|
|
||||||
|
|
||||||
void StartListening();
|
|
||||||
|
|
||||||
void StopListening();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MusicControlServiceFactory
|
public static class MusicControlServiceFactory
|
||||||
@@ -163,25 +118,4 @@ internal sealed class NoOpMusicControlService : IMusicControlService
|
|||||||
{
|
{
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> ToggleFavoriteAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MusicQueueState> GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult(MusicQueueState.Unsupported());
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
|
|
||||||
public event EventHandler<MusicQueueState>? QueueChanged;
|
|
||||||
|
|
||||||
public void StartListening()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopListening()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -10,9 +9,8 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisposable
|
public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||||
{
|
{
|
||||||
// WinRT Type Resolution
|
|
||||||
private static readonly Type? SessionManagerType = ResolveWinRtType("Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager");
|
private static readonly Type? SessionManagerType = ResolveWinRtType("Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager");
|
||||||
private static readonly Type? AppInfoType = ResolveWinRtType("Windows.ApplicationModel.AppInfo");
|
private static readonly Type? AppInfoType = ResolveWinRtType("Windows.ApplicationModel.AppInfo");
|
||||||
private static readonly MethodInfo? RequestSessionManagerAsyncMethod =
|
private static readonly MethodInfo? RequestSessionManagerAsyncMethod =
|
||||||
@@ -20,250 +18,15 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
private static readonly MethodInfo? AsTaskGenericMethodDefinition = ResolveAsTaskGenericMethod();
|
private static readonly MethodInfo? AsTaskGenericMethodDefinition = ResolveAsTaskGenericMethod();
|
||||||
private static readonly MethodInfo? AsStreamForReadMethod = ResolveAsStreamForReadMethod();
|
private static readonly MethodInfo? AsStreamForReadMethod = ResolveAsStreamForReadMethod();
|
||||||
|
|
||||||
// Synchronization
|
|
||||||
private static readonly SemaphoreSlim ManagerLock = new(1, 1);
|
private static readonly SemaphoreSlim ManagerLock = new(1, 1);
|
||||||
private static object? _sessionManager;
|
private static object? _sessionManager;
|
||||||
|
|
||||||
// Instance State
|
|
||||||
private readonly ConcurrentDictionary<string, string> _sourceAppNameCache = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, string> _sourceAppNameCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _stateGate = new(1, 1);
|
private readonly SemaphoreSlim _stateGate = new(1, 1);
|
||||||
private readonly object _sessionLock = new();
|
|
||||||
|
|
||||||
// Event State
|
|
||||||
private object? _currentSession;
|
|
||||||
private bool _isListening;
|
|
||||||
private readonly List<Delegate> _eventHandlers = new();
|
|
||||||
|
|
||||||
// Thumbnail Cache
|
|
||||||
private string _thumbnailKey = string.Empty;
|
private string _thumbnailKey = string.Empty;
|
||||||
private byte[]? _thumbnailBytesCache;
|
private byte[]? _thumbnailBytesCache;
|
||||||
|
|
||||||
// Events
|
|
||||||
public event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
|
|
||||||
public event EventHandler<MusicQueueState>? QueueChanged;
|
|
||||||
|
|
||||||
public void StartListening()
|
|
||||||
{
|
|
||||||
if (_isListening || !IsRuntimeSupported())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isListening = true;
|
|
||||||
_ = InitializeSessionManagerAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopListening()
|
|
||||||
{
|
|
||||||
if (!_isListening)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isListening = false;
|
|
||||||
UnsubscribeFromSessionEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeSessionManagerAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var manager = await GetSessionManagerAsync(CancellationToken.None);
|
|
||||||
if (manager is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to CurrentSessionChanged event
|
|
||||||
var currentSessionChangedEvent = SessionManagerType?.GetEvent("CurrentSessionChanged");
|
|
||||||
if (currentSessionChangedEvent is not null)
|
|
||||||
{
|
|
||||||
var handler = CreateTypedEventHandler(
|
|
||||||
currentSessionChangedEvent.EventHandlerType,
|
|
||||||
OnCurrentSessionChanged);
|
|
||||||
currentSessionChangedEvent.AddEventHandler(manager, handler);
|
|
||||||
_eventHandlers.Add(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get initial session and subscribe to its events
|
|
||||||
await UpdateCurrentSessionAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("MusicControl", "Failed to initialize SMTC session manager", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnCurrentSessionChanged(object? sender, object? args)
|
|
||||||
{
|
|
||||||
await UpdateCurrentSessionAsync();
|
|
||||||
await RaisePlaybackStateChangedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateCurrentSessionAsync()
|
|
||||||
{
|
|
||||||
lock (_sessionLock)
|
|
||||||
{
|
|
||||||
UnsubscribeFromSessionEvents();
|
|
||||||
_currentSession = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await GetCurrentSessionAsync(CancellationToken.None);
|
|
||||||
if (session is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sessionLock)
|
|
||||||
{
|
|
||||||
_currentSession = session;
|
|
||||||
SubscribeToSessionEvents(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubscribeToSessionEvents(object session)
|
|
||||||
{
|
|
||||||
if (!_isListening)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// MediaPropertiesChanged event
|
|
||||||
var mediaPropertiesChanged = session.GetType().GetEvent("MediaPropertiesChanged");
|
|
||||||
if (mediaPropertiesChanged is not null)
|
|
||||||
{
|
|
||||||
var handler = CreateTypedEventHandler(
|
|
||||||
mediaPropertiesChanged.EventHandlerType,
|
|
||||||
async (s, e) => await RaisePlaybackStateChangedAsync());
|
|
||||||
mediaPropertiesChanged.AddEventHandler(session, handler);
|
|
||||||
_eventHandlers.Add(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybackInfoChanged event
|
|
||||||
var playbackInfoChanged = session.GetType().GetEvent("PlaybackInfoChanged");
|
|
||||||
if (playbackInfoChanged is not null)
|
|
||||||
{
|
|
||||||
var handler = CreateTypedEventHandler(
|
|
||||||
playbackInfoChanged.EventHandlerType,
|
|
||||||
async (s, e) => await RaisePlaybackStateChangedAsync());
|
|
||||||
playbackInfoChanged.AddEventHandler(session, handler);
|
|
||||||
_eventHandlers.Add(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimelinePropertiesChanged event
|
|
||||||
var timelinePropertiesChanged = session.GetType().GetEvent("TimelinePropertiesChanged");
|
|
||||||
if (timelinePropertiesChanged is not null)
|
|
||||||
{
|
|
||||||
var handler = CreateTypedEventHandler(
|
|
||||||
timelinePropertiesChanged.EventHandlerType,
|
|
||||||
async (s, e) => await RaisePlaybackStateChangedAsync());
|
|
||||||
timelinePropertiesChanged.AddEventHandler(session, handler);
|
|
||||||
_eventHandlers.Add(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("MusicControl", "Failed to subscribe to session events", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnsubscribeFromSessionEvents()
|
|
||||||
{
|
|
||||||
if (_currentSession is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sessionType = _currentSession.GetType();
|
|
||||||
|
|
||||||
// Remove MediaPropertiesChanged
|
|
||||||
var mediaPropertiesChanged = sessionType.GetEvent("MediaPropertiesChanged");
|
|
||||||
if (mediaPropertiesChanged is not null)
|
|
||||||
{
|
|
||||||
foreach (var handler in _eventHandlers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mediaPropertiesChanged.RemoveEventHandler(_currentSession, handler);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove PlaybackInfoChanged
|
|
||||||
var playbackInfoChanged = sessionType.GetEvent("PlaybackInfoChanged");
|
|
||||||
if (playbackInfoChanged is not null)
|
|
||||||
{
|
|
||||||
foreach (var handler in _eventHandlers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
playbackInfoChanged.RemoveEventHandler(_currentSession, handler);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove TimelinePropertiesChanged
|
|
||||||
var timelinePropertiesChanged = sessionType.GetEvent("TimelinePropertiesChanged");
|
|
||||||
if (timelinePropertiesChanged is not null)
|
|
||||||
{
|
|
||||||
foreach (var handler in _eventHandlers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
timelinePropertiesChanged.RemoveEventHandler(_currentSession, handler);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
_eventHandlers.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Delegate CreateTypedEventHandler(Type eventHandlerType, Func<object?, object?, Task> asyncAction)
|
|
||||||
{
|
|
||||||
// Create a delegate that wraps the async action
|
|
||||||
var handler = new EventHandler<object>((sender, args) =>
|
|
||||||
{
|
|
||||||
_ = asyncAction(sender, args);
|
|
||||||
});
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RaisePlaybackStateChangedAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var state = await GetCurrentStateAsync(CancellationToken.None);
|
|
||||||
PlaybackStateChanged?.Invoke(this, state);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("MusicControl", "Failed to raise playback state changed event", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RaiseQueueChangedAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var queue = await GetPlaybackQueueAsync(20, CancellationToken.None);
|
|
||||||
QueueChanged?.Invoke(this, queue);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("MusicControl", "Failed to raise queue changed event", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MusicPlaybackState> GetCurrentStateAsync(CancellationToken cancellationToken = default)
|
public async Task<MusicPlaybackState> GetCurrentStateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!IsRuntimeSupported())
|
if (!IsRuntimeSupported())
|
||||||
@@ -293,17 +56,10 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
var canSkipNext = ReadBoolProperty(controls, "IsNextEnabled");
|
var canSkipNext = ReadBoolProperty(controls, "IsNextEnabled");
|
||||||
var canSkipPrevious = ReadBoolProperty(controls, "IsPreviousEnabled");
|
var canSkipPrevious = ReadBoolProperty(controls, "IsPreviousEnabled");
|
||||||
|
|
||||||
// Check for AutoRepeatModeChange and ShuffleEnabledChange support (indicates advanced SMTC)
|
|
||||||
var canToggleFavorite = ReadBoolProperty(controls, "IsChannelDownEnabled") || ReadBoolProperty(controls, "IsChannelUpEnabled");
|
|
||||||
|
|
||||||
// Try to get IsFavorite from mediaProperties (some apps support this)
|
|
||||||
var isFavorite = ReadBoolProperty(mediaProperties, "IsFavorite");
|
|
||||||
|
|
||||||
var sourceAppId = ReadStringProperty(session, "SourceAppUserModelId");
|
var sourceAppId = ReadStringProperty(session, "SourceAppUserModelId");
|
||||||
var sourceAppName = await ResolveSourceAppDisplayNameAsync(sourceAppId, cancellationToken);
|
var sourceAppName = await ResolveSourceAppDisplayNameAsync(sourceAppId, cancellationToken);
|
||||||
|
|
||||||
// Use async method to get timeline properties
|
var timeline = InvokeMethod(session, "GetTimelineProperties");
|
||||||
var timeline = await TryGetTimelinePropertiesAsync(session, cancellationToken);
|
|
||||||
var position = ReadTimeSpanProperty(timeline, "Position");
|
var position = ReadTimeSpanProperty(timeline, "Position");
|
||||||
var start = ReadTimeSpanProperty(timeline, "StartTime");
|
var start = ReadTimeSpanProperty(timeline, "StartTime");
|
||||||
var end = ReadTimeSpanProperty(timeline, "EndTime");
|
var end = ReadTimeSpanProperty(timeline, "EndTime");
|
||||||
@@ -347,9 +103,7 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
PlaybackStatus: MapPlaybackStatus(playbackStatusRaw),
|
PlaybackStatus: MapPlaybackStatus(playbackStatusRaw),
|
||||||
CanPlayPause: canPlayPause,
|
CanPlayPause: canPlayPause,
|
||||||
CanSkipPrevious: canSkipPrevious,
|
CanSkipPrevious: canSkipPrevious,
|
||||||
CanSkipNext: canSkipNext,
|
CanSkipNext: canSkipNext);
|
||||||
CanToggleFavorite: canToggleFavorite,
|
|
||||||
IsFavorite: isFavorite);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -445,113 +199,6 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
return await AwaitBooleanWinRtOperationAsync(InvokeMethod(session, "TrySkipPreviousAsync"), cancellationToken);
|
return await AwaitBooleanWinRtOperationAsync(InvokeMethod(session, "TrySkipPreviousAsync"), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ToggleFavoriteAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (!IsRuntimeSupported())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await GetCurrentSessionAsync(cancellationToken);
|
|
||||||
if (session is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to toggle favorite using RateAndReview (some apps support this)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var playbackInfo = GetPropertyValue(session, "PlaybackInfo") ?? InvokeMethod(session, "GetPlaybackInfo");
|
|
||||||
var controls = GetPropertyValue(playbackInfo, "Controls");
|
|
||||||
|
|
||||||
// Check if RateAndReview is supported
|
|
||||||
if (ReadBoolProperty(controls, "IsRateEnabled"))
|
|
||||||
{
|
|
||||||
var operation = InvokeMethod(session, "TryRateAsync");
|
|
||||||
return await AwaitBooleanWinRtOperationAsync(operation, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Try ChannelUp/ChannelDown as favorite toggle
|
|
||||||
if (ReadBoolProperty(controls, "IsChannelUpEnabled"))
|
|
||||||
{
|
|
||||||
var operation = InvokeMethod(session, "TryChannelUpAsync");
|
|
||||||
return await AwaitBooleanWinRtOperationAsync(operation, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MusicQueueState> GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (!IsRuntimeSupported())
|
|
||||||
{
|
|
||||||
return MusicQueueState.Unsupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await GetCurrentSessionAsync(cancellationToken);
|
|
||||||
if (session is null)
|
|
||||||
{
|
|
||||||
return MusicQueueState.Empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Try to get playback queue using GetPlaybackInfo
|
|
||||||
var playbackInfo = GetPropertyValue(session, "PlaybackInfo") ?? InvokeMethod(session, "GetPlaybackInfo");
|
|
||||||
|
|
||||||
// Check if shuffle/repeat controls exist (indicates queue support)
|
|
||||||
var controls = GetPropertyValue(playbackInfo, "Controls");
|
|
||||||
var canShuffle = ReadBoolProperty(controls, "IsShuffleEnabled");
|
|
||||||
var canRepeat = ReadBoolProperty(controls, "IsRepeatEnabled");
|
|
||||||
|
|
||||||
// Since SMTC doesn't expose the actual queue directly, we'll return a simplified state
|
|
||||||
// indicating whether queue navigation is supported
|
|
||||||
var items = new List<MusicQueueItem>();
|
|
||||||
|
|
||||||
// Try to get current media properties as the current item
|
|
||||||
var mediaProperties = await TryGetMediaPropertiesAsync(session, cancellationToken);
|
|
||||||
if (mediaProperties is not null)
|
|
||||||
{
|
|
||||||
var title = ReadStringProperty(mediaProperties, "Title");
|
|
||||||
var artist = ReadStringProperty(mediaProperties, "Artist");
|
|
||||||
var albumTitle = ReadStringProperty(mediaProperties, "AlbumTitle");
|
|
||||||
var thumbnailBytes = await ResolveThumbnailBytesAsync(
|
|
||||||
mediaProperties,
|
|
||||||
ReadStringProperty(session, "SourceAppUserModelId"),
|
|
||||||
title, artist, albumTitle,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
// Get duration
|
|
||||||
var timeline = await TryGetTimelinePropertiesAsync(session, cancellationToken);
|
|
||||||
var duration = ReadTimeSpanProperty(timeline, "EndTime") - ReadTimeSpanProperty(timeline, "StartTime");
|
|
||||||
|
|
||||||
items.Add(new MusicQueueItem(
|
|
||||||
Id: "current",
|
|
||||||
Title: title,
|
|
||||||
Artist: artist,
|
|
||||||
AlbumTitle: albumTitle,
|
|
||||||
ThumbnailBytes: thumbnailBytes,
|
|
||||||
Duration: duration > TimeSpan.Zero ? duration : TimeSpan.Zero,
|
|
||||||
IsCurrentItem: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If shuffle or repeat is supported, we assume there's a queue
|
|
||||||
var hasMoreItems = canShuffle || canRepeat || ReadBoolProperty(controls, "IsNextEnabled");
|
|
||||||
|
|
||||||
return new MusicQueueState(
|
|
||||||
IsSupported: true,
|
|
||||||
Items: items,
|
|
||||||
CurrentIndex: 0,
|
|
||||||
HasMoreItems: hasMoreItems);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return MusicQueueState.Empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default)
|
public async Task<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!IsRuntimeSupported())
|
if (!IsRuntimeSupported())
|
||||||
@@ -612,20 +259,6 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
return await AwaitWinRtOperationAsync(operation, cancellationToken);
|
return await AwaitWinRtOperationAsync(operation, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<object?> TryGetTimelinePropertiesAsync(object session, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Use the async method TryGetTimelinePropertiesAsync if available
|
|
||||||
var tryGetTimelineMethod = session.GetType().GetMethod("TryGetTimelinePropertiesAsync");
|
|
||||||
if (tryGetTimelineMethod is not null)
|
|
||||||
{
|
|
||||||
var operation = tryGetTimelineMethod.Invoke(session, null);
|
|
||||||
return await AwaitWinRtOperationAsync(operation, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to synchronous method
|
|
||||||
return InvokeMethod(session, "GetTimelineProperties");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<byte[]?> ResolveThumbnailBytesAsync(
|
private async Task<byte[]?> ResolveThumbnailBytesAsync(
|
||||||
object? mediaProperties,
|
object? mediaProperties,
|
||||||
string sourceAppId,
|
string sourceAppId,
|
||||||
@@ -943,11 +576,4 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisp
|
|||||||
_ => MusicPlaybackStatus.Unknown
|
_ => MusicPlaybackStatus.Unknown
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
StopListening();
|
|
||||||
_stateGate.Dispose();
|
|
||||||
ManagerLock.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(14 * scale, 14 * scale, null, 0.55d);
|
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||||
ApplyModeVisualIfNeeded();
|
ApplyModeVisualIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -382,21 +382,15 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
RootBorder.Padding = new Thickness(0);
|
||||||
10 * softScale,
|
|
||||||
8 * softScale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||||
var cardPadding = CardBorder.Padding;
|
var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d));
|
||||||
var innerWidth = Math.Max(120, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
|
||||||
var innerHeight = Math.Max(72, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom);
|
|
||||||
var rowSpacing = Math.Clamp(6 * softScale, 2, 9);
|
var rowSpacing = Math.Clamp(6 * softScale, 2, 9);
|
||||||
ContentGrid.RowSpacing = rowSpacing;
|
ContentGrid.RowSpacing = rowSpacing;
|
||||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||||
|
|||||||
@@ -387,21 +387,15 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
RootBorder.Padding = new Thickness(0);
|
||||||
10 * softScale,
|
|
||||||
8 * softScale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||||
var cardPadding = CardBorder.Padding;
|
var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d));
|
||||||
var innerWidth = Math.Max(120, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
|
||||||
var innerHeight = Math.Max(72, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom);
|
|
||||||
var rowSpacing = Math.Clamp(6 * softScale, 2, 9);
|
var rowSpacing = Math.Clamp(6 * softScale, 2, 9);
|
||||||
ContentGrid.RowSpacing = rowSpacing;
|
ContentGrid.RowSpacing = rowSpacing;
|
||||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||||
|
|||||||
@@ -80,14 +80,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(_currentCellSize * 0.20, 8, 18));
|
|
||||||
|
|
||||||
WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
||||||
AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
||||||
AddressBarBorder.Padding = new Thickness(
|
AddressBarBorder.Padding = new Thickness(8, 6);
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12),
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10));
|
|
||||||
|
|
||||||
if (RootBorder.Child is Grid rootGrid)
|
if (RootBorder.Child is Grid rootGrid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -500,10 +500,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var secondarySize = Math.Clamp(29 * scale, 10, 28);
|
var secondarySize = Math.Clamp(29 * scale, 10, 28);
|
||||||
var lineSpacing = Math.Clamp(4 * scale, 1.5, 8);
|
var lineSpacing = Math.Clamp(4 * scale, 1.5, 8);
|
||||||
var itemPadding = new Thickness(
|
var itemPadding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(6 * scale, 3, 10),
|
Math.Clamp(6 * scale, 3, 10),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8),
|
Math.Clamp(4 * scale, 2, 8),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8),
|
Math.Clamp(4 * scale, 2, 8),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8));
|
Math.Clamp(4 * scale, 2, 8));
|
||||||
var maxVisibleItems = ResolveMaxVisibleItems(scale);
|
var maxVisibleItems = ResolveMaxVisibleItems(scale);
|
||||||
|
|
||||||
var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
||||||
|
|||||||
@@ -546,24 +546,14 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
RootBorder.Padding = new Thickness(0);
|
||||||
10 * scale,
|
|
||||||
8 * scale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
CardBorder.Padding = new Thickness(
|
||||||
Math.Clamp(16 * scale, 8, 24),
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
Math.Clamp(14 * scale, 7, 22),
|
Math.Clamp(14 * scale, 7, 22),
|
||||||
null,
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
0.55d);
|
Math.Clamp(14 * scale, 7, 22));
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
|
||||||
var cardPadding = CardBorder.Padding;
|
|
||||||
var contentWidth = Math.Max(
|
|
||||||
150,
|
|
||||||
totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
|
||||||
|
|
||||||
var headlineFont = Math.Clamp(24 * scale, 12, 34);
|
var headlineFont = Math.Clamp(24 * scale, 12, 34);
|
||||||
BrandPrimaryTextBlock.FontSize = headlineFont;
|
BrandPrimaryTextBlock.FontSize = headlineFont;
|
||||||
@@ -577,7 +567,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
RefreshGlyphIcon.FontSize = Math.Clamp(19 * scale, 11, 24);
|
RefreshGlyphIcon.FontSize = Math.Clamp(19 * scale, 11, 24);
|
||||||
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
|
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
|
||||||
|
|
||||||
var imageWidth = Math.Clamp(contentWidth * 0.20, 60, 170);
|
var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170);
|
||||||
var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94);
|
var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94);
|
||||||
News1ImageHost.Width = imageWidth;
|
News1ImageHost.Width = imageWidth;
|
||||||
News1ImageHost.Height = imageHeight;
|
News1ImageHost.Height = imageHeight;
|
||||||
@@ -594,7 +584,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
var availableTextWidth = Math.Max(
|
var availableTextWidth = Math.Max(
|
||||||
84,
|
84,
|
||||||
contentWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
|
totalWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
|
||||||
News1TitleTextBlock.MaxWidth = availableTextWidth;
|
News1TitleTextBlock.MaxWidth = availableTextWidth;
|
||||||
News2TitleTextBlock.MaxWidth = availableTextWidth;
|
News2TitleTextBlock.MaxWidth = availableTextWidth;
|
||||||
|
|
||||||
|
|||||||
@@ -69,28 +69,4 @@ internal static class ComponentChromeCornerRadiusHelper
|
|||||||
var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness);
|
var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness);
|
||||||
return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale);
|
return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Thickness SafeThickness(
|
|
||||||
double left,
|
|
||||||
double top,
|
|
||||||
double right,
|
|
||||||
double bottom,
|
|
||||||
ComponentChromeContext? chromeContext = null,
|
|
||||||
double responsiveness = 0.45d)
|
|
||||||
{
|
|
||||||
return new Thickness(
|
|
||||||
SafeValue(left, 0, left, chromeContext, responsiveness),
|
|
||||||
SafeValue(top, 0, top, chromeContext, responsiveness),
|
|
||||||
SafeValue(right, 0, right, chromeContext, responsiveness),
|
|
||||||
SafeValue(bottom, 0, bottom, chromeContext, responsiveness));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Thickness SafeThickness(
|
|
||||||
double horizontal,
|
|
||||||
double vertical,
|
|
||||||
ComponentChromeContext? chromeContext = null,
|
|
||||||
double responsiveness = 0.45d)
|
|
||||||
{
|
|
||||||
return SafeThickness(horizontal, vertical, horizontal, vertical, chromeContext, responsiveness);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,23 +102,18 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
12 * scale,
|
|
||||||
10 * scale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
InfoPanel.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
InfoPanel.Padding = new Thickness(
|
||||||
18 * scale,
|
Math.Clamp(18 * scale, 10, 28),
|
||||||
14 * scale,
|
Math.Clamp(14 * scale, 8, 22),
|
||||||
null,
|
Math.Clamp(18 * scale, 10, 28),
|
||||||
0.52d);
|
Math.Clamp(14 * scale, 8, 22));
|
||||||
|
|
||||||
DateInfoStack.Margin = new Thickness(
|
DateInfoStack.Margin = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(18 * scale, 8, 30),
|
Math.Clamp(18 * scale, 8, 30),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 26));
|
Math.Clamp(16 * scale, 8, 26));
|
||||||
DateInfoStack.Spacing = Math.Clamp(4 * scale, 2, 10);
|
DateInfoStack.Spacing = Math.Clamp(4 * scale, 2, 10);
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24);
|
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24);
|
||||||
@@ -430,18 +425,16 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
var rootPadding = RootBorder.Padding;
|
|
||||||
|
|
||||||
var leftStar = totalWidth < _currentCellSize * 4.2 ? 2.0 : 2.08;
|
var leftStar = totalWidth < _currentCellSize * 4.2 ? 2.0 : 2.08;
|
||||||
MainLayoutGrid.ColumnDefinitions[0].Width = new GridLength(leftStar, GridUnitType.Star);
|
MainLayoutGrid.ColumnDefinitions[0].Width = new GridLength(leftStar, GridUnitType.Star);
|
||||||
MainLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
|
MainLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
|
||||||
|
|
||||||
var availableWidth = Math.Max(84, totalWidth - rootPadding.Left - rootPadding.Right);
|
var rightPanelWidth = Math.Max(84, totalWidth / (leftStar + 1));
|
||||||
var rightPanelWidth = Math.Max(84, availableWidth / (leftStar + 1));
|
|
||||||
var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right);
|
var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right);
|
||||||
var leftPanelWidth = Math.Max(84, availableWidth - rightPanelWidth);
|
var leftPanelWidth = Math.Max(84, totalWidth - rightPanelWidth);
|
||||||
var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10);
|
var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10);
|
||||||
var leftContentHeight = Math.Max(30, totalHeight - rootPadding.Top - rootPadding.Bottom - DateInfoStack.Margin.Bottom - 10);
|
var leftContentHeight = Math.Max(30, totalHeight - DateInfoStack.Margin.Bottom - 10);
|
||||||
|
|
||||||
var dateStackSpacing = Math.Clamp(4 * scale, 2, 10);
|
var dateStackSpacing = Math.Clamp(4 * scale, 2, 10);
|
||||||
DateInfoStack.Spacing = dateStackSpacing;
|
DateInfoStack.Spacing = dateStackSpacing;
|
||||||
@@ -471,7 +464,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
lineHeightFactor: 1.10);
|
lineHeightFactor: 1.10);
|
||||||
WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10;
|
WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10;
|
||||||
|
|
||||||
var rightContentHeight = Math.Max(42, totalHeight - rootPadding.Top - rootPadding.Bottom - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom);
|
var rightContentHeight = Math.Max(42, totalHeight - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom);
|
||||||
var titleBottomMargin = Math.Clamp(8 * scale, 4, 14);
|
var titleBottomMargin = Math.Clamp(8 * scale, 4, 14);
|
||||||
var separatorBottomMargin = Math.Clamp(10 * scale, 4, 14);
|
var separatorBottomMargin = Math.Clamp(10 * scale, 4, 14);
|
||||||
var bottomStackSpacing = Math.Clamp(3 * scale, 2, 8);
|
var bottomStackSpacing = Math.Clamp(3 * scale, 2, 8);
|
||||||
|
|||||||
@@ -93,25 +93,25 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
RootBorder.Padding = new Thickness(
|
||||||
20 * scale,
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
16 * scale,
|
Math.Clamp(16 * scale, 8, 28),
|
||||||
null,
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
0.55d);
|
Math.Clamp(14 * scale, 7, 24));
|
||||||
|
|
||||||
QuoteMarkTextBlock.FontSize = Math.Clamp(80 * scale, 32, 120);
|
QuoteMarkTextBlock.FontSize = Math.Clamp(80 * scale, 32, 120);
|
||||||
QuoteMarkTextBlock.LineHeight = Math.Clamp(68 * scale, 26, 100);
|
QuoteMarkTextBlock.LineHeight = Math.Clamp(68 * scale, 26, 100);
|
||||||
QuoteMarkTextBlock.Margin = new Thickness(Math.Clamp(1 * scale, 0, 3), 0, 0, 0);
|
QuoteMarkTextBlock.Margin = new Thickness(Math.Clamp(1 * scale, 0, 3), 0, 0, 0);
|
||||||
|
|
||||||
PoetryContentTextBlock.Margin = new Thickness(
|
PoetryContentTextBlock.Margin = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 16),
|
Math.Clamp(8 * scale, 4, 16),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(2 * scale, 0, 8),
|
Math.Clamp(2 * scale, 0, 8),
|
||||||
0,
|
0,
|
||||||
0);
|
0);
|
||||||
|
|
||||||
AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5);
|
AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5);
|
||||||
AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34);
|
AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34);
|
||||||
AuthorAccent.Margin = new Thickness(0, 0, ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 13), 0);
|
AuthorAccent.Margin = new Thickness(0, 0, Math.Clamp(8 * scale, 4, 13), 0);
|
||||||
AuthorAccent.CornerRadius = new CornerRadius(Math.Clamp(3 * scale, 1.5, 4.5));
|
AuthorAccent.CornerRadius = new CornerRadius(Math.Clamp(3 * scale, 1.5, 4.5));
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(17 * scale, 9, 26);
|
StatusTextBlock.FontSize = Math.Clamp(17 * scale, 9, 26);
|
||||||
@@ -328,13 +328,15 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var rootPadding = isNightMode
|
|
||||||
? ComponentChromeCornerRadiusHelper.SafeThickness(20 * scale, 15 * scale, null, 0.55d)
|
|
||||||
: ComponentChromeCornerRadiusHelper.SafeThickness(20 * scale, 14 * scale, null, 0.55d);
|
|
||||||
|
|
||||||
if (isNightMode)
|
if (isNightMode)
|
||||||
{
|
{
|
||||||
RootBorder.Background = CreateBrush("#C5070D");
|
RootBorder.Background = CreateBrush("#C5070D");
|
||||||
|
RootBorder.Padding = new Thickness(
|
||||||
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
|
Math.Clamp(15 * scale, 7, 24),
|
||||||
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
|
Math.Clamp(14 * scale, 7, 24));
|
||||||
|
|
||||||
QuoteMarkTextBlock.IsVisible = true;
|
QuoteMarkTextBlock.IsVisible = true;
|
||||||
QuoteMarkTextBlock.Foreground = CreateBrush("#4AF4C5A6");
|
QuoteMarkTextBlock.Foreground = CreateBrush("#4AF4C5A6");
|
||||||
@@ -358,6 +360,11 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
RootBorder.Background = CreateBrush("#F2F2F3");
|
RootBorder.Background = CreateBrush("#F2F2F3");
|
||||||
|
RootBorder.Padding = new Thickness(
|
||||||
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
|
Math.Clamp(14 * scale, 6, 24),
|
||||||
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
|
Math.Clamp(14 * scale, 7, 24));
|
||||||
|
|
||||||
QuoteMarkTextBlock.IsVisible = false;
|
QuoteMarkTextBlock.IsVisible = false;
|
||||||
|
|
||||||
@@ -378,7 +385,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
StatusTextBlock.Foreground = CreateBrush("#8A8F98");
|
StatusTextBlock.Foreground = CreateBrush("#8A8F98");
|
||||||
}
|
}
|
||||||
|
|
||||||
RootBorder.Padding = rootPadding;
|
|
||||||
UpdateRefreshButtonState();
|
UpdateRefreshButtonState();
|
||||||
ApplyAdaptiveTextLayout(isNightMode, scale, totalWidth, totalHeight);
|
ApplyAdaptiveTextLayout(isNightMode, scale, totalWidth, totalHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ using Avalonia.Media;
|
|||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -330,17 +329,12 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
12 * scale,
|
|
||||||
10 * scale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
CardBorder.Padding = new Thickness(
|
||||||
12 * scale,
|
Math.Clamp(12 * scale, 8, 18),
|
||||||
11 * scale,
|
Math.Clamp(11 * scale, 7, 16),
|
||||||
null,
|
Math.Clamp(12 * scale, 8, 18),
|
||||||
0.55d);
|
Math.Clamp(11 * scale, 7, 16));
|
||||||
|
|
||||||
var refreshSize = Math.Clamp(30 * scale, 20, 38);
|
var refreshSize = Math.Clamp(30 * scale, 20, 38);
|
||||||
RefreshButton.Width = refreshSize;
|
RefreshButton.Width = refreshSize;
|
||||||
@@ -348,60 +342,44 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
||||||
RefreshIcon.FontSize = Math.Clamp(14 * scale, 10, 20);
|
RefreshIcon.FontSize = Math.Clamp(14 * scale, 10, 20);
|
||||||
|
|
||||||
var contentWidth = Math.Max(
|
var contentWidth = Math.Max(80, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
||||||
80,
|
|
||||||
totalWidth - RootBorder.Padding.Left - RootBorder.Padding.Right - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
|
||||||
var wordWidth = Math.Max(48, contentWidth - refreshSize - Math.Clamp(6 * scale, 4, 10));
|
var wordWidth = Math.Max(48, contentWidth - refreshSize - Math.Clamp(6 * scale, 4, 10));
|
||||||
WordTextBlock.MaxWidth = wordWidth;
|
WordTextBlock.MaxWidth = wordWidth;
|
||||||
|
|
||||||
var contentHeight = Math.Max(
|
var contentHeight = Math.Max(52, totalHeight - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
||||||
52,
|
|
||||||
totalHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
|
||||||
var wordHeightBudget = Math.Max(18, contentHeight * 0.34);
|
var wordHeightBudget = Math.Max(18, contentHeight * 0.34);
|
||||||
var detailHeightBudget = Math.Max(18, contentHeight - wordHeightBudget - Math.Clamp(8 * scale, 4, 14));
|
var detailHeightBudget = Math.Max(18, contentHeight - wordHeightBudget - Math.Clamp(8 * scale, 4, 14));
|
||||||
|
|
||||||
var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
WordTextBlock.FontSize = FitFontSize(
|
||||||
WordTextBlock.Text,
|
WordTextBlock.Text,
|
||||||
wordWidth,
|
wordWidth,
|
||||||
wordHeightBudget,
|
wordHeightBudget,
|
||||||
1,
|
maxLines: 1,
|
||||||
1,
|
minFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||||
Math.Clamp(18 * scale, 12, 22),
|
maxFontSize: Math.Clamp(38 * scale, 20, 50),
|
||||||
Math.Clamp(38 * scale, 20, 50),
|
weight: FontWeight.Bold,
|
||||||
[FontWeight.Bold],
|
lineHeightFactor: 1.02);
|
||||||
1.02,
|
WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.02;
|
||||||
MiSansFontFamily);
|
|
||||||
WordTextBlock.FontSize = wordLayout.FontSize;
|
|
||||||
WordTextBlock.FontWeight = wordLayout.Weight;
|
|
||||||
WordTextBlock.MaxLines = wordLayout.MaxLines;
|
|
||||||
WordTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
WordTextBlock.LineHeight = wordLayout.LineHeight;
|
|
||||||
|
|
||||||
var detailLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var detailFont = FitFontSize(
|
||||||
MeaningTextBlock.IsVisible ? MeaningTextBlock.Text : HiddenHintTextBlock.Text,
|
MeaningTextBlock.IsVisible ? MeaningTextBlock.Text : HiddenHintTextBlock.Text,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
detailHeightBudget,
|
detailHeightBudget,
|
||||||
1,
|
maxLines: MeaningTextBlock.IsVisible ? 5 : 4,
|
||||||
MeaningTextBlock.IsVisible ? 5 : 4,
|
minFontSize: Math.Clamp(12 * scale, 9, 14),
|
||||||
Math.Clamp(12 * scale, 9, 14),
|
maxFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||||
Math.Clamp(18 * scale, 12, 22),
|
weight: FontWeight.SemiBold,
|
||||||
[FontWeight.SemiBold, FontWeight.Medium],
|
lineHeightFactor: 1.10);
|
||||||
1.10,
|
|
||||||
MiSansFontFamily);
|
|
||||||
|
|
||||||
MeaningTextBlock.MaxWidth = contentWidth;
|
MeaningTextBlock.MaxWidth = contentWidth;
|
||||||
MeaningTextBlock.FontSize = detailLayout.FontSize;
|
MeaningTextBlock.FontSize = detailFont;
|
||||||
MeaningTextBlock.FontWeight = detailLayout.Weight;
|
MeaningTextBlock.LineHeight = detailFont * 1.10;
|
||||||
MeaningTextBlock.LineHeight = detailLayout.LineHeight;
|
MeaningTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 4 : 5;
|
||||||
MeaningTextBlock.MaxLines = Math.Min(detailLayout.MaxLines, totalHeight < _currentCellSize * 1.8 ? 4 : 5);
|
|
||||||
MeaningTextBlock.TextWrapping = MeaningTextBlock.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
|
|
||||||
HiddenHintTextBlock.MaxWidth = contentWidth;
|
HiddenHintTextBlock.MaxWidth = contentWidth;
|
||||||
HiddenHintTextBlock.FontSize = detailLayout.FontSize;
|
HiddenHintTextBlock.FontSize = detailFont;
|
||||||
HiddenHintTextBlock.FontWeight = detailLayout.Weight;
|
HiddenHintTextBlock.LineHeight = detailFont * 1.10;
|
||||||
HiddenHintTextBlock.LineHeight = detailLayout.LineHeight;
|
HiddenHintTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 3 : 4;
|
||||||
HiddenHintTextBlock.MaxLines = Math.Min(detailLayout.MaxLines, totalHeight < _currentCellSize * 1.8 ? 3 : 4);
|
|
||||||
HiddenHintTextBlock.TextWrapping = HiddenHintTextBlock.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
StatusTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
||||||
}
|
}
|
||||||
@@ -531,4 +509,58 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double FitFontSize(
|
||||||
|
string? text,
|
||||||
|
double maxWidth,
|
||||||
|
double maxHeight,
|
||||||
|
int maxLines,
|
||||||
|
double minFontSize,
|
||||||
|
double maxFontSize,
|
||||||
|
FontWeight weight,
|
||||||
|
double lineHeightFactor)
|
||||||
|
{
|
||||||
|
var content = string.IsNullOrWhiteSpace(text) ? " " : text.Trim();
|
||||||
|
var min = Math.Max(6, minFontSize);
|
||||||
|
var max = Math.Max(min, maxFontSize);
|
||||||
|
var low = min;
|
||||||
|
var high = max;
|
||||||
|
var best = min;
|
||||||
|
|
||||||
|
for (var i = 0; i < 18; i++)
|
||||||
|
{
|
||||||
|
var candidate = (low + high) / 2d;
|
||||||
|
var lineHeight = candidate * lineHeightFactor;
|
||||||
|
var size = MeasureTextSize(content, candidate, weight, Math.Max(1, maxWidth), lineHeight);
|
||||||
|
var lineCount = Math.Max(1, (int)Math.Ceiling(size.Height / Math.Max(1, lineHeight)));
|
||||||
|
var fits = size.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines);
|
||||||
|
|
||||||
|
if (fits)
|
||||||
|
{
|
||||||
|
best = candidate;
|
||||||
|
low = candidate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Size MeasureTextSize(string text, double fontSize, FontWeight weight, double maxWidth, double lineHeight)
|
||||||
|
{
|
||||||
|
var probe = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = fontSize,
|
||||||
|
FontWeight = weight,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
LineHeight = lineHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity));
|
||||||
|
return probe.DesiredSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using Avalonia.Interactivity;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -301,18 +300,14 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
|
|
||||||
var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.CornerRadius = containerRadius;
|
RootBorder.CornerRadius = containerRadius;
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
RootBorder.Padding = new Thickness(0);
|
||||||
10 * scale,
|
|
||||||
8 * scale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
CardBorder.CornerRadius = containerRadius;
|
CardBorder.CornerRadius = containerRadius;
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
CardBorder.Padding = new Thickness(
|
||||||
16 * scale,
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
14 * scale,
|
Math.Clamp(14 * scale, 7, 22),
|
||||||
null,
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
0.55d);
|
Math.Clamp(14 * scale, 7, 22));
|
||||||
|
|
||||||
var refreshSize = Math.Clamp(38 * scale, 22, 48);
|
var refreshSize = Math.Clamp(38 * scale, 22, 48);
|
||||||
RefreshButton.Width = refreshSize;
|
RefreshButton.Width = refreshSize;
|
||||||
@@ -351,90 +346,65 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
exampleHeightBudget += Math.Clamp(11 * scale, 5, 18);
|
exampleHeightBudget += Math.Clamp(11 * scale, 5, 18);
|
||||||
}
|
}
|
||||||
|
|
||||||
var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var wordBase = Math.Clamp(56 * scale, 18, 72);
|
||||||
|
WordTextBlock.FontSize = FitFontSize(
|
||||||
WordTextBlock.Text,
|
WordTextBlock.Text,
|
||||||
wordWidth,
|
wordWidth,
|
||||||
wordHeightBudget,
|
wordHeightBudget,
|
||||||
1,
|
maxLines: 1,
|
||||||
1,
|
minFontSize: Math.Max(14, wordBase * 0.56),
|
||||||
Math.Max(14, Math.Clamp(56 * scale, 18, 72) * 0.56),
|
maxFontSize: wordBase,
|
||||||
Math.Clamp(56 * scale, 18, 72),
|
weight: FontWeight.Bold,
|
||||||
[FontWeight.Bold],
|
lineHeightFactor: 1.04);
|
||||||
1.04,
|
WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.04;
|
||||||
MiSansFontFamily);
|
|
||||||
WordTextBlock.FontSize = wordLayout.FontSize;
|
|
||||||
WordTextBlock.FontWeight = wordLayout.Weight;
|
|
||||||
WordTextBlock.MaxLines = wordLayout.MaxLines;
|
|
||||||
WordTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
WordTextBlock.LineHeight = wordLayout.LineHeight;
|
|
||||||
|
|
||||||
var pronunciationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var pronunciationBase = Math.Clamp(27 * scale, 10, 36);
|
||||||
|
PronunciationTextBlock.FontSize = FitFontSize(
|
||||||
PronunciationTextBlock.Text,
|
PronunciationTextBlock.Text,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
pronunciationHeightBudget,
|
pronunciationHeightBudget,
|
||||||
1,
|
maxLines: 1,
|
||||||
1,
|
minFontSize: Math.Max(8.6, pronunciationBase * 0.62),
|
||||||
Math.Max(8.6, Math.Clamp(27 * scale, 10, 36) * 0.62),
|
maxFontSize: pronunciationBase,
|
||||||
Math.Clamp(27 * scale, 10, 36),
|
weight: FontWeight.SemiBold,
|
||||||
[FontWeight.SemiBold, FontWeight.Medium],
|
lineHeightFactor: 1.08);
|
||||||
1.08,
|
PronunciationTextBlock.LineHeight = PronunciationTextBlock.FontSize * 1.08;
|
||||||
MiSansFontFamily);
|
|
||||||
PronunciationTextBlock.FontSize = pronunciationLayout.FontSize;
|
|
||||||
PronunciationTextBlock.FontWeight = pronunciationLayout.Weight;
|
|
||||||
PronunciationTextBlock.MaxLines = pronunciationLayout.MaxLines;
|
|
||||||
PronunciationTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
PronunciationTextBlock.LineHeight = pronunciationLayout.LineHeight;
|
|
||||||
|
|
||||||
var meaningLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var meaningBase = Math.Clamp(25 * scale, 10, 34);
|
||||||
|
MeaningTextBlock.FontSize = FitFontSize(
|
||||||
MeaningTextBlock.Text,
|
MeaningTextBlock.Text,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
meaningHeightBudget,
|
meaningHeightBudget,
|
||||||
1,
|
maxLines: Math.Max(1, MeaningTextBlock.MaxLines),
|
||||||
Math.Max(1, MeaningTextBlock.MaxLines),
|
minFontSize: Math.Max(9.2, meaningBase * 0.60),
|
||||||
Math.Max(9.2, Math.Clamp(25 * scale, 10, 34) * 0.60),
|
maxFontSize: meaningBase,
|
||||||
Math.Clamp(25 * scale, 10, 34),
|
weight: FontWeight.SemiBold,
|
||||||
[FontWeight.SemiBold, FontWeight.Medium],
|
lineHeightFactor: 1.10);
|
||||||
1.10,
|
MeaningTextBlock.LineHeight = MeaningTextBlock.FontSize * 1.10;
|
||||||
MiSansFontFamily);
|
|
||||||
MeaningTextBlock.FontSize = meaningLayout.FontSize;
|
|
||||||
MeaningTextBlock.FontWeight = meaningLayout.Weight;
|
|
||||||
MeaningTextBlock.MaxLines = meaningLayout.MaxLines;
|
|
||||||
MeaningTextBlock.TextWrapping = meaningLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
MeaningTextBlock.LineHeight = meaningLayout.LineHeight;
|
|
||||||
|
|
||||||
var exampleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var exampleBase = Math.Clamp(22 * scale, 9, 30);
|
||||||
|
ExampleTextBlock.FontSize = FitFontSize(
|
||||||
ExampleTextBlock.Text,
|
ExampleTextBlock.Text,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
exampleHeightBudget,
|
exampleHeightBudget,
|
||||||
1,
|
maxLines: Math.Max(1, ExampleTextBlock.MaxLines),
|
||||||
Math.Max(1, ExampleTextBlock.MaxLines),
|
minFontSize: Math.Max(8.8, exampleBase * 0.58),
|
||||||
Math.Max(8.8, Math.Clamp(22 * scale, 9, 30) * 0.58),
|
maxFontSize: exampleBase,
|
||||||
Math.Clamp(22 * scale, 9, 30),
|
weight: FontWeight.Medium,
|
||||||
[FontWeight.Medium, FontWeight.SemiBold],
|
lineHeightFactor: 1.08);
|
||||||
1.08,
|
ExampleTextBlock.LineHeight = ExampleTextBlock.FontSize * 1.08;
|
||||||
MiSansFontFamily);
|
|
||||||
ExampleTextBlock.FontSize = exampleLayout.FontSize;
|
|
||||||
ExampleTextBlock.FontWeight = exampleLayout.Weight;
|
|
||||||
ExampleTextBlock.MaxLines = exampleLayout.MaxLines;
|
|
||||||
ExampleTextBlock.TextWrapping = exampleLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
ExampleTextBlock.LineHeight = exampleLayout.LineHeight;
|
|
||||||
|
|
||||||
var translationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
var translationBase = Math.Clamp(20 * scale, 8, 28);
|
||||||
|
ExampleTranslationTextBlock.FontSize = FitFontSize(
|
||||||
ExampleTranslationTextBlock.Text,
|
ExampleTranslationTextBlock.Text,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
Math.Max(10, exampleHeightBudget * 0.44),
|
Math.Max(10, exampleHeightBudget * 0.44),
|
||||||
1,
|
maxLines: 1,
|
||||||
1,
|
minFontSize: Math.Max(7.8, translationBase * 0.62),
|
||||||
Math.Max(7.8, Math.Clamp(20 * scale, 8, 28) * 0.62),
|
maxFontSize: translationBase,
|
||||||
Math.Clamp(20 * scale, 8, 28),
|
weight: FontWeight.Medium,
|
||||||
[FontWeight.Medium, FontWeight.Normal],
|
lineHeightFactor: 1.06);
|
||||||
1.06,
|
ExampleTranslationTextBlock.LineHeight = ExampleTranslationTextBlock.FontSize * 1.06;
|
||||||
MiSansFontFamily);
|
|
||||||
ExampleTranslationTextBlock.FontSize = translationLayout.FontSize;
|
|
||||||
ExampleTranslationTextBlock.FontWeight = translationLayout.Weight;
|
|
||||||
ExampleTranslationTextBlock.MaxLines = translationLayout.MaxLines;
|
|
||||||
ExampleTranslationTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
ExampleTranslationTextBlock.LineHeight = translationLayout.LineHeight;
|
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24);
|
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24);
|
||||||
}
|
}
|
||||||
@@ -617,4 +587,58 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double FitFontSize(
|
||||||
|
string? text,
|
||||||
|
double maxWidth,
|
||||||
|
double maxHeight,
|
||||||
|
int maxLines,
|
||||||
|
double minFontSize,
|
||||||
|
double maxFontSize,
|
||||||
|
FontWeight weight,
|
||||||
|
double lineHeightFactor)
|
||||||
|
{
|
||||||
|
var content = string.IsNullOrWhiteSpace(text) ? " " : text.Trim();
|
||||||
|
var min = Math.Max(6, minFontSize);
|
||||||
|
var max = Math.Max(min, maxFontSize);
|
||||||
|
var low = min;
|
||||||
|
var high = max;
|
||||||
|
var best = min;
|
||||||
|
|
||||||
|
for (var i = 0; i < 18; i++)
|
||||||
|
{
|
||||||
|
var candidate = (low + high) / 2d;
|
||||||
|
var lineHeight = candidate * lineHeightFactor;
|
||||||
|
var size = MeasureTextSize(content, candidate, weight, Math.Max(1, maxWidth), lineHeight);
|
||||||
|
var lineCount = Math.Max(1, (int)Math.Ceiling(size.Height / Math.Max(1, lineHeight)));
|
||||||
|
var fits = size.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines);
|
||||||
|
|
||||||
|
if (fits)
|
||||||
|
{
|
||||||
|
best = candidate;
|
||||||
|
low = candidate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Size MeasureTextSize(string text, double fontSize, FontWeight weight, double maxWidth, double lineHeight)
|
||||||
|
{
|
||||||
|
var probe = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = fontSize,
|
||||||
|
FontWeight = weight,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
LineHeight = lineHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity));
|
||||||
|
return probe.DesiredSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
@@ -254,36 +253,8 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
|
|
||||||
// 4x2 widget has less vertical space than 2x2. Compress only on 6-row months.
|
// 4x2 widget has less vertical space than 2x2. Compress only on 6-row months.
|
||||||
var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0;
|
var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0;
|
||||||
var availableCalendarHeight = Math.Max(
|
var dayFontSize = Math.Clamp(_calendarDayFontSize * rowDensity, 8, 24);
|
||||||
1,
|
var todayDotSize = Math.Clamp(_calendarTodayDotSize * rowDensity, 13.5, 32);
|
||||||
220
|
|
||||||
- RootBorder.Padding.Top
|
|
||||||
- RootBorder.Padding.Bottom
|
|
||||||
- (GregorianHeadlineTextBlock.FontSize * 1.16)
|
|
||||||
- LeftPanelGrid.RowSpacing
|
|
||||||
- (_weekdayFontSize * 1.10)
|
|
||||||
- WeekdayHeaderGrid.Margin.Top
|
|
||||||
- WeekdayHeaderGrid.Margin.Bottom
|
|
||||||
- CalendarGrid.Margin.Top
|
|
||||||
- CalendarGrid.Margin.Bottom);
|
|
||||||
var calendarCellHeight = availableCalendarHeight / Math.Max(1, _calendarVisibleRows);
|
|
||||||
var todayBadge = ComponentTypographyLayoutService.ResolveBadgeBox(
|
|
||||||
calendarCellHeight,
|
|
||||||
calendarCellHeight,
|
|
||||||
preferredSizeScale: 0.92d,
|
|
||||||
minSize: 14,
|
|
||||||
maxSize: 32,
|
|
||||||
insetScale: 0.14d);
|
|
||||||
var todayDotSize = Math.Min(todayBadge.Width, todayBadge.Height);
|
|
||||||
var todayGlyphBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
|
||||||
todayDotSize,
|
|
||||||
todayDotSize,
|
|
||||||
preferredSizeScale: 0.74d,
|
|
||||||
minSize: 8,
|
|
||||||
maxSize: 20,
|
|
||||||
insetScale: 0.12d);
|
|
||||||
var todayGlyphSize = Math.Min(todayGlyphBox.Width, todayGlyphBox.Height);
|
|
||||||
var dayFontSize = Math.Clamp(Math.Min(_calendarDayFontSize * rowDensity, calendarCellHeight * 0.46), 8, 22);
|
|
||||||
|
|
||||||
for (var day = 1; day <= daysInMonth; day++)
|
for (var day = 1; day <= daysInMonth; day++)
|
||||||
{
|
{
|
||||||
@@ -315,10 +286,6 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
: Brushes.White;
|
: Brushes.White;
|
||||||
|
|
||||||
dayText.Foreground = onAccentBrush;
|
dayText.Foreground = onAccentBrush;
|
||||||
dayText.Width = todayGlyphBox.Width;
|
|
||||||
dayText.Height = todayGlyphBox.Height;
|
|
||||||
dayText.TextAlignment = TextAlignment.Center;
|
|
||||||
dayText.LineHeight = todayGlyphSize * 1.03;
|
|
||||||
var dot = new Border
|
var dot = new Border
|
||||||
{
|
{
|
||||||
Width = todayDotSize,
|
Width = todayDotSize,
|
||||||
@@ -359,28 +326,28 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(13 * scale, 8 * scale, null, 0.55d);
|
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
||||||
|
|
||||||
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
||||||
LeftPanelGrid.RowSpacing = Math.Clamp(6.2 * scale, 3, 11);
|
LeftPanelGrid.RowSpacing = Math.Clamp(5.2 * scale, 2.5, 10);
|
||||||
WeekdayHeaderGrid.Margin = new Thickness(
|
WeekdayHeaderGrid.Margin = new Thickness(
|
||||||
0,
|
0,
|
||||||
Math.Clamp(0.8 * scale, 0, 2.5),
|
Math.Clamp(0.5 * scale, 0, 2),
|
||||||
0,
|
0,
|
||||||
Math.Clamp(3.0 * scale, 1.5, 4.5));
|
Math.Clamp(2.4 * scale, 1, 4));
|
||||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * scale, 0.5, 2.5));
|
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
||||||
|
|
||||||
LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34);
|
LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34);
|
||||||
LunarCardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 9 * scale, null, 0.55d);
|
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
||||||
RightPanelGrid.RowSpacing = Math.Clamp(8.2 * scale, 4, 12);
|
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
||||||
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
||||||
|
|
||||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||||
var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(GregorianHeadlineTextBlock.Text));
|
var headerTextLength = Math.Max(1, GregorianHeadlineTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||||
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
||||||
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
||||||
|
|
||||||
GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 40);
|
GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 42);
|
||||||
GregorianHeadlineTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
GregorianHeadlineTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||||
GregorianHeadlineTextBlock.LineHeight = GregorianHeadlineTextBlock.FontSize * 1.03;
|
GregorianHeadlineTextBlock.LineHeight = GregorianHeadlineTextBlock.FontSize * 1.03;
|
||||||
|
|
||||||
@@ -393,9 +360,9 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
block.LineHeight = _weekdayFontSize * 1.02;
|
block.LineHeight = _weekdayFontSize * 1.02;
|
||||||
}
|
}
|
||||||
|
|
||||||
_calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 20);
|
_calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 22);
|
||||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.42, 15, 30);
|
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.30, 13.5, 31);
|
||||||
|
|
||||||
var rightDensity = scale <= 0.72 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.38 ? 1.03 : 1.0;
|
var rightDensity = scale <= 0.72 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.38 ? 1.03 : 1.0;
|
||||||
LunarDateTextBlock.FontSize = Math.Clamp(30 * scale * rightDensity, 14, 44);
|
LunarDateTextBlock.FontSize = Math.Clamp(30 * scale * rightDensity, 14, 44);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(12 * scale, 12 * scale, null, 0.55d);
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 6, 18));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
Padding="14">
|
Padding="14">
|
||||||
<Grid x:Name="LayoutRoot"
|
<Grid x:Name="LayoutRoot"
|
||||||
RowDefinitions="1.15*,2.45*,0.65*,0.82*,1.0*"
|
RowDefinitions="1.1*,2.3*,0.62*,0.78*,0.95*"
|
||||||
RowSpacing="8">
|
RowSpacing="8">
|
||||||
<TextBlock x:Name="TitleTextBlock"
|
<TextBlock x:Name="TitleTextBlock"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -22,12 +22,11 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="#61697C"
|
Foreground="#61697C"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Bottom"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap"
|
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="2"
|
MaxLines="1"
|
||||||
Margin="10,0,10,0" />
|
Margin="8,0,8,0" />
|
||||||
|
|
||||||
<Viewbox Grid.Row="1"
|
<Viewbox Grid.Row="1"
|
||||||
Stretch="Uniform"
|
Stretch="Uniform"
|
||||||
@@ -109,10 +108,10 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="NoWrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="2"
|
MaxLines="1"
|
||||||
Margin="10,0,10,0" />
|
Margin="8,0,8,0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
@@ -212,81 +211,46 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
|||||||
var scale = ResolveScale(width, height);
|
var scale = ResolveScale(width, height);
|
||||||
var isCompact = width < 170 || height < 170;
|
var isCompact = width < 170 || height < 170;
|
||||||
var isUltraCompact = width < 130 || height < 130;
|
var isUltraCompact = width < 130 || height < 130;
|
||||||
var titleUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(TitleTextBlock.Text);
|
var titleUnits = GetDisplayUnits(TitleTextBlock.Text);
|
||||||
var dateUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(DateTextBlock.Text);
|
var dateUnits = GetDisplayUnits(DateTextBlock.Text);
|
||||||
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
||||||
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
var padding = ComponentChromeCornerRadiusHelper.SafeValue(shortSide * 0.05, 4.5, 21);
|
||||||
shortSide * 0.055,
|
RootBorder.Padding = new Thickness(padding);
|
||||||
shortSide * 0.05,
|
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12);
|
||||||
null,
|
|
||||||
0.55d);
|
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.034, 3.2, 14);
|
|
||||||
var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines);
|
var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines);
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
var innerWidth = Math.Max(1, width - padding * 2);
|
||||||
var innerWidth = Math.Max(1, width - rootPadding.Left - rootPadding.Right);
|
var innerHeight = Math.Max(1, height - padding * 2);
|
||||||
var innerHeight = Math.Max(1, height - rootPadding.Top - rootPadding.Bottom);
|
|
||||||
var totalWeight = Math.Max(0.001, rowWeights[0] + rowWeights[1] + rowWeights[2] + rowWeights[3] + rowWeights[4]);
|
var totalWeight = Math.Max(0.001, rowWeights[0] + rowWeights[1] + rowWeights[2] + rowWeights[3] + rowWeights[4]);
|
||||||
var row0Height = innerHeight * (rowWeights[0] / totalWeight);
|
var row0Height = innerHeight * (rowWeights[0] / totalWeight);
|
||||||
var row1Height = innerHeight * (rowWeights[1] / totalWeight);
|
var row1Height = innerHeight * (rowWeights[1] / totalWeight);
|
||||||
var row3Height = innerHeight * (rowWeights[3] / totalWeight);
|
var row3Height = innerHeight * (rowWeights[3] / totalWeight);
|
||||||
var row4Height = innerHeight * (rowWeights[4] / totalWeight);
|
var row4Height = innerHeight * (rowWeights[4] / totalWeight);
|
||||||
var horizontalMargin = Math.Clamp(9 * scale, 5, 16);
|
var horizontalMargin = Math.Clamp(8 * scale, 4, 14);
|
||||||
var titleMaxWidth = Math.Max(24, innerWidth - horizontalMargin * 2);
|
var titleMaxWidth = Math.Max(24, innerWidth - horizontalMargin * 2);
|
||||||
var dateMaxWidth = titleMaxWidth;
|
var dateMaxWidth = titleMaxWidth;
|
||||||
var titleContentBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
|
||||||
titleMaxWidth,
|
|
||||||
row0Height,
|
|
||||||
preferredSizeScale: 0.84d,
|
|
||||||
minSize: 24,
|
|
||||||
maxSize: 170,
|
|
||||||
insetScale: 0.10d);
|
|
||||||
var countContentBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
|
||||||
titleMaxWidth,
|
|
||||||
row1Height,
|
|
||||||
preferredSizeScale: 0.80d,
|
|
||||||
minSize: 28,
|
|
||||||
maxSize: 170,
|
|
||||||
insetScale: 0.08d);
|
|
||||||
var unitContentBox = ComponentTypographyLayoutService.ResolveBadgeBox(
|
|
||||||
titleMaxWidth,
|
|
||||||
row3Height,
|
|
||||||
preferredSizeScale: 0.42d,
|
|
||||||
minSize: 10,
|
|
||||||
maxSize: 72,
|
|
||||||
insetScale: 0.12d);
|
|
||||||
var dateContentBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
|
||||||
dateMaxWidth,
|
|
||||||
row4Height,
|
|
||||||
preferredSizeScale: 0.78d,
|
|
||||||
minSize: 22,
|
|
||||||
maxSize: 92,
|
|
||||||
insetScale: 0.10d);
|
|
||||||
|
|
||||||
var titlePreferred = Math.Clamp(24 * scale, 9.2, 34);
|
var titlePreferred = Math.Clamp(24 * scale, 8.8, 34);
|
||||||
var titleHeightCap = Math.Max(10, row0Height * 0.94);
|
var titleHeightCap = Math.Max(10, row0Height * 0.94);
|
||||||
var titleLineCount = titleNeedsTwoLines ? 2 : 1;
|
var titleLineCount = titleNeedsTwoLines ? 2 : 1;
|
||||||
|
TitleTextBlock.MaxLines = titleLineCount;
|
||||||
|
TitleTextBlock.TextWrapping = titleLineCount > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
||||||
TitleTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0);
|
TitleTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0);
|
||||||
var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
TitleTextBlock.FontSize = FitTextSize(
|
||||||
TitleTextBlock.Text,
|
TitleTextBlock.Text,
|
||||||
Math.Max(24, titleContentBox.Width),
|
TitleTextBlock.FontWeight,
|
||||||
titleHeightCap,
|
|
||||||
1,
|
|
||||||
titleLineCount,
|
|
||||||
8.6,
|
|
||||||
Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)),
|
Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)),
|
||||||
[TitleTextBlock.FontWeight],
|
8.6,
|
||||||
1.10);
|
titleMaxWidth,
|
||||||
TitleTextBlock.FontSize = titleLayout.FontSize;
|
titleHeightCap,
|
||||||
TitleTextBlock.FontWeight = titleLayout.Weight;
|
titleLineCount,
|
||||||
TitleTextBlock.MaxLines = titleLayout.MaxLines;
|
lineHeightFactor: 1.10);
|
||||||
TitleTextBlock.TextWrapping = titleLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
TitleTextBlock.LineHeight = TitleTextBlock.FontSize * 1.10;
|
||||||
TitleTextBlock.LineHeight = titleLayout.LineHeight;
|
|
||||||
|
|
||||||
var digitCount = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(CountTextBlock.Text));
|
var digitCount = Math.Max(1, CountTextBlock.Text?.Trim().Length ?? 1);
|
||||||
var digitCompression = digitCount switch
|
var digitCompression = digitCount switch
|
||||||
{
|
{
|
||||||
>= 5 => 0.68,
|
>= 5 => 0.68,
|
||||||
@@ -297,60 +261,39 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
|||||||
var countCompactFactor = isUltraCompact ? 0.86 : isCompact ? 0.93 : 1.0;
|
var countCompactFactor = isUltraCompact ? 0.86 : isCompact ? 0.93 : 1.0;
|
||||||
var countPreferred = Math.Clamp(132 * scale * digitCompression * countCompactFactor, 28, 170);
|
var countPreferred = Math.Clamp(132 * scale * digitCompression * countCompactFactor, 28, 170);
|
||||||
var countHeightCap = Math.Max(30, row1Height * 0.96);
|
var countHeightCap = Math.Max(30, row1Height * 0.96);
|
||||||
var countLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
CountTextBlock.FontSize = FitTextSize(
|
||||||
CountTextBlock.Text,
|
CountTextBlock.Text,
|
||||||
Math.Max(24, countContentBox.Width),
|
CountTextBlock.FontWeight,
|
||||||
countHeightCap,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
24,
|
|
||||||
Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)),
|
Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)),
|
||||||
[CountTextBlock.FontWeight],
|
24,
|
||||||
1.08);
|
titleMaxWidth,
|
||||||
CountTextBlock.FontSize = countLayout.FontSize;
|
countHeightCap,
|
||||||
CountTextBlock.FontWeight = countLayout.Weight;
|
maxLines: 1,
|
||||||
CountTextBlock.MaxLines = countLayout.MaxLines;
|
lineHeightFactor: 1.08);
|
||||||
CountTextBlock.TextWrapping = TextWrapping.NoWrap;
|
CountTextBlock.LineHeight = CountTextBlock.FontSize * 1.08;
|
||||||
CountTextBlock.LineHeight = countLayout.LineHeight;
|
|
||||||
|
|
||||||
var unitCompactFactor = isUltraCompact ? 0.8 : isCompact ? 0.9 : 1.0;
|
var unitCompactFactor = isUltraCompact ? 0.8 : isCompact ? 0.9 : 1.0;
|
||||||
var unitPreferred = Math.Min(Math.Clamp(52 * scale * unitCompactFactor, 10, 72), Math.Max(10, row3Height * 0.64));
|
DayUnitTextBlock.FontSize = Math.Clamp(52 * scale * unitCompactFactor, 10, 72);
|
||||||
var unitLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
DayUnitTextBlock.FontSize = Math.Min(DayUnitTextBlock.FontSize, Math.Max(10, row3Height * 0.64));
|
||||||
DayUnitTextBlock.Text,
|
DayUnitTextBlock.LineHeight = DayUnitTextBlock.FontSize * 1.02;
|
||||||
Math.Max(18, unitContentBox.Width),
|
|
||||||
Math.Max(10, row3Height * 0.64),
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
unitPreferred,
|
|
||||||
[DayUnitTextBlock.FontWeight],
|
|
||||||
1.02);
|
|
||||||
DayUnitTextBlock.FontSize = unitLayout.FontSize;
|
|
||||||
DayUnitTextBlock.FontWeight = unitLayout.Weight;
|
|
||||||
DayUnitTextBlock.MaxLines = 1;
|
|
||||||
DayUnitTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
DayUnitTextBlock.LineHeight = unitLayout.LineHeight;
|
|
||||||
|
|
||||||
var dateCompactFactor = isUltraCompact ? 0.84 : isCompact ? 0.92 : 1.0;
|
var dateCompactFactor = isUltraCompact ? 0.84 : isCompact ? 0.92 : 1.0;
|
||||||
var datePreferred = Math.Clamp(32 * scale * dateCompactFactor, 9, 46);
|
var datePreferred = Math.Clamp(32 * scale * dateCompactFactor, 9, 46);
|
||||||
var dateHeightCap = Math.Max(10, row4Height * 0.96);
|
var dateHeightCap = Math.Max(10, row4Height * 0.96);
|
||||||
var dateLineCount = dateNeedsTwoLines ? 2 : 1;
|
var dateLineCount = dateNeedsTwoLines ? 2 : 1;
|
||||||
|
DateTextBlock.MaxLines = dateLineCount;
|
||||||
|
DateTextBlock.TextWrapping = dateLineCount > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
||||||
DateTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0);
|
DateTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0);
|
||||||
var dateLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
DateTextBlock.FontSize = FitTextSize(
|
||||||
DateTextBlock.Text,
|
DateTextBlock.Text,
|
||||||
Math.Max(24, dateContentBox.Width),
|
DateTextBlock.FontWeight,
|
||||||
dateHeightCap,
|
|
||||||
1,
|
|
||||||
dateLineCount,
|
|
||||||
8.5,
|
|
||||||
Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)),
|
Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)),
|
||||||
[DateTextBlock.FontWeight],
|
8.5,
|
||||||
1.12);
|
dateMaxWidth,
|
||||||
DateTextBlock.FontSize = dateLayout.FontSize;
|
dateHeightCap,
|
||||||
DateTextBlock.FontWeight = dateLayout.Weight;
|
dateLineCount,
|
||||||
DateTextBlock.MaxLines = dateLayout.MaxLines;
|
lineHeightFactor: 1.12);
|
||||||
DateTextBlock.TextWrapping = dateLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.12;
|
||||||
DateTextBlock.LineHeight = dateLayout.LineHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double[] ApplyAdaptiveRowHeights(
|
private double[] ApplyAdaptiveRowHeights(
|
||||||
@@ -400,6 +343,66 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
|||||||
return weights;
|
return weights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetDisplayUnits(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var units = 0;
|
||||||
|
foreach (var ch in text.Trim())
|
||||||
|
{
|
||||||
|
if (char.IsWhiteSpace(ch))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
units += ch > 0x7F ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double FitTextSize(
|
||||||
|
string? text,
|
||||||
|
FontWeight fontWeight,
|
||||||
|
double preferredSize,
|
||||||
|
double minSize,
|
||||||
|
double maxWidth,
|
||||||
|
double maxHeight,
|
||||||
|
int maxLines,
|
||||||
|
double lineHeightFactor)
|
||||||
|
{
|
||||||
|
var safeText = string.IsNullOrWhiteSpace(text) ? " " : text.Trim();
|
||||||
|
var safeMaxWidth = Math.Max(1, maxWidth);
|
||||||
|
var safeMaxHeight = Math.Max(1, maxHeight);
|
||||||
|
var safeMaxLines = Math.Max(1, maxLines);
|
||||||
|
|
||||||
|
var probe = new TextBlock
|
||||||
|
{
|
||||||
|
Text = safeText,
|
||||||
|
FontWeight = fontWeight,
|
||||||
|
MaxLines = safeMaxLines,
|
||||||
|
TextWrapping = safeMaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var size = preferredSize; size >= minSize; size -= 0.5)
|
||||||
|
{
|
||||||
|
probe.FontSize = size;
|
||||||
|
probe.LineHeight = size * lineHeightFactor;
|
||||||
|
probe.Measure(new Size(safeMaxWidth, double.PositiveInfinity));
|
||||||
|
var desired = probe.DesiredSize;
|
||||||
|
if (desired.Width <= safeMaxWidth + 0.6 &&
|
||||||
|
desired.Height <= safeMaxHeight + 0.6)
|
||||||
|
{
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minSize;
|
||||||
|
}
|
||||||
|
|
||||||
private double ResolveScale(double width, double height)
|
private double ResolveScale(double width, double height)
|
||||||
{
|
{
|
||||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.56, 2.0);
|
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.56, 2.0);
|
||||||
@@ -407,5 +410,4 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
|||||||
var heightScale = Math.Clamp(height / 220d, 0.5, 2.0);
|
var heightScale = Math.Clamp(height / 220d, 0.5, 2.0);
|
||||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.02), 0.5, 2.0);
|
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.02), 0.5, 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,26 +401,18 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
12 * softScale,
|
|
||||||
10 * softScale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
|
||||||
var cardPadding = CardBorder.Padding;
|
|
||||||
|
|
||||||
var rowSpacing = Math.Clamp(8 * softScale, 4, 12);
|
var rowSpacing = Math.Clamp(8 * softScale, 4, 12);
|
||||||
ContentGrid.RowSpacing = rowSpacing;
|
ContentGrid.RowSpacing = rowSpacing;
|
||||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||||
|
|
||||||
var innerWidth = Math.Max(150, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
var innerWidth = Math.Max(150, totalWidth - horizontalPadding * 2d);
|
||||||
var innerHeight = Math.Max(160, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom);
|
var innerHeight = Math.Max(160, totalHeight - verticalPadding * 2d);
|
||||||
var availableRowsHeight = Math.Max(120, innerHeight - rowSpacing * 4d);
|
var availableRowsHeight = Math.Max(120, innerHeight - rowSpacing * 4d);
|
||||||
var headerHeight = Math.Clamp(availableRowsHeight * 0.16, 24, 54);
|
var headerHeight = Math.Clamp(availableRowsHeight * 0.16, 24, 54);
|
||||||
var itemHeight = Math.Max(32, (availableRowsHeight - headerHeight) / 4d);
|
var itemHeight = Math.Max(32, (availableRowsHeight - headerHeight) / 4d);
|
||||||
|
|||||||
@@ -11,13 +11,13 @@
|
|||||||
Background="#EFE6D9"
|
Background="#EFE6D9"
|
||||||
CornerRadius="30"
|
CornerRadius="30"
|
||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
Padding="18">
|
Padding="16">
|
||||||
<Viewbox Stretch="Uniform">
|
<Viewbox Stretch="Uniform">
|
||||||
<Grid x:Name="LayoutRoot"
|
<Grid x:Name="LayoutRoot"
|
||||||
Width="300"
|
Width="300"
|
||||||
Height="300"
|
Height="300"
|
||||||
RowDefinitions="Auto,Auto,Auto,*"
|
RowDefinitions="Auto,Auto,Auto,*"
|
||||||
RowSpacing="12">
|
RowSpacing="10">
|
||||||
<TextBlock x:Name="GregorianLineTextBlock"
|
<TextBlock x:Name="GregorianLineTextBlock"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Text="10/9 Thu"
|
Text="10/9 Thu"
|
||||||
@@ -32,25 +32,21 @@
|
|||||||
FontSize="88"
|
FontSize="88"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Foreground="#6B4936"
|
Foreground="#6B4936"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center" />
|
||||||
TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
MaxLines="2"
|
|
||||||
Margin="4,0,4,0" />
|
|
||||||
|
|
||||||
<Border x:Name="DividerBorder"
|
<Border x:Name="DividerBorder"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Height="1"
|
Height="1"
|
||||||
Margin="10,8,10,3"
|
Margin="8,8,8,2"
|
||||||
Background="#D2C6B7" />
|
Background="#D2C6B7" />
|
||||||
|
|
||||||
<Grid x:Name="AuspiciousGrid"
|
<Grid x:Name="AuspiciousGrid"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
RowDefinitions="Auto,Auto"
|
RowDefinitions="Auto,Auto"
|
||||||
RowSpacing="14">
|
RowSpacing="12">
|
||||||
<Grid Grid.Row="0"
|
<Grid Grid.Row="0"
|
||||||
ColumnDefinitions="Auto,*"
|
ColumnDefinitions="Auto,*"
|
||||||
ColumnSpacing="10">
|
ColumnSpacing="8">
|
||||||
<TextBlock x:Name="YiLabelTextBlock"
|
<TextBlock x:Name="YiLabelTextBlock"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Text="Yi"
|
Text="Yi"
|
||||||
@@ -63,13 +59,12 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="#6B4936"
|
Foreground="#6B4936"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
TextWrapping="Wrap"
|
MaxLines="1" />
|
||||||
MaxLines="2" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
<Grid Grid.Row="1"
|
||||||
ColumnDefinitions="Auto,*"
|
ColumnDefinitions="Auto,*"
|
||||||
ColumnSpacing="10">
|
ColumnSpacing="8">
|
||||||
<TextBlock x:Name="JiLabelTextBlock"
|
<TextBlock x:Name="JiLabelTextBlock"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Text="Ji"
|
Text="Ji"
|
||||||
@@ -82,8 +77,7 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Foreground="#6B4936"
|
Foreground="#6B4936"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
TextWrapping="Wrap"
|
MaxLines="1" />
|
||||||
MaxLines="2" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
@@ -182,109 +181,36 @@ public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private void ApplyAdaptiveTypography()
|
private void ApplyAdaptiveTypography()
|
||||||
{
|
{
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var lunarUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(LunarDateTextBlock.Text);
|
|
||||||
var itemUnits = Math.Max(
|
|
||||||
ComponentTypographyLayoutService.CountTextDisplayUnits(YiItemsTextBlock.Text),
|
|
||||||
ComponentTypographyLayoutService.CountTextDisplayUnits(JiItemsTextBlock.Text));
|
|
||||||
var lunarNeedsTwoLines = lunarUnits >= (scale <= 0.82 ? 18 : 24);
|
|
||||||
var itemsNeedTwoLines = itemUnits >= (scale <= 0.82 ? 18 : 24);
|
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(18 * scale, 18 * scale, null, 0.58d);
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 24));
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(12 * scale, 6, 20);
|
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
|
||||||
DividerBorder.Margin = new Thickness(
|
DividerBorder.Margin = new Thickness(
|
||||||
Math.Clamp(10 * scale, 4, 16),
|
|
||||||
Math.Clamp(8 * scale, 3, 14),
|
Math.Clamp(8 * scale, 3, 14),
|
||||||
Math.Clamp(10 * scale, 4, 16),
|
Math.Clamp(8 * scale, 3, 14),
|
||||||
Math.Clamp(3 * scale, 1, 7));
|
Math.Clamp(8 * scale, 3, 14),
|
||||||
AuspiciousGrid.RowSpacing = Math.Clamp(14 * scale, 7, 22);
|
Math.Clamp(2 * scale, 1, 6));
|
||||||
|
AuspiciousGrid.RowSpacing = Math.Clamp(12 * scale, 6, 20);
|
||||||
|
|
||||||
var densityBoost = scale <= 0.72 ? 0.90 : scale <= 0.88 ? 0.95 : scale >= 1.42 ? 1.04 : 1.0;
|
var densityBoost = scale <= 0.72 ? 0.90 : scale <= 0.88 ? 0.95 : scale >= 1.42 ? 1.04 : 1.0;
|
||||||
var lunarTitleBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 38);
|
||||||
Math.Max(1, Bounds.Width > 1 ? Bounds.Width : 300),
|
LunarDateTextBlock.FontSize = Math.Clamp(88 * scale * densityBoost, 28, 134);
|
||||||
Math.Max(1, Bounds.Height > 1 ? Bounds.Height : 300),
|
YiLabelTextBlock.FontSize = Math.Clamp(30 * scale * densityBoost, 12, 46);
|
||||||
preferredSizeScale: 0.50d,
|
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
|
||||||
minSize: 28,
|
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 36);
|
||||||
maxSize: 134,
|
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
|
||||||
insetScale: 0.12d);
|
|
||||||
var lunarItemBox = ComponentTypographyLayoutService.ResolveBadgeBox(
|
|
||||||
Math.Max(1, Bounds.Width > 1 ? Bounds.Width : 300),
|
|
||||||
Math.Max(1, Bounds.Height > 1 ? Bounds.Height : 300),
|
|
||||||
preferredSizeScale: 0.32d,
|
|
||||||
minSize: 16,
|
|
||||||
maxSize: 84,
|
|
||||||
insetScale: 0.12d);
|
|
||||||
|
|
||||||
var gregorianLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
_gregorianLineWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||||
GregorianLineTextBlock.Text,
|
_lunarDateWeight = ToVariableWeight(Lerp(650, 780, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||||
Math.Max(120, lunarTitleBox.Width * 0.9),
|
_labelWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||||
Math.Max(16, 44 * scale * densityBoost),
|
_itemsWeight = ToVariableWeight(Lerp(520, 670, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
Math.Clamp(24 * scale * densityBoost, 10, 38),
|
|
||||||
[FontWeight.SemiBold, FontWeight.Medium],
|
|
||||||
1.06);
|
|
||||||
GregorianLineTextBlock.MaxLines = gregorianLayout.MaxLines;
|
|
||||||
GregorianLineTextBlock.TextWrapping = TextWrapping.NoWrap;
|
|
||||||
GregorianLineTextBlock.FontSize = gregorianLayout.FontSize;
|
|
||||||
GregorianLineTextBlock.FontWeight = gregorianLayout.Weight;
|
|
||||||
GregorianLineTextBlock.Margin = new Thickness(4 * scale, 0, 4 * scale, 0);
|
|
||||||
_gregorianLineWeight = gregorianLayout.Weight;
|
|
||||||
|
|
||||||
var lunarLineCount = lunarNeedsTwoLines ? 2 : 1;
|
GregorianLineTextBlock.FontWeight = _gregorianLineWeight;
|
||||||
LunarDateTextBlock.Margin = new Thickness(2 * scale, 0, 2 * scale, 0);
|
LunarDateTextBlock.FontWeight = _lunarDateWeight;
|
||||||
var lunarLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
YiLabelTextBlock.FontWeight = _labelWeight;
|
||||||
LunarDateTextBlock.Text,
|
JiLabelTextBlock.FontWeight = _labelWeight;
|
||||||
Math.Max(120, lunarTitleBox.Width - (LunarDateTextBlock.Margin.Left + LunarDateTextBlock.Margin.Right)),
|
YiItemsTextBlock.FontWeight = _itemsWeight;
|
||||||
lunarLineCount > 1
|
JiItemsTextBlock.FontWeight = _itemsWeight;
|
||||||
? Math.Clamp(120 * scale * densityBoost, 44, 160)
|
|
||||||
: Math.Clamp(88 * scale * densityBoost, 28, 126),
|
|
||||||
1,
|
|
||||||
lunarLineCount,
|
|
||||||
24,
|
|
||||||
Math.Clamp(88 * scale * densityBoost, 28, 134),
|
|
||||||
[FontWeight.Bold, FontWeight.SemiBold],
|
|
||||||
1.02);
|
|
||||||
LunarDateTextBlock.MaxLines = lunarLayout.MaxLines;
|
|
||||||
LunarDateTextBlock.TextWrapping = lunarLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
LunarDateTextBlock.FontSize = lunarLayout.FontSize;
|
|
||||||
LunarDateTextBlock.FontWeight = lunarLayout.Weight;
|
|
||||||
|
|
||||||
var labelSize = Math.Clamp(30 * scale * densityBoost, 12, 46);
|
|
||||||
YiLabelTextBlock.FontSize = labelSize;
|
|
||||||
JiLabelTextBlock.FontSize = labelSize;
|
|
||||||
|
|
||||||
var itemMaxLines = itemsNeedTwoLines ? 2 : 1;
|
|
||||||
YiItemsTextBlock.Margin = new Thickness(0, 1 * scale, 0, 0);
|
|
||||||
var yiLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
|
||||||
YiItemsTextBlock.Text,
|
|
||||||
Math.Max(92, lunarItemBox.Width),
|
|
||||||
Math.Clamp(42 * scale * densityBoost, 16, 84),
|
|
||||||
1,
|
|
||||||
itemMaxLines,
|
|
||||||
9,
|
|
||||||
Math.Clamp(24 * scale * densityBoost, 10, 36),
|
|
||||||
[FontWeight.SemiBold, FontWeight.Medium],
|
|
||||||
1.10);
|
|
||||||
YiItemsTextBlock.MaxLines = yiLayout.MaxLines;
|
|
||||||
YiItemsTextBlock.TextWrapping = yiLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
|
||||||
YiItemsTextBlock.FontSize = yiLayout.FontSize;
|
|
||||||
YiItemsTextBlock.FontWeight = yiLayout.Weight;
|
|
||||||
YiItemsTextBlock.LineHeight = yiLayout.LineHeight;
|
|
||||||
|
|
||||||
JiItemsTextBlock.MaxLines = itemMaxLines;
|
|
||||||
JiItemsTextBlock.TextWrapping = YiItemsTextBlock.TextWrapping;
|
|
||||||
JiItemsTextBlock.Margin = new Thickness(0, 1 * scale, 0, 0);
|
|
||||||
JiItemsTextBlock.FontSize = yiLayout.FontSize;
|
|
||||||
JiItemsTextBlock.FontWeight = yiLayout.Weight;
|
|
||||||
JiItemsTextBlock.LineHeight = yiLayout.LineHeight;
|
|
||||||
|
|
||||||
LunarDateTextBlock.FontWeight = lunarLayout.Weight;
|
|
||||||
YiLabelTextBlock.FontWeight = ToVariableWeight(Lerp(620, 740, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
|
||||||
JiLabelTextBlock.FontWeight = YiLabelTextBlock.FontWeight;
|
|
||||||
YiItemsTextBlock.FontWeight = yiLayout.Weight;
|
|
||||||
JiItemsTextBlock.FontWeight = yiLayout.Weight;
|
|
||||||
|
|
||||||
_auspiciousItemCount = scale switch
|
_auspiciousItemCount = scale switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
@@ -27,7 +26,6 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private double _calendarDayFontSize = 22;
|
private double _calendarDayFontSize = 22;
|
||||||
private FontWeight _calendarDayFontWeight = FontWeight.SemiBold;
|
private FontWeight _calendarDayFontWeight = FontWeight.SemiBold;
|
||||||
private double _calendarTodayDotSize = 44;
|
private double _calendarTodayDotSize = 44;
|
||||||
private int _calendarVisibleRows = 6;
|
|
||||||
|
|
||||||
public MonthCalendarWidget()
|
public MonthCalendarWidget()
|
||||||
{
|
{
|
||||||
@@ -150,36 +148,6 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var firstDayOfMonth = new DateTime(year, month, 1);
|
var firstDayOfMonth = new DateTime(year, month, 1);
|
||||||
var daysInMonth = DateTime.DaysInMonth(year, month);
|
var daysInMonth = DateTime.DaysInMonth(year, month);
|
||||||
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
|
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
|
||||||
var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0;
|
|
||||||
var headerReserve = HeaderTextBlock.FontSize * 1.15;
|
|
||||||
var weekdayReserve = _weekdayFontSize * 1.12;
|
|
||||||
var gridReserve = LayoutRoot.RowSpacing * 2;
|
|
||||||
var availableCalendarHeight = Math.Max(
|
|
||||||
1,
|
|
||||||
LayoutRoot.Height
|
|
||||||
- RootBorder.Padding.Top
|
|
||||||
- RootBorder.Padding.Bottom
|
|
||||||
- headerReserve
|
|
||||||
- weekdayReserve
|
|
||||||
- gridReserve);
|
|
||||||
var calendarCellHeight = availableCalendarHeight / Math.Max(1, _calendarVisibleRows);
|
|
||||||
var todayBadge = ComponentTypographyLayoutService.ResolveBadgeBox(
|
|
||||||
calendarCellHeight,
|
|
||||||
calendarCellHeight,
|
|
||||||
preferredSizeScale: 0.94d,
|
|
||||||
minSize: 15,
|
|
||||||
maxSize: 32,
|
|
||||||
insetScale: 0.14d);
|
|
||||||
var todayDotSize = Math.Min(todayBadge.Width, todayBadge.Height);
|
|
||||||
var todayGlyphBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
|
||||||
todayDotSize,
|
|
||||||
todayDotSize,
|
|
||||||
preferredSizeScale: 0.74d,
|
|
||||||
minSize: 8,
|
|
||||||
maxSize: 20,
|
|
||||||
insetScale: 0.12d);
|
|
||||||
var todayGlyphSize = Math.Min(todayGlyphBox.Width, todayGlyphBox.Height);
|
|
||||||
var dayFontSize = Math.Clamp(Math.Min(_calendarDayFontSize * rowDensity, calendarCellHeight * 0.46), 8, 24);
|
|
||||||
|
|
||||||
for (var day = 1; day <= daysInMonth; day++)
|
for (var day = 1; day <= daysInMonth; day++)
|
||||||
{
|
{
|
||||||
@@ -195,8 +163,7 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
Text = day.ToString(CultureInfo.CurrentCulture),
|
Text = day.ToString(CultureInfo.CurrentCulture),
|
||||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||||
FontSize = dayFontSize,
|
FontSize = _calendarDayFontSize,
|
||||||
LineHeight = dayFontSize * 1.04,
|
|
||||||
FontWeight = _calendarDayFontWeight,
|
FontWeight = _calendarDayFontWeight,
|
||||||
Tag = "day"
|
Tag = "day"
|
||||||
};
|
};
|
||||||
@@ -211,15 +178,11 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
: Brushes.White;
|
: Brushes.White;
|
||||||
|
|
||||||
dayText.Foreground = onAccentBrush;
|
dayText.Foreground = onAccentBrush;
|
||||||
dayText.Width = todayGlyphBox.Width;
|
|
||||||
dayText.Height = todayGlyphBox.Height;
|
|
||||||
dayText.TextAlignment = TextAlignment.Center;
|
|
||||||
dayText.LineHeight = todayGlyphSize * 1.03;
|
|
||||||
var dot = new Border
|
var dot = new Border
|
||||||
{
|
{
|
||||||
Width = todayDotSize,
|
Width = _calendarTodayDotSize,
|
||||||
Height = todayDotSize,
|
Height = _calendarTodayDotSize,
|
||||||
CornerRadius = new CornerRadius(todayDotSize * 0.5),
|
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
|
||||||
Background = accentBrush,
|
Background = accentBrush,
|
||||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||||
@@ -255,21 +218,21 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 15 * scale, null, 0.55d);
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22));
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(11 * scale, 5.5, 17);
|
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
|
||||||
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
||||||
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
||||||
|
|
||||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||||
var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(HeaderTextBlock.Text));
|
var headerTextLength = Math.Max(1, HeaderTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||||
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
||||||
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
||||||
|
|
||||||
HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 58);
|
HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 62);
|
||||||
HeaderTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.62) / 1.2, 0, 1)));
|
HeaderTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.62) / 1.2, 0, 1)));
|
||||||
HeaderTextBlock.LineHeight = HeaderTextBlock.FontSize * 1.05;
|
HeaderTextBlock.LineHeight = HeaderTextBlock.FontSize * 1.05;
|
||||||
|
|
||||||
_weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 24);
|
_weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 27);
|
||||||
_weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
_weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||||
foreach (var block in GetWeekdayHeaderBlocks())
|
foreach (var block in GetWeekdayHeaderBlocks())
|
||||||
{
|
{
|
||||||
@@ -278,9 +241,9 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
block.LineHeight = _weekdayFontSize * 1.06;
|
block.LineHeight = _weekdayFontSize * 1.06;
|
||||||
}
|
}
|
||||||
|
|
||||||
_calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 28);
|
_calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 32);
|
||||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.88, 16, 58);
|
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.95, 16, 62);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double ResolveScale()
|
private double ResolveScale()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<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:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -240,8 +240,7 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Classes="music-action"
|
Classes="music-action"
|
||||||
Width="31"
|
Width="31"
|
||||||
Height="31"
|
Height="31">
|
||||||
Click="OnQueueButtonClick">
|
|
||||||
<fi:SymbolIcon x:Name="QueueIcon"
|
<fi:SymbolIcon x:Name="QueueIcon"
|
||||||
Symbol="List"
|
Symbol="List"
|
||||||
IconVariant="Regular"
|
IconVariant="Regular"
|
||||||
@@ -292,8 +291,7 @@
|
|||||||
Grid.Column="4"
|
Grid.Column="4"
|
||||||
Classes="music-action"
|
Classes="music-action"
|
||||||
Width="31"
|
Width="31"
|
||||||
Height="31"
|
Height="31">
|
||||||
Click="OnFavoriteButtonClick">
|
|
||||||
<fi:SymbolIcon x:Name="FavoriteIcon"
|
<fi:SymbolIcon x:Name="FavoriteIcon"
|
||||||
Symbol="Heart"
|
Symbol="Heart"
|
||||||
IconVariant="Regular"
|
IconVariant="Regular"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -17,43 +17,42 @@ using LanMountainDesktop.Theme;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||||
{
|
{
|
||||||
private const Symbol PlaySymbol = Symbol.Play;
|
private const Symbol PlaySymbol = Symbol.Play;
|
||||||
private const Symbol PauseSymbol = Symbol.Pause;
|
private const Symbol PauseSymbol = Symbol.Pause;
|
||||||
private const Symbol HeartSymbol = Symbol.Heart;
|
|
||||||
private const Symbol HeartFilledSymbol = Symbol.Heart;
|
private readonly DispatcherTimer _refreshTimer = new()
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(2.4)
|
||||||
|
};
|
||||||
|
|
||||||
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
|
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
|
||||||
private readonly MonetColorService _monetColorService = new();
|
private readonly MonetColorService _monetColorService = new();
|
||||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||||
private readonly LocalizationService _localizationService = new();
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
|
||||||
private CancellationTokenSource? _commandCts;
|
private CancellationTokenSource? _refreshCts;
|
||||||
private Bitmap? _coverBitmap;
|
private Bitmap? _coverBitmap;
|
||||||
private MusicPlaybackState _currentState = MusicPlaybackState.NoSession(isSupported: true);
|
private MusicPlaybackState _currentState = MusicPlaybackState.NoSession(isSupported: true);
|
||||||
private MusicQueueState _currentQueue = MusicQueueState.Empty();
|
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
|
private bool _isRefreshing;
|
||||||
private bool _isExecutingCommand;
|
private bool _isExecutingCommand;
|
||||||
private double _progressRatio;
|
private double _progressRatio;
|
||||||
private bool _isProgressIndeterminate;
|
private bool _isProgressIndeterminate;
|
||||||
private bool _isListening;
|
|
||||||
|
|
||||||
public MusicControlWidget()
|
public MusicControlWidget()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
|
|
||||||
// Subscribe to service events
|
|
||||||
_musicControlService.PlaybackStateChanged += OnPlaybackStateChanged;
|
|
||||||
_musicControlService.QueueChanged += OnQueueChanged;
|
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
ApplyDynamicBackground(null);
|
ApplyDynamicBackground(null);
|
||||||
ApplyState(MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()));
|
ApplyState(MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()));
|
||||||
@@ -64,19 +63,21 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
var rootRadius = Math.Clamp(30 * scale, 16, 44);
|
||||||
|
var rootCornerRadius = new CornerRadius(rootRadius);
|
||||||
|
|
||||||
RootBorder.CornerRadius = rootCornerRadius;
|
RootBorder.CornerRadius = rootCornerRadius;
|
||||||
ContentPaddingBorder.Padding = new Thickness(
|
ContentPaddingBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
|
Math.Clamp(14 * scale, 9, 22),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18),
|
Math.Clamp(11 * scale, 7, 18),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
|
Math.Clamp(14 * scale, 9, 22),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18));
|
Math.Clamp(11 * scale, 7, 18));
|
||||||
LayoutGrid.RowSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
|
LayoutGrid.RowSpacing = Math.Clamp(9 * scale, 6, 14);
|
||||||
HeaderRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18);
|
HeaderRowGrid.ColumnSpacing = Math.Clamp(11 * scale, 8, 18);
|
||||||
MetaStackPanel.Spacing = ComponentChromeCornerRadiusHelper.SafeValue(3 * scale, 1, 6);
|
MetaStackPanel.Spacing = Math.Clamp(3 * scale, 1, 6);
|
||||||
TimelineRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
|
TimelineRowGrid.ColumnSpacing = Math.Clamp(9 * scale, 6, 14);
|
||||||
ActionRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20);
|
ActionRowGrid.ColumnSpacing = Math.Clamp(12 * scale, 8, 20);
|
||||||
ActionRowGrid.Margin = new Thickness(0, ComponentChromeCornerRadiusHelper.SafeValue(1 * scale, 0, 4), 0, 0);
|
ActionRowGrid.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 4), 0, 0);
|
||||||
DynamicBackgroundBase.CornerRadius = rootCornerRadius;
|
DynamicBackgroundBase.CornerRadius = rootCornerRadius;
|
||||||
BackdropCoverHost.CornerRadius = rootCornerRadius;
|
BackdropCoverHost.CornerRadius = rootCornerRadius;
|
||||||
DynamicGradientOverlay.CornerRadius = rootCornerRadius;
|
DynamicGradientOverlay.CornerRadius = rootCornerRadius;
|
||||||
@@ -84,7 +85,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
|
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
|
||||||
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
|
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
|
||||||
CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 8, 16);
|
CoverBorder.CornerRadius = new CornerRadius(Math.Clamp(12 * scale, 8, 16));
|
||||||
|
|
||||||
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
|
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
|
||||||
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
||||||
@@ -131,11 +132,10 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_ = isEditMode;
|
_ = isEditMode;
|
||||||
var wasOnActivePage = _isOnActivePage;
|
var wasOnActivePage = _isOnActivePage;
|
||||||
_isOnActivePage = isOnActivePage;
|
_isOnActivePage = isOnActivePage;
|
||||||
UpdateListeningState();
|
UpdateRefreshTimerState();
|
||||||
|
|
||||||
if (!wasOnActivePage && _isOnActivePage && _isAttached)
|
if (!wasOnActivePage && _isOnActivePage && _isAttached)
|
||||||
{
|
{
|
||||||
// Refresh state when becoming visible again
|
|
||||||
_ = RefreshStateAsync();
|
_ = RefreshStateAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,15 +143,18 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
_isAttached = true;
|
_isAttached = true;
|
||||||
UpdateListeningState();
|
UpdateRefreshTimerState();
|
||||||
_ = RefreshStateAsync();
|
if (_isOnActivePage)
|
||||||
|
{
|
||||||
|
_ = RefreshStateAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
_isAttached = false;
|
_isAttached = false;
|
||||||
UpdateListeningState();
|
UpdateRefreshTimerState();
|
||||||
CancelCommandRequest();
|
CancelRefreshRequest();
|
||||||
DisposeCoverBitmap();
|
DisposeCoverBitmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,87 +163,9 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStateChanged(object? sender, MusicPlaybackState state)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
await RefreshStateAsync();
|
||||||
{
|
|
||||||
if (!_isAttached || !_isOnActivePage)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentState = state;
|
|
||||||
ApplyState(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnQueueChanged(object? sender, MusicQueueState queue)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (!_isAttached || !_isOnActivePage)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentQueue = queue;
|
|
||||||
UpdateQueueButtonState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateListeningState()
|
|
||||||
{
|
|
||||||
var shouldListen = _isAttached && _isOnActivePage;
|
|
||||||
|
|
||||||
if (shouldListen && !_isListening)
|
|
||||||
{
|
|
||||||
_musicControlService.StartListening();
|
|
||||||
_isListening = true;
|
|
||||||
}
|
|
||||||
else if (!shouldListen && _isListening)
|
|
||||||
{
|
|
||||||
_musicControlService.StopListening();
|
|
||||||
_isListening = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshStateAsync()
|
|
||||||
{
|
|
||||||
if (!_isAttached || !_isOnActivePage)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateLanguageCode();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
var state = await _musicControlService.GetCurrentStateAsync(cts.Token);
|
|
||||||
|
|
||||||
if (cts.IsCancellationRequested || !_isAttached)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentState = state;
|
|
||||||
ApplyState(state);
|
|
||||||
|
|
||||||
// Also refresh queue
|
|
||||||
var queue = await _musicControlService.GetPlaybackQueueAsync(20, cts.Token);
|
|
||||||
_currentQueue = queue;
|
|
||||||
UpdateQueueButtonState();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Ignore cancellation.
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var fallbackState = MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows());
|
|
||||||
_currentState = fallbackState;
|
|
||||||
ApplyState(fallbackState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPlayPauseButtonClick(object? sender, RoutedEventArgs e)
|
private async void OnPlayPauseButtonClick(object? sender, RoutedEventArgs e)
|
||||||
@@ -258,25 +183,6 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
await ExecuteCommandAsync(token => _musicControlService.SkipNextAsync(token));
|
await ExecuteCommandAsync(token => _musicControlService.SkipNextAsync(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnFavoriteButtonClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
await ExecuteCommandAsync(token => _musicControlService.ToggleFavoriteAsync(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnQueueButtonClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// Show queue flyout or panel
|
|
||||||
// For now, just refresh the queue
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
var queue = await _musicControlService.GetPlaybackQueueAsync(20, cts.Token);
|
|
||||||
_currentQueue = queue;
|
|
||||||
UpdateQueueButtonState();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnSourceAppButtonClick(object? sender, RoutedEventArgs e)
|
private async void OnSourceAppButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
await ExecuteCommandAsync(
|
await ExecuteCommandAsync(
|
||||||
@@ -302,39 +208,85 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CancelCommandRequest();
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
|
||||||
_commandCts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
|
_ = await command(cts.Token);
|
||||||
_ = await command(_commandCts.Token);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore command transport errors and recover on next event.
|
// Ignore command transport errors and recover on next poll.
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isExecutingCommand = false;
|
_isExecutingCommand = false;
|
||||||
CancelCommandRequest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refreshAfterCommand)
|
if (refreshAfterCommand)
|
||||||
{
|
{
|
||||||
// The event-driven system will update the UI automatically,
|
|
||||||
// but we also do a manual refresh to ensure consistency
|
|
||||||
await Task.Delay(100);
|
|
||||||
await RefreshStateAsync();
|
await RefreshStateAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelCommandRequest()
|
private async Task RefreshStateAsync()
|
||||||
{
|
{
|
||||||
var cts = Interlocked.Exchange(ref _commandCts, null);
|
if (!_isAttached || !_isOnActivePage || _isRefreshing)
|
||||||
if (cts is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cts.Cancel();
|
_isRefreshing = true;
|
||||||
cts.Dispose();
|
UpdateLanguageCode();
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
var previous = Interlocked.Exchange(ref _refreshCts, cts);
|
||||||
|
previous?.Cancel();
|
||||||
|
previous?.Dispose();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var state = await _musicControlService.GetCurrentStateAsync(cts.Token);
|
||||||
|
if (cts.IsCancellationRequested || !_isAttached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentState = state;
|
||||||
|
ApplyState(state);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancellation.
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var fallbackState = MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows());
|
||||||
|
_currentState = fallbackState;
|
||||||
|
ApplyState(fallbackState);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_refreshCts, cts))
|
||||||
|
{
|
||||||
|
_refreshCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
_isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRefreshTimerState()
|
||||||
|
{
|
||||||
|
if (_isAttached && _isOnActivePage)
|
||||||
|
{
|
||||||
|
if (!_refreshTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_refreshTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshTimer.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyState(MusicPlaybackState state)
|
private void ApplyState(MusicPlaybackState state)
|
||||||
@@ -412,10 +364,6 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
? PauseSymbol
|
? PauseSymbol
|
||||||
: PlaySymbol;
|
: PlaySymbol;
|
||||||
|
|
||||||
// Update favorite button
|
|
||||||
FavoriteIcon.Symbol = state.IsFavorite ? HeartFilledSymbol : HeartSymbol;
|
|
||||||
FavoriteIcon.IconVariant = state.IsFavorite ? IconVariant.Filled : IconVariant.Regular;
|
|
||||||
|
|
||||||
SetCoverImage(state.ThumbnailBytes);
|
SetCoverImage(state.ThumbnailBytes);
|
||||||
ApplyActionButtonState(state);
|
ApplyActionButtonState(state);
|
||||||
UpdateSourceAppButtonTooltip();
|
UpdateSourceAppButtonTooltip();
|
||||||
@@ -437,16 +385,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
: showNoSessionStyle;
|
: showNoSessionStyle;
|
||||||
SourceAppButton.IsEnabled = !_isExecutingCommand && state.IsSupported;
|
SourceAppButton.IsEnabled = !_isExecutingCommand && state.IsSupported;
|
||||||
QueueButton.IsEnabled = canOperate || showNoSessionStyle;
|
QueueButton.IsEnabled = canOperate || showNoSessionStyle;
|
||||||
FavoriteButton.IsEnabled = canOperate
|
FavoriteButton.IsEnabled = canOperate || showNoSessionStyle;
|
||||||
? state.CanToggleFavorite
|
|
||||||
: showNoSessionStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateQueueButtonState()
|
|
||||||
{
|
|
||||||
// Update queue button visual state based on queue availability
|
|
||||||
var hasQueue = _currentQueue.IsSupported && _currentQueue.HasMoreItems;
|
|
||||||
QueueIcon.Opacity = hasQueue ? 1.0 : 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyNoMediaVisualTheme()
|
private void ApplyNoMediaVisualTheme()
|
||||||
@@ -502,10 +441,6 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
SourceAppGlyphBadge.BorderBrush = new SolidColorBrush(Color.Parse("#00FFFFFF"));
|
SourceAppGlyphBadge.BorderBrush = new SolidColorBrush(Color.Parse("#00FFFFFF"));
|
||||||
SourceAppIcon.IconVariant = IconVariant.Filled;
|
SourceAppIcon.IconVariant = IconVariant.Filled;
|
||||||
SourceAppIcon.Foreground = new SolidColorBrush(Color.Parse("#FBFFFFFF"));
|
SourceAppIcon.Foreground = new SolidColorBrush(Color.Parse("#FBFFFFFF"));
|
||||||
|
|
||||||
// Reset favorite icon
|
|
||||||
FavoriteIcon.Symbol = HeartSymbol;
|
|
||||||
FavoriteIcon.IconVariant = IconVariant.Regular;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyActiveVisualTheme()
|
private void ApplyActiveVisualTheme()
|
||||||
@@ -539,6 +474,18 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelRefreshRequest()
|
||||||
|
{
|
||||||
|
var cts = Interlocked.Exchange(ref _refreshCts, null);
|
||||||
|
if (cts is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private string ResolveStatusText(MusicPlaybackStatus status)
|
private string ResolveStatusText(MusicPlaybackStatus status)
|
||||||
{
|
{
|
||||||
return status switch
|
return status switch
|
||||||
@@ -749,18 +696,4 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var safeIndex = Math.Clamp(index, 0, colors.Count - 1);
|
var safeIndex = Math.Clamp(index, 0, colors.Count - 1);
|
||||||
return colors[safeIndex];
|
return colors[safeIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_musicControlService.PlaybackStateChanged -= OnPlaybackStateChanged;
|
|
||||||
_musicControlService.QueueChanged -= OnQueueChanged;
|
|
||||||
_musicControlService.StopListening();
|
|
||||||
if (_musicControlService is IDisposable disposableService)
|
|
||||||
{
|
|
||||||
disposableService.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
CancelCommandRequest();
|
|
||||||
DisposeCoverBitmap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,62 +9,37 @@
|
|||||||
d:DesignHeight="320"
|
d:DesignHeight="320"
|
||||||
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
|
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
|
||||||
|
|
||||||
<UserControl.Resources>
|
|
||||||
<CornerRadius x:Key="OfficeRecentDocumentsRootCornerRadius">34</CornerRadius>
|
|
||||||
<Thickness x:Key="OfficeRecentDocumentsRootPadding">12,10,12,10</Thickness>
|
|
||||||
<Thickness x:Key="OfficeRecentDocumentsContentMargin">16,14,16,14</Thickness>
|
|
||||||
<Thickness x:Key="OfficeRecentDocumentsScrollMargin">0,4,0,0</Thickness>
|
|
||||||
<Thickness x:Key="OfficeRecentDocumentsCardPadding">10</Thickness>
|
|
||||||
<CornerRadius x:Key="OfficeRecentDocumentsCardCornerRadius">12</CornerRadius>
|
|
||||||
<CornerRadius x:Key="OfficeRecentDocumentsRefreshCornerRadius">14</CornerRadius>
|
|
||||||
<CornerRadius x:Key="OfficeRecentDocumentsAccentCornerRadius">70</CornerRadius>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsHeaderFontSize">18</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsStatusFontSize">14</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsRefreshIconFontSize">14</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsContentRowSpacing">8</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsRefreshButtonSize">28</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsAccentSize">140</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsDocumentCardWidth">130</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsDocumentCardHeight">90</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsDocumentTitleFontSize">12</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsDocumentTimeFontSize">10</x:Double>
|
|
||||||
<x:Double x:Key="OfficeRecentDocumentsDocumentSpacing">8</x:Double>
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Border x:Name="RootBorder"
|
<Border x:Name="RootBorder"
|
||||||
CornerRadius="{DynamicResource OfficeRecentDocumentsRootCornerRadius}"
|
CornerRadius="34"
|
||||||
Background="#2D5A8E"
|
Background="#2D5A8E"
|
||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Padding="{DynamicResource OfficeRecentDocumentsRootPadding}">
|
Padding="0">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Border x:Name="AccentCorner"
|
<Border x:Name="AccentCorner"
|
||||||
Width="{DynamicResource OfficeRecentDocumentsAccentSize}"
|
Width="140"
|
||||||
Height="{DynamicResource OfficeRecentDocumentsAccentSize}"
|
Height="140"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="0,-40,-40,0"
|
Margin="0,-40,-40,0"
|
||||||
CornerRadius="{DynamicResource OfficeRecentDocumentsAccentCornerRadius}"
|
CornerRadius="70"
|
||||||
Background="#4A90D9"
|
Background="#4A90D9"
|
||||||
Opacity="0.3"
|
Opacity="0.3"
|
||||||
IsHitTestVisible="False" />
|
IsHitTestVisible="False" />
|
||||||
|
|
||||||
<Grid x:Name="ContentGrid"
|
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
||||||
RowDefinitions="Auto,*"
|
|
||||||
RowSpacing="{DynamicResource OfficeRecentDocumentsContentRowSpacing}"
|
|
||||||
Margin="{DynamicResource OfficeRecentDocumentsContentMargin}">
|
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
<TextBlock x:Name="HeaderTextBlock"
|
<TextBlock x:Name="HeaderTextBlock"
|
||||||
Text="最近文档"
|
Text="最近文档"
|
||||||
Foreground="#D8FFFFFF"
|
Foreground="#D8FFFFFF"
|
||||||
FontSize="{DynamicResource OfficeRecentDocumentsHeaderFontSize}"
|
FontSize="18"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<Button x:Name="RefreshButton"
|
<Button x:Name="RefreshButton"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
|
Width="28"
|
||||||
Height="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
|
Height="28"
|
||||||
CornerRadius="{DynamicResource OfficeRecentDocumentsRefreshCornerRadius}"
|
CornerRadius="14"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderBrush="Transparent"
|
BorderBrush="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
@@ -72,7 +47,7 @@
|
|||||||
Focusable="False"
|
Focusable="False"
|
||||||
PointerPressed="OnRefreshPointerPressed">
|
PointerPressed="OnRefreshPointerPressed">
|
||||||
<fi:SymbolIcon Symbol="ArrowSync"
|
<fi:SymbolIcon Symbol="ArrowSync"
|
||||||
FontSize="{DynamicResource OfficeRecentDocumentsRefreshIconFontSize}"
|
FontSize="14"
|
||||||
Foreground="#B8FFFFFF" />
|
Foreground="#B8FFFFFF" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -80,29 +55,28 @@
|
|||||||
<ScrollViewer Grid.Row="1"
|
<ScrollViewer Grid.Row="1"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Disabled"
|
VerticalScrollBarVisibility="Disabled"
|
||||||
Margin="{DynamicResource OfficeRecentDocumentsScrollMargin}">
|
Margin="0,4,0,0">
|
||||||
<ItemsControl x:Name="DocumentsItemsControl">
|
<ItemsControl x:Name="DocumentsItemsControl">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel Orientation="Horizontal"
|
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||||
Spacing="{DynamicResource OfficeRecentDocumentsDocumentSpacing}" />
|
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
|
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
|
||||||
<Border x:Name="DocumentCard"
|
<Border x:Name="DocumentCard"
|
||||||
Width="{DynamicResource OfficeRecentDocumentsDocumentCardWidth}"
|
Width="130"
|
||||||
Height="{DynamicResource OfficeRecentDocumentsDocumentCardHeight}"
|
Height="90"
|
||||||
CornerRadius="{DynamicResource OfficeRecentDocumentsCardCornerRadius}"
|
CornerRadius="10"
|
||||||
Background="#3AFFFFFF"
|
Background="#3AFFFFFF"
|
||||||
Padding="{DynamicResource OfficeRecentDocumentsCardPadding}"
|
Padding="10"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
PointerPressed="OnDocumentCardPointerPressed">
|
PointerPressed="OnDocumentCardPointerPressed">
|
||||||
<Grid RowDefinitions="Auto,*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock Grid.Row="0"
|
||||||
Text="{Binding FileName}"
|
Text="{Binding FileName}"
|
||||||
Foreground="#D8FFFFFF"
|
Foreground="#D8FFFFFF"
|
||||||
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTitleFontSize}"
|
FontSize="12"
|
||||||
FontWeight="Medium"
|
FontWeight="Medium"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="2"
|
MaxLines="2"
|
||||||
@@ -111,7 +85,7 @@
|
|||||||
<TextBlock Grid.Row="2"
|
<TextBlock Grid.Row="2"
|
||||||
Text="{Binding TimeAgo}"
|
Text="{Binding TimeAgo}"
|
||||||
Foreground="#9AFFFFFF"
|
Foreground="#9AFFFFFF"
|
||||||
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTimeFontSize}"
|
FontSize="10"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="1" />
|
MaxLines="1" />
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -126,7 +100,7 @@
|
|||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Text="暂无最近文档"
|
Text="暂无最近文档"
|
||||||
Foreground="#9AFFFFFF"
|
Foreground="#9AFFFFFF"
|
||||||
FontSize="{DynamicResource OfficeRecentDocumentsStatusFontSize}"
|
FontSize="14"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
@@ -37,54 +36,8 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedCellSize = Math.Max(1, cellSize);
|
var scale = cellSize / 100.0;
|
||||||
var scale = Math.Clamp(normalizedCellSize / 48d, 0.72d, 1.65d);
|
RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale));
|
||||||
var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
|
||||||
|
|
||||||
RootBorder.CornerRadius = rootCornerRadius;
|
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
10 * scale,
|
|
||||||
8 * scale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
|
|
||||||
Resources["OfficeRecentDocumentsRootCornerRadius"] = rootCornerRadius;
|
|
||||||
Resources["OfficeRecentDocumentsRootPadding"] = RootBorder.Padding;
|
|
||||||
|
|
||||||
var contentMargin = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
16 * scale,
|
|
||||||
14 * scale,
|
|
||||||
null,
|
|
||||||
0.55d);
|
|
||||||
Resources["OfficeRecentDocumentsContentMargin"] = contentMargin;
|
|
||||||
Resources["OfficeRecentDocumentsScrollMargin"] = new Thickness(
|
|
||||||
0,
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8, null, 0.40d),
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
Resources["OfficeRecentDocumentsContentRowSpacing"] = ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 12, null, 0.40d);
|
|
||||||
|
|
||||||
var refreshButtonSize = Math.Clamp(28 * scale, 20, 40);
|
|
||||||
Resources["OfficeRecentDocumentsRefreshButtonSize"] = refreshButtonSize;
|
|
||||||
Resources["OfficeRecentDocumentsRefreshCornerRadius"] = new CornerRadius(refreshButtonSize / 2d);
|
|
||||||
Resources["OfficeRecentDocumentsRefreshIconFontSize"] = Math.Clamp(14 * scale, 10, 20);
|
|
||||||
|
|
||||||
var accentSize = Math.Clamp(140 * scale, 88, 188);
|
|
||||||
Resources["OfficeRecentDocumentsAccentSize"] = accentSize;
|
|
||||||
Resources["OfficeRecentDocumentsAccentCornerRadius"] = new CornerRadius(accentSize / 2d);
|
|
||||||
|
|
||||||
Resources["OfficeRecentDocumentsHeaderFontSize"] = Math.Clamp(18 * scale, 12, 24);
|
|
||||||
Resources["OfficeRecentDocumentsStatusFontSize"] = Math.Clamp(14 * scale, 10, 18);
|
|
||||||
Resources["OfficeRecentDocumentsDocumentSpacing"] = ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 12, null, 0.40d);
|
|
||||||
|
|
||||||
var cardWidth = Math.Clamp(130 * scale, 96, 180);
|
|
||||||
var cardHeight = Math.Clamp(90 * scale, 68, 124);
|
|
||||||
Resources["OfficeRecentDocumentsDocumentCardWidth"] = cardWidth;
|
|
||||||
Resources["OfficeRecentDocumentsDocumentCardHeight"] = cardHeight;
|
|
||||||
Resources["OfficeRecentDocumentsCardCornerRadius"] = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 10, 24);
|
|
||||||
Resources["OfficeRecentDocumentsCardPadding"] = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16, null, 0.50d));
|
|
||||||
Resources["OfficeRecentDocumentsDocumentTitleFontSize"] = Math.Clamp(12 * scale, 10, 18);
|
|
||||||
Resources["OfficeRecentDocumentsDocumentTimeFontSize"] = Math.Clamp(10 * scale, 8, 14);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||||
|
|||||||
@@ -65,11 +65,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
|
|
||||||
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
||||||
RootBorder.CornerRadius = rootRadius;
|
RootBorder.CornerRadius = rootRadius;
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(0);
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * contentScale, 10, 22),
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * contentScale, 8, 18),
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * contentScale, 10, 22),
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * contentScale, 8, 18));
|
|
||||||
RecorderCardBorder.CornerRadius = rootRadius;
|
RecorderCardBorder.CornerRadius = rootRadius;
|
||||||
RecorderContentGrid.Margin = new Thickness(
|
RecorderContentGrid.Margin = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
||||||
|
|||||||
@@ -603,20 +603,12 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
|
||||||
10 * softScale,
|
|
||||||
8 * softScale,
|
|
||||||
null,
|
|
||||||
0.45d);
|
|
||||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
||||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
CardBorder.Padding = new Thickness(
|
||||||
Math.Clamp(12 * softScale, 8, 18),
|
Math.Clamp(12 * softScale, 8, 18),
|
||||||
Math.Clamp(12 * softScale, 8, 18),
|
Math.Clamp(12 * softScale, 8, 18),
|
||||||
null,
|
Math.Clamp(12 * softScale, 8, 18),
|
||||||
0.55d);
|
Math.Clamp(12 * softScale, 8, 18));
|
||||||
|
|
||||||
var rootPadding = RootBorder.Padding;
|
|
||||||
var cardPadding = CardBorder.Padding;
|
|
||||||
|
|
||||||
var rowSpacing = Math.Clamp(6 * softScale, 3, 10);
|
var rowSpacing = Math.Clamp(6 * softScale, 3, 10);
|
||||||
ContentGrid.RowSpacing = rowSpacing;
|
ContentGrid.RowSpacing = rowSpacing;
|
||||||
@@ -633,7 +625,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
||||||
RefreshGlyphIcon.FontSize = Math.Clamp(16 * softScale, 10, 20);
|
RefreshGlyphIcon.FontSize = Math.Clamp(16 * softScale, 10, 20);
|
||||||
|
|
||||||
var innerWidth = Math.Max(100, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
var innerWidth = Math.Max(100, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
||||||
var rowPaddingHorizontal = Math.Clamp(8 * softScale, 5, 14);
|
var rowPaddingHorizontal = Math.Clamp(8 * softScale, 5, 14);
|
||||||
var rowPaddingVertical = Math.Clamp(6 * softScale, 3, 10);
|
var rowPaddingVertical = Math.Clamp(6 * softScale, 3, 10);
|
||||||
var avatarSize = Math.Clamp(30 * softScale, 20, 40);
|
var avatarSize = Math.Clamp(30 * softScale, 20, 40);
|
||||||
@@ -648,10 +640,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
var availablePostsHeight = Math.Max(
|
var availablePostsHeight = Math.Max(
|
||||||
0d,
|
0d,
|
||||||
totalHeight -
|
totalHeight -
|
||||||
rootPadding.Top -
|
CardBorder.Padding.Top -
|
||||||
rootPadding.Bottom -
|
CardBorder.Padding.Bottom -
|
||||||
cardPadding.Top -
|
|
||||||
cardPadding.Bottom -
|
|
||||||
estimatedHeaderHeight -
|
estimatedHeaderHeight -
|
||||||
rowSpacing);
|
rowSpacing);
|
||||||
var rowFootprint = Math.Max(1d, estimatedRowHeight + rowSpacing);
|
var rowFootprint = Math.Max(1d, estimatedRowHeight + rowSpacing);
|
||||||
|
|||||||
@@ -231,8 +231,8 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
|||||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16));
|
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||||
|
|
||||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(4 * scale, 2, 6)
|
? Math.Clamp(4 * scale, 2, 6)
|
||||||
@@ -271,8 +271,8 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
|||||||
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12));
|
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12));
|
||||||
|
|
||||||
var rowPadding = new Thickness(
|
var rowPadding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14),
|
Math.Clamp(10 * scale * compactMultiplier, 5, 14),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(7 * scale * compactMultiplier, 3, 10));
|
Math.Clamp(7 * scale * compactMultiplier, 3, 10));
|
||||||
SustainedRowBorder.Padding = rowPadding;
|
SustainedRowBorder.Padding = rowPadding;
|
||||||
TimeRowBorder.Padding = rowPadding;
|
TimeRowBorder.Padding = rowPadding;
|
||||||
SegmentRowBorder.Padding = rowPadding;
|
SegmentRowBorder.Padding = rowPadding;
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 20),
|
Math.Clamp(14 * scale, 8, 20),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16));
|
Math.Clamp(10 * scale, 6, 16));
|
||||||
|
|
||||||
StatusTitleTextBlock.FontSize = Math.Clamp(11 * scale, 9, 18);
|
StatusTitleTextBlock.FontSize = Math.Clamp(11 * scale, 9, 18);
|
||||||
StatusValueTextBlock.FontSize = Math.Clamp(20 * scale, 12, 34);
|
StatusValueTextBlock.FontSize = Math.Clamp(20 * scale, 12, 34);
|
||||||
|
|||||||
@@ -257,8 +257,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
|||||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16));
|
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||||
|
|
||||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(3 * scale, 2, 5)
|
? Math.Clamp(3 * scale, 2, 5)
|
||||||
@@ -297,8 +297,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
|||||||
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12));
|
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12));
|
||||||
|
|
||||||
var cardPadding = new Thickness(
|
var cardPadding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14),
|
Math.Clamp(10 * scale * compactMultiplier, 5, 14),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(6 * scale * compactMultiplier, 3, 9));
|
Math.Clamp(6 * scale * compactMultiplier, 3, 9));
|
||||||
CountCardBorder.Padding = cardPadding;
|
CountCardBorder.Padding = cardPadding;
|
||||||
DurationCardBorder.Padding = cardPadding;
|
DurationCardBorder.Padding = cardPadding;
|
||||||
|
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
|||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 14, 42);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 14, 42);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22),
|
Math.Clamp(14 * scale, 8, 22),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16));
|
Math.Clamp(10 * scale, 6, 16));
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 12, 30);
|
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 12, 30);
|
||||||
RealtimeValueTextBlock.FontSize = Math.Clamp(18 * scale, 12, 34);
|
RealtimeValueTextBlock.FontSize = Math.Clamp(18 * scale, 12, 34);
|
||||||
|
|||||||
@@ -325,8 +325,8 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
|||||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 34);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 34);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16));
|
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||||
|
|
||||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(4 * scale, 2, 5)
|
? Math.Clamp(4 * scale, 2, 5)
|
||||||
|
|||||||
@@ -260,8 +260,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
|||||||
var expandedMultiplier = _isExpandedMode ? 1.12 : 1.0;
|
var expandedMultiplier = _isExpandedMode ? 1.12 : 1.0;
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.50, 14, 42);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.50, 14, 42);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale * compactMultiplier * expandedMultiplier, 8, 30),
|
Math.Clamp(16 * scale * compactMultiplier * expandedMultiplier, 8, 30),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier * expandedMultiplier, 6, 26));
|
Math.Clamp(14 * scale * compactMultiplier * expandedMultiplier, 6, 26));
|
||||||
|
|
||||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(4 * scale, 2, 5)
|
? Math.Clamp(4 * scale, 2, 5)
|
||||||
@@ -303,8 +303,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
|||||||
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 5, 14));
|
ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 5, 14));
|
||||||
|
|
||||||
var cardPadding = new Thickness(
|
var cardPadding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier * expandedMultiplier, 6, 20),
|
Math.Clamp(10 * scale * compactMultiplier * expandedMultiplier, 6, 20),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(8 * scale * compactMultiplier * expandedMultiplier, 4, 16));
|
Math.Clamp(8 * scale * compactMultiplier * expandedMultiplier, 4, 16));
|
||||||
var cardCornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * scale, 6, 18);
|
var cardCornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * scale, 6, 18);
|
||||||
AverageCardBorder.Padding = cardPadding;
|
AverageCardBorder.Padding = cardPadding;
|
||||||
MinimumCardBorder.Padding = cardPadding;
|
MinimumCardBorder.Padding = cardPadding;
|
||||||
|
|||||||
@@ -270,8 +270,8 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
|||||||
var compactMultiplier = _isUltraCompactMode ? 0.78 : _isCompactMode ? 0.90 : 1.0;
|
var compactMultiplier = _isUltraCompactMode ? 0.78 : _isCompactMode ? 0.90 : 1.0;
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier, 7, 22),
|
Math.Clamp(14 * scale * compactMultiplier, 7, 22),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16));
|
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||||
|
|
||||||
LayoutGrid.ColumnSpacing = _isUltraCompactMode
|
LayoutGrid.ColumnSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(6 * scale, 3, 8)
|
? Math.Clamp(6 * scale, 3, 8)
|
||||||
|
|||||||
@@ -241,9 +241,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
Background = new SolidColorBrush(rowBackground),
|
Background = new SolidColorBrush(rowBackground),
|
||||||
BorderBrush = new SolidColorBrush(rowBorderColor),
|
BorderBrush = new SolidColorBrush(rowBorderColor),
|
||||||
BorderThickness = new Thickness(1),
|
BorderThickness = new Thickness(1),
|
||||||
Padding = new Thickness(
|
Padding = new Thickness(Math.Clamp(8, 6, 12), Math.Clamp(6, 4, 10))
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12),
|
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var panelComposite = ToOpaqueAgainst(panelColor, DarkSubstrate);
|
var panelComposite = ToOpaqueAgainst(panelColor, DarkSubstrate);
|
||||||
@@ -357,7 +355,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
Width = _isUltraCompactMode ? 26 : 34,
|
Width = _isUltraCompactMode ? 26 : 34,
|
||||||
Height = Math.Clamp(26 * (_isCompactMode ? 0.90 : 1.0), 24, 30),
|
Height = Math.Clamp(26 * (_isCompactMode ? 0.90 : 1.0), 24, 30),
|
||||||
Padding = new Thickness(0),
|
Padding = new Thickness(0),
|
||||||
CornerRadius = ComponentChromeCornerRadiusHelper.Scale(10, 8, 12),
|
CornerRadius = new CornerRadius(10),
|
||||||
Background = new SolidColorBrush(buttonBackground),
|
Background = new SolidColorBrush(buttonBackground),
|
||||||
BorderBrush = Brushes.Transparent,
|
BorderBrush = Brushes.Transparent,
|
||||||
BorderThickness = new Thickness(0),
|
BorderThickness = new Thickness(0),
|
||||||
@@ -592,8 +590,8 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 36);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 36);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 7, 22),
|
Math.Clamp(12 * scale, 7, 22),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 5, 16));
|
Math.Clamp(9 * scale, 5, 16));
|
||||||
|
|
||||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||||
? Math.Clamp(4 * scale, 2, 6)
|
? Math.Clamp(4 * scale, 2, 6)
|
||||||
@@ -606,12 +604,12 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
|||||||
: Math.Clamp(6 * scale, 3, 8);
|
: Math.Clamp(6 * scale, 3, 8);
|
||||||
|
|
||||||
DialogOverlayBorder.Padding = new Thickness(
|
DialogOverlayBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20),
|
Math.Clamp(12 * scale, 8, 20),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 8, 18));
|
Math.Clamp(10 * scale, 8, 18));
|
||||||
DialogCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 10, 18);
|
DialogCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 10, 18);
|
||||||
DialogCardBorder.Padding = new Thickness(
|
DialogCardBorder.Padding = new Thickness(
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 9, 20),
|
Math.Clamp(12 * scale, 9, 20),
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18));
|
Math.Clamp(11 * scale, 8, 18));
|
||||||
DialogTitleTextBlock.FontSize = Math.Clamp(14 * scale, 11, 20);
|
DialogTitleTextBlock.FontSize = Math.Clamp(14 * scale, 11, 20);
|
||||||
DialogMessageTextBlock.FontSize = Math.Clamp(12 * scale, 10, 17);
|
DialogMessageTextBlock.FontSize = Math.Clamp(12 * scale, 10, 17);
|
||||||
DialogRenameTextBox.FontSize = Math.Clamp(11.5 * scale, 10, 16);
|
DialogRenameTextBox.FontSize = Math.Clamp(11.5 * scale, 10, 16);
|
||||||
|
|||||||
@@ -198,8 +198,7 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 12, 48);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 12, 48);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22));
|
||||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 7, 22));
|
|
||||||
TimerPanelBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * scale, 12, 42);
|
TimerPanelBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * scale, 12, 42);
|
||||||
|
|
||||||
PlayButtonBorder.Width = Math.Clamp(42 * scale, 28, 58);
|
PlayButtonBorder.Width = Math.Clamp(42 * scale, 28, 58);
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
|||||||
|
|
||||||
var horizontalPadding = Math.Clamp(10 * scale, 4, 26);
|
var horizontalPadding = Math.Clamp(10 * scale, 4, 26);
|
||||||
var verticalPadding = Math.Clamp(8 * scale, 3, 22);
|
var verticalPadding = Math.Clamp(8 * scale, 3, 22);
|
||||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46);
|
||||||
|
|
||||||
var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);
|
var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user