完善了日历组件
This commit is contained in:
lincube
2026-03-02 20:02:14 +08:00
parent 87f47e1887
commit 2436e43f65
25 changed files with 3660 additions and 609 deletions

View File

@@ -3,21 +3,31 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="220"
d:DesignHeight="70"
d:DesignWidth="180"
d:DesignHeight="48"
x:Class="LanMontainDesktop.Views.Components.ClockWidget">
<Border x:Name="RootBorder"
Classes="glass-panel"
Padding="8"
CornerRadius="8">
<TextBlock x:Name="TimeTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
FontSize="26"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Padding="0"
CornerRadius="24">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock x:Name="MainTimeTextBlock"
FontFeatures="tnum"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="SecondsTextBlock"
FontFeatures="tnum"
FontSize="14"
FontWeight="Normal"
Opacity="0.75"
Margin="4,2,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -8,6 +8,12 @@ using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public enum ClockDisplayFormat
{
HourMinuteSecond, // HH:mm:ss
HourMinute // HH:mm
}
public partial class ClockWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
@@ -16,6 +22,7 @@ public partial class ClockWidget : UserControl
};
private TimeZoneService? _timeZoneService;
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
public ClockWidget()
{
@@ -27,9 +34,21 @@ public partial class ClockWidget : UserControl
UpdateClock();
}
/// <summary>
/// 设置时区服务
/// </summary>
public ClockDisplayFormat DisplayFormat
{
get => _displayFormat;
set
{
_displayFormat = value;
UpdateClock();
}
}
public void SetDisplayFormat(ClockDisplayFormat format)
{
DisplayFormat = format;
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -66,17 +85,45 @@ public partial class ClockWidget : UserControl
private void UpdateClock()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
TimeTextBlock.Text = now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
MainTimeTextBlock.Text = now.ToString("HH:mm", CultureInfo.CurrentCulture);
SecondsTextBlock.Text = now.ToString("ss", CultureInfo.CurrentCulture);
SecondsTextBlock.IsVisible = _displayFormat == ClockDisplayFormat.HourMinuteSecond;
}
public void ApplyCellSize(double cellSize)
{
var padding = Math.Clamp(cellSize * 0.12, 2, 14);
RootBorder.Padding = new Thickness(padding);
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 4, 18));
// --- Class Island “满盈”风格算法 ---
// 1. 计算组件高度:保持与任务栏核心比例一致 (0.74x)
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
RootBorder.Height = targetHeight;
// 2. 动态圆角:确保始终是完美的胶囊半圆
RootBorder.CornerRadius = new CornerRadius(targetHeight / 2);
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
// 3. 核心:满盈字阶 (Filled Typography)
// 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力
var mainFontSize = targetHeight * 0.68;
MainTimeTextBlock.FontSize = mainFontSize;
MainTimeTextBlock.FontWeight = FontWeight.SemiBold;
// 4. 次级信息:秒数维持 0.7x 比例,并增强透明度呼吸感
SecondsTextBlock.FontSize = mainFontSize * 0.7;
SecondsTextBlock.Opacity = 0.55;
// 5. 视觉占比:占据约 2.2 个单元格的感官宽度 (cellSize * 2 + gaps)
RootBorder.MinWidth = cellSize * 2.2;
// Keep the time legible across dense and sparse grid layouts.
TimeTextBlock.FontSize = Math.Clamp(cellSize * 0.42, 10, 56);
TimeTextBlock.FontWeight = FontWeight.SemiBold;
// 6. 间距微调
if (MainTimeTextBlock.Parent is StackPanel panel)
{
panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8);
}
// 确保清除可能存在的固定 Padding由代码控制“紧密感”
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
}

View File

