mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.7.0.0
This commit is contained in:
@@ -4,5 +4,6 @@
|
||||
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
|
||||
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
|
||||
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);**\obj_audit\**</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
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,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -28,7 +29,9 @@ public sealed record MusicPlaybackState(
|
||||
MusicPlaybackStatus PlaybackStatus,
|
||||
bool CanPlayPause,
|
||||
bool CanSkipPrevious,
|
||||
bool CanSkipNext)
|
||||
bool CanSkipNext,
|
||||
bool CanToggleFavorite,
|
||||
bool IsFavorite)
|
||||
{
|
||||
public static MusicPlaybackState Unsupported()
|
||||
{
|
||||
@@ -46,7 +49,9 @@ public sealed record MusicPlaybackState(
|
||||
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
||||
CanPlayPause: false,
|
||||
CanSkipPrevious: false,
|
||||
CanSkipNext: false);
|
||||
CanSkipNext: false,
|
||||
CanToggleFavorite: false,
|
||||
IsFavorite: false);
|
||||
}
|
||||
|
||||
public static MusicPlaybackState NoSession(bool isSupported = true)
|
||||
@@ -65,7 +70,35 @@ public sealed record MusicPlaybackState(
|
||||
PlaybackStatus: MusicPlaybackStatus.Unknown,
|
||||
CanPlayPause: 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +113,18 @@ public interface IMusicControlService
|
||||
Task<bool> SkipPreviousAsync(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
|
||||
@@ -118,4 +163,25 @@ internal sealed class NoOpMusicControlService : IMusicControlService
|
||||
{
|
||||
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,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -9,8 +10,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisposable
|
||||
{
|
||||
// WinRT Type Resolution
|
||||
private static readonly Type? SessionManagerType = ResolveWinRtType("Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager");
|
||||
private static readonly Type? AppInfoType = ResolveWinRtType("Windows.ApplicationModel.AppInfo");
|
||||
private static readonly MethodInfo? RequestSessionManagerAsyncMethod =
|
||||
@@ -18,15 +20,250 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
private static readonly MethodInfo? AsTaskGenericMethodDefinition = ResolveAsTaskGenericMethod();
|
||||
private static readonly MethodInfo? AsStreamForReadMethod = ResolveAsStreamForReadMethod();
|
||||
|
||||
// Synchronization
|
||||
private static readonly SemaphoreSlim ManagerLock = new(1, 1);
|
||||
private static object? _sessionManager;
|
||||
|
||||
// Instance State
|
||||
private readonly ConcurrentDictionary<string, string> _sourceAppNameCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
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 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)
|
||||
{
|
||||
if (!IsRuntimeSupported())
|
||||
@@ -56,10 +293,17 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
var canSkipNext = ReadBoolProperty(controls, "IsNextEnabled");
|
||||
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 sourceAppName = await ResolveSourceAppDisplayNameAsync(sourceAppId, cancellationToken);
|
||||
|
||||
var timeline = InvokeMethod(session, "GetTimelineProperties");
|
||||
// Use async method to get timeline properties
|
||||
var timeline = await TryGetTimelinePropertiesAsync(session, cancellationToken);
|
||||
var position = ReadTimeSpanProperty(timeline, "Position");
|
||||
var start = ReadTimeSpanProperty(timeline, "StartTime");
|
||||
var end = ReadTimeSpanProperty(timeline, "EndTime");
|
||||
@@ -103,7 +347,9 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
PlaybackStatus: MapPlaybackStatus(playbackStatusRaw),
|
||||
CanPlayPause: canPlayPause,
|
||||
CanSkipPrevious: canSkipPrevious,
|
||||
CanSkipNext: canSkipNext);
|
||||
CanSkipNext: canSkipNext,
|
||||
CanToggleFavorite: canToggleFavorite,
|
||||
IsFavorite: isFavorite);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -199,6 +445,113 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
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)
|
||||
{
|
||||
if (!IsRuntimeSupported())
|
||||
@@ -259,6 +612,20 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
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(
|
||||
object? mediaProperties,
|
||||
string sourceAppId,
|
||||
@@ -576,4 +943,11 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
|
||||
_ => MusicPlaybackStatus.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopListening();
|
||||
_stateGate.Dispose();
|
||||
ManagerLock.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56);
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(14 * scale, 14 * scale, null, 0.55d);
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
|
||||
@@ -382,15 +382,21 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
10 * softScale,
|
||||
8 * softScale,
|
||||
null,
|
||||
0.45d);
|
||||
|
||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
||||
|
||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||
var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d));
|
||||
var rootPadding = RootBorder.Padding;
|
||||
var cardPadding = CardBorder.Padding;
|
||||
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);
|
||||
ContentGrid.RowSpacing = rowSpacing;
|
||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||
|
||||
@@ -387,15 +387,21 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
10 * softScale,
|
||||
8 * softScale,
|
||||
null,
|
||||
0.45d);
|
||||
|
||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
||||
|
||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||
var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d));
|
||||
var rootPadding = RootBorder.Padding;
|
||||
var cardPadding = CardBorder.Padding;
|
||||
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);
|
||||
ContentGrid.RowSpacing = rowSpacing;
|
||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||
|
||||
@@ -80,11 +80,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||
RootBorder.Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(_currentCellSize * 0.20, 8, 18));
|
||||
|
||||
WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
||||
AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
||||
AddressBarBorder.Padding = new Thickness(8, 6);
|
||||
AddressBarBorder.Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10));
|
||||
|
||||
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 lineSpacing = Math.Clamp(4 * scale, 1.5, 8);
|
||||
var itemPadding = new Thickness(
|
||||
Math.Clamp(6 * scale, 3, 10),
|
||||
Math.Clamp(4 * scale, 2, 8),
|
||||
Math.Clamp(4 * scale, 2, 8),
|
||||
Math.Clamp(4 * scale, 2, 8));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(6 * scale, 3, 10),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8));
|
||||
var maxVisibleItems = ResolveMaxVisibleItems(scale);
|
||||
|
||||
var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
||||
|
||||
@@ -546,14 +546,24 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
10 * scale,
|
||||
8 * scale,
|
||||
null,
|
||||
0.45d);
|
||||
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
CardBorder.Padding = new Thickness(
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
Math.Clamp(16 * scale, 8, 24),
|
||||
Math.Clamp(14 * scale, 7, 22),
|
||||
Math.Clamp(16 * scale, 8, 24),
|
||||
Math.Clamp(14 * scale, 7, 22));
|
||||
null,
|
||||
0.55d);
|
||||
|
||||
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);
|
||||
BrandPrimaryTextBlock.FontSize = headlineFont;
|
||||
@@ -567,7 +577,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshGlyphIcon.FontSize = Math.Clamp(19 * scale, 11, 24);
|
||||
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
|
||||
|
||||
var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170);
|
||||
var imageWidth = Math.Clamp(contentWidth * 0.20, 60, 170);
|
||||
var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94);
|
||||
News1ImageHost.Width = imageWidth;
|
||||
News1ImageHost.Height = imageHeight;
|
||||
@@ -584,7 +594,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
var availableTextWidth = Math.Max(
|
||||
84,
|
||||
totalWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
|
||||
contentWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
|
||||
News1TitleTextBlock.MaxWidth = availableTextWidth;
|
||||
News2TitleTextBlock.MaxWidth = availableTextWidth;
|
||||
|
||||
|
||||
@@ -69,4 +69,28 @@ internal static class ComponentChromeCornerRadiusHelper
|
||||
var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness);
|
||||
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,18 +102,23 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
12 * scale,
|
||||
10 * scale,
|
||||
null,
|
||||
0.45d);
|
||||
|
||||
InfoPanel.Padding = new Thickness(
|
||||
Math.Clamp(18 * scale, 10, 28),
|
||||
Math.Clamp(14 * scale, 8, 22),
|
||||
Math.Clamp(18 * scale, 10, 28),
|
||||
Math.Clamp(14 * scale, 8, 22));
|
||||
InfoPanel.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
18 * scale,
|
||||
14 * scale,
|
||||
null,
|
||||
0.52d);
|
||||
|
||||
DateInfoStack.Margin = new Thickness(
|
||||
Math.Clamp(18 * scale, 8, 30),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(18 * scale, 8, 30),
|
||||
0,
|
||||
0,
|
||||
Math.Clamp(16 * scale, 8, 26));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 26));
|
||||
DateInfoStack.Spacing = Math.Clamp(4 * scale, 2, 10);
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24);
|
||||
@@ -425,16 +430,18 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
var scale = ResolveScale();
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
var rootPadding = RootBorder.Padding;
|
||||
|
||||
var leftStar = totalWidth < _currentCellSize * 4.2 ? 2.0 : 2.08;
|
||||
MainLayoutGrid.ColumnDefinitions[0].Width = new GridLength(leftStar, GridUnitType.Star);
|
||||
MainLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
|
||||
|
||||
var rightPanelWidth = Math.Max(84, totalWidth / (leftStar + 1));
|
||||
var availableWidth = Math.Max(84, totalWidth - rootPadding.Left - rootPadding.Right);
|
||||
var rightPanelWidth = Math.Max(84, availableWidth / (leftStar + 1));
|
||||
var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right);
|
||||
var leftPanelWidth = Math.Max(84, totalWidth - rightPanelWidth);
|
||||
var leftPanelWidth = Math.Max(84, availableWidth - rightPanelWidth);
|
||||
var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10);
|
||||
var leftContentHeight = Math.Max(30, totalHeight - DateInfoStack.Margin.Bottom - 10);
|
||||
var leftContentHeight = Math.Max(30, totalHeight - rootPadding.Top - rootPadding.Bottom - DateInfoStack.Margin.Bottom - 10);
|
||||
|
||||
var dateStackSpacing = Math.Clamp(4 * scale, 2, 10);
|
||||
DateInfoStack.Spacing = dateStackSpacing;
|
||||
@@ -464,7 +471,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
lineHeightFactor: 1.10);
|
||||
WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10;
|
||||
|
||||
var rightContentHeight = Math.Max(42, totalHeight - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom);
|
||||
var rightContentHeight = Math.Max(42, totalHeight - rootPadding.Top - rootPadding.Bottom - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom);
|
||||
var titleBottomMargin = Math.Clamp(8 * scale, 4, 14);
|
||||
var separatorBottomMargin = Math.Clamp(10 * scale, 4, 14);
|
||||
var bottomStackSpacing = Math.Clamp(3 * scale, 2, 8);
|
||||
|
||||
@@ -93,25 +93,25 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(20 * scale, 10, 34),
|
||||
Math.Clamp(16 * scale, 8, 28),
|
||||
Math.Clamp(20 * scale, 10, 34),
|
||||
Math.Clamp(14 * scale, 7, 24));
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
20 * scale,
|
||||
16 * scale,
|
||||
null,
|
||||
0.55d);
|
||||
|
||||
QuoteMarkTextBlock.FontSize = Math.Clamp(80 * scale, 32, 120);
|
||||
QuoteMarkTextBlock.LineHeight = Math.Clamp(68 * scale, 26, 100);
|
||||
QuoteMarkTextBlock.Margin = new Thickness(Math.Clamp(1 * scale, 0, 3), 0, 0, 0);
|
||||
|
||||
PoetryContentTextBlock.Margin = new Thickness(
|
||||
Math.Clamp(8 * scale, 4, 16),
|
||||
Math.Clamp(2 * scale, 0, 8),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 16),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(2 * scale, 0, 8),
|
||||
0,
|
||||
0);
|
||||
|
||||
AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5);
|
||||
AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34);
|
||||
AuthorAccent.Margin = new Thickness(0, 0, Math.Clamp(8 * scale, 4, 13), 0);
|
||||
AuthorAccent.Margin = new Thickness(0, 0, ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 13), 0);
|
||||
AuthorAccent.CornerRadius = new CornerRadius(Math.Clamp(3 * scale, 1.5, 4.5));
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(17 * scale, 9, 26);
|
||||
@@ -328,15 +328,13 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
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)
|
||||
{
|
||||
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.Foreground = CreateBrush("#4AF4C5A6");
|
||||
@@ -360,11 +358,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
else
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -385,6 +378,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
StatusTextBlock.Foreground = CreateBrush("#8A8F98");
|
||||
}
|
||||
|
||||
RootBorder.Padding = rootPadding;
|
||||
UpdateRefreshButtonState();
|
||||
ApplyAdaptiveTextLayout(isNightMode, scale, totalWidth, totalHeight);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
@@ -329,12 +330,17 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
12 * scale,
|
||||
10 * scale,
|
||||
null,
|
||||
0.45d);
|
||||
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(11 * scale, 7, 16),
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(11 * scale, 7, 16));
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
12 * scale,
|
||||
11 * scale,
|
||||
null,
|
||||
0.55d);
|
||||
|
||||
var refreshSize = Math.Clamp(30 * scale, 20, 38);
|
||||
RefreshButton.Width = refreshSize;
|
||||
@@ -342,44 +348,60 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
||||
RefreshIcon.FontSize = Math.Clamp(14 * scale, 10, 20);
|
||||
|
||||
var contentWidth = Math.Max(80, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
||||
var contentWidth = Math.Max(
|
||||
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));
|
||||
WordTextBlock.MaxWidth = wordWidth;
|
||||
|
||||
var contentHeight = Math.Max(52, totalHeight - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
||||
var contentHeight = Math.Max(
|
||||
52,
|
||||
totalHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
||||
var wordHeightBudget = Math.Max(18, contentHeight * 0.34);
|
||||
var detailHeightBudget = Math.Max(18, contentHeight - wordHeightBudget - Math.Clamp(8 * scale, 4, 14));
|
||||
|
||||
WordTextBlock.FontSize = FitFontSize(
|
||||
var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
WordTextBlock.Text,
|
||||
wordWidth,
|
||||
wordHeightBudget,
|
||||
maxLines: 1,
|
||||
minFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||
maxFontSize: Math.Clamp(38 * scale, 20, 50),
|
||||
weight: FontWeight.Bold,
|
||||
lineHeightFactor: 1.02);
|
||||
WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.02;
|
||||
1,
|
||||
1,
|
||||
Math.Clamp(18 * scale, 12, 22),
|
||||
Math.Clamp(38 * scale, 20, 50),
|
||||
[FontWeight.Bold],
|
||||
1.02,
|
||||
MiSansFontFamily);
|
||||
WordTextBlock.FontSize = wordLayout.FontSize;
|
||||
WordTextBlock.FontWeight = wordLayout.Weight;
|
||||
WordTextBlock.MaxLines = wordLayout.MaxLines;
|
||||
WordTextBlock.TextWrapping = TextWrapping.NoWrap;
|
||||
WordTextBlock.LineHeight = wordLayout.LineHeight;
|
||||
|
||||
var detailFont = FitFontSize(
|
||||
var detailLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
MeaningTextBlock.IsVisible ? MeaningTextBlock.Text : HiddenHintTextBlock.Text,
|
||||
contentWidth,
|
||||
detailHeightBudget,
|
||||
maxLines: MeaningTextBlock.IsVisible ? 5 : 4,
|
||||
minFontSize: Math.Clamp(12 * scale, 9, 14),
|
||||
maxFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||
weight: FontWeight.SemiBold,
|
||||
lineHeightFactor: 1.10);
|
||||
1,
|
||||
MeaningTextBlock.IsVisible ? 5 : 4,
|
||||
Math.Clamp(12 * scale, 9, 14),
|
||||
Math.Clamp(18 * scale, 12, 22),
|
||||
[FontWeight.SemiBold, FontWeight.Medium],
|
||||
1.10,
|
||||
MiSansFontFamily);
|
||||
|
||||
MeaningTextBlock.MaxWidth = contentWidth;
|
||||
MeaningTextBlock.FontSize = detailFont;
|
||||
MeaningTextBlock.LineHeight = detailFont * 1.10;
|
||||
MeaningTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 4 : 5;
|
||||
MeaningTextBlock.FontSize = detailLayout.FontSize;
|
||||
MeaningTextBlock.FontWeight = detailLayout.Weight;
|
||||
MeaningTextBlock.LineHeight = detailLayout.LineHeight;
|
||||
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.FontSize = detailFont;
|
||||
HiddenHintTextBlock.LineHeight = detailFont * 1.10;
|
||||
HiddenHintTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 3 : 4;
|
||||
HiddenHintTextBlock.FontSize = detailLayout.FontSize;
|
||||
HiddenHintTextBlock.FontWeight = detailLayout.Weight;
|
||||
HiddenHintTextBlock.LineHeight = detailLayout.LineHeight;
|
||||
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);
|
||||
}
|
||||
@@ -509,58 +531,4 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
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,6 +10,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
@@ -300,14 +301,18 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
|
||||
var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.CornerRadius = containerRadius;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
10 * scale,
|
||||
8 * scale,
|
||||
null,
|
||||
0.45d);
|
||||
|
||||
CardBorder.CornerRadius = containerRadius;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(16 * scale, 8, 24),
|
||||
Math.Clamp(14 * scale, 7, 22),
|
||||
Math.Clamp(16 * scale, 8, 24),
|
||||
Math.Clamp(14 * scale, 7, 22));
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
16 * scale,
|
||||
14 * scale,
|
||||
null,
|
||||
0.55d);
|
||||
|
||||
var refreshSize = Math.Clamp(38 * scale, 22, 48);
|
||||
RefreshButton.Width = refreshSize;
|
||||
@@ -346,65 +351,90 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
exampleHeightBudget += Math.Clamp(11 * scale, 5, 18);
|
||||
}
|
||||
|
||||
var wordBase = Math.Clamp(56 * scale, 18, 72);
|
||||
WordTextBlock.FontSize = FitFontSize(
|
||||
var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
WordTextBlock.Text,
|
||||
wordWidth,
|
||||
wordHeightBudget,
|
||||
maxLines: 1,
|
||||
minFontSize: Math.Max(14, wordBase * 0.56),
|
||||
maxFontSize: wordBase,
|
||||
weight: FontWeight.Bold,
|
||||
lineHeightFactor: 1.04);
|
||||
WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.04;
|
||||
1,
|
||||
1,
|
||||
Math.Max(14, Math.Clamp(56 * scale, 18, 72) * 0.56),
|
||||
Math.Clamp(56 * scale, 18, 72),
|
||||
[FontWeight.Bold],
|
||||
1.04,
|
||||
MiSansFontFamily);
|
||||
WordTextBlock.FontSize = wordLayout.FontSize;
|
||||
WordTextBlock.FontWeight = wordLayout.Weight;
|
||||
WordTextBlock.MaxLines = wordLayout.MaxLines;
|
||||
WordTextBlock.TextWrapping = TextWrapping.NoWrap;
|
||||
WordTextBlock.LineHeight = wordLayout.LineHeight;
|
||||
|
||||
var pronunciationBase = Math.Clamp(27 * scale, 10, 36);
|
||||
PronunciationTextBlock.FontSize = FitFontSize(
|
||||
var pronunciationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
PronunciationTextBlock.Text,
|
||||
contentWidth,
|
||||
pronunciationHeightBudget,
|
||||
maxLines: 1,
|
||||
minFontSize: Math.Max(8.6, pronunciationBase * 0.62),
|
||||
maxFontSize: pronunciationBase,
|
||||
weight: FontWeight.SemiBold,
|
||||
lineHeightFactor: 1.08);
|
||||
PronunciationTextBlock.LineHeight = PronunciationTextBlock.FontSize * 1.08;
|
||||
1,
|
||||
1,
|
||||
Math.Max(8.6, Math.Clamp(27 * scale, 10, 36) * 0.62),
|
||||
Math.Clamp(27 * scale, 10, 36),
|
||||
[FontWeight.SemiBold, FontWeight.Medium],
|
||||
1.08,
|
||||
MiSansFontFamily);
|
||||
PronunciationTextBlock.FontSize = pronunciationLayout.FontSize;
|
||||
PronunciationTextBlock.FontWeight = pronunciationLayout.Weight;
|
||||
PronunciationTextBlock.MaxLines = pronunciationLayout.MaxLines;
|
||||
PronunciationTextBlock.TextWrapping = TextWrapping.NoWrap;
|
||||
PronunciationTextBlock.LineHeight = pronunciationLayout.LineHeight;
|
||||
|
||||
var meaningBase = Math.Clamp(25 * scale, 10, 34);
|
||||
MeaningTextBlock.FontSize = FitFontSize(
|
||||
var meaningLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
MeaningTextBlock.Text,
|
||||
contentWidth,
|
||||
meaningHeightBudget,
|
||||
maxLines: Math.Max(1, MeaningTextBlock.MaxLines),
|
||||
minFontSize: Math.Max(9.2, meaningBase * 0.60),
|
||||
maxFontSize: meaningBase,
|
||||
weight: FontWeight.SemiBold,
|
||||
lineHeightFactor: 1.10);
|
||||
MeaningTextBlock.LineHeight = MeaningTextBlock.FontSize * 1.10;
|
||||
1,
|
||||
Math.Max(1, MeaningTextBlock.MaxLines),
|
||||
Math.Max(9.2, Math.Clamp(25 * scale, 10, 34) * 0.60),
|
||||
Math.Clamp(25 * scale, 10, 34),
|
||||
[FontWeight.SemiBold, FontWeight.Medium],
|
||||
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 exampleBase = Math.Clamp(22 * scale, 9, 30);
|
||||
ExampleTextBlock.FontSize = FitFontSize(
|
||||
var exampleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
ExampleTextBlock.Text,
|
||||
contentWidth,
|
||||
exampleHeightBudget,
|
||||
maxLines: Math.Max(1, ExampleTextBlock.MaxLines),
|
||||
minFontSize: Math.Max(8.8, exampleBase * 0.58),
|
||||
maxFontSize: exampleBase,
|
||||
weight: FontWeight.Medium,
|
||||
lineHeightFactor: 1.08);
|
||||
ExampleTextBlock.LineHeight = ExampleTextBlock.FontSize * 1.08;
|
||||
1,
|
||||
Math.Max(1, ExampleTextBlock.MaxLines),
|
||||
Math.Max(8.8, Math.Clamp(22 * scale, 9, 30) * 0.58),
|
||||
Math.Clamp(22 * scale, 9, 30),
|
||||
[FontWeight.Medium, FontWeight.SemiBold],
|
||||
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 translationBase = Math.Clamp(20 * scale, 8, 28);
|
||||
ExampleTranslationTextBlock.FontSize = FitFontSize(
|
||||
var translationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
ExampleTranslationTextBlock.Text,
|
||||
contentWidth,
|
||||
Math.Max(10, exampleHeightBudget * 0.44),
|
||||
maxLines: 1,
|
||||
minFontSize: Math.Max(7.8, translationBase * 0.62),
|
||||
maxFontSize: translationBase,
|
||||
weight: FontWeight.Medium,
|
||||
lineHeightFactor: 1.06);
|
||||
ExampleTranslationTextBlock.LineHeight = ExampleTranslationTextBlock.FontSize * 1.06;
|
||||
1,
|
||||
1,
|
||||
Math.Max(7.8, Math.Clamp(20 * scale, 8, 28) * 0.62),
|
||||
Math.Clamp(20 * scale, 8, 28),
|
||||
[FontWeight.Medium, FontWeight.Normal],
|
||||
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);
|
||||
}
|
||||
@@ -587,58 +617,4 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
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,6 +6,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
@@ -253,8 +254,36 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
|
||||
// 4x2 widget has less vertical space than 2x2. Compress only on 6-row months.
|
||||
var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0;
|
||||
var dayFontSize = Math.Clamp(_calendarDayFontSize * rowDensity, 8, 24);
|
||||
var todayDotSize = Math.Clamp(_calendarTodayDotSize * rowDensity, 13.5, 32);
|
||||
var availableCalendarHeight = Math.Max(
|
||||
1,
|
||||
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++)
|
||||
{
|
||||
@@ -286,6 +315,10 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
: Brushes.White;
|
||||
|
||||
dayText.Foreground = onAccentBrush;
|
||||
dayText.Width = todayGlyphBox.Width;
|
||||
dayText.Height = todayGlyphBox.Height;
|
||||
dayText.TextAlignment = TextAlignment.Center;
|
||||
dayText.LineHeight = todayGlyphSize * 1.03;
|
||||
var dot = new Border
|
||||
{
|
||||
Width = todayDotSize,
|
||||
@@ -326,28 +359,28 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40);
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(13 * scale, 8 * scale, null, 0.55d);
|
||||
|
||||
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
||||
LeftPanelGrid.RowSpacing = Math.Clamp(5.2 * scale, 2.5, 10);
|
||||
LeftPanelGrid.RowSpacing = Math.Clamp(6.2 * scale, 3, 11);
|
||||
WeekdayHeaderGrid.Margin = new Thickness(
|
||||
0,
|
||||
Math.Clamp(0.5 * scale, 0, 2),
|
||||
Math.Clamp(0.8 * scale, 0, 2.5),
|
||||
0,
|
||||
Math.Clamp(2.4 * scale, 1, 4));
|
||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
||||
Math.Clamp(3.0 * scale, 1.5, 4.5));
|
||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * scale, 0.5, 2.5));
|
||||
|
||||
LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34);
|
||||
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
||||
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
||||
LunarCardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 9 * scale, null, 0.55d);
|
||||
RightPanelGrid.RowSpacing = Math.Clamp(8.2 * scale, 4, 12);
|
||||
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 headerTextLength = Math.Max(1, GregorianHeadlineTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||
var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(GregorianHeadlineTextBlock.Text));
|
||||
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;
|
||||
|
||||
GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 42);
|
||||
GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 40);
|
||||
GregorianHeadlineTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
GregorianHeadlineTextBlock.LineHeight = GregorianHeadlineTextBlock.FontSize * 1.03;
|
||||
|
||||
@@ -360,9 +393,9 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
block.LineHeight = _weekdayFontSize * 1.02;
|
||||
}
|
||||
|
||||
_calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 22);
|
||||
_calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 20);
|
||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.30, 13.5, 31);
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.42, 15, 30);
|
||||
|
||||
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);
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48);
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 6, 18));
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(12 * scale, 12 * scale, null, 0.55d);
|
||||
}
|
||||
|
||||
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
ClipToBounds="True"
|
||||
Padding="14">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
RowDefinitions="1.1*,2.3*,0.62*,0.78*,0.95*"
|
||||
RowDefinitions="1.15*,2.45*,0.65*,0.82*,1.0*"
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Grid.Row="0"
|
||||
@@ -22,11 +22,12 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#61697C"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1"
|
||||
Margin="8,0,8,0" />
|
||||
MaxLines="2"
|
||||
Margin="10,0,10,0" />
|
||||
|
||||
<Viewbox Grid.Row="1"
|
||||
Stretch="Uniform"
|
||||
@@ -108,10 +109,10 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="NoWrap"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1"
|
||||
Margin="8,0,8,0" />
|
||||
MaxLines="2"
|
||||
Margin="10,0,10,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
@@ -211,46 +212,81 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
||||
var scale = ResolveScale(width, height);
|
||||
var isCompact = width < 170 || height < 170;
|
||||
var isUltraCompact = width < 130 || height < 130;
|
||||
var titleUnits = GetDisplayUnits(TitleTextBlock.Text);
|
||||
var dateUnits = GetDisplayUnits(DateTextBlock.Text);
|
||||
var titleUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(TitleTextBlock.Text);
|
||||
var dateUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(DateTextBlock.Text);
|
||||
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
||||
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46);
|
||||
var padding = ComponentChromeCornerRadiusHelper.SafeValue(shortSide * 0.05, 4.5, 21);
|
||||
RootBorder.Padding = new Thickness(padding);
|
||||
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
shortSide * 0.055,
|
||||
shortSide * 0.05,
|
||||
null,
|
||||
0.55d);
|
||||
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.034, 3.2, 14);
|
||||
var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines);
|
||||
|
||||
var innerWidth = Math.Max(1, width - padding * 2);
|
||||
var innerHeight = Math.Max(1, height - padding * 2);
|
||||
var rootPadding = RootBorder.Padding;
|
||||
var innerWidth = Math.Max(1, width - rootPadding.Left - rootPadding.Right);
|
||||
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 row0Height = innerHeight * (rowWeights[0] / totalWeight);
|
||||
var row1Height = innerHeight * (rowWeights[1] / totalWeight);
|
||||
var row3Height = innerHeight * (rowWeights[3] / totalWeight);
|
||||
var row4Height = innerHeight * (rowWeights[4] / totalWeight);
|
||||
var horizontalMargin = Math.Clamp(8 * scale, 4, 14);
|
||||
var horizontalMargin = Math.Clamp(9 * scale, 5, 16);
|
||||
var titleMaxWidth = Math.Max(24, innerWidth - horizontalMargin * 2);
|
||||
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, 8.8, 34);
|
||||
var titlePreferred = Math.Clamp(24 * scale, 9.2, 34);
|
||||
var titleHeightCap = Math.Max(10, row0Height * 0.94);
|
||||
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.FontSize = FitTextSize(
|
||||
var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
TitleTextBlock.Text,
|
||||
TitleTextBlock.FontWeight,
|
||||
Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)),
|
||||
8.6,
|
||||
titleMaxWidth,
|
||||
Math.Max(24, titleContentBox.Width),
|
||||
titleHeightCap,
|
||||
1,
|
||||
titleLineCount,
|
||||
lineHeightFactor: 1.10);
|
||||
TitleTextBlock.LineHeight = TitleTextBlock.FontSize * 1.10;
|
||||
8.6,
|
||||
Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)),
|
||||
[TitleTextBlock.FontWeight],
|
||||
1.10);
|
||||
TitleTextBlock.FontSize = titleLayout.FontSize;
|
||||
TitleTextBlock.FontWeight = titleLayout.Weight;
|
||||
TitleTextBlock.MaxLines = titleLayout.MaxLines;
|
||||
TitleTextBlock.TextWrapping = titleLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
||||
TitleTextBlock.LineHeight = titleLayout.LineHeight;
|
||||
|
||||
var digitCount = Math.Max(1, CountTextBlock.Text?.Trim().Length ?? 1);
|
||||
var digitCount = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(CountTextBlock.Text));
|
||||
var digitCompression = digitCount switch
|
||||
{
|
||||
>= 5 => 0.68,
|
||||
@@ -261,39 +297,60 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
||||
var countCompactFactor = isUltraCompact ? 0.86 : isCompact ? 0.93 : 1.0;
|
||||
var countPreferred = Math.Clamp(132 * scale * digitCompression * countCompactFactor, 28, 170);
|
||||
var countHeightCap = Math.Max(30, row1Height * 0.96);
|
||||
CountTextBlock.FontSize = FitTextSize(
|
||||
var countLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
CountTextBlock.Text,
|
||||
CountTextBlock.FontWeight,
|
||||
Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)),
|
||||
24,
|
||||
titleMaxWidth,
|
||||
Math.Max(24, countContentBox.Width),
|
||||
countHeightCap,
|
||||
maxLines: 1,
|
||||
lineHeightFactor: 1.08);
|
||||
CountTextBlock.LineHeight = CountTextBlock.FontSize * 1.08;
|
||||
1,
|
||||
1,
|
||||
24,
|
||||
Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)),
|
||||
[CountTextBlock.FontWeight],
|
||||
1.08);
|
||||
CountTextBlock.FontSize = countLayout.FontSize;
|
||||
CountTextBlock.FontWeight = countLayout.Weight;
|
||||
CountTextBlock.MaxLines = countLayout.MaxLines;
|
||||
CountTextBlock.TextWrapping = TextWrapping.NoWrap;
|
||||
CountTextBlock.LineHeight = countLayout.LineHeight;
|
||||
|
||||
var unitCompactFactor = isUltraCompact ? 0.8 : isCompact ? 0.9 : 1.0;
|
||||
DayUnitTextBlock.FontSize = Math.Clamp(52 * scale * unitCompactFactor, 10, 72);
|
||||
DayUnitTextBlock.FontSize = Math.Min(DayUnitTextBlock.FontSize, Math.Max(10, row3Height * 0.64));
|
||||
DayUnitTextBlock.LineHeight = DayUnitTextBlock.FontSize * 1.02;
|
||||
var unitPreferred = Math.Min(Math.Clamp(52 * scale * unitCompactFactor, 10, 72), Math.Max(10, row3Height * 0.64));
|
||||
var unitLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
DayUnitTextBlock.Text,
|
||||
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 datePreferred = Math.Clamp(32 * scale * dateCompactFactor, 9, 46);
|
||||
var dateHeightCap = Math.Max(10, row4Height * 0.96);
|
||||
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.FontSize = FitTextSize(
|
||||
var dateLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
DateTextBlock.Text,
|
||||
DateTextBlock.FontWeight,
|
||||
Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)),
|
||||
8.5,
|
||||
dateMaxWidth,
|
||||
Math.Max(24, dateContentBox.Width),
|
||||
dateHeightCap,
|
||||
1,
|
||||
dateLineCount,
|
||||
lineHeightFactor: 1.12);
|
||||
DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.12;
|
||||
8.5,
|
||||
Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)),
|
||||
[DateTextBlock.FontWeight],
|
||||
1.12);
|
||||
DateTextBlock.FontSize = dateLayout.FontSize;
|
||||
DateTextBlock.FontWeight = dateLayout.Weight;
|
||||
DateTextBlock.MaxLines = dateLayout.MaxLines;
|
||||
DateTextBlock.TextWrapping = dateLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
||||
DateTextBlock.LineHeight = dateLayout.LineHeight;
|
||||
}
|
||||
|
||||
private double[] ApplyAdaptiveRowHeights(
|
||||
@@ -343,66 +400,6 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
||||
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)
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.56, 2.0);
|
||||
@@ -410,4 +407,5 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -401,18 +401,26 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
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);
|
||||
|
||||
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
||||
|
||||
var rootPadding = RootBorder.Padding;
|
||||
var cardPadding = CardBorder.Padding;
|
||||
|
||||
var rowSpacing = Math.Clamp(8 * softScale, 4, 12);
|
||||
ContentGrid.RowSpacing = rowSpacing;
|
||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16);
|
||||
|
||||
var innerWidth = Math.Max(150, totalWidth - horizontalPadding * 2d);
|
||||
var innerHeight = Math.Max(160, totalHeight - verticalPadding * 2d);
|
||||
var innerWidth = Math.Max(150, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
||||
var innerHeight = Math.Max(160, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom);
|
||||
var availableRowsHeight = Math.Max(120, innerHeight - rowSpacing * 4d);
|
||||
var headerHeight = Math.Clamp(availableRowsHeight * 0.16, 24, 54);
|
||||
var itemHeight = Math.Max(32, (availableRowsHeight - headerHeight) / 4d);
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
Background="#EFE6D9"
|
||||
CornerRadius="30"
|
||||
ClipToBounds="True"
|
||||
Padding="16">
|
||||
Padding="18">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="300"
|
||||
Height="300"
|
||||
RowDefinitions="Auto,Auto,Auto,*"
|
||||
RowSpacing="10">
|
||||
RowSpacing="12">
|
||||
<TextBlock x:Name="GregorianLineTextBlock"
|
||||
Grid.Row="0"
|
||||
Text="10/9 Thu"
|
||||
@@ -32,21 +32,25 @@
|
||||
FontSize="88"
|
||||
FontWeight="Bold"
|
||||
Foreground="#6B4936"
|
||||
HorizontalAlignment="Center" />
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2"
|
||||
Margin="4,0,4,0" />
|
||||
|
||||
<Border x:Name="DividerBorder"
|
||||
Grid.Row="2"
|
||||
Height="1"
|
||||
Margin="8,8,8,2"
|
||||
Margin="10,8,10,3"
|
||||
Background="#D2C6B7" />
|
||||
|
||||
<Grid x:Name="AuspiciousGrid"
|
||||
Grid.Row="3"
|
||||
RowDefinitions="Auto,Auto"
|
||||
RowSpacing="12">
|
||||
RowSpacing="14">
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
ColumnSpacing="10">
|
||||
<TextBlock x:Name="YiLabelTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="Yi"
|
||||
@@ -59,12 +63,13 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#6B4936"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
ColumnSpacing="10">
|
||||
<TextBlock x:Name="JiLabelTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="Ji"
|
||||
@@ -77,7 +82,8 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#6B4936"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
@@ -181,36 +182,109 @@ public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
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.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 24));
|
||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(18 * scale, 18 * scale, null, 0.58d);
|
||||
LayoutRoot.RowSpacing = Math.Clamp(12 * scale, 6, 20);
|
||||
DividerBorder.Margin = new Thickness(
|
||||
Math.Clamp(10 * scale, 4, 16),
|
||||
Math.Clamp(8 * scale, 3, 14),
|
||||
Math.Clamp(8 * scale, 3, 14),
|
||||
Math.Clamp(8 * scale, 3, 14),
|
||||
Math.Clamp(2 * scale, 1, 6));
|
||||
AuspiciousGrid.RowSpacing = Math.Clamp(12 * scale, 6, 20);
|
||||
Math.Clamp(10 * scale, 4, 16),
|
||||
Math.Clamp(3 * scale, 1, 7));
|
||||
AuspiciousGrid.RowSpacing = Math.Clamp(14 * scale, 7, 22);
|
||||
|
||||
var densityBoost = scale <= 0.72 ? 0.90 : scale <= 0.88 ? 0.95 : scale >= 1.42 ? 1.04 : 1.0;
|
||||
GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 38);
|
||||
LunarDateTextBlock.FontSize = Math.Clamp(88 * scale * densityBoost, 28, 134);
|
||||
YiLabelTextBlock.FontSize = Math.Clamp(30 * scale * densityBoost, 12, 46);
|
||||
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
|
||||
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 36);
|
||||
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
|
||||
var lunarTitleBox = ComponentTypographyLayoutService.ResolveGlyphBox(
|
||||
Math.Max(1, Bounds.Width > 1 ? Bounds.Width : 300),
|
||||
Math.Max(1, Bounds.Height > 1 ? Bounds.Height : 300),
|
||||
preferredSizeScale: 0.50d,
|
||||
minSize: 28,
|
||||
maxSize: 134,
|
||||
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);
|
||||
|
||||
_gregorianLineWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_lunarDateWeight = ToVariableWeight(Lerp(650, 780, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_labelWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_itemsWeight = ToVariableWeight(Lerp(520, 670, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
var gregorianLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
GregorianLineTextBlock.Text,
|
||||
Math.Max(120, lunarTitleBox.Width * 0.9),
|
||||
Math.Max(16, 44 * scale * densityBoost),
|
||||
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;
|
||||
|
||||
GregorianLineTextBlock.FontWeight = _gregorianLineWeight;
|
||||
LunarDateTextBlock.FontWeight = _lunarDateWeight;
|
||||
YiLabelTextBlock.FontWeight = _labelWeight;
|
||||
JiLabelTextBlock.FontWeight = _labelWeight;
|
||||
YiItemsTextBlock.FontWeight = _itemsWeight;
|
||||
JiItemsTextBlock.FontWeight = _itemsWeight;
|
||||
var lunarLineCount = lunarNeedsTwoLines ? 2 : 1;
|
||||
LunarDateTextBlock.Margin = new Thickness(2 * scale, 0, 2 * scale, 0);
|
||||
var lunarLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout(
|
||||
LunarDateTextBlock.Text,
|
||||
Math.Max(120, lunarTitleBox.Width - (LunarDateTextBlock.Margin.Left + LunarDateTextBlock.Margin.Right)),
|
||||
lunarLineCount > 1
|
||||
? 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
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.DesktopComponents.Runtime;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
@@ -26,6 +27,7 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
private double _calendarDayFontSize = 22;
|
||||
private FontWeight _calendarDayFontWeight = FontWeight.SemiBold;
|
||||
private double _calendarTodayDotSize = 44;
|
||||
private int _calendarVisibleRows = 6;
|
||||
|
||||
public MonthCalendarWidget()
|
||||
{
|
||||
@@ -148,6 +150,36 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
var firstDayOfMonth = new DateTime(year, month, 1);
|
||||
var daysInMonth = DateTime.DaysInMonth(year, month);
|
||||
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++)
|
||||
{
|
||||
@@ -163,7 +195,8 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
Text = day.ToString(CultureInfo.CurrentCulture),
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = _calendarDayFontSize,
|
||||
FontSize = dayFontSize,
|
||||
LineHeight = dayFontSize * 1.04,
|
||||
FontWeight = _calendarDayFontWeight,
|
||||
Tag = "day"
|
||||
};
|
||||
@@ -178,11 +211,15 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
: Brushes.White;
|
||||
|
||||
dayText.Foreground = onAccentBrush;
|
||||
dayText.Width = todayGlyphBox.Width;
|
||||
dayText.Height = todayGlyphBox.Height;
|
||||
dayText.TextAlignment = TextAlignment.Center;
|
||||
dayText.LineHeight = todayGlyphSize * 1.03;
|
||||
var dot = new Border
|
||||
{
|
||||
Width = _calendarTodayDotSize,
|
||||
Height = _calendarTodayDotSize,
|
||||
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
|
||||
Width = todayDotSize,
|
||||
Height = todayDotSize,
|
||||
CornerRadius = new CornerRadius(todayDotSize * 0.5),
|
||||
Background = accentBrush,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
@@ -218,21 +255,21 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40);
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22));
|
||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 15 * scale, null, 0.55d);
|
||||
LayoutRoot.RowSpacing = Math.Clamp(11 * scale, 5.5, 17);
|
||||
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
||||
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
||||
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var headerTextLength = Math.Max(1, HeaderTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||
var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(HeaderTextBlock.Text));
|
||||
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;
|
||||
|
||||
HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 62);
|
||||
HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 58);
|
||||
HeaderTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.62) / 1.2, 0, 1)));
|
||||
HeaderTextBlock.LineHeight = HeaderTextBlock.FontSize * 1.05;
|
||||
|
||||
_weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 27);
|
||||
_weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 24);
|
||||
_weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||
foreach (var block in GetWeekdayHeaderBlocks())
|
||||
{
|
||||
@@ -241,9 +278,9 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
block.LineHeight = _weekdayFontSize * 1.06;
|
||||
}
|
||||
|
||||
_calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 32);
|
||||
_calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 28);
|
||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.95, 16, 62);
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.88, 16, 58);
|
||||
}
|
||||
|
||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -240,7 +240,8 @@
|
||||
Grid.Column="0"
|
||||
Classes="music-action"
|
||||
Width="31"
|
||||
Height="31">
|
||||
Height="31"
|
||||
Click="OnQueueButtonClick">
|
||||
<fi:SymbolIcon x:Name="QueueIcon"
|
||||
Symbol="List"
|
||||
IconVariant="Regular"
|
||||
@@ -291,7 +292,8 @@
|
||||
Grid.Column="4"
|
||||
Classes="music-action"
|
||||
Width="31"
|
||||
Height="31">
|
||||
Height="31"
|
||||
Click="OnFavoriteButtonClick">
|
||||
<fi:SymbolIcon x:Name="FavoriteIcon"
|
||||
Symbol="Heart"
|
||||
IconVariant="Regular"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -17,42 +17,43 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private const Symbol PlaySymbol = Symbol.Play;
|
||||
private const Symbol PauseSymbol = Symbol.Pause;
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(2.4)
|
||||
};
|
||||
private const Symbol HeartSymbol = Symbol.Heart;
|
||||
private const Symbol HeartFilledSymbol = Symbol.Heart;
|
||||
|
||||
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private CancellationTokenSource? _refreshCts;
|
||||
private CancellationTokenSource? _commandCts;
|
||||
private Bitmap? _coverBitmap;
|
||||
private MusicPlaybackState _currentState = MusicPlaybackState.NoSession(isSupported: true);
|
||||
private MusicQueueState _currentQueue = MusicQueueState.Empty();
|
||||
private string _languageCode = "zh-CN";
|
||||
private double _currentCellSize = 48;
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _isRefreshing;
|
||||
private bool _isExecutingCommand;
|
||||
private double _progressRatio;
|
||||
private bool _isProgressIndeterminate;
|
||||
private bool _isListening;
|
||||
|
||||
public MusicControlWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
// Subscribe to service events
|
||||
_musicControlService.PlaybackStateChanged += OnPlaybackStateChanged;
|
||||
_musicControlService.QueueChanged += OnQueueChanged;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
ApplyDynamicBackground(null);
|
||||
ApplyState(MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()));
|
||||
@@ -63,21 +64,19 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
var rootRadius = Math.Clamp(30 * scale, 16, 44);
|
||||
var rootCornerRadius = new CornerRadius(rootRadius);
|
||||
|
||||
var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
||||
RootBorder.CornerRadius = rootCornerRadius;
|
||||
ContentPaddingBorder.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale, 9, 22),
|
||||
Math.Clamp(11 * scale, 7, 18),
|
||||
Math.Clamp(14 * scale, 9, 22),
|
||||
Math.Clamp(11 * scale, 7, 18));
|
||||
LayoutGrid.RowSpacing = Math.Clamp(9 * scale, 6, 14);
|
||||
HeaderRowGrid.ColumnSpacing = Math.Clamp(11 * scale, 8, 18);
|
||||
MetaStackPanel.Spacing = Math.Clamp(3 * scale, 1, 6);
|
||||
TimelineRowGrid.ColumnSpacing = Math.Clamp(9 * scale, 6, 14);
|
||||
ActionRowGrid.ColumnSpacing = Math.Clamp(12 * scale, 8, 20);
|
||||
ActionRowGrid.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 4), 0, 0);
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18));
|
||||
LayoutGrid.RowSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
|
||||
HeaderRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18);
|
||||
MetaStackPanel.Spacing = ComponentChromeCornerRadiusHelper.SafeValue(3 * scale, 1, 6);
|
||||
TimelineRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
|
||||
ActionRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20);
|
||||
ActionRowGrid.Margin = new Thickness(0, ComponentChromeCornerRadiusHelper.SafeValue(1 * scale, 0, 4), 0, 0);
|
||||
DynamicBackgroundBase.CornerRadius = rootCornerRadius;
|
||||
BackdropCoverHost.CornerRadius = rootCornerRadius;
|
||||
DynamicGradientOverlay.CornerRadius = rootCornerRadius;
|
||||
@@ -85,7 +84,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
|
||||
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
|
||||
CoverBorder.CornerRadius = new CornerRadius(Math.Clamp(12 * scale, 8, 16));
|
||||
CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 8, 16);
|
||||
|
||||
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
|
||||
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
||||
@@ -132,10 +131,11 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
_ = isEditMode;
|
||||
var wasOnActivePage = _isOnActivePage;
|
||||
_isOnActivePage = isOnActivePage;
|
||||
UpdateRefreshTimerState();
|
||||
UpdateListeningState();
|
||||
|
||||
if (!wasOnActivePage && _isOnActivePage && _isAttached)
|
||||
{
|
||||
// Refresh state when becoming visible again
|
||||
_ = RefreshStateAsync();
|
||||
}
|
||||
}
|
||||
@@ -143,18 +143,15 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
UpdateRefreshTimerState();
|
||||
if (_isOnActivePage)
|
||||
{
|
||||
_ = RefreshStateAsync();
|
||||
}
|
||||
UpdateListeningState();
|
||||
_ = RefreshStateAsync();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = false;
|
||||
UpdateRefreshTimerState();
|
||||
CancelRefreshRequest();
|
||||
UpdateListeningState();
|
||||
CancelCommandRequest();
|
||||
DisposeCoverBitmap();
|
||||
}
|
||||
|
||||
@@ -163,9 +160,87 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||
private void OnPlaybackStateChanged(object? sender, MusicPlaybackState state)
|
||||
{
|
||||
await RefreshStateAsync();
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
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)
|
||||
@@ -183,6 +258,25 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
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)
|
||||
{
|
||||
await ExecuteCommandAsync(
|
||||
@@ -208,85 +302,39 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
|
||||
_ = await command(cts.Token);
|
||||
CancelCommandRequest();
|
||||
_commandCts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
|
||||
_ = await command(_commandCts.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore command transport errors and recover on next poll.
|
||||
// Ignore command transport errors and recover on next event.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecutingCommand = false;
|
||||
CancelCommandRequest();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshStateAsync()
|
||||
private void CancelCommandRequest()
|
||||
{
|
||||
if (!_isAttached || !_isOnActivePage || _isRefreshing)
|
||||
var cts = Interlocked.Exchange(ref _commandCts, null);
|
||||
if (cts is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
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();
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
}
|
||||
|
||||
private void ApplyState(MusicPlaybackState state)
|
||||
@@ -364,6 +412,10 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
? PauseSymbol
|
||||
: PlaySymbol;
|
||||
|
||||
// Update favorite button
|
||||
FavoriteIcon.Symbol = state.IsFavorite ? HeartFilledSymbol : HeartSymbol;
|
||||
FavoriteIcon.IconVariant = state.IsFavorite ? IconVariant.Filled : IconVariant.Regular;
|
||||
|
||||
SetCoverImage(state.ThumbnailBytes);
|
||||
ApplyActionButtonState(state);
|
||||
UpdateSourceAppButtonTooltip();
|
||||
@@ -385,7 +437,16 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
: showNoSessionStyle;
|
||||
SourceAppButton.IsEnabled = !_isExecutingCommand && state.IsSupported;
|
||||
QueueButton.IsEnabled = canOperate || showNoSessionStyle;
|
||||
FavoriteButton.IsEnabled = canOperate || showNoSessionStyle;
|
||||
FavoriteButton.IsEnabled = canOperate
|
||||
? 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()
|
||||
@@ -441,6 +502,10 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
SourceAppGlyphBadge.BorderBrush = new SolidColorBrush(Color.Parse("#00FFFFFF"));
|
||||
SourceAppIcon.IconVariant = IconVariant.Filled;
|
||||
SourceAppIcon.Foreground = new SolidColorBrush(Color.Parse("#FBFFFFFF"));
|
||||
|
||||
// Reset favorite icon
|
||||
FavoriteIcon.Symbol = HeartSymbol;
|
||||
FavoriteIcon.IconVariant = IconVariant.Regular;
|
||||
}
|
||||
|
||||
private void ApplyActiveVisualTheme()
|
||||
@@ -474,18 +539,6 @@ 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)
|
||||
{
|
||||
return status switch
|
||||
@@ -696,4 +749,18 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
var safeIndex = Math.Clamp(index, 0, colors.Count - 1);
|
||||
return colors[safeIndex];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_musicControlService.PlaybackStateChanged -= OnPlaybackStateChanged;
|
||||
_musicControlService.QueueChanged -= OnQueueChanged;
|
||||
_musicControlService.StopListening();
|
||||
if (_musicControlService is IDisposable disposableService)
|
||||
{
|
||||
disposableService.Dispose();
|
||||
}
|
||||
|
||||
CancelCommandRequest();
|
||||
DisposeCoverBitmap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,37 +9,62 @@
|
||||
d:DesignHeight="320"
|
||||
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"
|
||||
CornerRadius="34"
|
||||
CornerRadius="{DynamicResource OfficeRecentDocumentsRootCornerRadius}"
|
||||
Background="#2D5A8E"
|
||||
ClipToBounds="True"
|
||||
BorderThickness="0"
|
||||
Padding="0">
|
||||
Padding="{DynamicResource OfficeRecentDocumentsRootPadding}">
|
||||
<Grid>
|
||||
<Border x:Name="AccentCorner"
|
||||
Width="140"
|
||||
Height="140"
|
||||
Width="{DynamicResource OfficeRecentDocumentsAccentSize}"
|
||||
Height="{DynamicResource OfficeRecentDocumentsAccentSize}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,-40,-40,0"
|
||||
CornerRadius="70"
|
||||
CornerRadius="{DynamicResource OfficeRecentDocumentsAccentCornerRadius}"
|
||||
Background="#4A90D9"
|
||||
Opacity="0.3"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,*"
|
||||
RowSpacing="{DynamicResource OfficeRecentDocumentsContentRowSpacing}"
|
||||
Margin="{DynamicResource OfficeRecentDocumentsContentMargin}">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="HeaderTextBlock"
|
||||
Text="最近文档"
|
||||
Foreground="#D8FFFFFF"
|
||||
FontSize="18"
|
||||
FontSize="{DynamicResource OfficeRecentDocumentsHeaderFontSize}"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.Column="1"
|
||||
Width="28"
|
||||
Height="28"
|
||||
CornerRadius="14"
|
||||
Width="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
|
||||
Height="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
|
||||
CornerRadius="{DynamicResource OfficeRecentDocumentsRefreshCornerRadius}"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
@@ -47,7 +72,7 @@
|
||||
Focusable="False"
|
||||
PointerPressed="OnRefreshPointerPressed">
|
||||
<fi:SymbolIcon Symbol="ArrowSync"
|
||||
FontSize="14"
|
||||
FontSize="{DynamicResource OfficeRecentDocumentsRefreshIconFontSize}"
|
||||
Foreground="#B8FFFFFF" />
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -55,28 +80,29 @@
|
||||
<ScrollViewer Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,4,0,0">
|
||||
Margin="{DynamicResource OfficeRecentDocumentsScrollMargin}">
|
||||
<ItemsControl x:Name="DocumentsItemsControl">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="{DynamicResource OfficeRecentDocumentsDocumentSpacing}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
|
||||
<Border x:Name="DocumentCard"
|
||||
Width="130"
|
||||
Height="90"
|
||||
CornerRadius="10"
|
||||
Width="{DynamicResource OfficeRecentDocumentsDocumentCardWidth}"
|
||||
Height="{DynamicResource OfficeRecentDocumentsDocumentCardHeight}"
|
||||
CornerRadius="{DynamicResource OfficeRecentDocumentsCardCornerRadius}"
|
||||
Background="#3AFFFFFF"
|
||||
Padding="10"
|
||||
Padding="{DynamicResource OfficeRecentDocumentsCardPadding}"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnDocumentCardPointerPressed">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding FileName}"
|
||||
Foreground="#D8FFFFFF"
|
||||
FontSize="12"
|
||||
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTitleFontSize}"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
@@ -85,7 +111,7 @@
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding TimeAgo}"
|
||||
Foreground="#9AFFFFFF"
|
||||
FontSize="10"
|
||||
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTimeFontSize}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</Grid>
|
||||
@@ -100,7 +126,7 @@
|
||||
IsVisible="False"
|
||||
Text="暂无最近文档"
|
||||
Foreground="#9AFFFFFF"
|
||||
FontSize="14"
|
||||
FontSize="{DynamicResource OfficeRecentDocumentsStatusFontSize}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
@@ -36,8 +37,54 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = cellSize / 100.0;
|
||||
RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale));
|
||||
var normalizedCellSize = Math.Max(1, cellSize);
|
||||
var scale = Math.Clamp(normalizedCellSize / 48d, 0.72d, 1.65d);
|
||||
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)
|
||||
|
||||
@@ -65,7 +65,11 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
||||
|
||||
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
||||
RootBorder.CornerRadius = rootRadius;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RootBorder.Padding = new Thickness(
|
||||
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;
|
||||
RecorderContentGrid.Margin = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
||||
|
||||
@@ -603,12 +603,20 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
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.Padding = new Thickness(
|
||||
CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(
|
||||
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,
|
||||
0.55d);
|
||||
|
||||
var rootPadding = RootBorder.Padding;
|
||||
var cardPadding = CardBorder.Padding;
|
||||
|
||||
var rowSpacing = Math.Clamp(6 * softScale, 3, 10);
|
||||
ContentGrid.RowSpacing = rowSpacing;
|
||||
@@ -625,7 +633,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
||||
RefreshGlyphIcon.FontSize = Math.Clamp(16 * softScale, 10, 20);
|
||||
|
||||
var innerWidth = Math.Max(100, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
||||
var innerWidth = Math.Max(100, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right);
|
||||
var rowPaddingHorizontal = Math.Clamp(8 * softScale, 5, 14);
|
||||
var rowPaddingVertical = Math.Clamp(6 * softScale, 3, 10);
|
||||
var avatarSize = Math.Clamp(30 * softScale, 20, 40);
|
||||
@@ -640,8 +648,10 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
var availablePostsHeight = Math.Max(
|
||||
0d,
|
||||
totalHeight -
|
||||
CardBorder.Padding.Top -
|
||||
CardBorder.Padding.Bottom -
|
||||
rootPadding.Top -
|
||||
rootPadding.Bottom -
|
||||
cardPadding.Top -
|
||||
cardPadding.Bottom -
|
||||
estimatedHeaderHeight -
|
||||
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;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||
? 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));
|
||||
|
||||
var rowPadding = new Thickness(
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 14),
|
||||
Math.Clamp(7 * scale * compactMultiplier, 3, 10));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(7 * scale * compactMultiplier, 3, 10));
|
||||
SustainedRowBorder.Padding = rowPadding;
|
||||
TimeRowBorder.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.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale, 8, 20),
|
||||
Math.Clamp(10 * scale, 6, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 20),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16));
|
||||
|
||||
StatusTitleTextBlock.FontSize = Math.Clamp(11 * scale, 9, 18);
|
||||
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;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||
? 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));
|
||||
|
||||
var cardPadding = new Thickness(
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 14),
|
||||
Math.Clamp(6 * scale * compactMultiplier, 3, 9));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(6 * scale * compactMultiplier, 3, 9));
|
||||
CountCardBorder.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.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale, 8, 22),
|
||||
Math.Clamp(10 * scale, 6, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16));
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 12, 30);
|
||||
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;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 34);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||
? Math.Clamp(4 * scale, 2, 5)
|
||||
|
||||
@@ -260,8 +260,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
var expandedMultiplier = _isExpandedMode ? 1.12 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.50, 14, 42);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(16 * scale * compactMultiplier * expandedMultiplier, 8, 30),
|
||||
Math.Clamp(14 * scale * compactMultiplier * expandedMultiplier, 6, 26));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale * compactMultiplier * expandedMultiplier, 8, 30),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier * expandedMultiplier, 6, 26));
|
||||
|
||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||
? 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));
|
||||
|
||||
var cardPadding = new Thickness(
|
||||
Math.Clamp(10 * scale * compactMultiplier * expandedMultiplier, 6, 20),
|
||||
Math.Clamp(8 * scale * compactMultiplier * expandedMultiplier, 4, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier * expandedMultiplier, 6, 20),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(8 * scale * compactMultiplier * expandedMultiplier, 4, 16));
|
||||
var cardCornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * scale, 6, 18);
|
||||
AverageCardBorder.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;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale * compactMultiplier, 7, 22),
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier, 7, 22),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
LayoutGrid.ColumnSpacing = _isUltraCompactMode
|
||||
? Math.Clamp(6 * scale, 3, 8)
|
||||
|
||||
@@ -241,7 +241,9 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
Background = new SolidColorBrush(rowBackground),
|
||||
BorderBrush = new SolidColorBrush(rowBorderColor),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(Math.Clamp(8, 6, 12), Math.Clamp(6, 4, 10))
|
||||
Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10))
|
||||
};
|
||||
|
||||
var panelComposite = ToOpaqueAgainst(panelColor, DarkSubstrate);
|
||||
@@ -355,7 +357,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
Width = _isUltraCompactMode ? 26 : 34,
|
||||
Height = Math.Clamp(26 * (_isCompactMode ? 0.90 : 1.0), 24, 30),
|
||||
Padding = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
CornerRadius = ComponentChromeCornerRadiusHelper.Scale(10, 8, 12),
|
||||
Background = new SolidColorBrush(buttonBackground),
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
@@ -590,8 +592,8 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 36);
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 7, 22),
|
||||
Math.Clamp(9 * scale, 5, 16));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 7, 22),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 5, 16));
|
||||
|
||||
ContentRootGrid.RowSpacing = _isUltraCompactMode
|
||||
? Math.Clamp(4 * scale, 2, 6)
|
||||
@@ -604,12 +606,12 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
: Math.Clamp(6 * scale, 3, 8);
|
||||
|
||||
DialogOverlayBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 8, 20),
|
||||
Math.Clamp(10 * scale, 8, 18));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 8, 18));
|
||||
DialogCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 10, 18);
|
||||
DialogCardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 9, 20),
|
||||
Math.Clamp(11 * scale, 8, 18));
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 9, 20),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18));
|
||||
DialogTitleTextBlock.FontSize = Math.Clamp(14 * scale, 11, 20);
|
||||
DialogMessageTextBlock.FontSize = Math.Clamp(12 * scale, 10, 17);
|
||||
DialogRenameTextBox.FontSize = Math.Clamp(11.5 * scale, 10, 16);
|
||||
|
||||
@@ -198,7 +198,8 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 12, 48);
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22));
|
||||
RootBorder.Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 7, 22));
|
||||
TimerPanelBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * scale, 12, 42);
|
||||
|
||||
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 verticalPadding = Math.Clamp(8 * scale, 3, 22);
|
||||
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||
RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46);
|
||||
|
||||
var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);
|
||||
|
||||
Reference in New Issue
Block a user