mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.2.1
完善了日历组件
This commit is contained in:
@@ -5,4 +5,6 @@ public static class BuiltInComponentIds
|
||||
public const string Clock = "Clock";
|
||||
public const string Blank2x4 = "Blank2x4";
|
||||
public const string Date = "Date";
|
||||
public const string MonthCalendar = "MonthCalendar";
|
||||
public const string LunarCalendar = "LunarCalendar";
|
||||
}
|
||||
|
||||
@@ -26,18 +26,36 @@ public sealed class ComponentRegistry
|
||||
"Clock",
|
||||
"Clock",
|
||||
"Status",
|
||||
MinWidthCells: 1,
|
||||
MinWidthCells: 3,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: true,
|
||||
AllowDesktopPlacement: false),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Date,
|
||||
"Date",
|
||||
"Calendar",
|
||||
"Calendar",
|
||||
"Date",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.MonthCalendar,
|
||||
"Month Calendar",
|
||||
"CalendarMonth",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.LunarCalendar,
|
||||
"Lunar Calendar",
|
||||
"Calendar",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"app.title": "LanMontainDesktop",
|
||||
"button.back_to_windows": "Back to Windows",
|
||||
"tooltip.back_to_windows": "Back to Windows",
|
||||
@@ -15,6 +15,7 @@
|
||||
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
|
||||
"settings.wallpaper.current_label": "Current Wallpaper",
|
||||
"settings.wallpaper.placement_label": "Placement",
|
||||
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
|
||||
"settings.wallpaper.pick_button": "Browse Files",
|
||||
"settings.wallpaper.clear_button": "Reset to Solid Color",
|
||||
"settings.wallpaper.no_selection": "No wallpaper selected.",
|
||||
@@ -38,6 +39,11 @@
|
||||
"settings.grid.title": "Grid Layout",
|
||||
"settings.grid.description": "Every component must occupy at least one cell (minimum 1x1).",
|
||||
"settings.grid.short_side_label": "Short Side Cells",
|
||||
"settings.grid.spacing_label": "Grid Spacing",
|
||||
"settings.grid.spacing_relaxed": "Relaxed (iOS)",
|
||||
"settings.grid.spacing_compact": "Compact (Android)",
|
||||
"settings.grid.edge_inset_label": "Screen Inset",
|
||||
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
|
||||
"settings.grid.apply_button": "Apply",
|
||||
"settings.grid.info_format": "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
|
||||
"settings.color.title": "Color",
|
||||
@@ -61,6 +67,13 @@
|
||||
"settings.status_bar.description": "Choose which components appear on the top status bar.",
|
||||
"settings.status_bar.clock_header": "Clock Component",
|
||||
"settings.status_bar.clock_description": "Display a clock on the top status bar.",
|
||||
"settings.status_bar.spacing_header": "Component Spacing",
|
||||
"settings.status_bar.spacing_desc": "Adjust spacing between status bar components.",
|
||||
"settings.status_bar.spacing_mode_compact": "Compact",
|
||||
"settings.status_bar.spacing_mode_relaxed": "Relaxed",
|
||||
"settings.status_bar.spacing_mode_custom": "Custom",
|
||||
"settings.status_bar.spacing_custom_label": "Custom spacing (%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.region.title": "Region",
|
||||
"settings.region.description": "Choose language and apply immediately to settings and key UI.",
|
||||
"settings.region.language_header": "Language",
|
||||
@@ -86,12 +99,17 @@
|
||||
"launcher.folder_items_format": "{0} apps",
|
||||
"button.component_library": "Edit Desktop",
|
||||
"tooltip.component_library": "Edit Desktop",
|
||||
"component_library.title": "Edit Desktop",
|
||||
"component_library.title": "Widgets",
|
||||
"component_library.empty": "Swipe to pick a category, tap to open, then drag a widget onto the desktop.",
|
||||
"component_library.drag_hint": "Drag to place",
|
||||
"component_category.date": "Date",
|
||||
"component.date": "Date",
|
||||
"component.delete": "Delete",
|
||||
"component.edit": "Edit",
|
||||
"component_category.date": "Calendar",
|
||||
"component.date": "Calendar",
|
||||
"component.month_calendar": "Month Calendar",
|
||||
"component.lunar_calendar": "Lunar Calendar",
|
||||
"desktop.add_page": "Add page",
|
||||
"desktop.delete_page": "Delete page",
|
||||
"placement.fill": "Fill",
|
||||
"placement.fit": "Fit",
|
||||
"placement.stretch": "Stretch",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"app.title": "LanMontainDesktop",
|
||||
"button.back_to_windows": "回到Windows",
|
||||
"tooltip.back_to_windows": "回到Windows",
|
||||
@@ -15,6 +15,7 @@
|
||||
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.current_label": "当前壁纸",
|
||||
"settings.wallpaper.placement_label": "显示方式",
|
||||
"settings.wallpaper.placement_desc": "调整图像在桌面上的填充方式。",
|
||||
"settings.wallpaper.pick_button": "浏览文件",
|
||||
"settings.wallpaper.clear_button": "恢复纯色",
|
||||
"settings.wallpaper.no_selection": "未选择壁纸。",
|
||||
@@ -38,6 +39,11 @@
|
||||
"settings.grid.title": "网格布局",
|
||||
"settings.grid.description": "每个组件至少占用一个格子(最小 1x1)。",
|
||||
"settings.grid.short_side_label": "短边格数",
|
||||
"settings.grid.spacing_label": "网格间距",
|
||||
"settings.grid.spacing_relaxed": "宽松(iOS)",
|
||||
"settings.grid.spacing_compact": "紧凑(Android)",
|
||||
"settings.grid.edge_inset_label": "屏幕边距",
|
||||
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
|
||||
"settings.grid.apply_button": "应用",
|
||||
"settings.grid.info_format": "网格:{0} 列 x {1} 行 | 单元格 {2:F1}px(1:1)",
|
||||
"settings.color.title": "颜色",
|
||||
@@ -61,6 +67,13 @@
|
||||
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
|
||||
"settings.status_bar.clock_header": "时间组件",
|
||||
"settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
|
||||
"settings.status_bar.spacing_header": "组件间距",
|
||||
"settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。",
|
||||
"settings.status_bar.spacing_mode_compact": "紧凑",
|
||||
"settings.status_bar.spacing_mode_relaxed": "宽松",
|
||||
"settings.status_bar.spacing_mode_custom": "自定义",
|
||||
"settings.status_bar.spacing_custom_label": "自定义间距(%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.region.title": "地区",
|
||||
"settings.region.description": "选择语言并立即应用到设置与主要界面。",
|
||||
"settings.region.language_header": "语言",
|
||||
@@ -89,9 +102,14 @@
|
||||
"component_library.title": "桌面编辑",
|
||||
"component_library.empty": "左右滑动选择类别,点击进入,然后拖动组件到桌面放置。",
|
||||
"component_library.drag_hint": "拖动放置",
|
||||
"component_category.date": "日期",
|
||||
"component.delete": "删除",
|
||||
"component.edit": "编辑",
|
||||
"component_category.date": "日历",
|
||||
"component.date": "日历",
|
||||
"component.month_calendar": "月历",
|
||||
"component.lunar_calendar": "农历",
|
||||
"desktop.add_page": "新增页面",
|
||||
"desktop.delete_page": "删除页面",
|
||||
"placement.fill": "填充",
|
||||
"placement.fit": "适应",
|
||||
"placement.stretch": "拉伸",
|
||||
|
||||
@@ -6,6 +6,10 @@ public sealed class AppSettingsSnapshot
|
||||
{
|
||||
public int GridShortSideCells { get; set; } = 12;
|
||||
|
||||
public string GridSpacingPreset { get; set; } = "Relaxed";
|
||||
|
||||
public int DesktopEdgeInsetPercent { get; set; } = 18;
|
||||
|
||||
public bool? IsNightMode { get; set; }
|
||||
|
||||
public string? ThemeColor { get; set; }
|
||||
@@ -28,10 +32,16 @@ public sealed class AppSettingsSnapshot
|
||||
TaskbarActionId.OpenSettings.ToString()
|
||||
];
|
||||
|
||||
public bool EnableDynamicTaskbarActions { get; set; } = false;
|
||||
public bool EnableDynamicTaskbarActions { get; set; } = true;
|
||||
|
||||
public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle";
|
||||
|
||||
public string ClockDisplayFormat { get; set; } = "HourMinuteSecond";
|
||||
|
||||
public string StatusBarSpacingMode { get; set; } = "Relaxed";
|
||||
|
||||
public int StatusBarCustomSpacingPercent { get; set; } = 12;
|
||||
|
||||
public int DesktopPageCount { get; set; } = 1;
|
||||
|
||||
public int CurrentDesktopSurfaceIndex { get; set; } = 0;
|
||||
|
||||
@@ -4,5 +4,8 @@ public enum TaskbarActionId
|
||||
{
|
||||
MinimizeToWindows,
|
||||
OpenSettings,
|
||||
AddDesktopPage
|
||||
AddDesktopPage,
|
||||
DeleteDesktopPage,
|
||||
DeleteComponent,
|
||||
EditComponent
|
||||
}
|
||||
|
||||
227
LanMontainDesktop/Services/LunarCalendarService.cs
Normal file
227
LanMontainDesktop/Services/LunarCalendarService.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LanMontainDesktop.Services;
|
||||
|
||||
public sealed class LunarCalendarService
|
||||
{
|
||||
private static readonly ChineseLunisolarCalendar Calendar = new();
|
||||
|
||||
private static readonly string[] HeavenlyStemsZh =
|
||||
[
|
||||
"\u7532",
|
||||
"\u4e59",
|
||||
"\u4e19",
|
||||
"\u4e01",
|
||||
"\u620a",
|
||||
"\u5df1",
|
||||
"\u5e9a",
|
||||
"\u8f9b",
|
||||
"\u58ec",
|
||||
"\u7678"
|
||||
];
|
||||
|
||||
private static readonly string[] EarthlyBranchesZh =
|
||||
[
|
||||
"\u5b50",
|
||||
"\u4e11",
|
||||
"\u5bc5",
|
||||
"\u536f",
|
||||
"\u8fb0",
|
||||
"\u5df3",
|
||||
"\u5348",
|
||||
"\u672a",
|
||||
"\u7533",
|
||||
"\u9149",
|
||||
"\u620c",
|
||||
"\u4ea5"
|
||||
];
|
||||
|
||||
private static readonly string[] HeavenlyStemsEn =
|
||||
["Jia", "Yi", "Bing", "Ding", "Wu", "Ji", "Geng", "Xin", "Ren", "Gui"];
|
||||
|
||||
private static readonly string[] EarthlyBranchesEn =
|
||||
["Zi", "Chou", "Yin", "Mao", "Chen", "Si", "Wu", "Wei", "Shen", "You", "Xu", "Hai"];
|
||||
|
||||
private static readonly string[] ZodiacsZh =
|
||||
[
|
||||
"\u9f20",
|
||||
"\u725b",
|
||||
"\u864e",
|
||||
"\u5154",
|
||||
"\u9f99",
|
||||
"\u86c7",
|
||||
"\u9a6c",
|
||||
"\u7f8a",
|
||||
"\u7334",
|
||||
"\u9e21",
|
||||
"\u72d7",
|
||||
"\u732a"
|
||||
];
|
||||
|
||||
private static readonly string[] ZodiacsEn =
|
||||
["Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig"];
|
||||
|
||||
private static readonly string[] LunarMonthsZh =
|
||||
[
|
||||
"\u6b63",
|
||||
"\u4e8c",
|
||||
"\u4e09",
|
||||
"\u56db",
|
||||
"\u4e94",
|
||||
"\u516d",
|
||||
"\u4e03",
|
||||
"\u516b",
|
||||
"\u4e5d",
|
||||
"\u5341",
|
||||
"\u51ac",
|
||||
"\u814a"
|
||||
];
|
||||
|
||||
private static readonly string[] LunarDayDigitsZh =
|
||||
[
|
||||
"\u4e00",
|
||||
"\u4e8c",
|
||||
"\u4e09",
|
||||
"\u56db",
|
||||
"\u4e94",
|
||||
"\u516d",
|
||||
"\u4e03",
|
||||
"\u516b",
|
||||
"\u4e5d",
|
||||
"\u5341"
|
||||
];
|
||||
|
||||
public LunarCalendarInfo GetLunarInfo(DateTime dateTime)
|
||||
{
|
||||
var date = dateTime.Date;
|
||||
try
|
||||
{
|
||||
var lunarYear = Calendar.GetYear(date);
|
||||
var rawLunarMonth = Calendar.GetMonth(date);
|
||||
var lunarDay = Calendar.GetDayOfMonth(date);
|
||||
|
||||
var (lunarMonth, isLeapMonth) = NormalizeLunarMonth(lunarYear, rawLunarMonth);
|
||||
|
||||
var sexagenaryYear = Calendar.GetSexagenaryYear(date);
|
||||
var stemIndex = Calendar.GetCelestialStem(sexagenaryYear) - 1;
|
||||
var branchIndex = Calendar.GetTerrestrialBranch(sexagenaryYear) - 1;
|
||||
|
||||
var ganzhiYearZh = $"{HeavenlyStemsZh[stemIndex]}{EarthlyBranchesZh[branchIndex]}";
|
||||
var ganzhiYearEn = $"{HeavenlyStemsEn[stemIndex]}-{EarthlyBranchesEn[branchIndex]}";
|
||||
var zodiacZh = ZodiacsZh[branchIndex];
|
||||
var zodiacEn = ZodiacsEn[branchIndex];
|
||||
|
||||
return new LunarCalendarInfo(
|
||||
LunarYear: lunarYear,
|
||||
LunarMonth: lunarMonth,
|
||||
LunarDay: lunarDay,
|
||||
IsLeapMonth: isLeapMonth,
|
||||
LunarDateZh: BuildLunarDateZh(lunarMonth, lunarDay, isLeapMonth),
|
||||
LunarDateEn: BuildLunarDateEn(lunarMonth, lunarDay, isLeapMonth),
|
||||
GanzhiYearZh: ganzhiYearZh,
|
||||
GanzhiYearEn: ganzhiYearEn,
|
||||
ZodiacZh: zodiacZh,
|
||||
ZodiacEn: zodiacEn);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// ChineseLunisolarCalendar has a limited date range.
|
||||
return new LunarCalendarInfo(
|
||||
LunarYear: date.Year,
|
||||
LunarMonth: date.Month,
|
||||
LunarDay: date.Day,
|
||||
IsLeapMonth: false,
|
||||
LunarDateZh: "\u65e5\u671f\u8d85\u51fa\u519c\u5386\u652f\u6301\u8303\u56f4",
|
||||
LunarDateEn: date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
GanzhiYearZh: "-",
|
||||
GanzhiYearEn: "-",
|
||||
ZodiacZh: "-",
|
||||
ZodiacEn: "-");
|
||||
}
|
||||
}
|
||||
|
||||
private static (int Month, bool IsLeapMonth) NormalizeLunarMonth(int lunarYear, int rawMonth)
|
||||
{
|
||||
var leapMonth = Calendar.GetLeapMonth(lunarYear);
|
||||
if (leapMonth == 0)
|
||||
{
|
||||
return (rawMonth, false);
|
||||
}
|
||||
|
||||
if (rawMonth == leapMonth)
|
||||
{
|
||||
return (rawMonth - 1, true);
|
||||
}
|
||||
|
||||
if (rawMonth > leapMonth)
|
||||
{
|
||||
return (rawMonth - 1, false);
|
||||
}
|
||||
|
||||
return (rawMonth, false);
|
||||
}
|
||||
|
||||
private static string BuildLunarDateZh(int lunarMonth, int lunarDay, bool isLeapMonth)
|
||||
{
|
||||
var monthName = lunarMonth is >= 1 and <= 12
|
||||
? LunarMonthsZh[lunarMonth - 1]
|
||||
: lunarMonth.ToString(CultureInfo.InvariantCulture);
|
||||
var leapPrefix = isLeapMonth ? "\u95f0" : string.Empty;
|
||||
return $"{leapPrefix}{monthName}\u6708{BuildLunarDayZh(lunarDay)}";
|
||||
}
|
||||
|
||||
private static string BuildLunarDateEn(int lunarMonth, int lunarDay, bool isLeapMonth)
|
||||
{
|
||||
var leapPrefix = isLeapMonth ? "Leap " : string.Empty;
|
||||
return $"{leapPrefix}M{lunarMonth} D{lunarDay}";
|
||||
}
|
||||
|
||||
private static string BuildLunarDayZh(int day)
|
||||
{
|
||||
if (day <= 0)
|
||||
{
|
||||
return day.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (day <= 10)
|
||||
{
|
||||
return day == 10 ? "\u521d\u5341" : $"\u521d{LunarDayDigitsZh[day - 1]}";
|
||||
}
|
||||
|
||||
if (day < 20)
|
||||
{
|
||||
return $"\u5341{LunarDayDigitsZh[day - 11]}";
|
||||
}
|
||||
|
||||
if (day == 20)
|
||||
{
|
||||
return "\u4e8c\u5341";
|
||||
}
|
||||
|
||||
if (day < 30)
|
||||
{
|
||||
return $"\u5eff{LunarDayDigitsZh[day - 21]}";
|
||||
}
|
||||
|
||||
if (day == 30)
|
||||
{
|
||||
return "\u4e09\u5341";
|
||||
}
|
||||
|
||||
return day.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LunarCalendarInfo(
|
||||
int LunarYear,
|
||||
int LunarMonth,
|
||||
int LunarDay,
|
||||
bool IsLeapMonth,
|
||||
string LunarDateZh,
|
||||
string LunarDateEn,
|
||||
string GanzhiYearZh,
|
||||
string GanzhiYearEn,
|
||||
string ZodiacZh,
|
||||
string ZodiacEn);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="CornerRadius" Value="20" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Padding" Value="16,10" />
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<Style Selector="Button.swatch-button">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Opacity" Value="0.88" />
|
||||
</Style>
|
||||
|
||||
@@ -70,25 +70,39 @@
|
||||
<Style Selector="Border.glass-panel">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="BorderThickness" Value="1.2" />
|
||||
<Setter Property="CornerRadius" Value="28" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
|
||||
<Setter Property="BoxShadow" Value="0 1 2 #1A000000" />
|
||||
<Setter Property="BoxShadow" Value="0 4 12 #1A000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.glass-strong">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="CornerRadius" Value="32" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||
<Setter Property="BoxShadow" Value="0 2 4 #26000000" />
|
||||
<Setter Property="BoxShadow" Value="0 8 24 #26000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.glass-island">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="CornerRadius" Value="36" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<ThicknessTransition Property="Padding" Duration="0:0:0.2" Easing="QuarticEaseOut" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.mica-strong">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="CornerRadius" Value="36" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||
<Setter Property="BoxShadow" Value="0 8 22 #2A000000" />
|
||||
</Style>
|
||||
|
||||
@@ -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"
|
||||
Padding="0"
|
||||
CornerRadius="24">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="26"
|
||||
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>
|
||||
|
||||
@@ -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 “满盈”风格算法 ---
|
||||
|
||||
// Keep the time legible across dense and sparse grid layouts.
|
||||
TimeTextBlock.FontSize = Math.Clamp(cellSize * 0.42, 10, 56);
|
||||
TimeTextBlock.FontWeight = FontWeight.SemiBold;
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Padding="12">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid Width="460"
|
||||
Height="220"
|
||||
ColumnDefinitions="1.2*,1*"
|
||||
ColumnSpacing="12">
|
||||
|
||||
<Grid x:Name="LeftPanelGrid"
|
||||
Grid.Column="0"
|
||||
Padding="12"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<!-- 月份年份 -->
|
||||
<TextBlock x:Name="CalendarMonthYearTextBlock"
|
||||
RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="GregorianHeadlineTextBlock"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Margin="0,0,0,8" />
|
||||
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="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>
|
||||
|
||||
<!-- 右侧:今日详情 -->
|
||||
<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}" />
|
||||
RowDefinitions="*,*,*,*,*"
|
||||
ColumnDefinitions="*,*,*,*,*,*,*" />
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var lunar = LunarCalendarService.GetLunarInfo(now);
|
||||
|
||||
// 右侧:今日详情
|
||||
TodayDayTextBlock.Text = now.Day.ToString();
|
||||
TodayWeekdayTextBlock.Text = now.ToString("dddd", culture);
|
||||
GregorianHeadlineTextBlock.Text = isZh
|
||||
? $"{now.Month}月{now.Day}日 {ToChineseWeekday(now.DayOfWeek)}"
|
||||
: now.ToString("MMM d ddd", culture);
|
||||
|
||||
// 左侧:月历
|
||||
CalendarMonthYearTextBlock.Text = now.ToString("yyyy年M月", 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);
|
||||
}
|
||||
@@ -100,32 +155,31 @@ public partial class DateWidget : UserControl
|
||||
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
|
||||
: Brushes.Blue;
|
||||
@@ -134,31 +188,28 @@ public partial class DateWidget : UserControl
|
||||
: 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));
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
// 调整字体大小
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class DateWidgetSettingsWindow : UserControl
|
||||
{
|
||||
public DateWidgetSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
87
LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml
Normal file
87
LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml
Normal 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>
|
||||
|
||||
239
LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
Normal file
239
LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
49
LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml
Normal file
49
LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml
Normal 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>
|
||||
|
||||
244
LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
Normal file
244
LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,8 +98,10 @@ public partial class MainWindow
|
||||
|
||||
var viewportRow = gridMetrics.RowCount > 2 ? 1 : 0;
|
||||
var viewportRowSpan = gridMetrics.RowCount > 2 ? gridMetrics.RowCount - 2 : 1;
|
||||
var pageWidth = Math.Max(1, gridMetrics.ColumnCount * gridMetrics.CellSize);
|
||||
var pageHeight = Math.Max(1, viewportRowSpan * gridMetrics.CellSize);
|
||||
var pageWidth = Math.Max(1, gridMetrics.GridWidthPx);
|
||||
var pageHeight = Math.Max(
|
||||
1,
|
||||
viewportRowSpan * gridMetrics.CellSize + Math.Max(0, viewportRowSpan - 1) * gridMetrics.GapPx);
|
||||
|
||||
Grid.SetRow(DesktopPagesViewport, viewportRow);
|
||||
Grid.SetColumn(DesktopPagesViewport, 0);
|
||||
@@ -137,6 +139,8 @@ public partial class MainWindow
|
||||
{
|
||||
Width = pageWidth,
|
||||
Height = pageHeight,
|
||||
RowSpacing = gridMetrics.GapPx,
|
||||
ColumnSpacing = gridMetrics.GapPx,
|
||||
Background = Brushes.Transparent,
|
||||
ShowGridLines = false
|
||||
};
|
||||
@@ -309,6 +313,16 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果在组件编辑模式下点击空白区域,取消组件选中
|
||||
if (_isComponentLibraryOpen && _selectedDesktopComponentHost is not null)
|
||||
{
|
||||
if (!IsInteractivePointerSource(e.Source))
|
||||
{
|
||||
ClearDesktopComponentSelection();
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanSwipeDesktopSurface())
|
||||
{
|
||||
return;
|
||||
@@ -519,7 +533,7 @@ public partial class MainWindow
|
||||
Classes = { "glass-panel" },
|
||||
BorderThickness = new Thickness(0),
|
||||
Margin = new Thickness(0, 0, 12, 12),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
CornerRadius = new CornerRadius(20),
|
||||
Child = panel
|
||||
// 不设置固定 Width 和 Height,由 UpdateLauncherTileLayout 动态设置
|
||||
};
|
||||
@@ -598,7 +612,7 @@ public partial class MainWindow
|
||||
Classes = { "glass-panel" },
|
||||
Margin = new Thickness(0, 0, 12, 12),
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
CornerRadius = new CornerRadius(20),
|
||||
Padding = new Thickness(10),
|
||||
Content = content
|
||||
// 不设置固定 Width 和 Height,由 UpdateLauncherTileLayout 动态设置
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using FluentIcons.Avalonia;
|
||||
using FluentIcons.Common;
|
||||
|
||||
namespace LanMontainDesktop.Views;
|
||||
|
||||
@@ -59,11 +62,11 @@ public partial class MainWindow
|
||||
WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
|
||||
ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows"));
|
||||
|
||||
OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
|
||||
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
|
||||
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
|
||||
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Edit Desktop"));
|
||||
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Edit Desktop");
|
||||
OpenComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
|
||||
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
|
||||
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
|
||||
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "编辑桌面"));
|
||||
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "小组件");
|
||||
ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close"));
|
||||
ComponentLibraryEmptyTextBlock.Text = L(
|
||||
"component_library.empty",
|
||||
@@ -88,17 +91,55 @@ public partial class MainWindow
|
||||
ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "重置");
|
||||
|
||||
GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout");
|
||||
GridSpacingPresetLabelTextBlock.Text = L("settings.grid.spacing_label", "Grid Spacing");
|
||||
GridSpacingRelaxedComboBoxItem.Content = L("settings.grid.spacing_relaxed", "Relaxed");
|
||||
GridSpacingCompactComboBoxItem.Content = L("settings.grid.spacing_compact", "Compact");
|
||||
GridEdgeInsetLabelTextBlock.Text = L("settings.grid.edge_inset_label", "Screen Inset");
|
||||
ApplyGridButton.Content = L("settings.grid.apply_button", "Apply");
|
||||
UpdateGridEdgeInsetComputedPxText(_currentDesktopCellSize);
|
||||
|
||||
ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color");
|
||||
ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night");
|
||||
NightModeToggleSwitch.OnContent = L("settings.color.day_night_on", "Night");
|
||||
NightModeToggleSwitch.OffContent = L("settings.color.day_night_off", "Day");
|
||||
NightModeToggleSwitch.OffContent = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 6,
|
||||
Children =
|
||||
{
|
||||
new SymbolIcon { Symbol = Symbol.WeatherSunny, IconVariant = IconVariant.Regular, FontSize = 14 },
|
||||
new TextBlock
|
||||
{
|
||||
Text = L("settings.color.day_night_off", "Day"),
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
}
|
||||
}
|
||||
};
|
||||
NightModeToggleSwitch.OnContent = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 6,
|
||||
Children =
|
||||
{
|
||||
new SymbolIcon { Symbol = Symbol.WeatherMoon, IconVariant = IconVariant.Regular, FontSize = 14 },
|
||||
new TextBlock
|
||||
{
|
||||
Text = L("settings.color.day_night_on", "Night"),
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
}
|
||||
}
|
||||
};
|
||||
RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors");
|
||||
SystemMonetColorsLabelTextBlock.Text = L("settings.color.system_monet_label", "System Monet Colors");
|
||||
RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh");
|
||||
|
||||
StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar");
|
||||
StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock");
|
||||
StatusBarSpacingSettingsExpander.Header = L("settings.status_bar.spacing_header", "Component Spacing");
|
||||
StatusBarSpacingSettingsExpander.Description = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
|
||||
StatusBarSpacingModeCompactItem.Content = L("settings.status_bar.spacing_mode_compact", "Compact");
|
||||
StatusBarSpacingModeRelaxedItem.Content = L("settings.status_bar.spacing_mode_relaxed", "Relaxed");
|
||||
StatusBarSpacingModeCustomItem.Content = L("settings.status_bar.spacing_mode_custom", "Custom");
|
||||
StatusBarSpacingCustomLabelTextBlock.Text = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
|
||||
|
||||
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
|
||||
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using FluentIcons.Avalonia;
|
||||
using FluentIcons.Common;
|
||||
using LanMontainDesktop.Views.Components;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -60,7 +61,8 @@ public partial class MainWindow
|
||||
WallpaperSettingsPanel is null ||
|
||||
ColorSettingsPanel is null ||
|
||||
StatusBarSettingsPanel is null ||
|
||||
RegionSettingsPanel is null)
|
||||
RegionSettingsPanel is null ||
|
||||
AboutSettingsPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -71,6 +73,7 @@ public partial class MainWindow
|
||||
ColorSettingsPanel.IsVisible = selectedIndex == 2;
|
||||
StatusBarSettingsPanel.IsVisible = selectedIndex == 3;
|
||||
RegionSettingsPanel.IsVisible = selectedIndex == 4;
|
||||
AboutSettingsPanel.IsVisible = selectedIndex == 5;
|
||||
|
||||
if (selectedIndex == 1)
|
||||
{
|
||||
@@ -633,6 +636,8 @@ public partial class MainWindow
|
||||
var snapshot = new AppSettingsSnapshot
|
||||
{
|
||||
GridShortSideCells = _targetShortSideCells,
|
||||
GridSpacingPreset = _gridSpacingPreset,
|
||||
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
|
||||
IsNightMode = _isNightMode,
|
||||
ThemeColor = _selectedThemeColor.ToString(),
|
||||
WallpaperPath = _wallpaperPath,
|
||||
@@ -644,6 +649,9 @@ public partial class MainWindow
|
||||
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
|
||||
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
|
||||
TaskbarLayoutMode = _taskbarLayoutMode,
|
||||
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
|
||||
StatusBarSpacingMode = _statusBarSpacingMode,
|
||||
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
|
||||
DesktopPageCount = _desktopPageCount,
|
||||
CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex,
|
||||
DesktopComponentPlacements = _desktopComponentPlacements.ToList()
|
||||
@@ -652,6 +660,23 @@ public partial class MainWindow
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
private IDisposable? _persistSettingsDebounceTimer;
|
||||
|
||||
private void SchedulePersistSettings(int delayMs = 200)
|
||||
{
|
||||
if (_suppressSettingsPersistence)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_persistSettingsDebounceTimer?.Dispose();
|
||||
_persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() =>
|
||||
{
|
||||
_persistSettingsDebounceTimer = null;
|
||||
PersistSettings();
|
||||
}, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
|
||||
}
|
||||
|
||||
private void UpdateAdaptiveTextSystem()
|
||||
{
|
||||
var isLightBackground = _isSettingsOpen
|
||||
@@ -980,8 +1005,13 @@ public partial class MainWindow
|
||||
UpdateAdaptiveTextSystem();
|
||||
ApplyWallpaperBrush();
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
if (_settingsContentPanelTransform is not null)
|
||||
{
|
||||
_settingsContentPanelTransform.Y = 30;
|
||||
}
|
||||
SettingsPage.IsVisible = true;
|
||||
SettingsPage.Opacity = 0;
|
||||
UpdateSettingsViewportInsets(Math.Max(1, _currentDesktopCellSize));
|
||||
|
||||
UpdateWallpaperPreviewLayout();
|
||||
|
||||
@@ -992,6 +1022,10 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settingsContentPanelTransform is not null)
|
||||
{
|
||||
_settingsContentPanelTransform.Y = 0;
|
||||
}
|
||||
SettingsPage.Opacity = 1;
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
@@ -1011,10 +1045,18 @@ public partial class MainWindow
|
||||
if (immediate)
|
||||
{
|
||||
SettingsPage.Opacity = 0;
|
||||
if (_settingsContentPanelTransform is not null)
|
||||
{
|
||||
_settingsContentPanelTransform.Y = 30;
|
||||
}
|
||||
SettingsPage.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settingsContentPanelTransform is not null)
|
||||
{
|
||||
_settingsContentPanelTransform.Y = 30;
|
||||
}
|
||||
SettingsPage.Opacity = 0;
|
||||
|
||||
DispatcherTimer.RunOnce(() =>
|
||||
@@ -1059,6 +1101,15 @@ public partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
if (StatusBarSpacingSettingsExpander is not null)
|
||||
{
|
||||
StatusBarSpacingSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.TextLineSpacing,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (LanguageSettingsExpander is not null)
|
||||
{
|
||||
LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
Grid.Column="1"
|
||||
Classes="glass-panel"
|
||||
ClipToBounds="False"
|
||||
CornerRadius="18"
|
||||
CornerRadius="36"
|
||||
Padding="18">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<StackPanel Spacing="4">
|
||||
@@ -160,7 +160,7 @@
|
||||
Margin="52"
|
||||
MaxWidth="760"
|
||||
MaxHeight="520"
|
||||
CornerRadius="18"
|
||||
CornerRadius="36"
|
||||
Padding="14">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform Y="42" />
|
||||
@@ -232,12 +232,13 @@
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BottomTaskbarContainer"
|
||||
Classes="glass-strong"
|
||||
Classes="glass-island"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="1"
|
||||
Margin="4"
|
||||
CornerRadius="18"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0"
|
||||
CornerRadius="36"
|
||||
Padding="6">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
@@ -374,14 +375,14 @@
|
||||
</Border.RenderTransform>
|
||||
|
||||
<Border Classes="mica-strong"
|
||||
CornerRadius="14"
|
||||
CornerRadius="32"
|
||||
Padding="18">
|
||||
<Grid ColumnDefinitions="220,*"
|
||||
ColumnSpacing="16">
|
||||
<Border x:Name="SettingsNavPanelBorder"
|
||||
Classes="glass-panel"
|
||||
Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
CornerRadius="28"
|
||||
Padding="10">
|
||||
<Border.Styles>
|
||||
<Style Selector="ListBox#SettingsNavListBox">
|
||||
@@ -394,7 +395,7 @@
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="Margin" Value="0,2" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="ListBox#SettingsNavListBox ListBoxItem:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemHoverBackgroundBrush}" />
|
||||
@@ -443,13 +444,19 @@
|
||||
<TextBlock x:Name="SettingsNavRegionTextBlock" Text="地区" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem x:Name="SettingsNavAboutItem" ToolTip.Tip="关于">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<fi:SymbolIcon x:Name="SettingsNavAboutIcon" Symbol="Info" IconVariant="Regular" />
|
||||
<TextBlock x:Name="SettingsNavAboutTextBlock" Text="关于" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Classes="glass-panel"
|
||||
CornerRadius="10"
|
||||
CornerRadius="20"
|
||||
Padding="14">
|
||||
<Grid>
|
||||
<Grid x:Name="WallpaperSettingsPanel"
|
||||
@@ -468,17 +475,17 @@
|
||||
<Border x:Name="WallpaperPreviewHost"
|
||||
Grid.Row="1" Grid.Column="0"
|
||||
Margin="0,0,16,0"
|
||||
VerticalAlignment="Top"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch">
|
||||
<!-- Monitor Frame (Bezel) -->
|
||||
<Border x:Name="WallpaperPreviewFrame"
|
||||
HorizontalAlignment="Stretch"
|
||||
CornerRadius="14"
|
||||
CornerRadius="28"
|
||||
Background="#FF1A1A1A"
|
||||
Padding="12">
|
||||
<Border x:Name="WallpaperPreviewViewport"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
CornerRadius="12"
|
||||
Background="#30111827">
|
||||
<Grid>
|
||||
<vlc:VideoView x:Name="WallpaperPreviewVideoView"
|
||||
@@ -497,13 +504,8 @@
|
||||
<StackPanel x:Name="WallpaperPreviewTopStatusComponentsPanel"
|
||||
Orientation="Horizontal"
|
||||
Spacing="3">
|
||||
<Border x:Name="WallpaperPreviewClockContainer"
|
||||
IsVisible="False">
|
||||
<TextBlock x:Name="WallpaperPreviewClockTextBlock"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="12:34" />
|
||||
</Border>
|
||||
<comp:ClockWidget x:Name="WallpaperPreviewClockWidget"
|
||||
IsVisible="False" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -511,7 +513,7 @@
|
||||
Classes="glass-strong"
|
||||
Grid.Row="1"
|
||||
Margin="3"
|
||||
CornerRadius="8"
|
||||
CornerRadius="16"
|
||||
Padding="2">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="3">
|
||||
@@ -619,15 +621,18 @@
|
||||
HorizontalAlignment="Stretch">
|
||||
<Border x:Name="GridPreviewFrame"
|
||||
HorizontalAlignment="Stretch"
|
||||
CornerRadius="14"
|
||||
CornerRadius="28"
|
||||
Background="#FF1A1A1A"
|
||||
Padding="12">
|
||||
<Border x:Name="GridPreviewViewport"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
CornerRadius="16"
|
||||
Background="#30111827">
|
||||
<Panel>
|
||||
<Canvas x:Name="GridPreviewLinesCanvas" />
|
||||
<Canvas x:Name="GridPreviewLinesCanvas"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False" />
|
||||
<Grid x:Name="GridPreviewGrid"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
@@ -645,7 +650,7 @@
|
||||
Classes="glass-strong"
|
||||
Grid.Row="1"
|
||||
Margin="3"
|
||||
CornerRadius="8"
|
||||
CornerRadius="16"
|
||||
Padding="2">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="3">
|
||||
@@ -701,6 +706,47 @@
|
||||
Value="12" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="GridSpacingPresetLabelTextBlock"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Spacing" />
|
||||
|
||||
<ComboBox x:Name="GridSpacingPresetComboBox"
|
||||
Width="220"
|
||||
SelectionChanged="OnGridSpacingPresetSelectionChanged">
|
||||
<ComboBoxItem x:Name="GridSpacingRelaxedComboBoxItem" Tag="Relaxed" Content="Relaxed" />
|
||||
<ComboBoxItem x:Name="GridSpacingCompactComboBoxItem" Tag="Compact" Content="Compact" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock x:Name="GridEdgeInsetLabelTextBlock"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Screen Inset" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<Slider x:Name="GridEdgeInsetSlider"
|
||||
Grid.Column="0"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
TickFrequency="1"
|
||||
TickPlacement="None"
|
||||
Value="18"
|
||||
ValueChanged="OnGridEdgeInsetSliderChanged" />
|
||||
<ui:NumberBox x:Name="GridEdgeInsetNumberBox"
|
||||
Grid.Column="1"
|
||||
Width="80"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Value="18" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="≈ 0 px" />
|
||||
|
||||
<Button x:Name="ApplyGridButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="0,10"
|
||||
@@ -762,7 +808,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch1"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="RecommendedColorButton2"
|
||||
@@ -773,7 +819,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch2"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="RecommendedColorButton3"
|
||||
@@ -784,7 +830,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch3"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="RecommendedColorButton4"
|
||||
@@ -795,7 +841,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch4"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="RecommendedColorButton5"
|
||||
@@ -806,7 +852,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch5"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="RecommendedColorButton6"
|
||||
@@ -817,7 +863,7 @@
|
||||
<Border x:Name="RecommendedColorSwatch6"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
@@ -852,7 +898,7 @@
|
||||
<Border x:Name="MonetColorSwatch1"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="MonetColorButton2"
|
||||
@@ -863,7 +909,7 @@
|
||||
<Border x:Name="MonetColorSwatch2"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="MonetColorButton3"
|
||||
@@ -874,7 +920,7 @@
|
||||
<Border x:Name="MonetColorSwatch3"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="MonetColorButton4"
|
||||
@@ -885,7 +931,7 @@
|
||||
<Border x:Name="MonetColorSwatch4"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="MonetColorButton5"
|
||||
@@ -896,7 +942,7 @@
|
||||
<Border x:Name="MonetColorSwatch5"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
<Button x:Name="MonetColorButton6"
|
||||
@@ -907,7 +953,7 @@
|
||||
<Border x:Name="MonetColorSwatch6"
|
||||
Width="26"
|
||||
Height="26"
|
||||
CornerRadius="6"
|
||||
CornerRadius="12"
|
||||
BorderThickness="0" />
|
||||
</Button>
|
||||
</WrapPanel>
|
||||
@@ -932,7 +978,8 @@
|
||||
|
||||
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
|
||||
Header="时间组件"
|
||||
Description="在顶部状态栏显示时钟。">
|
||||
Description="在顶部状态栏显示时钟。"
|
||||
IsExpanded="False">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
|
||||
</ui:SettingsExpander.IconSource>
|
||||
@@ -943,6 +990,72 @@
|
||||
Checked="OnStatusBarClockChecked"
|
||||
Unchecked="OnStatusBarClockUnchecked" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<StackPanel Margin="0,8,0,0" Spacing="12">
|
||||
<TextBlock Text="显示格式" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<RadioButton x:Name="ClockFormatHMSSRadio"
|
||||
Content="时分秒 (HH:mm:ss)"
|
||||
GroupName="ClockFormat"
|
||||
Checked="OnClockFormatChanged"
|
||||
Tag="Hms" />
|
||||
<RadioButton x:Name="ClockFormatHMRadio"
|
||||
Content="时分 (HH:mm)"
|
||||
GroupName="ClockFormat"
|
||||
Checked="OnClockFormatChanged"
|
||||
Tag="Hm" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
|
||||
Header="Component spacing"
|
||||
Description="Adjust spacing between status bar components."
|
||||
IsExpanded="False">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox x:Name="StatusBarSpacingModeComboBox"
|
||||
Width="220"
|
||||
SelectionChanged="OnStatusBarSpacingModeChanged">
|
||||
<ComboBoxItem x:Name="StatusBarSpacingModeCompactItem" Tag="Compact" Content="Compact" />
|
||||
<ComboBoxItem x:Name="StatusBarSpacingModeRelaxedItem" Tag="Relaxed" Content="Relaxed" />
|
||||
<ComboBoxItem x:Name="StatusBarSpacingModeCustomItem" Tag="Custom" Content="Custom" />
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
|
||||
<StackPanel x:Name="StatusBarSpacingCustomPanel"
|
||||
Margin="0,8,0,0"
|
||||
Spacing="12"
|
||||
IsVisible="False">
|
||||
<TextBlock x:Name="StatusBarSpacingCustomLabelTextBlock"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Custom spacing" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<Slider x:Name="StatusBarSpacingSlider"
|
||||
Grid.Column="0"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
TickFrequency="1"
|
||||
TickPlacement="None"
|
||||
Value="12"
|
||||
ValueChanged="OnStatusBarSpacingSliderChanged" />
|
||||
<ui:NumberBox x:Name="StatusBarSpacingNumberBox"
|
||||
Grid.Column="1"
|
||||
Width="80"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Value="12" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="≈ 0 px" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
|
||||
@@ -983,6 +1096,19 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="AboutSettingsPanel" IsVisible="False" Spacing="20">
|
||||
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Text="关于" />
|
||||
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="20" Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="LanMontainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock Text="现代化桌面壳层应用" FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
|
||||
<TextBlock x:Name="VersionTextBlock" Text="版本号: 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock x:Name="CodeNameTextBlock" Text="版本代号: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -990,6 +1116,47 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="ComponentSettingsWindow"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
Classes="glass-strong"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="400"
|
||||
MinWidth="300"
|
||||
MaxWidth="500"
|
||||
Height="300"
|
||||
MinHeight="200"
|
||||
Margin="24,24,24,100"
|
||||
CornerRadius="36"
|
||||
Padding="0">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource AdaptiveAccentBrush}"
|
||||
CornerRadius="36,36,0,0"
|
||||
Padding="16,12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="组件设置"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseComponentSettingsClick">
|
||||
<fi:FluentIcon Icon="Dismiss"
|
||||
FontSize="14"
|
||||
Foreground="White" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ContentControl x:Name="ComponentSettingsContentHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="ComponentLibraryWindow"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
@@ -1002,8 +1169,11 @@
|
||||
Height="260"
|
||||
MinHeight="220"
|
||||
Margin="24,24,24,100"
|
||||
CornerRadius="18"
|
||||
Padding="14">
|
||||
CornerRadius="36"
|
||||
Padding="14"
|
||||
PointerPressed="OnComponentLibraryWindowPointerPressed"
|
||||
PointerMoved="OnComponentLibraryWindowPointerMoved"
|
||||
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
||||
<Border.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
|
||||
@@ -1018,7 +1188,7 @@
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="组件库" />
|
||||
Text="小组件" />
|
||||
<Button x:Name="CloseComponentLibraryButton"
|
||||
Grid.Column="1"
|
||||
Padding="8"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Avalonia.Layout;
|
||||
@@ -22,6 +23,7 @@ using LanMontainDesktop.ComponentSystem.Extensions;
|
||||
using LanMontainDesktop.Models;
|
||||
using LanMontainDesktop.Services;
|
||||
using LanMontainDesktop.Theme;
|
||||
using LanMontainDesktop.Views.Components;
|
||||
using LibVLCSharp.Shared;
|
||||
|
||||
namespace LanMontainDesktop.Views;
|
||||
@@ -47,6 +49,9 @@ public partial class MainWindow : Window
|
||||
private const int StatusBarRowIndex = 0;
|
||||
private const int MinShortSideCells = 6;
|
||||
private const int MaxShortSideCells = 96;
|
||||
private const int MinEdgeInsetPercent = 0;
|
||||
private const int MaxEdgeInsetPercent = 30;
|
||||
private const int DefaultEdgeInsetPercent = 18;
|
||||
private const int SettingsTransitionDurationMs = 240;
|
||||
private const double WallpaperPreviewMaxWidth = 520;
|
||||
private const double LightBackgroundLuminanceThreshold = 0.57;
|
||||
@@ -64,7 +69,17 @@ public partial class MainWindow : Window
|
||||
TaskbarActionId.MinimizeToWindows,
|
||||
TaskbarActionId.OpenSettings
|
||||
];
|
||||
private readonly record struct GridMetrics(int ColumnCount, int RowCount, double CellSize);
|
||||
private readonly record struct GridMetrics(
|
||||
int ColumnCount,
|
||||
int RowCount,
|
||||
double CellSize,
|
||||
double GapPx,
|
||||
double EdgeInsetPx,
|
||||
double GridWidthPx,
|
||||
double GridHeightPx)
|
||||
{
|
||||
public double Pitch => CellSize + GapPx;
|
||||
}
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
@@ -87,6 +102,7 @@ public partial class MainWindow : Window
|
||||
private bool _suppressSettingsPersistence;
|
||||
private bool _isUpdatingWallpaperPreviewLayout;
|
||||
private bool _isComponentLibraryOpen;
|
||||
private Border? _selectedDesktopComponentHost;
|
||||
private bool _reopenSettingsAfterComponentLibraryClose;
|
||||
private TranslateTransform? _settingsContentPanelTransform;
|
||||
private IBrush? _defaultDesktopBackground;
|
||||
@@ -104,8 +120,20 @@ public partial class MainWindow : Window
|
||||
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
|
||||
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
|
||||
private double _currentDesktopCellSize;
|
||||
private double _currentDesktopCellGap;
|
||||
private double _currentDesktopEdgeInset;
|
||||
private string _gridSpacingPreset = "Relaxed";
|
||||
private string _statusBarSpacingMode = "Relaxed";
|
||||
private int _statusBarCustomSpacingPercent = 12;
|
||||
private bool _suppressGridSpacingEvents;
|
||||
private bool _suppressGridInsetEvents;
|
||||
private bool _suppressStatusBarSpacingEvents;
|
||||
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
|
||||
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
|
||||
private string _languageCode = "zh-CN";
|
||||
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
|
||||
|
||||
private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -131,12 +159,40 @@ public partial class MainWindow : Window
|
||||
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
|
||||
MinShortSideCells,
|
||||
MaxShortSideCells);
|
||||
|
||||
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
|
||||
_suppressGridSpacingEvents = true;
|
||||
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
_suppressGridSpacingEvents = false;
|
||||
|
||||
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
_suppressGridInsetEvents = true;
|
||||
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
|
||||
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
|
||||
_suppressGridInsetEvents = false;
|
||||
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
|
||||
|
||||
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
|
||||
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
|
||||
_suppressStatusBarSpacingEvents = true;
|
||||
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
|
||||
{
|
||||
"Compact" => 0,
|
||||
"Custom" => 2,
|
||||
_ => 1
|
||||
};
|
||||
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
|
||||
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
|
||||
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
|
||||
_suppressStatusBarSpacingEvents = false;
|
||||
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
|
||||
|
||||
GridSizeNumberBox.Value = _targetShortSideCells;
|
||||
GridSizeSlider.Value = _targetShortSideCells;
|
||||
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
|
||||
GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged;
|
||||
|
||||
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 4);
|
||||
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 5);
|
||||
UpdateSettingsTabContent();
|
||||
|
||||
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
|
||||
@@ -199,6 +255,8 @@ public partial class MainWindow : Window
|
||||
GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged;
|
||||
GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged;
|
||||
GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged;
|
||||
GridEdgeInsetNumberBox.ValueChanged -= OnGridEdgeInsetNumberBoxChanged;
|
||||
StatusBarSpacingNumberBox.ValueChanged -= OnStatusBarSpacingNumberBoxChanged;
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
@@ -245,12 +303,152 @@ public partial class MainWindow : Window
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppressGridInsetEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var value = (int)Math.Round(GridEdgeInsetSlider.Value);
|
||||
SetPendingGridEdgeInsetPercent(value, updateSlider: false, updateNumberBox: true);
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
|
||||
{
|
||||
if (_suppressGridInsetEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var value = (int)Math.Round(GridEdgeInsetNumberBox.Value);
|
||||
SetPendingGridEdgeInsetPercent(value, updateSlider: true, updateNumberBox: false);
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
private void SetPendingGridEdgeInsetPercent(int percent, bool updateSlider, bool updateNumberBox)
|
||||
{
|
||||
var clamped = Math.Clamp(percent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
|
||||
_suppressGridInsetEvents = true;
|
||||
try
|
||||
{
|
||||
if (updateSlider && Math.Abs(GridEdgeInsetSlider.Value - clamped) > double.Epsilon)
|
||||
{
|
||||
GridEdgeInsetSlider.Value = clamped;
|
||||
}
|
||||
|
||||
if (updateNumberBox && Math.Abs(GridEdgeInsetNumberBox.Value - clamped) > double.Epsilon)
|
||||
{
|
||||
GridEdgeInsetNumberBox.Value = clamped;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressGridInsetEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGridSpacingPresetSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressGridSpacingEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(
|
||||
TryGetSelectedComboBoxTag(StatusBarSpacingModeComboBox) ?? _statusBarSpacingMode);
|
||||
|
||||
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
ApplyDesktopStatusBarComponentSpacing();
|
||||
UpdateWallpaperPreviewLayout();
|
||||
UpdateGridPreviewLayout();
|
||||
SchedulePersistSettings();
|
||||
}
|
||||
|
||||
private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = (int)Math.Round(StatusBarSpacingSlider.Value);
|
||||
SetStatusBarCustomSpacingPercent(percent, updateSlider: false, updateNumberBox: true);
|
||||
|
||||
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ApplyDesktopStatusBarComponentSpacing();
|
||||
UpdateWallpaperPreviewLayout();
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
SchedulePersistSettings();
|
||||
}
|
||||
|
||||
private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
|
||||
{
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = (int)Math.Round(StatusBarSpacingNumberBox.Value);
|
||||
SetStatusBarCustomSpacingPercent(percent, updateSlider: true, updateNumberBox: false);
|
||||
|
||||
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ApplyDesktopStatusBarComponentSpacing();
|
||||
UpdateWallpaperPreviewLayout();
|
||||
UpdateGridPreviewLayout();
|
||||
}
|
||||
|
||||
SchedulePersistSettings();
|
||||
}
|
||||
|
||||
private void SetStatusBarCustomSpacingPercent(int percent, bool updateSlider, bool updateNumberBox)
|
||||
{
|
||||
percent = Math.Clamp(percent, 0, 30);
|
||||
_statusBarCustomSpacingPercent = percent;
|
||||
|
||||
_suppressStatusBarSpacingEvents = true;
|
||||
try
|
||||
{
|
||||
if (updateSlider && Math.Abs(StatusBarSpacingSlider.Value - percent) > double.Epsilon)
|
||||
{
|
||||
StatusBarSpacingSlider.Value = percent;
|
||||
}
|
||||
|
||||
if (updateNumberBox && Math.Abs(StatusBarSpacingNumberBox.Value - percent) > double.Epsilon)
|
||||
{
|
||||
StatusBarSpacingNumberBox.Value = percent;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressStatusBarSpacingEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGridPreviewLayout()
|
||||
{
|
||||
if (GridPreviewFrame is null ||
|
||||
GridPreviewHost is null ||
|
||||
GridPreviewViewport is null ||
|
||||
GridPreviewGrid is null)
|
||||
GridPreviewGrid is null ||
|
||||
GridPreviewLinesCanvas is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -279,14 +477,24 @@ public partial class MainWindow : Window
|
||||
|
||||
var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding);
|
||||
var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding);
|
||||
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells);
|
||||
var preset = NormalizeGridSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
|
||||
var gapRatio = ResolveGridGapRatio(preset);
|
||||
var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
|
||||
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
|
||||
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GridPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
|
||||
GridPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
|
||||
var inset = new Thickness(gridMetrics.EdgeInsetPx);
|
||||
GridPreviewGrid.Margin = inset;
|
||||
GridPreviewGrid.RowSpacing = gridMetrics.GapPx;
|
||||
GridPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
|
||||
GridPreviewGrid.Width = gridMetrics.GridWidthPx;
|
||||
GridPreviewGrid.Height = gridMetrics.GridHeightPx;
|
||||
|
||||
GridPreviewLinesCanvas.Margin = inset;
|
||||
|
||||
GridPreviewGrid.RowDefinitions.Clear();
|
||||
GridPreviewGrid.ColumnDefinitions.Clear();
|
||||
@@ -316,6 +524,8 @@ public partial class MainWindow : Window
|
||||
Grid.SetColumnSpan(GridPreviewBottomTaskbarContainer, gridMetrics.ColumnCount);
|
||||
|
||||
ApplyGridPreviewWidgetSizing(gridMetrics.CellSize);
|
||||
ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
|
||||
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
|
||||
|
||||
GridInfoTextBlock.Text = Lf(
|
||||
"settings.grid.info_format",
|
||||
@@ -344,21 +554,19 @@ public partial class MainWindow : Window
|
||||
GridPreviewLinesCanvas.Children.Clear();
|
||||
|
||||
var cellSize = gridMetrics.CellSize;
|
||||
var gridWidth = gridMetrics.ColumnCount * cellSize;
|
||||
var gridHeight = gridMetrics.RowCount * cellSize;
|
||||
var pitch = gridMetrics.Pitch;
|
||||
var gridWidth = gridMetrics.GridWidthPx;
|
||||
var gridHeight = gridMetrics.GridHeightPx;
|
||||
|
||||
GridPreviewLinesCanvas.Width = gridWidth;
|
||||
GridPreviewLinesCanvas.Height = gridHeight;
|
||||
|
||||
Canvas.SetLeft(GridPreviewLinesCanvas, 0);
|
||||
Canvas.SetTop(GridPreviewLinesCanvas, 0);
|
||||
|
||||
var dashLength = cellSize * 0.3;
|
||||
var gapLength = cellSize * 0.2;
|
||||
|
||||
for (var row = 0; row <= gridMetrics.RowCount; row++)
|
||||
{
|
||||
var y = row * cellSize;
|
||||
var y = row == gridMetrics.RowCount ? gridHeight : row * pitch;
|
||||
var line = new Line
|
||||
{
|
||||
StartPoint = new Point(0, y),
|
||||
@@ -373,7 +581,7 @@ public partial class MainWindow : Window
|
||||
|
||||
for (var col = 0; col <= gridMetrics.ColumnCount; col++)
|
||||
{
|
||||
var x = col * cellSize;
|
||||
var x = col == gridMetrics.ColumnCount ? gridWidth : col * pitch;
|
||||
var line = new Line
|
||||
{
|
||||
StartPoint = new Point(x, 0),
|
||||
@@ -389,13 +597,12 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplyGridPreviewWidgetSizing(double cellSize)
|
||||
{
|
||||
var margin = Math.Clamp(cellSize * 0.08, 1, 6);
|
||||
var previewTaskbarCell = Math.Clamp(cellSize, 10, 36);
|
||||
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 30);
|
||||
var iconSize = Math.Clamp(cellSize * 0.35, 8, 16);
|
||||
|
||||
GridPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4));
|
||||
GridPreviewBottomTaskbarContainer.Margin = new Thickness(margin);
|
||||
GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10));
|
||||
GridPreviewTopStatusBarHost.Padding = new Thickness(0);
|
||||
GridPreviewBottomTaskbarContainer.Margin = new Thickness(0);
|
||||
GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 16, 32));
|
||||
GridPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4));
|
||||
|
||||
GridPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13);
|
||||
@@ -411,6 +618,10 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_gridSpacingPreset = NormalizeGridSpacingPreset(
|
||||
TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
|
||||
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
|
||||
|
||||
var requested = (int)Math.Round(GridSizeNumberBox.Value);
|
||||
if (requested <= 0)
|
||||
{
|
||||
@@ -429,26 +640,56 @@ public partial class MainWindow : Window
|
||||
GridSizeSlider.Value = _targetShortSideCells;
|
||||
}
|
||||
|
||||
SetPendingGridEdgeInsetPercent(_desktopEdgeInsetPercent, updateSlider: true, updateNumberBox: true);
|
||||
|
||||
RebuildDesktopGrid();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnClockFormatChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not RadioButton radioButton || radioButton.Tag is not string formatTag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_clockDisplayFormat = formatTag == "Hm"
|
||||
? ClockDisplayFormat.HourMinute
|
||||
: ClockDisplayFormat.HourMinuteSecond;
|
||||
|
||||
if (ClockWidget is ClockWidget clock)
|
||||
{
|
||||
clock.SetDisplayFormat(_clockDisplayFormat);
|
||||
}
|
||||
|
||||
ApplyTopStatusComponentVisibility();
|
||||
UpdateWallpaperPreviewLayout();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void RebuildDesktopGrid()
|
||||
{
|
||||
var gridMetrics = CalculateGridMetrics(
|
||||
DesktopHost.Bounds.Width,
|
||||
DesktopHost.Bounds.Height,
|
||||
_targetShortSideCells);
|
||||
var hostWidth = DesktopHost.Bounds.Width;
|
||||
var hostHeight = DesktopHost.Bounds.Height;
|
||||
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_currentDesktopCellSize = gridMetrics.CellSize;
|
||||
_currentDesktopCellGap = gridMetrics.GapPx;
|
||||
_currentDesktopEdgeInset = gridMetrics.EdgeInsetPx;
|
||||
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
|
||||
|
||||
DesktopGrid.RowDefinitions.Clear();
|
||||
DesktopGrid.ColumnDefinitions.Clear();
|
||||
DesktopGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
|
||||
DesktopGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
|
||||
DesktopGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
|
||||
DesktopGrid.RowSpacing = gridMetrics.GapPx;
|
||||
DesktopGrid.ColumnSpacing = gridMetrics.GapPx;
|
||||
DesktopGrid.Width = gridMetrics.GridWidthPx;
|
||||
DesktopGrid.Height = gridMetrics.GridHeightPx;
|
||||
|
||||
for (var row = 0; row < gridMetrics.RowCount; row++)
|
||||
{
|
||||
@@ -476,6 +717,7 @@ public partial class MainWindow : Window
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
|
||||
ApplyWidgetSizing(gridMetrics.CellSize);
|
||||
ApplyDesktopStatusBarComponentSpacing();
|
||||
UpdateDesktopSurfaceLayout(gridMetrics);
|
||||
UpdateSettingsViewportInsets(gridMetrics.CellSize);
|
||||
|
||||
@@ -489,26 +731,190 @@ public partial class MainWindow : Window
|
||||
UpdateWallpaperPreviewLayout();
|
||||
}
|
||||
|
||||
private static GridMetrics CalculateGridMetrics(double hostWidth, double hostHeight, int targetShortSideCells)
|
||||
private void ApplyDesktopStatusBarComponentSpacing()
|
||||
{
|
||||
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize);
|
||||
UpdateStatusBarSpacingComputedPxText(_currentDesktopCellSize);
|
||||
}
|
||||
|
||||
private int ResolveStatusBarSpacingPercent()
|
||||
{
|
||||
return _statusBarSpacingMode switch
|
||||
{
|
||||
"Compact" => 6,
|
||||
"Custom" => Math.Clamp(_statusBarCustomSpacingPercent, 0, 30),
|
||||
_ => 12
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyStatusBarComponentSpacingForPanel(StackPanel? panel, double cellSize)
|
||||
{
|
||||
if (panel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = ResolveStatusBarSpacingPercent();
|
||||
var spacingPx = Math.Max(0, cellSize) * (percent / 100d);
|
||||
panel.Spacing = spacingPx;
|
||||
}
|
||||
|
||||
private void UpdateStatusBarSpacingComputedPxText(double cellSize)
|
||||
{
|
||||
if (StatusBarSpacingComputedPxTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = ResolveStatusBarSpacingPercent();
|
||||
var spacingPx = Math.Max(0, cellSize) * (percent / 100d);
|
||||
StatusBarSpacingComputedPxTextBlock.Text = Lf(
|
||||
"settings.status_bar.spacing_custom_px_format",
|
||||
"鈮?{0:F1}px",
|
||||
spacingPx);
|
||||
}
|
||||
|
||||
private int ResolvePendingGridEdgeInsetPercent()
|
||||
{
|
||||
var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value);
|
||||
return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
}
|
||||
|
||||
private void UpdateGridEdgeInsetComputedPxText(double cellSize)
|
||||
{
|
||||
if (GridEdgeInsetComputedPxTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = ResolvePendingGridEdgeInsetPercent();
|
||||
var insetPx = Math.Clamp(Math.Max(0, cellSize) * (percent / 100d), 0, 80);
|
||||
GridEdgeInsetComputedPxTextBlock.Text = Lf(
|
||||
"settings.grid.edge_inset_px_format",
|
||||
"{0:F1}px",
|
||||
insetPx);
|
||||
}
|
||||
|
||||
private static string NormalizeGridSpacingPreset(string? value)
|
||||
{
|
||||
return string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase)
|
||||
? "Compact"
|
||||
: "Relaxed";
|
||||
}
|
||||
|
||||
private static string NormalizeStatusBarSpacingMode(string? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
_ when string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase) => "Compact",
|
||||
_ when string.Equals(value, "Custom", StringComparison.OrdinalIgnoreCase) => "Custom",
|
||||
_ => "Relaxed"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? TryGetSelectedComboBoxTag(ComboBox? comboBox)
|
||||
{
|
||||
if (comboBox?.SelectedItem is ComboBoxItem item)
|
||||
{
|
||||
return item.Tag?.ToString();
|
||||
}
|
||||
|
||||
return comboBox?.SelectedItem?.ToString();
|
||||
}
|
||||
|
||||
private static double ResolveGridGapRatio(string preset)
|
||||
{
|
||||
return string.Equals(preset, "Compact", StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12;
|
||||
}
|
||||
|
||||
private static double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
|
||||
{
|
||||
if (hostWidth <= 1 || hostHeight <= 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var cells = Math.Max(1, shortSideCells);
|
||||
var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight));
|
||||
var baseCell = shortSidePx / cells;
|
||||
|
||||
// --- 姣斾緥鍖栫暀鐧?(Proportional Inset) ---
|
||||
// 鍏佽鐢ㄦ埛鐧惧垎姣旇皟鑺傦紝浣嗚瀹氭洿鍚堢悊鐨勫熀鍑嗗拰闄愬埗
|
||||
var clampedPercent = Math.Clamp(insetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
var insetRatio = clampedPercent / 100d;
|
||||
|
||||
// 纭繚鏈€灏忕暀鐧借兘瀹圭撼涓€瀹氱殑闃村奖鎵╁睍
|
||||
// 鍏佽 0 杈硅窛锛屾渶澶т笂闄愮淮鎸?80px
|
||||
return Math.Clamp(baseCell * insetRatio, 0, 80);
|
||||
}
|
||||
|
||||
private static GridMetrics CalculateGridMetrics(
|
||||
double hostWidth,
|
||||
double hostHeight,
|
||||
int shortSideCells,
|
||||
double gapRatio,
|
||||
double edgeInsetPx)
|
||||
{
|
||||
if (hostWidth <= 1 || hostHeight <= 1)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var shortSideCells = Math.Max(1, targetShortSideCells);
|
||||
var shortSide = Math.Max(1, shortSideCells);
|
||||
var clampedGapRatio = Math.Max(0, gapRatio);
|
||||
var inset = Math.Max(0, edgeInsetPx);
|
||||
|
||||
// Edge inset should come only from user setting.
|
||||
// Remaining free space is handled by container centering, not baked into inset.
|
||||
var availableWidth = Math.Max(1, hostWidth - inset * 2);
|
||||
var availableHeight = Math.Max(1, hostHeight - inset * 2);
|
||||
|
||||
if (hostWidth >= hostHeight)
|
||||
{
|
||||
var rowCount = shortSideCells;
|
||||
var cellSize = hostHeight / rowCount;
|
||||
var columnCount = Math.Max(1, (int)Math.Floor(hostWidth / cellSize));
|
||||
return new GridMetrics(columnCount, rowCount, cellSize);
|
||||
var rowCount = shortSide;
|
||||
var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio;
|
||||
if (denominator <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var columns = shortSideCells;
|
||||
var size = hostWidth / columns;
|
||||
var rows = Math.Max(1, (int)Math.Floor(hostHeight / size));
|
||||
return new GridMetrics(columns, rows, size);
|
||||
var cellSize = availableHeight / denominator;
|
||||
var gapPx = cellSize * clampedGapRatio;
|
||||
var pitch = cellSize + gapPx;
|
||||
if (pitch <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch));
|
||||
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
|
||||
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
|
||||
|
||||
return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
var columnCount = shortSide;
|
||||
var denominator = columnCount + Math.Max(0, columnCount - 1) * clampedGapRatio;
|
||||
if (denominator <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var cellSize = availableWidth / denominator;
|
||||
var gapPx = cellSize * clampedGapRatio;
|
||||
var pitch = cellSize + gapPx;
|
||||
if (pitch <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var rowCount = Math.Max(1, (int)Math.Floor((availableHeight + gapPx) / pitch));
|
||||
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
|
||||
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
|
||||
|
||||
return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private static int ClampComponentSpan(int requestedSpan, int axisCellCount)
|
||||
@@ -537,57 +943,77 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplyWidgetSizing(double cellSize)
|
||||
{
|
||||
var margin = Math.Clamp(cellSize * 0.08, 1.5, 10);
|
||||
var verticalPadding = Math.Clamp(cellSize * 0.08, 2, 12);
|
||||
var horizontalPadding = Math.Clamp(cellSize * 0.20, 4, 22);
|
||||
var taskbarCell = Math.Clamp(cellSize, 28, 128);
|
||||
var unifiedFontSize = Math.Clamp(cellSize * 0.22, 8, 22);
|
||||
var unifiedIconSize = Math.Clamp(cellSize * 0.28, 10, 26);
|
||||
var taskbarCellHeight = Math.Clamp(cellSize * 0.76, 36, 76);
|
||||
var taskbarTextSize = Math.Clamp(taskbarCellHeight * 0.36, 12, 22);
|
||||
var taskbarIconSize = Math.Clamp(taskbarCellHeight * 0.46, 16, 34);
|
||||
var taskbarButtonInset = Math.Clamp(taskbarCellHeight * 0.22, 6, 16);
|
||||
var compactButtonInset = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
|
||||
var buttonContentSpacing = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
|
||||
var taskbarButtonPadding = new Thickness(taskbarButtonInset);
|
||||
|
||||
TopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1.5, 10));
|
||||
ClockWidget.Margin = new Thickness(margin);
|
||||
// Status bar and taskbar are special surfaces: they should fill their row.
|
||||
TopStatusBarHost.Margin = new Thickness(0);
|
||||
TopStatusBarHost.Padding = new Thickness(0);
|
||||
|
||||
BottomTaskbarContainer.Margin = new Thickness(0);
|
||||
BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(taskbarCellHeight * 0.58, 20, 44));
|
||||
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
|
||||
|
||||
ClockWidget.Margin = new Thickness(0);
|
||||
ClockWidget.ApplyCellSize(cellSize);
|
||||
|
||||
BottomTaskbarContainer.Margin = new Thickness(Math.Clamp(cellSize * 0.18, 6, 18));
|
||||
BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 10, 24));
|
||||
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 2, 10));
|
||||
var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340);
|
||||
|
||||
BackToWindowsButton.Margin = new Thickness(0);
|
||||
BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||
BackToWindowsButton.FontSize = unifiedFontSize;
|
||||
BackToWindowsButton.MinHeight = taskbarCell;
|
||||
BackToWindowsButton.MinWidth = Math.Clamp(cellSize * 2.3, 90, 320);
|
||||
BackToWindowsIcon.FontSize = unifiedIconSize;
|
||||
BackToWindowsButton.Padding = taskbarButtonPadding;
|
||||
BackToWindowsButton.FontSize = taskbarTextSize;
|
||||
BackToWindowsButton.MinHeight = taskbarCellHeight;
|
||||
BackToWindowsButton.MinWidth = buttonMinWidth;
|
||||
BackToWindowsIcon.FontSize = taskbarIconSize;
|
||||
BackToWindowsTextBlock.FontSize = taskbarTextSize;
|
||||
SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing);
|
||||
|
||||
OpenComponentLibraryButton.Margin = new Thickness(0);
|
||||
OpenComponentLibraryButton.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||
OpenComponentLibraryButton.FontSize = unifiedFontSize;
|
||||
OpenComponentLibraryButton.MinHeight = taskbarCell;
|
||||
OpenComponentLibraryButton.MinWidth = Math.Clamp(cellSize * 2.0, 88, 300);
|
||||
OpenComponentLibraryIcon.FontSize = unifiedIconSize;
|
||||
OpenComponentLibraryButton.Padding = taskbarButtonPadding;
|
||||
OpenComponentLibraryButton.FontSize = taskbarTextSize;
|
||||
OpenComponentLibraryButton.MinHeight = taskbarCellHeight;
|
||||
OpenComponentLibraryButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.15, 92, 320);
|
||||
OpenComponentLibraryIcon.FontSize = taskbarIconSize;
|
||||
OpenComponentLibraryTextBlock.FontSize = taskbarTextSize;
|
||||
SetButtonContentSpacing(OpenComponentLibraryButton, buttonContentSpacing);
|
||||
|
||||
OpenSettingsButton.Margin = new Thickness(0);
|
||||
OpenSettingsButton.Height = taskbarCell;
|
||||
OpenSettingsButton.MinHeight = taskbarCell;
|
||||
OpenSettingsIcon.FontSize = unifiedIconSize;
|
||||
OpenSettingsButton.Height = taskbarCellHeight;
|
||||
OpenSettingsButton.MinHeight = taskbarCellHeight;
|
||||
OpenSettingsButton.FontSize = taskbarTextSize;
|
||||
OpenSettingsButtonTextBlock.FontSize = taskbarTextSize;
|
||||
OpenSettingsIcon.FontSize = taskbarIconSize;
|
||||
SetButtonContentSpacing(OpenSettingsButton, Math.Clamp(taskbarCellHeight * 0.18, 4, 10));
|
||||
|
||||
if (_isSettingsOpen)
|
||||
{
|
||||
OpenSettingsButton.Width = double.NaN;
|
||||
OpenSettingsButton.MinWidth = Math.Clamp(cellSize * 2.3, 120, 340);
|
||||
OpenSettingsButton.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||
OpenSettingsButton.FontSize = unifiedFontSize;
|
||||
OpenSettingsButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.45, 120, 360);
|
||||
OpenSettingsButton.Padding = taskbarButtonPadding;
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenSettingsButton.Width = taskbarCell;
|
||||
OpenSettingsButton.MinWidth = taskbarCell;
|
||||
OpenSettingsButton.Padding = new Thickness(Math.Clamp(taskbarCell * 0.2, 4, 12));
|
||||
OpenSettingsButton.Width = taskbarCellHeight;
|
||||
OpenSettingsButton.MinWidth = taskbarCellHeight;
|
||||
OpenSettingsButton.Padding = new Thickness(compactButtonInset);
|
||||
}
|
||||
|
||||
UpdateComponentLibraryLayout(cellSize);
|
||||
}
|
||||
|
||||
private static void SetButtonContentSpacing(Button? button, double spacing)
|
||||
{
|
||||
if (button?.Content is StackPanel contentPanel)
|
||||
{
|
||||
contentPanel.Spacing = spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateComponentLibraryLayout(double cellSize)
|
||||
{
|
||||
if (ComponentLibraryWindow is null)
|
||||
@@ -597,8 +1023,14 @@ public partial class MainWindow : Window
|
||||
|
||||
var horizontalMargin = Math.Clamp(cellSize * 0.7, 18, 44);
|
||||
var bottomMargin = Math.Clamp(cellSize * 1.4, 56, 190);
|
||||
ComponentLibraryWindow.Margin = new Thickness(horizontalMargin, 20, horizontalMargin, bottomMargin);
|
||||
ComponentLibraryWindow.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 12, 24));
|
||||
var defaultMargin = new Thickness(horizontalMargin, 20, horizontalMargin, bottomMargin);
|
||||
if (!_isComponentLibraryWindowPositionCustomized)
|
||||
{
|
||||
_savedComponentLibraryMargin = defaultMargin;
|
||||
}
|
||||
|
||||
ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
|
||||
ComponentLibraryWindow.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 24, 44));
|
||||
ComponentLibraryWindow.Height = Math.Clamp(cellSize * 4.8, 220, 360);
|
||||
ComponentLibraryWindow.Width = Math.Clamp(cellSize * 9.2, 360, 760);
|
||||
}
|
||||
@@ -613,10 +1045,26 @@ public partial class MainWindow : Window
|
||||
var clampedCell = Math.Max(1, cellSize);
|
||||
var horizontalInset = Math.Clamp(clampedCell * 0.45, 12, 64);
|
||||
var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18);
|
||||
var topInset = clampedCell + verticalGap;
|
||||
var bottomInset = clampedCell + verticalGap;
|
||||
var edgeInset = Math.Max(0, _currentDesktopEdgeInset);
|
||||
|
||||
// 添加额外的安全边距以确保圆角不被裁剪
|
||||
var taskbarCellHeight = Math.Clamp(clampedCell * 0.76, 36, 76);
|
||||
var taskbarPadding = Math.Clamp(taskbarCellHeight * 0.16, 6, 14);
|
||||
var taskbarVisualHeight = Math.Max(clampedCell, taskbarCellHeight + taskbarPadding * 2);
|
||||
if (BottomTaskbarContainer is not null && BottomTaskbarContainer.Bounds.Height > 1)
|
||||
{
|
||||
taskbarVisualHeight = Math.Max(taskbarVisualHeight, BottomTaskbarContainer.Bounds.Height);
|
||||
}
|
||||
|
||||
var statusBarVisualHeight = clampedCell;
|
||||
if (TopStatusBarHost is not null && TopStatusBarHost.Bounds.Height > 1)
|
||||
{
|
||||
statusBarVisualHeight = Math.Max(statusBarVisualHeight, TopStatusBarHost.Bounds.Height);
|
||||
}
|
||||
|
||||
var topInset = Math.Max(clampedCell + verticalGap, edgeInset + statusBarVisualHeight + verticalGap);
|
||||
var bottomInset = Math.Max(clampedCell + verticalGap, edgeInset + taskbarVisualHeight + verticalGap);
|
||||
|
||||
// Add extra safety margin so rounded panel corners never clip against viewport edges.
|
||||
var cornerSafetyMargin = Math.Clamp(clampedCell * 0.12, 4, 12);
|
||||
var inset = new Thickness(
|
||||
horizontalInset + cornerSafetyMargin,
|
||||
@@ -624,8 +1072,7 @@ public partial class MainWindow : Window
|
||||
horizontalInset + cornerSafetyMargin,
|
||||
bottomInset + cornerSafetyMargin);
|
||||
|
||||
// 使用 Margin 来定位,而不是直接设置 Width/Height
|
||||
// 这样可以让面板自然填充可用空间,同时保持边距
|
||||
// Keep panel stretched with explicit viewport insets so it never overlaps fixed chrome.
|
||||
SettingsContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
SettingsContentPanel.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
SettingsContentPanel.Margin = inset;
|
||||
@@ -656,29 +1103,46 @@ public partial class MainWindow : Window
|
||||
var aspectRatio = desktopWidth / desktopHeight;
|
||||
|
||||
var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width);
|
||||
var availableHeight = WallpaperPreviewHost.Bounds.Height;
|
||||
// During initial measure, host height can be too small and cause the preview to collapse.
|
||||
// Ignore tiny heights so width-driven sizing can stabilize first.
|
||||
if (availableHeight < 120)
|
||||
{
|
||||
availableHeight = double.PositiveInfinity;
|
||||
}
|
||||
|
||||
var framePadding = WallpaperPreviewFrame.Padding;
|
||||
var horizontalPadding = framePadding.Left + framePadding.Right;
|
||||
var verticalPadding = framePadding.Top + framePadding.Bottom;
|
||||
|
||||
var previewWidth = availableWidth;
|
||||
var previewWidth = Math.Min(availableWidth, WallpaperPreviewMaxWidth);
|
||||
var previewHeight = previewWidth / aspectRatio;
|
||||
if (double.IsFinite(availableHeight) && previewHeight > availableHeight)
|
||||
{
|
||||
previewHeight = availableHeight;
|
||||
previewWidth = previewHeight * aspectRatio;
|
||||
}
|
||||
|
||||
WallpaperPreviewFrame.Width = previewWidth;
|
||||
WallpaperPreviewFrame.Height = previewHeight;
|
||||
|
||||
WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
|
||||
|
||||
|
||||
var innerWidth = Math.Max(1, previewWidth - horizontalPadding);
|
||||
var innerHeight = Math.Max(1, previewHeight - verticalPadding);
|
||||
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells);
|
||||
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WallpaperPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
|
||||
WallpaperPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
|
||||
WallpaperPreviewGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
|
||||
WallpaperPreviewGrid.RowSpacing = gridMetrics.GapPx;
|
||||
WallpaperPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
|
||||
WallpaperPreviewGrid.Width = gridMetrics.GridWidthPx;
|
||||
WallpaperPreviewGrid.Height = gridMetrics.GridHeightPx;
|
||||
|
||||
// This can be triggered by layout changes; always rebuild the preview grid definitions
|
||||
// to avoid definitions accumulating and shifting overlay components out of place.
|
||||
@@ -712,6 +1176,7 @@ public partial class MainWindow : Window
|
||||
ApplyTopStatusComponentVisibility();
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
ApplyPreviewWidgetSizing(gridMetrics.CellSize);
|
||||
ApplyStatusBarComponentSpacingForPanel(WallpaperPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -721,22 +1186,33 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplyPreviewWidgetSizing(double cellSize)
|
||||
{
|
||||
var margin = Math.Clamp(cellSize * 0.08, 1, 6);
|
||||
var previewTaskbarCell = Math.Clamp(cellSize, 10, 36);
|
||||
WallpaperPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4));
|
||||
WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(margin);
|
||||
WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10));
|
||||
WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4));
|
||||
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 28);
|
||||
var previewTextSize = Math.Clamp(previewTaskbarCell * 0.38, 7, 14);
|
||||
var previewIconSize = Math.Clamp(previewTaskbarCell * 0.46, 8, 16);
|
||||
var previewInset = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
|
||||
var previewContentSpacing = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
|
||||
|
||||
// Match desktop behavior: special bars fill their preview row.
|
||||
WallpaperPreviewTopStatusBarHost.Margin = new Thickness(0);
|
||||
WallpaperPreviewTopStatusBarHost.Padding = new Thickness(0);
|
||||
|
||||
WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(0);
|
||||
WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 6, 14));
|
||||
WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(previewInset);
|
||||
|
||||
WallpaperPreviewClockWidget.ApplyCellSize(cellSize);
|
||||
WallpaperPreviewBackButtonTextBlock.FontSize = previewTextSize;
|
||||
WallpaperPreviewComponentLibraryTextBlock.FontSize = previewTextSize;
|
||||
WallpaperPreviewBackButtonVisual.Spacing = previewContentSpacing;
|
||||
WallpaperPreviewComponentLibraryVisual.Spacing = previewContentSpacing;
|
||||
|
||||
WallpaperPreviewClockTextBlock.FontSize = Math.Clamp(cellSize * 0.30, 6, 18);
|
||||
WallpaperPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13);
|
||||
WallpaperPreviewComponentLibraryTextBlock.FontSize = Math.Clamp(cellSize * 0.18, 5, 12);
|
||||
WallpaperPreviewBackButtonVisual.MinHeight = previewTaskbarCell;
|
||||
WallpaperPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120);
|
||||
WallpaperPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell;
|
||||
WallpaperPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110);
|
||||
WallpaperPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
|
||||
WallpaperPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
|
||||
|
||||
WallpaperPreviewSettingsButtonIcon.Width = previewIconSize;
|
||||
WallpaperPreviewSettingsButtonIcon.Height = previewIconSize;
|
||||
}
|
||||
|
||||
private void OnMinimizeClick(object? sender, RoutedEventArgs e)
|
||||
@@ -767,7 +1243,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void InitializeTimeZoneSettings()
|
||||
{
|
||||
// 填充时区下拉框
|
||||
// Populate timezone dropdown items before selecting current timezone.
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
var timeZones = _timeZoneService.GetAllTimeZones();
|
||||
foreach (var tz in timeZones)
|
||||
@@ -780,7 +1256,7 @@ public partial class MainWindow : Window
|
||||
};
|
||||
TimeZoneComboBox.Items.Add(item);
|
||||
|
||||
// 选中当前时区
|
||||
// 閫変腑褰撳墠鏃跺尯
|
||||
if (tz.Id == _timeZoneService.CurrentTimeZone.Id)
|
||||
{
|
||||
TimeZoneComboBox.SelectedItem = item;
|
||||
@@ -805,3 +1281,4 @@ public partial class MainWindow : Window
|
||||
PersistSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
177
docs/CORNER_RADIUS_SPEC.md
Normal file
177
docs/CORNER_RADIUS_SPEC.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 圆角设计规范 (Corner Radius Design System)
|
||||
|
||||
> 基于小米澎湃OS 3 (HyperOS) 设计语言
|
||||
|
||||
## 设计理念
|
||||
|
||||
澎湃OS 3 采用**"生命感美学"**设计语言,强调:
|
||||
- **全局圆角设计** - 所有界面元素均采用圆角
|
||||
- **视觉舒适统一** - 柔和、现代、细腻
|
||||
- **多级渲染** - 配合模糊混色与阴影
|
||||
- **层级分明** - 大容器使用大圆角,小元素使用小圆角
|
||||
|
||||
## 圆角数值体系
|
||||
|
||||
### 核心数值
|
||||
|
||||
| 级别 | 圆角值 (px) | 用途 |
|
||||
|------|-------------|------|
|
||||
| **Level 0** | 0 | 特殊场景(无圆角需求) |
|
||||
| **Level 1** | 12 | 小元素、图标内边角、ListBoxItem |
|
||||
| **Level 2** | 16 | 色块按钮、小组件 |
|
||||
| **Level 3** | 20 | 普通按钮、组件预览 |
|
||||
| **Level 4** | 24 | 输入框、小型面板 |
|
||||
| **Level 5** | 28 | 面板/卡片 (glass-panel) |
|
||||
| **Level 6** | 32 | Mica 风格面板 (mica-strong) |
|
||||
| **Level 7** | 36 | 大容器 (glass-strong)、任务栏、窗口 |
|
||||
|
||||
### 动态圆角
|
||||
|
||||
动态圆角根据格子大小(cellSize)动态计算:
|
||||
|
||||
```csharp
|
||||
// 小元素
|
||||
CornerRadius = Math.Clamp(cellSize * 0.35, 16, 28);
|
||||
|
||||
// 小组件
|
||||
CornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
|
||||
|
||||
// 大容器(任务栏/窗口)
|
||||
CornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
|
||||
```
|
||||
|
||||
**系数参考**:
|
||||
- 系数范围:`0.35 - 0.45`
|
||||
- 最小值限制:`12 - 24 px`
|
||||
- 最大值限制:`28 - 44 px`
|
||||
|
||||
## 组件圆角速查表
|
||||
|
||||
### 基础控件
|
||||
|
||||
| 控件 | 圆角值 | 代码位置 |
|
||||
|------|--------|---------|
|
||||
| Button | 20px | GlassModule.axaml |
|
||||
| ToggleSwitch | 继承系统 | - |
|
||||
| TextBox | 20px | glass-panel |
|
||||
| ComboBox | 20px | glass-panel |
|
||||
| NumberBox | 20px | glass-panel |
|
||||
|
||||
### 容器样式类
|
||||
|
||||
| 样式类 | 圆角值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `.glass-panel` | 28px | 普通玻璃面板 |
|
||||
| `.glass-strong` | 36px | 加强玻璃面板(任务栏) |
|
||||
| `.mica-strong` | 36px | Mica 风格面板(设置页) |
|
||||
| `.glass-overlay` | 0px | 覆盖层(无圆角) |
|
||||
|
||||
### 特殊场景
|
||||
|
||||
| 场景 | 圆角值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 窗口整体 | 36px | 组件库/设置窗口 |
|
||||
| 窗口标题栏 | 36px | 仅顶部圆角 (`36,36,0,0`) |
|
||||
| 颜色选择器色块 | 12px | Monet 颜色/推荐色 |
|
||||
| 设置页 ListBoxItem | 12px | 导航项 |
|
||||
| 预览视口 | 12-16px | 壁纸/网格预览 |
|
||||
|
||||
## 圆角层级视觉示例
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Level 7: 大容器 (36px) │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Level 6: Mica 面板 (36px) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Level 5: 玻璃面板 (28px) │ │ │
|
||||
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ │ Level 4: 输入面板 (24px) │ │ │ │
|
||||
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ Level 3: 按钮 (20px) │ │ │ │ │
|
||||
│ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │
|
||||
│ │ │ │ │ │ Level 2: 色块 (16px) │ │ │ │ │ │
|
||||
│ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ Level 1: 小元素 │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ (12px) │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │
|
||||
│ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │
|
||||
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
|
||||
│ │ │ └─────────────────────────────────────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 在 XAML 中使用
|
||||
|
||||
### 直接使用固定值
|
||||
|
||||
```xml
|
||||
<Border Classes="glass-strong" CornerRadius="36">
|
||||
<!-- 内容 -->
|
||||
</Border>
|
||||
|
||||
<Button CornerRadius="20">点击</Button>
|
||||
```
|
||||
|
||||
### 使用样式类
|
||||
|
||||
```xml
|
||||
<!-- 使用预定义的 glass-panel 样式 -->
|
||||
<Border Classes="glass-panel">
|
||||
<TextBlock Text="面板内容" />
|
||||
</Border>
|
||||
|
||||
<!-- 组合多个样式类 -->
|
||||
<Border Classes="glass-strong mica-strong">
|
||||
<!-- 内容 -->
|
||||
</Border>
|
||||
```
|
||||
|
||||
### 动态圆角(Code-Behind)
|
||||
|
||||
```csharp
|
||||
// 根据格子大小动态计算圆角
|
||||
var cellSize = 100; // 假设格子大小
|
||||
var cornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
|
||||
|
||||
BottomTaskbarContainer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
```
|
||||
|
||||
## 新增控件时的圆角规范
|
||||
|
||||
1. **确定元素层级** - 根据容器大小选择合适的级别
|
||||
2. **遵循视觉一致性** - 同层级的元素使用相同圆角
|
||||
3. **考虑内容安全区** - 圆角不应遮挡重要内容
|
||||
4. **响应式适配** - 大屏幕使用较大圆角,小屏幕使用较小圆角
|
||||
|
||||
### 快速参考
|
||||
|
||||
```
|
||||
新控件圆角选择流程:
|
||||
|
||||
1. 是窗口/大容器? → Level 7 (36px)
|
||||
2. 是面板/卡片? → Level 5-6 (28-36px)
|
||||
3. 是按钮/输入框? → Level 3-4 (20-24px)
|
||||
4. 是小组件/色块? → Level 2 (16px)
|
||||
5. 是图标/小元素? → Level 1 (12px)
|
||||
```
|
||||
|
||||
## 附录:修改历史
|
||||
|
||||
| 日期 | 修改人 | 说明 |
|
||||
|------|--------|------|
|
||||
| 2026-03-02 | AI Assistant | 初始规范,基于澎湃OS 3 设计语言 |
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 澎湃OS 生命感美学设计
|
||||
- Xiaomi HyperOS Design Guidelines
|
||||
- 小米小部件审核规范 (dev.mi.com)
|
||||
Reference in New Issue
Block a user