@@ -3,81 +3,111 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="200"
d:DesignWidth="460"
d:DesignHeight="220"
x:Class="LanMontainDesktop.Views.Components.DateWidget">
<Border x:Name="RootBorder"
Background="Transparent"
CornerRadius="16"
ClipToBounds="True">
<Grid ColumnDefinitions="*,*">
<!-- 左侧:月历 -->
<Border x:Name="CalendarBackgroundBorder"
Grid.Column="0"
Padding="12"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<Grid RowDefinitions="Auto,*">
<!-- 月份年份 -->
<TextBlock x:Name="CalendarMonthYearTextBlock"
Grid.Row="0"
HorizontalAlignment="Center"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,8" />
<!-- 月历网格 -->
<Grid x:Name="CalendarGrid"
Grid.Row="1"
RowDefinitions="Auto,*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*">
<!-- 星期标题 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="日" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="一" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="二" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="3" Text="三" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="4" Text="四" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="5" Text="五" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="6" Text="六" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</Grid>
</Grid>
</Border>
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
CornerRadius="28"
ClipToBounds="True"
Padding="12">
<Viewbox Stretch="Uniform">
<Grid Width="460"
Height="220"
ColumnDefinitions="1.2*,1*"
ColumnSpacing="12">
<!-- 右侧:今日详情 -->
<Border x:Name="TodayBackgroundBorder"
Grid.Column="1"
Background="{DynamicResource AdaptiveAccentBrush}"
Padding="16">
<Grid RowDefinitions="Auto,*,Auto">
<!-- 今日标签 -->
<TextBlock Grid.Row="0"
Text="今天"
HorizontalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Opacity="0.8"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
<!-- 日期数字 -->
<TextBlock x:Name="TodayDayTextBlock"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="56"
FontWeight="Light"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
<!-- 星期 -->
<TextBlock x:Name="TodayWeekdayTextBlock"
Grid.Row="2"
HorizontalAlignment="Center"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
<Grid x:Name="LeftPanelGrid"
Grid.Column="0"
RowDefinitions="Auto,Auto,*"
RowSpacing="8">
<TextBlock x:Name="GregorianHeadlineTextBlock"
Grid.Row="0"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<UniformGrid Grid.Row="1"
Columns="7">
<TextBlock x:Name="WeekdayText0" Text="日" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText1" Text="一" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText2" Text="二" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText3" Text="三" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText4" Text="四" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText5" Text="五" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText6" Text="六" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
</UniformGrid>
<Grid x:Name="CalendarGrid"
Grid.Row="2"
RowDefinitions="*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*" />
</Grid>
</Border>
</Grid>
<Border x:Name="LunarCardBorder"
Grid.Column="1"
Background="{DynamicResource AdaptiveLayer2Brush}"
CornerRadius="24"
Padding="14">
<Grid x:Name="RightPanelGrid"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
RowSpacing="10">
<TextBlock x:Name="LunarDateTextBlock"
Grid.Row="0"
FontSize="28"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="LunarMetaTextBlock"
Grid.Row="1"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Opacity="0.88"
TextWrapping="Wrap" />
<Border Grid.Row="2"
Height="1"
Margin="0,2,0,2"
Background="{DynamicResource AdaptiveStrokeBrush}" />
<Grid Grid.Row="3"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="YiLabelTextBlock"
Grid.Column="0"
Text="宜"
FontSize="18"
FontWeight="Bold"
Foreground="#4E7D3A" />
<TextBlock x:Name="YiItemsTextBlock"
Grid.Column="1"
FontSize="16"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" />
</Grid>
<Grid Grid.Row="4"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="JiLabelTextBlock"
Grid.Column="0"
Text="忌"
FontSize="18"
FontWeight="Bold"
Foreground="#A1473E" />
<TextBlock x:Name="JiItemsTextBlock"
Grid.Column="1"
FontSize="16"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" />
</Grid>
</Grid>
</Border>
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
@@ -15,8 +15,15 @@ public partial class DateWidget : UserControl
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly LunarCalendarService LunarCalendarService = new();
private static readonly string[] ZhWeekdayHeaders = ["日", "一", "二", "三", "四", "五", "六"];
private static readonly string[] EnWeekdayHeaders = ["S", "M", "T", "W", "T", "F", "S"];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 64;
private double _calendarDayFontSize = 14;
private double _calendarTodayDotSize = 28;
public DateWidget()
{
@@ -25,12 +32,10 @@ public partial class DateWidget : UserControl
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateDate();
}
/// <summary>
/// 设置时区服务
/// </summary>
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -54,6 +59,11 @@ public partial class DateWidget : UserControl
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateDate();
@@ -68,30 +78,75 @@ public partial class DateWidget : UserControl
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
// 右侧:今日详情
TodayDayTextBlock.Text = now.Day.ToString();
TodayWeekdayTextBlock.Text = now.ToString("dddd", culture);
// 左侧:月历
CalendarMonthYearTextBlock.Text = now.ToString("yyyy年M月", culture);
// 生成月历
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
var lunar = LunarCalendarService.GetLunarInfo(now);
GregorianHeadlineTextBlock.Text = isZh
? $"{now.Month}月{now.Day}日 {ToChineseWeekday(now.DayOfWeek)}"
: now.ToString("MMM d ddd", culture);
if (isZh)
{
LunarDateTextBlock.Text = $"农历 {lunar.LunarDateZh}";
LunarMetaTextBlock.Text = $"{lunar.GanzhiYearZh}年({lunar.ZodiacZh}年)";
YiLabelTextBlock.Text = "宜";
JiLabelTextBlock.Text = "忌";
YiItemsTextBlock.Text = "祭祀 祈福 出行 会友";
JiItemsTextBlock.Text = "动土 诉讼 远航 争执";
}
else
{
LunarDateTextBlock.Text = $"Lunar {lunar.LunarDateEn}";
LunarMetaTextBlock.Text = $"Ganzhi year: {lunar.GanzhiYearEn} ({lunar.ZodiacEn})";
YiLabelTextBlock.Text = "Do";
JiLabelTextBlock.Text = "Avoid";
YiItemsTextBlock.Text = "Worship Blessing Travel Meet";
JiItemsTextBlock.Text = "Groundwork Lawsuit Voyage Dispute";
}
UpdateWeekdayHeaders(isZh);
GenerateCalendar(now);
}
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "周日",
DayOfWeek.Monday => "周一",
DayOfWeek.Tuesday => "周二",
DayOfWeek.Wednesday => "周三",
DayOfWeek.Thursday => "周四",
DayOfWeek.Friday => "周五",
_ => "周六"
};
}
private void UpdateWeekdayHeaders(bool isZh)
{
var headers = isZh ? ZhWeekdayHeaders : EnWeekdayHeaders;
WeekdayText0.Text = headers[0];
WeekdayText1.Text = headers[1];
WeekdayText2.Text = headers[2];
WeekdayText3.Text = headers[3];
WeekdayText4.Text = headers[4];
WeekdayText5.Text = headers[5];
WeekdayText6.Text = headers[6];
}
private void GenerateCalendar(DateTime currentDate)
{
// 清空之前的日期(保留星期标题)
var childrenToRemove = new List<Control>();
var removeList = new List<Control>();
foreach (var child in CalendarGrid.Children)
{
if (child is TextBlock tb && tb.Tag?.ToString() == "day")
if (child is Control control && control.Tag is string tag &&
(tag == "day" || tag == "today-dot"))
{
childrenToRemove.Add(tb);
removeList.Add(control);
}
}
foreach (var child in childrenToRemove)
foreach (var child in removeList)
{
CalendarGrid.Children.Remove(child);
}
@@ -99,66 +154,62 @@ public partial class DateWidget : UserControl
var year = currentDate.Year;
var month = currentDate.Month;
var today = currentDate.Day;
// 获取该月第一天
var firstDayOfMonth = new DateTime(year, month, 1);
var daysInMonth = DateTime.DaysInMonth(year, month);
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek; // 0 = Sunday
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
// 生成日期
for (int day = 1; day <= daysInMonth; day++)
for (var day = 1; day <= daysInMonth; day++)
{
var row = ((day + startDayOfWeek - 1) / 7) + 1; // +1 because row 0 is weekday headers
var row = (day + startDayOfWeek - 1) / 7;
var col = (day + startDayOfWeek - 1) % 7;
if (row > 5) continue; // 最多显示6行
if (row > 4)
{
continue;
}
var dayText = new TextBlock
{
Text = day.ToString(),
Text = day.ToString(CultureInfo.CurrentCulture),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = 10,
FontSize = _calendarDayFontSize,
FontWeight = FontWeight.SemiBold,
Tag = "day"
};
// 今天高亮
if (day == today)
{
// 使用主题色高亮今天
var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
? accent as IBrush
var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
? accent as IBrush
: Brushes.Blue;
var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
? onAccent as IBrush
var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
? onAccent as IBrush
: Brushes.White;
dayText.Foreground = onAccentBrush;
dayText.FontWeight = FontWeight.Bold;
dayText.Background = new SolidColorBrush(Colors.Transparent);
// 添加背景圆
var highlight = new Border
var dot = new Border
{
Width = _calendarTodayDotSize,
Height = _calendarTodayDotSize,
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
Background = accentBrush,
CornerRadius = new CornerRadius(10),
Width = 20,
Height = 20,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Child = dayText
Child = dayText,
Tag = "today-dot"
};
Grid.SetRow(highlight, row);
Grid.SetColumn(highlight, col);
CalendarGrid.Children.Add(highlight);
Grid.SetRow(dot, row);
Grid.SetColumn(dot, col);
CalendarGrid.Children.Add(dot);
}
else
{
// 使用主题次要文本颜色
var secondaryBrush = this.TryFindResource("AdaptiveTextSecondaryBrush", out var secondary)
? secondary as IBrush
: Brushes.Gray;
dayText.Foreground = secondaryBrush;
var isWeekend = col is 0 or 6;
dayText.Foreground = isWeekend
? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.82)
: GetThemeBrush("AdaptiveTextPrimaryBrush", 0.92);
Grid.SetRow(dayText, row);
Grid.SetColumn(dayText, col);
CalendarGrid.Children.Add(dayText);
@@ -168,13 +219,59 @@ public partial class DateWidget : UserControl
public void ApplyCellSize(double cellSize)
{
// 根据格子大小调整圆角
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 8, 20));
// 调整字体大小
var baseFontSize = cellSize * 0.25;
TodayDayTextBlock.FontSize = Math.Clamp(baseFontSize * 2.8, 28, 72);
TodayWeekdayTextBlock.FontSize = Math.Clamp(baseFontSize * 0.6, 10, 16);
CalendarMonthYearTextBlock.FontSize = Math.Clamp(baseFontSize * 0.55, 9, 14);
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 16, 40));
RootBorder.Padding = new Thickness(Math.Clamp(12 * scale, 8, 18));
LeftPanelGrid.RowSpacing = Math.Clamp(8 * scale, 5, 14);
RightPanelGrid.RowSpacing = Math.Clamp(10 * scale, 6, 16);
LunarCardBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 14, 34));
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 9, 20));
GregorianHeadlineTextBlock.FontSize = Math.Clamp(22 * scale, 14, 34);
WeekdayText0.FontSize = Math.Clamp(13 * scale, 9, 18);
WeekdayText1.FontSize = WeekdayText0.FontSize;
WeekdayText2.FontSize = WeekdayText0.FontSize;
WeekdayText3.FontSize = WeekdayText0.FontSize;
WeekdayText4.FontSize = WeekdayText0.FontSize;
WeekdayText5.FontSize = WeekdayText0.FontSize;
WeekdayText6.FontSize = WeekdayText0.FontSize;
LunarDateTextBlock.FontSize = Math.Clamp(28 * scale, 17, 44);
LunarMetaTextBlock.FontSize = Math.Clamp(14 * scale, 10, 22);
YiLabelTextBlock.FontSize = Math.Clamp(18 * scale, 12, 28);
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
YiItemsTextBlock.FontSize = Math.Clamp(16 * scale, 11, 24);
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
_calendarDayFontSize = Math.Clamp(14 * scale, 9, 22);
_calendarTodayDotSize = Math.Clamp(28 * scale, 17, 38);
UpdateDate();
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 48d, 0.72, 1.55);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 220d, 0.65, 1.65) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 460d, 0.65, 1.65) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.08), 0.65, 1.6);
}
private IBrush GetThemeBrush(string key, double opacity)
{
if (this.TryFindResource(key, out var value) && value is IBrush brush)
{
if (brush is ISolidColorBrush solid)
{
return new SolidColorBrush(solid.Color, opacity);
}
return brush;
}
return new SolidColorBrush(Colors.Gray, opacity);
}
}

View File

@@ -0,0 +1,20 @@
<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"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="300"
x:Class="LanMontainDesktop.Views.Components.DateWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="日历组件设置"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LanMontainDesktop.Views.Components;
public partial class DateWidgetSettingsWindow : UserControl
{
public DateWidgetSettingsWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,87 @@
<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"
mc:Ignorable="d"
d:DesignWidth="300"
d:DesignHeight="300"
x:Class="LanMontainDesktop.Views.Components.LunarCalendarWidget">
<Border x:Name="RootBorder"
Background="#EFE6D9"
CornerRadius="30"
ClipToBounds="True"
Padding="16">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="300"
Height="300"
RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="GregorianLineTextBlock"
Grid.Row="0"
Text="10/9 Thu"
FontSize="24"
FontWeight="SemiBold"
Foreground="#7A5A47"
HorizontalAlignment="Center" />
<TextBlock x:Name="LunarDateTextBlock"
Grid.Row="1"
Text="Lunar"
FontSize="88"
FontWeight="Bold"
Foreground="#6B4936"
HorizontalAlignment="Center" />
<Border x:Name="DividerBorder"
Grid.Row="2"
Height="1"
Margin="8,8,8,2"
Background="#D2C6B7" />
<Grid x:Name="AuspiciousGrid"
Grid.Row="3"
RowDefinitions="Auto,Auto"
RowSpacing="12">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="YiLabelTextBlock"
Grid.Column="0"
Text="Yi"
FontSize="30"
FontWeight="Bold"
Foreground="#5F6D2E" />
<TextBlock x:Name="YiItemsTextBlock"
Grid.Column="1"
FontSize="24"
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="JiLabelTextBlock"
Grid.Column="0"
Text="Ji"
FontSize="30"
FontWeight="Bold"
Foreground="#8A4A3A" />
<TextBlock x:Name="JiItemsTextBlock"
Grid.Column="1"
FontSize="24"
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
</Grid>
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class LunarCalendarWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly LunarCalendarService LunarCalendarService = new();
private static readonly string[] ZhYiCandidates =
[
"\u796d\u7940",
"\u7948\u798f",
"\u4f1a\u53cb",
"\u51fa\u884c",
"\u6c42\u8d22",
"\u5f00\u5e02",
"\u4ea4\u6613",
"\u5ac1\u5a36",
"\u6c42\u5b66",
"\u4fee\u9020",
"\u5b89\u5e8a",
"\u7eb3\u91c7"
];
private static readonly string[] ZhJiCandidates =
[
"\u52a8\u571f",
"\u8bc9\u8bbc",
"\u8fdc\u822a",
"\u4e89\u6267",
"\u7834\u571f",
"\u5b89\u846c",
"\u4f10\u6728",
"\u6398\u4e95",
"\u8fc1\u5f99",
"\u5f00\u4ed3",
"\u7f6e\u4ea7",
"\u5f00\u6e20"
];
private static readonly string[] EnYiCandidates =
[
"Worship",
"Blessing",
"Travel",
"Meetings",
"Trade",
"Business",
"Study",
"Build",
"Gathering",
"Planning"
];
private static readonly string[] EnJiCandidates =
[
"Dispute",
"Lawsuit",
"Major move",
"Groundwork",
"Burial",
"Long voyage",
"Contract rush",
"Risky purchase",
"Heavy repair",
"Conflict"
];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;
public LunarCalendarWidget()
{
InitializeComponent();
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateContent();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService is not null)
{
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
}
_timeZoneService = timeZoneService;
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
UpdateContent();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
UpdateContent();
_timer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateContent();
}
private void OnTimeZoneChanged(object? sender, EventArgs e)
{
UpdateContent();
}
private void UpdateContent()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
var lunar = LunarCalendarService.GetLunarInfo(now);
GregorianLineTextBlock.Text = isZh
? $"{now.Month}\u6708{now.Day}\u65e5 {ToChineseWeekday(now.DayOfWeek)}"
: now.ToString("MMM d ddd", culture);
LunarDateTextBlock.Text = isZh ? lunar.LunarDateZh : lunar.LunarDateEn;
YiLabelTextBlock.Text = isZh ? "\u5b9c" : "Do";
JiLabelTextBlock.Text = isZh ? "\u5fcc" : "Avoid";
YiItemsTextBlock.Text = BuildDailySelection(
now.Date,
isZh ? ZhYiCandidates : EnYiCandidates,
count: 4,
salt: 17,
useChineseSpacing: isZh);
JiItemsTextBlock.Text = BuildDailySelection(
now.Date,
isZh ? ZhJiCandidates : EnJiCandidates,
count: 4,
salt: 29,
useChineseSpacing: isZh);
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 16, 44));
RootBorder.Padding = new Thickness(Math.Clamp(16 * scale, 8, 24));
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
DividerBorder.Margin = new Thickness(
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);
GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
LunarDateTextBlock.FontSize = Math.Clamp(88 * scale, 30, 130);
YiLabelTextBlock.FontSize = Math.Clamp(30 * scale, 13, 44);
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.62, 1.95);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
}
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "\u5468\u65e5",
DayOfWeek.Monday => "\u5468\u4e00",
DayOfWeek.Tuesday => "\u5468\u4e8c",
DayOfWeek.Wednesday => "\u5468\u4e09",
DayOfWeek.Thursday => "\u5468\u56db",
DayOfWeek.Friday => "\u5468\u4e94",
_ => "\u5468\u516d"
};
}
private static string BuildDailySelection(
DateTime date,
string[] pool,
int count,
int salt,
bool useChineseSpacing)
{
if (pool.Length == 0 || count <= 0)
{
return string.Empty;
}
var target = Math.Min(count, pool.Length);
var selected = new List<string>(target);
var usedIndices = new HashSet<int>();
var cursor = Math.Abs(date.Year * 1009 + date.DayOfYear * 37 + salt * 211);
var step = (salt % Math.Max(1, pool.Length - 1)) + 1;
for (var i = 0; i < pool.Length * 3 && selected.Count < target; i++)
{
var index = (cursor + i * step) % pool.Length;
if (usedIndices.Add(index))
{
selected.Add(pool[index]);
}
}
if (selected.Count == 0)
{
return string.Empty;
}
return string.Join(useChineseSpacing ? " " : ", ", selected);
}
}

View File

@@ -0,0 +1,49 @@
<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"
mc:Ignorable="d"
d:DesignWidth="280"
d:DesignHeight="280"
x:Class="LanMontainDesktop.Views.Components.MonthCalendarWidget">
<Border x:Name="RootBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="28"
ClipToBounds="True"
Padding="14">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="280"
Height="280"
RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="HeaderTextBlock"
Grid.Row="0"
FontSize="42"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<UniformGrid Grid.Row="1"
Columns="7">
<TextBlock x:Name="WeekdayText0" Text="S" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText1" Text="M" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText2" Text="T" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText3" Text="W" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText4" Text="T" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText5" Text="F" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText6" Text="S" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
</UniformGrid>
<Grid x:Name="CalendarGrid"
Grid.Row="2"
RowDefinitions="*,*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*" />
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class MonthCalendarWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly string[] ZhWeekdayHeaders = ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d"];
private static readonly string[] EnWeekdayHeaders = ["S", "M", "T", "W", "T", "F", "S"];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;
private double _calendarDayFontSize = 22;
private double _calendarTodayDotSize = 44;
public MonthCalendarWidget()
{
InitializeComponent();
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateCalendar();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService is not null)
{
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
}
_timeZoneService = timeZoneService;
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
UpdateCalendar();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
UpdateCalendar();
_timer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateCalendar();
}
private void OnTimeZoneChanged(object? sender, EventArgs e)
{
UpdateCalendar();
}
private void UpdateCalendar()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
HeaderTextBlock.Text = isZh
? $"{now.Month}\u6708{now.Day}\u65e5"
: now.ToString("MMM d", culture);
UpdateWeekdayHeaders(isZh);
GenerateCalendar(now);
}
private void UpdateWeekdayHeaders(bool isZh)
{
var headers = isZh ? ZhWeekdayHeaders : EnWeekdayHeaders;
var blocks = GetWeekdayHeaderBlocks();
for (var i = 0; i < blocks.Count; i++)
{
blocks[i].Text = headers[i];
}
}
private IReadOnlyList<TextBlock> GetWeekdayHeaderBlocks()
{
return
[
WeekdayText0,
WeekdayText1,
WeekdayText2,
WeekdayText3,
WeekdayText4,
WeekdayText5,
WeekdayText6
];
}
private void GenerateCalendar(DateTime currentDate)
{
var removeList = new List<Control>();
foreach (var child in CalendarGrid.Children)
{
if (child is Control control &&
control.Tag is string tag &&
(tag == "day" || tag == "today-dot"))
{
removeList.Add(control);
}
}
foreach (var child in removeList)
{
CalendarGrid.Children.Remove(child);
}
var year = currentDate.Year;
var month = currentDate.Month;
var today = currentDate.Day;
var firstDayOfMonth = new DateTime(year, month, 1);
var daysInMonth = DateTime.DaysInMonth(year, month);
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
for (var day = 1; day <= daysInMonth; day++)
{
var row = (day + startDayOfWeek - 1) / 7;
var col = (day + startDayOfWeek - 1) % 7;
if (row > 5)
{
continue;
}
var dayText = new TextBlock
{
Text = day.ToString(CultureInfo.CurrentCulture),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = _calendarDayFontSize,
FontWeight = FontWeight.SemiBold,
Tag = "day"
};
if (day == today)
{
var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
? accent as IBrush
: Brushes.Blue;
var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
? onAccent as IBrush
: Brushes.White;
dayText.Foreground = onAccentBrush;
var dot = new Border
{
Width = _calendarTodayDotSize,
Height = _calendarTodayDotSize,
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
Background = accentBrush,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Child = dayText,
Tag = "today-dot"
};
Grid.SetRow(dot, row);
Grid.SetColumn(dot, col);
CalendarGrid.Children.Add(dot);
}
else
{
var isWeekend = col is 0 or 6;
dayText.Foreground = isWeekend
? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.78)
: GetThemeBrush("AdaptiveTextPrimaryBrush", 0.94);
Grid.SetRow(dayText, row);
Grid.SetColumn(dayText, col);
CalendarGrid.Children.Add(dayText);
}
}
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 14, 40));
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 22));
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
HeaderTextBlock.FontSize = Math.Clamp(42 * scale, 14, 58);
var weekdayFontSize = Math.Clamp(20 * scale, 8, 26);
foreach (var block in GetWeekdayHeaderBlocks())
{
block.FontSize = weekdayFontSize;
}
_calendarDayFontSize = Math.Clamp(22 * scale, 8, 30);
_calendarTodayDotSize = Math.Clamp(44 * scale, 16, 58);
UpdateCalendar();
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.65, 1.85);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 280d, 0.60, 1.90) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 280d, 0.60, 1.90) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.06), 0.60, 1.85);
}
private IBrush GetThemeBrush(string key, double opacity)
{
if (this.TryFindResource(key, out var value) && value is IBrush brush)
{
if (brush is ISolidColorBrush solid)
{
return new SolidColorBrush(solid.Color, opacity);
}
return brush;
}
return new SolidColorBrush(Colors.Gray, opacity);
}
}