diff --git a/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs
index f714f8b..4ba2ade 100644
--- a/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs
+++ b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs
@@ -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";
}
diff --git a/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs
index 1f48d84..b7cad36 100644
--- a/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs
+++ b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs
@@ -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)
};
diff --git a/LanMontainDesktop/Localization/en-US.json b/LanMontainDesktop/Localization/en-US.json
index c60da5a..47cf348 100644
--- a/LanMontainDesktop/Localization/en-US.json
+++ b/LanMontainDesktop/Localization/en-US.json
@@ -1,100 +1,118 @@
-{
- "app.title": "LanMontainDesktop",
- "button.back_to_windows": "Back to Windows",
- "tooltip.back_to_windows": "Back to Windows",
- "tooltip.open_settings": "Settings",
- "settings.title": "Settings",
- "settings.back_to_desktop": "Back to Desktop",
- "settings.nav_header": "Settings",
- "settings.nav.wallpaper": "Wallpaper",
- "settings.nav.grid": "Grid",
- "settings.nav.color": "Color",
- "settings.nav.status_bar": "Status Bar",
- "settings.nav.region": "Region",
- "settings.wallpaper.title": "Wallpaper",
- "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.pick_button": "Browse Files",
- "settings.wallpaper.clear_button": "Reset to Solid Color",
- "settings.wallpaper.no_selection": "No wallpaper selected.",
- "settings.wallpaper.storage_unavailable": "Storage provider is unavailable.",
- "settings.wallpaper.import_failed": "Failed to import wallpaper file.",
- "settings.wallpaper.image_applied": "Image wallpaper applied.",
- "settings.wallpaper.video_applied": "Video wallpaper applied.",
- "settings.wallpaper.unsupported_file": "Selected file type is not supported.",
- "settings.wallpaper.apply_failed_format": "Failed to apply wallpaper: {0}",
- "settings.wallpaper.mode_format": "Wallpaper mode: {0}.",
- "settings.wallpaper.video_mode": "Video wallpaper uses automatic fill mode.",
- "settings.wallpaper.cleared": "Background reset to solid color.",
- "settings.wallpaper.default_status": "Current background uses solid color.",
- "settings.wallpaper.saved_not_found": "Saved wallpaper file was not found. Using solid color background.",
- "settings.wallpaper.restored": "Wallpaper restored from saved settings.",
- "settings.wallpaper.video_restored": "Video wallpaper restored from saved settings.",
- "settings.wallpaper.restore_failed": "Failed to restore saved wallpaper. Using solid color background.",
- "settings.wallpaper.video_not_found": "Video wallpaper file not found.",
- "settings.wallpaper.video_player_unavailable": "Video player is unavailable.",
- "settings.wallpaper.video_play_failed_format": "Failed to play video wallpaper: {0}",
- "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.apply_button": "Apply",
- "settings.grid.info_format": "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
- "settings.color.title": "Color",
- "settings.color.description": "Switch day/night mode and choose app accent colors.",
- "settings.color.day_night_label": "Day/Night Mode",
- "settings.color.day_night_on": "Night",
- "settings.color.day_night_off": "Day",
- "settings.color.recommended_label": "Recommended Colors",
- "settings.color.system_monet_label": "System Monet Colors",
- "settings.color.refresh_button": "Refresh",
- "settings.color.mode_night": "Night mode enabled",
- "settings.color.mode_day": "Day mode enabled",
- "settings.color.mode_status_format": "Theme mode: {0}.",
- "settings.color.monet_refreshed": "Monet colors refreshed.",
- "settings.color.theme_ready_format": "Theme color ready: {0}.",
- "settings.color.theme_applied_format": "{0} color applied: {1}.",
- "settings.color.theme_updated_wallpaper": "Wallpaper updated. Monet colors refreshed.",
- "settings.color.theme_updated_video": "Video wallpaper updated. Theme colors refreshed.",
- "settings.color.theme_cleared_wallpaper": "Wallpaper cleared. Monet colors refreshed.",
- "settings.status_bar.title": "Status Bar",
- "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.region.title": "Region",
- "settings.region.description": "Choose language and apply immediately to settings and key UI.",
- "settings.region.language_header": "Language",
- "settings.region.language_label": "Language",
- "settings.region.language_zh": "Chinese",
- "settings.region.language_en": "English",
- "settings.region.applied_format": "Language switched to: {0}",
- "settings.footer": "LanMontainDesktop Settings",
- "filepicker.title": "Select wallpaper",
- "filepicker.image_files": "Image files",
- "filepicker.video_files": "Video files",
- "common.day": "Day",
- "common.night": "Night",
- "common.back": "Back",
- "common.close": "Close",
- "common.recommended": "Recommended",
- "common.monet": "Monet",
- "desktop.page_index_format": "Desktop {0}",
- "launcher.title": "App Launcher",
- "launcher.subtitle": "Apps and folders from Windows Start Menu",
- "launcher.empty": "No Start Menu entries found.",
- "launcher.empty_folder": "This folder is empty.",
- "launcher.folder_items_format": "{0} apps",
- "button.component_library": "Edit Desktop",
- "tooltip.component_library": "Edit Desktop",
- "component_library.title": "Edit Desktop",
- "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",
- "desktop.add_page": "Add page",
- "placement.fill": "Fill",
- "placement.fit": "Fit",
- "placement.stretch": "Stretch",
- "placement.center": "Center",
- "placement.tile": "Tile"
+{
+ "app.title": "LanMontainDesktop",
+ "button.back_to_windows": "Back to Windows",
+ "tooltip.back_to_windows": "Back to Windows",
+ "tooltip.open_settings": "Settings",
+ "settings.title": "Settings",
+ "settings.back_to_desktop": "Back to Desktop",
+ "settings.nav_header": "Settings",
+ "settings.nav.wallpaper": "Wallpaper",
+ "settings.nav.grid": "Grid",
+ "settings.nav.color": "Color",
+ "settings.nav.status_bar": "Status Bar",
+ "settings.nav.region": "Region",
+ "settings.wallpaper.title": "Wallpaper",
+ "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.",
+ "settings.wallpaper.storage_unavailable": "Storage provider is unavailable.",
+ "settings.wallpaper.import_failed": "Failed to import wallpaper file.",
+ "settings.wallpaper.image_applied": "Image wallpaper applied.",
+ "settings.wallpaper.video_applied": "Video wallpaper applied.",
+ "settings.wallpaper.unsupported_file": "Selected file type is not supported.",
+ "settings.wallpaper.apply_failed_format": "Failed to apply wallpaper: {0}",
+ "settings.wallpaper.mode_format": "Wallpaper mode: {0}.",
+ "settings.wallpaper.video_mode": "Video wallpaper uses automatic fill mode.",
+ "settings.wallpaper.cleared": "Background reset to solid color.",
+ "settings.wallpaper.default_status": "Current background uses solid color.",
+ "settings.wallpaper.saved_not_found": "Saved wallpaper file was not found. Using solid color background.",
+ "settings.wallpaper.restored": "Wallpaper restored from saved settings.",
+ "settings.wallpaper.video_restored": "Video wallpaper restored from saved settings.",
+ "settings.wallpaper.restore_failed": "Failed to restore saved wallpaper. Using solid color background.",
+ "settings.wallpaper.video_not_found": "Video wallpaper file not found.",
+ "settings.wallpaper.video_player_unavailable": "Video player is unavailable.",
+ "settings.wallpaper.video_play_failed_format": "Failed to play video wallpaper: {0}",
+ "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",
+ "settings.color.description": "Switch day/night mode and choose app accent colors.",
+ "settings.color.day_night_label": "Day/Night Mode",
+ "settings.color.day_night_on": "Night",
+ "settings.color.day_night_off": "Day",
+ "settings.color.recommended_label": "Recommended Colors",
+ "settings.color.system_monet_label": "System Monet Colors",
+ "settings.color.refresh_button": "Refresh",
+ "settings.color.mode_night": "Night mode enabled",
+ "settings.color.mode_day": "Day mode enabled",
+ "settings.color.mode_status_format": "Theme mode: {0}.",
+ "settings.color.monet_refreshed": "Monet colors refreshed.",
+ "settings.color.theme_ready_format": "Theme color ready: {0}.",
+ "settings.color.theme_applied_format": "{0} color applied: {1}.",
+ "settings.color.theme_updated_wallpaper": "Wallpaper updated. Monet colors refreshed.",
+ "settings.color.theme_updated_video": "Video wallpaper updated. Theme colors refreshed.",
+ "settings.color.theme_cleared_wallpaper": "Wallpaper cleared. Monet colors refreshed.",
+ "settings.status_bar.title": "Status Bar",
+ "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",
+ "settings.region.language_label": "Language",
+ "settings.region.language_zh": "Chinese",
+ "settings.region.language_en": "English",
+ "settings.region.applied_format": "Language switched to: {0}",
+ "settings.footer": "LanMontainDesktop Settings",
+ "filepicker.title": "Select wallpaper",
+ "filepicker.image_files": "Image files",
+ "filepicker.video_files": "Video files",
+ "common.day": "Day",
+ "common.night": "Night",
+ "common.back": "Back",
+ "common.close": "Close",
+ "common.recommended": "Recommended",
+ "common.monet": "Monet",
+ "desktop.page_index_format": "Desktop {0}",
+ "launcher.title": "App Launcher",
+ "launcher.subtitle": "Apps and folders from Windows Start Menu",
+ "launcher.empty": "No Start Menu entries found.",
+ "launcher.empty_folder": "This folder is empty.",
+ "launcher.folder_items_format": "{0} apps",
+ "button.component_library": "Edit Desktop",
+ "tooltip.component_library": "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.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",
+ "placement.center": "Center",
+ "placement.tile": "Tile"
}
diff --git a/LanMontainDesktop/Localization/zh-CN.json b/LanMontainDesktop/Localization/zh-CN.json
index 9a75b9b..579335f 100644
--- a/LanMontainDesktop/Localization/zh-CN.json
+++ b/LanMontainDesktop/Localization/zh-CN.json
@@ -1,100 +1,118 @@
-{
- "app.title": "LanMontainDesktop",
- "button.back_to_windows": "回到Windows",
- "tooltip.back_to_windows": "回到Windows",
- "tooltip.open_settings": "设置",
- "settings.title": "设置",
- "settings.back_to_desktop": "返回桌面",
- "settings.nav_header": "设置选项",
- "settings.nav.wallpaper": "壁纸",
- "settings.nav.grid": "网格",
- "settings.nav.color": "颜色",
- "settings.nav.status_bar": "状态栏",
- "settings.nav.region": "地区",
- "settings.wallpaper.title": "壁纸",
- "settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
- "settings.wallpaper.current_label": "当前壁纸",
- "settings.wallpaper.placement_label": "显示方式",
- "settings.wallpaper.pick_button": "浏览文件",
- "settings.wallpaper.clear_button": "恢复纯色",
- "settings.wallpaper.no_selection": "未选择壁纸。",
- "settings.wallpaper.storage_unavailable": "存储提供器不可用。",
- "settings.wallpaper.import_failed": "导入壁纸文件失败。",
- "settings.wallpaper.image_applied": "图片壁纸已应用。",
- "settings.wallpaper.video_applied": "视频壁纸已应用。",
- "settings.wallpaper.unsupported_file": "所选文件类型不受支持。",
- "settings.wallpaper.apply_failed_format": "应用壁纸失败:{0}",
- "settings.wallpaper.mode_format": "壁纸模式:{0}。",
- "settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
- "settings.wallpaper.cleared": "背景已恢复为纯色。",
- "settings.wallpaper.default_status": "当前使用纯色背景。",
- "settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,已使用纯色背景。",
- "settings.wallpaper.restored": "已恢复保存的壁纸。",
- "settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
- "settings.wallpaper.restore_failed": "恢复已保存壁纸失败,已使用纯色背景。",
- "settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
- "settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
- "settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}",
- "settings.grid.title": "网格布局",
- "settings.grid.description": "每个组件至少占用一个格子(最小 1x1)。",
- "settings.grid.short_side_label": "短边格数",
- "settings.grid.apply_button": "应用",
- "settings.grid.info_format": "网格:{0} 列 x {1} 行 | 单元格 {2:F1}px(1:1)",
- "settings.color.title": "颜色",
- "settings.color.description": "切换日夜模式并选择应用主题色。",
- "settings.color.day_night_label": "日夜模式",
- "settings.color.day_night_on": "夜间",
- "settings.color.day_night_off": "日间",
- "settings.color.recommended_label": "推荐色",
- "settings.color.system_monet_label": "系统莫奈色",
- "settings.color.refresh_button": "刷新",
- "settings.color.mode_night": "夜间模式已启用",
- "settings.color.mode_day": "日间模式已启用",
- "settings.color.mode_status_format": "主题模式:{0}。",
- "settings.color.monet_refreshed": "莫奈色已刷新。",
- "settings.color.theme_ready_format": "主题色已就绪:{0}。",
- "settings.color.theme_applied_format": "{0}主题色已应用:{1}。",
- "settings.color.theme_updated_wallpaper": "壁纸已更新,莫奈色已刷新。",
- "settings.color.theme_updated_video": "视频壁纸已更新,主题色已刷新。",
- "settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。",
- "settings.status_bar.title": "状态栏",
- "settings.status_bar.description": "选择顶部状态栏显示的组件。",
- "settings.status_bar.clock_header": "时间组件",
- "settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
- "settings.region.title": "地区",
- "settings.region.description": "选择语言并立即应用到设置与主要界面。",
- "settings.region.language_header": "语言",
- "settings.region.language_label": "语言",
- "settings.region.language_zh": "中文",
- "settings.region.language_en": "英文",
- "settings.region.applied_format": "语言已切换为:{0}",
- "settings.footer": "LanMontainDesktop 设置",
- "filepicker.title": "选择壁纸",
- "filepicker.image_files": "图片文件",
- "filepicker.video_files": "视频文件",
- "common.day": "日间",
- "common.night": "夜间",
- "common.back": "返回",
- "common.close": "关闭",
- "common.recommended": "推荐",
- "common.monet": "莫奈",
- "desktop.page_index_format": "桌面 {0}",
- "launcher.title": "应用启动台",
- "launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
- "launcher.empty": "未找到开始菜单条目。",
- "launcher.empty_folder": "此文件夹为空。",
- "launcher.folder_items_format": "{0} 个应用",
- "button.component_library": "桌面编辑",
- "tooltip.component_library": "桌面编辑",
- "component_library.title": "桌面编辑",
- "component_library.empty": "左右滑动选择类别,点击进入,然后拖动组件到桌面放置。",
- "component_library.drag_hint": "拖动放置",
- "component_category.date": "日期",
- "component.date": "日历",
- "desktop.add_page": "新增页面",
- "placement.fill": "填充",
- "placement.fit": "适应",
- "placement.stretch": "拉伸",
- "placement.center": "居中",
- "placement.tile": "平铺"
+{
+ "app.title": "LanMontainDesktop",
+ "button.back_to_windows": "回到Windows",
+ "tooltip.back_to_windows": "回到Windows",
+ "tooltip.open_settings": "设置",
+ "settings.title": "设置",
+ "settings.back_to_desktop": "返回桌面",
+ "settings.nav_header": "设置选项",
+ "settings.nav.wallpaper": "壁纸",
+ "settings.nav.grid": "网格",
+ "settings.nav.color": "颜色",
+ "settings.nav.status_bar": "状态栏",
+ "settings.nav.region": "地区",
+ "settings.wallpaper.title": "壁纸",
+ "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": "未选择壁纸。",
+ "settings.wallpaper.storage_unavailable": "存储提供器不可用。",
+ "settings.wallpaper.import_failed": "导入壁纸文件失败。",
+ "settings.wallpaper.image_applied": "图片壁纸已应用。",
+ "settings.wallpaper.video_applied": "视频壁纸已应用。",
+ "settings.wallpaper.unsupported_file": "所选文件类型不受支持。",
+ "settings.wallpaper.apply_failed_format": "应用壁纸失败:{0}",
+ "settings.wallpaper.mode_format": "壁纸模式:{0}。",
+ "settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
+ "settings.wallpaper.cleared": "背景已恢复为纯色。",
+ "settings.wallpaper.default_status": "当前使用纯色背景。",
+ "settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,已使用纯色背景。",
+ "settings.wallpaper.restored": "已恢复保存的壁纸。",
+ "settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
+ "settings.wallpaper.restore_failed": "恢复已保存壁纸失败,已使用纯色背景。",
+ "settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
+ "settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
+ "settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}",
+ "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": "颜色",
+ "settings.color.description": "切换日夜模式并选择应用主题色。",
+ "settings.color.day_night_label": "日夜模式",
+ "settings.color.day_night_on": "夜间",
+ "settings.color.day_night_off": "日间",
+ "settings.color.recommended_label": "推荐色",
+ "settings.color.system_monet_label": "系统莫奈色",
+ "settings.color.refresh_button": "刷新",
+ "settings.color.mode_night": "夜间模式已启用",
+ "settings.color.mode_day": "日间模式已启用",
+ "settings.color.mode_status_format": "主题模式:{0}。",
+ "settings.color.monet_refreshed": "莫奈色已刷新。",
+ "settings.color.theme_ready_format": "主题色已就绪:{0}。",
+ "settings.color.theme_applied_format": "{0}主题色已应用:{1}。",
+ "settings.color.theme_updated_wallpaper": "壁纸已更新,莫奈色已刷新。",
+ "settings.color.theme_updated_video": "视频壁纸已更新,主题色已刷新。",
+ "settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。",
+ "settings.status_bar.title": "状态栏",
+ "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": "语言",
+ "settings.region.language_label": "语言",
+ "settings.region.language_zh": "中文",
+ "settings.region.language_en": "英文",
+ "settings.region.applied_format": "语言已切换为:{0}",
+ "settings.footer": "LanMontainDesktop 设置",
+ "filepicker.title": "选择壁纸",
+ "filepicker.image_files": "图片文件",
+ "filepicker.video_files": "视频文件",
+ "common.day": "日间",
+ "common.night": "夜间",
+ "common.back": "返回",
+ "common.close": "关闭",
+ "common.recommended": "推荐",
+ "common.monet": "莫奈",
+ "desktop.page_index_format": "桌面 {0}",
+ "launcher.title": "应用启动台",
+ "launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
+ "launcher.empty": "未找到开始菜单条目。",
+ "launcher.empty_folder": "此文件夹为空。",
+ "launcher.folder_items_format": "{0} 个应用",
+ "button.component_library": "桌面编辑",
+ "tooltip.component_library": "桌面编辑",
+ "component_library.title": "桌面编辑",
+ "component_library.empty": "左右滑动选择类别,点击进入,然后拖动组件到桌面放置。",
+ "component_library.drag_hint": "拖动放置",
+ "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": "拉伸",
+ "placement.center": "居中",
+ "placement.tile": "平铺"
}
diff --git a/LanMontainDesktop/Models/AppSettingsSnapshot.cs b/LanMontainDesktop/Models/AppSettingsSnapshot.cs
index e621957..12f605b 100644
--- a/LanMontainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMontainDesktop/Models/AppSettingsSnapshot.cs
@@ -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;
diff --git a/LanMontainDesktop/Models/TaskbarActionId.cs b/LanMontainDesktop/Models/TaskbarActionId.cs
index 7a89acf..a81c430 100644
--- a/LanMontainDesktop/Models/TaskbarActionId.cs
+++ b/LanMontainDesktop/Models/TaskbarActionId.cs
@@ -4,5 +4,8 @@ public enum TaskbarActionId
{
MinimizeToWindows,
OpenSettings,
- AddDesktopPage
+ AddDesktopPage,
+ DeleteDesktopPage,
+ DeleteComponent,
+ EditComponent
}
diff --git a/LanMontainDesktop/Services/LunarCalendarService.cs b/LanMontainDesktop/Services/LunarCalendarService.cs
new file mode 100644
index 0000000..74e59c8
--- /dev/null
+++ b/LanMontainDesktop/Services/LunarCalendarService.cs
@@ -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);
+
diff --git a/LanMontainDesktop/Styles/GlassModule.axaml b/LanMontainDesktop/Styles/GlassModule.axaml
index f7bfb27..ad3e4c3 100644
--- a/LanMontainDesktop/Styles/GlassModule.axaml
+++ b/LanMontainDesktop/Styles/GlassModule.axaml
@@ -10,7 +10,7 @@
-
+
@@ -57,7 +57,7 @@
@@ -70,25 +70,39 @@
+
+
diff --git a/LanMontainDesktop/Views/Components/ClockWidget.axaml b/LanMontainDesktop/Views/Components/ClockWidget.axaml
index a17cc3f..68b5a8e 100644
--- a/LanMontainDesktop/Views/Components/ClockWidget.axaml
+++ b/LanMontainDesktop/Views/Components/ClockWidget.axaml
@@ -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">
-
+ Padding="0"
+ CornerRadius="24">
+
+
+
+
diff --git a/LanMontainDesktop/Views/Components/ClockWidget.axaml.cs b/LanMontainDesktop/Views/Components/ClockWidget.axaml.cs
index f889c5a..68dd416 100644
--- a/LanMontainDesktop/Views/Components/ClockWidget.axaml.cs
+++ b/LanMontainDesktop/Views/Components/ClockWidget.axaml.cs
@@ -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();
}
- ///
- /// 设置时区服务
- ///
+ public ClockDisplayFormat DisplayFormat
+ {
+ get => _displayFormat;
+ set
+ {
+ _displayFormat = value;
+ UpdateClock();
+ }
+ }
+
+ public void SetDisplayFormat(ClockDisplayFormat format)
+ {
+ DisplayFormat = format;
+ }
+
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -66,17 +85,45 @@ public partial class ClockWidget : UserControl
private void UpdateClock()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
- TimeTextBlock.Text = now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
+
+ MainTimeTextBlock.Text = now.ToString("HH:mm", CultureInfo.CurrentCulture);
+ SecondsTextBlock.Text = now.ToString("ss", CultureInfo.CurrentCulture);
+
+ SecondsTextBlock.IsVisible = _displayFormat == ClockDisplayFormat.HourMinuteSecond;
}
public void ApplyCellSize(double cellSize)
{
- var padding = Math.Clamp(cellSize * 0.12, 2, 14);
- RootBorder.Padding = new Thickness(padding);
- RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 4, 18));
+ // --- Class Island “满盈”风格算法 ---
+
+ // 1. 计算组件高度:保持与任务栏核心比例一致 (0.74x)
+ var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
+ RootBorder.Height = targetHeight;
+
+ // 2. 动态圆角:确保始终是完美的胶囊半圆
+ RootBorder.CornerRadius = new CornerRadius(targetHeight / 2);
+ RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
+
+ // 3. 核心:满盈字阶 (Filled Typography)
+ // 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力
+ var mainFontSize = targetHeight * 0.68;
+ MainTimeTextBlock.FontSize = mainFontSize;
+ MainTimeTextBlock.FontWeight = FontWeight.SemiBold;
+
+ // 4. 次级信息:秒数维持 0.7x 比例,并增强透明度呼吸感
+ SecondsTextBlock.FontSize = mainFontSize * 0.7;
+ SecondsTextBlock.Opacity = 0.55;
+
+ // 5. 视觉占比:占据约 2.2 个单元格的感官宽度 (cellSize * 2 + gaps)
+ RootBorder.MinWidth = cellSize * 2.2;
- // Keep the time legible across dense and sparse grid layouts.
- TimeTextBlock.FontSize = Math.Clamp(cellSize * 0.42, 10, 56);
- TimeTextBlock.FontWeight = FontWeight.SemiBold;
+ // 6. 间距微调
+ if (MainTimeTextBlock.Parent is StackPanel panel)
+ {
+ panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8);
+ }
+
+ // 确保清除可能存在的固定 Padding,由代码控制“紧密感”
+ RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
}
diff --git a/LanMontainDesktop/Views/Components/DateWidget.axaml b/LanMontainDesktop/Views/Components/DateWidget.axaml
index 3e56cf2..82feef6 100644
--- a/LanMontainDesktop/Views/Components/DateWidget.axaml
+++ b/LanMontainDesktop/Views/Components/DateWidget.axaml
@@ -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">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
+ CornerRadius="28"
+ ClipToBounds="True"
+ Padding="12">
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMontainDesktop/Views/Components/DateWidget.axaml.cs b/LanMontainDesktop/Views/Components/DateWidget.axaml.cs
index c296f1c..62f421d 100644
--- a/LanMontainDesktop/Views/Components/DateWidget.axaml.cs
+++ b/LanMontainDesktop/Views/Components/DateWidget.axaml.cs
@@ -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();
}
- ///
- /// 设置时区服务
- ///
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -54,6 +59,11 @@ public partial class DateWidget : UserControl
_timer.Stop();
}
+ private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ ApplyCellSize(_currentCellSize);
+ }
+
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateDate();
@@ -68,30 +78,75 @@ public partial class DateWidget : UserControl
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
-
- // 右侧:今日详情
- TodayDayTextBlock.Text = now.Day.ToString();
- TodayWeekdayTextBlock.Text = now.ToString("dddd", culture);
-
- // 左侧:月历
- CalendarMonthYearTextBlock.Text = now.ToString("yyyy年M月", culture);
-
- // 生成月历
+ var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
+ var lunar = LunarCalendarService.GetLunarInfo(now);
+
+ GregorianHeadlineTextBlock.Text = isZh
+ ? $"{now.Month}月{now.Day}日 {ToChineseWeekday(now.DayOfWeek)}"
+ : now.ToString("MMM d ddd", culture);
+
+ if (isZh)
+ {
+ LunarDateTextBlock.Text = $"农历 {lunar.LunarDateZh}";
+ LunarMetaTextBlock.Text = $"{lunar.GanzhiYearZh}年({lunar.ZodiacZh}年)";
+ YiLabelTextBlock.Text = "宜";
+ JiLabelTextBlock.Text = "忌";
+ YiItemsTextBlock.Text = "祭祀 祈福 出行 会友";
+ JiItemsTextBlock.Text = "动土 诉讼 远航 争执";
+ }
+ else
+ {
+ LunarDateTextBlock.Text = $"Lunar {lunar.LunarDateEn}";
+ LunarMetaTextBlock.Text = $"Ganzhi year: {lunar.GanzhiYearEn} ({lunar.ZodiacEn})";
+ YiLabelTextBlock.Text = "Do";
+ JiLabelTextBlock.Text = "Avoid";
+ YiItemsTextBlock.Text = "Worship Blessing Travel Meet";
+ JiItemsTextBlock.Text = "Groundwork Lawsuit Voyage Dispute";
+ }
+
+ UpdateWeekdayHeaders(isZh);
GenerateCalendar(now);
}
+ private static string ToChineseWeekday(DayOfWeek dayOfWeek)
+ {
+ return dayOfWeek switch
+ {
+ DayOfWeek.Sunday => "周日",
+ DayOfWeek.Monday => "周一",
+ DayOfWeek.Tuesday => "周二",
+ DayOfWeek.Wednesday => "周三",
+ DayOfWeek.Thursday => "周四",
+ DayOfWeek.Friday => "周五",
+ _ => "周六"
+ };
+ }
+
+ private void UpdateWeekdayHeaders(bool isZh)
+ {
+ var headers = isZh ? ZhWeekdayHeaders : EnWeekdayHeaders;
+ WeekdayText0.Text = headers[0];
+ WeekdayText1.Text = headers[1];
+ WeekdayText2.Text = headers[2];
+ WeekdayText3.Text = headers[3];
+ WeekdayText4.Text = headers[4];
+ WeekdayText5.Text = headers[5];
+ WeekdayText6.Text = headers[6];
+ }
+
private void GenerateCalendar(DateTime currentDate)
{
- // 清空之前的日期(保留星期标题)
- var childrenToRemove = new List();
+ var removeList = new List();
foreach (var child in CalendarGrid.Children)
{
- if (child is TextBlock tb && tb.Tag?.ToString() == "day")
+ if (child is Control control && control.Tag is string tag &&
+ (tag == "day" || tag == "today-dot"))
{
- childrenToRemove.Add(tb);
+ removeList.Add(control);
}
}
- foreach (var child in childrenToRemove)
+
+ foreach (var child in removeList)
{
CalendarGrid.Children.Remove(child);
}
@@ -99,66 +154,62 @@ public partial class DateWidget : UserControl
var year = currentDate.Year;
var month = currentDate.Month;
var today = currentDate.Day;
-
- // 获取该月第一天
+
var firstDayOfMonth = new DateTime(year, month, 1);
var daysInMonth = DateTime.DaysInMonth(year, month);
- var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek; // 0 = Sunday
+ var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
- // 生成日期
- for (int day = 1; day <= daysInMonth; day++)
+ for (var day = 1; day <= daysInMonth; day++)
{
- var row = ((day + startDayOfWeek - 1) / 7) + 1; // +1 because row 0 is weekday headers
+ var row = (day + startDayOfWeek - 1) / 7;
var col = (day + startDayOfWeek - 1) % 7;
-
- if (row > 5) continue; // 最多显示6行
+ if (row > 4)
+ {
+ continue;
+ }
var dayText = new TextBlock
{
- Text = day.ToString(),
+ Text = day.ToString(CultureInfo.CurrentCulture),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
- FontSize = 10,
+ FontSize = _calendarDayFontSize,
+ FontWeight = FontWeight.SemiBold,
Tag = "day"
};
- // 今天高亮
if (day == today)
{
- // 使用主题色高亮今天
- var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
- ? accent as IBrush
+ var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
+ ? accent as IBrush
: Brushes.Blue;
- var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
- ? onAccent as IBrush
+ var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
+ ? onAccent as IBrush
: Brushes.White;
-
+
dayText.Foreground = onAccentBrush;
- dayText.FontWeight = FontWeight.Bold;
- dayText.Background = new SolidColorBrush(Colors.Transparent);
-
- // 添加背景圆
- var highlight = new Border
+ var dot = new Border
{
+ Width = _calendarTodayDotSize,
+ Height = _calendarTodayDotSize,
+ CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
Background = accentBrush,
- CornerRadius = new CornerRadius(10),
- Width = 20,
- Height = 20,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
- Child = dayText
+ Child = dayText,
+ Tag = "today-dot"
};
- Grid.SetRow(highlight, row);
- Grid.SetColumn(highlight, col);
- CalendarGrid.Children.Add(highlight);
+
+ Grid.SetRow(dot, row);
+ Grid.SetColumn(dot, col);
+ CalendarGrid.Children.Add(dot);
}
else
{
- // 使用主题次要文本颜色
- var secondaryBrush = this.TryFindResource("AdaptiveTextSecondaryBrush", out var secondary)
- ? secondary as IBrush
- : Brushes.Gray;
- dayText.Foreground = secondaryBrush;
+ var isWeekend = col is 0 or 6;
+ dayText.Foreground = isWeekend
+ ? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.82)
+ : GetThemeBrush("AdaptiveTextPrimaryBrush", 0.92);
Grid.SetRow(dayText, row);
Grid.SetColumn(dayText, col);
CalendarGrid.Children.Add(dayText);
@@ -168,13 +219,59 @@ public partial class DateWidget : UserControl
public void ApplyCellSize(double cellSize)
{
- // 根据格子大小调整圆角
- RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 8, 20));
-
- // 调整字体大小
- var baseFontSize = cellSize * 0.25;
- TodayDayTextBlock.FontSize = Math.Clamp(baseFontSize * 2.8, 28, 72);
- TodayWeekdayTextBlock.FontSize = Math.Clamp(baseFontSize * 0.6, 10, 16);
- CalendarMonthYearTextBlock.FontSize = Math.Clamp(baseFontSize * 0.55, 9, 14);
+ _currentCellSize = Math.Max(1, cellSize);
+ var scale = ResolveScale();
+
+ RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 16, 40));
+ RootBorder.Padding = new Thickness(Math.Clamp(12 * scale, 8, 18));
+
+ LeftPanelGrid.RowSpacing = Math.Clamp(8 * scale, 5, 14);
+ RightPanelGrid.RowSpacing = Math.Clamp(10 * scale, 6, 16);
+ LunarCardBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 14, 34));
+ LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 9, 20));
+
+ GregorianHeadlineTextBlock.FontSize = Math.Clamp(22 * scale, 14, 34);
+ WeekdayText0.FontSize = Math.Clamp(13 * scale, 9, 18);
+ WeekdayText1.FontSize = WeekdayText0.FontSize;
+ WeekdayText2.FontSize = WeekdayText0.FontSize;
+ WeekdayText3.FontSize = WeekdayText0.FontSize;
+ WeekdayText4.FontSize = WeekdayText0.FontSize;
+ WeekdayText5.FontSize = WeekdayText0.FontSize;
+ WeekdayText6.FontSize = WeekdayText0.FontSize;
+
+ LunarDateTextBlock.FontSize = Math.Clamp(28 * scale, 17, 44);
+ LunarMetaTextBlock.FontSize = Math.Clamp(14 * scale, 10, 22);
+ YiLabelTextBlock.FontSize = Math.Clamp(18 * scale, 12, 28);
+ JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
+ YiItemsTextBlock.FontSize = Math.Clamp(16 * scale, 11, 24);
+ JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
+
+ _calendarDayFontSize = Math.Clamp(14 * scale, 9, 22);
+ _calendarTodayDotSize = Math.Clamp(28 * scale, 17, 38);
+
+ UpdateDate();
+ }
+
+ private double ResolveScale()
+ {
+ var cellScale = Math.Clamp(_currentCellSize / 48d, 0.72, 1.55);
+ var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 220d, 0.65, 1.65) : 1;
+ var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 460d, 0.65, 1.65) : 1;
+ return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.08), 0.65, 1.6);
+ }
+
+ private IBrush GetThemeBrush(string key, double opacity)
+ {
+ if (this.TryFindResource(key, out var value) && value is IBrush brush)
+ {
+ if (brush is ISolidColorBrush solid)
+ {
+ return new SolidColorBrush(solid.Color, opacity);
+ }
+
+ return brush;
+ }
+
+ return new SolidColorBrush(Colors.Gray, opacity);
}
}
diff --git a/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml b/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml
new file mode 100644
index 0000000..56a2b7d
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs b/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs
new file mode 100644
index 0000000..a21e949
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace LanMontainDesktop.Views.Components;
+
+public partial class DateWidgetSettingsWindow : UserControl
+{
+ public DateWidgetSettingsWindow()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml b/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml
new file mode 100644
index 0000000..67dfa87
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml.cs b/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
new file mode 100644
index 0000000..cb97303
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
@@ -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(target);
+ var usedIndices = new HashSet();
+ 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);
+ }
+}
diff --git a/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml b/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml
new file mode 100644
index 0000000..f5d7a93
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml.cs b/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
new file mode 100644
index 0000000..1195dfa
--- /dev/null
+++ b/LanMontainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
@@ -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 GetWeekdayHeaderBlocks()
+ {
+ return
+ [
+ WeekdayText0,
+ WeekdayText1,
+ WeekdayText2,
+ WeekdayText3,
+ WeekdayText4,
+ WeekdayText5,
+ WeekdayText6
+ ];
+ }
+
+ private void GenerateCalendar(DateTime currentDate)
+ {
+ var removeList = new List();
+ 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);
+ }
+}
+
diff --git a/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs
index 334c9f3..5eb3aeb 100644
--- a/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -1,13 +1,15 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
+using Avalonia.VisualTree;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMontainDesktop.ComponentSystem;
@@ -23,10 +25,14 @@ public partial class MainWindow
private const string DesktopComponentClass = "desktop-component";
private const string DesktopComponentHostClass = "desktop-component-host";
+ private const string DesktopComponentContentHostTag = "desktop-component-content-host";
+ private const string DesktopComponentResizeHandleTag = "desktop-component-resize-handle";
private bool _isDesktopComponentDragActive;
private DesktopComponentDragState? _desktopComponentDrag;
private Border? _desktopComponentDragGhost;
+ private bool _isDesktopComponentResizeActive;
+ private DesktopComponentResizeState? _desktopComponentResize;
private string? _componentLibraryActiveCategoryId;
private int _componentLibraryCategoryIndex;
@@ -67,6 +73,22 @@ public partial class MainWindow
public int TargetColumn { get; set; }
}
+ private sealed class DesktopComponentResizeState
+ {
+ public string PlacementId { get; init; } = string.Empty;
+ public string ComponentId { get; init; } = string.Empty;
+ public Border SourceHost { get; init; } = null!;
+ public int StartWidthCells { get; init; }
+ public int StartHeightCells { get; init; }
+ public int MinWidthCells { get; init; }
+ public int MinHeightCells { get; init; }
+ public int MaxWidthCells { get; init; }
+ public int MaxHeightCells { get; init; }
+ public Point StartPointerInViewport { get; init; }
+ public int CurrentWidthCells { get; set; }
+ public int CurrentHeightCells { get; set; }
+ }
+
private sealed record ComponentLibraryCategory(
string Id,
Symbol Icon,
@@ -96,6 +118,11 @@ public partial class MainWindow
CloseComponentLibraryWindow(reopenSettings: true);
}
+ private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
+ {
+ CloseComponentSettingsWindow();
+ }
+
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
@@ -167,6 +194,30 @@ public partial class MainWindow
_taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode)
? TaskbarLayoutBottomFullRowMacStyle
: snapshot.TaskbarLayoutMode;
+
+ _clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute"
+ ? ClockDisplayFormat.HourMinute
+ : ClockDisplayFormat.HourMinuteSecond;
+
+ if (ClockWidget is not null)
+ {
+ ClockWidget.SetDisplayFormat(_clockDisplayFormat);
+ }
+
+ if (_clockDisplayFormat == ClockDisplayFormat.HourMinute)
+ {
+ if (ClockFormatHMRadio is not null)
+ {
+ ClockFormatHMRadio.IsChecked = true;
+ }
+ }
+ else
+ {
+ if (ClockFormatHMSSRadio is not null)
+ {
+ ClockFormatHMSSRadio.IsChecked = true;
+ }
+ }
}
private void ApplyTopStatusComponentVisibility()
@@ -176,16 +227,21 @@ public partial class MainWindow
if (ClockWidget is not null)
{
ClockWidget.IsVisible = showClock;
+ if (showClock)
+ {
+ ClockWidget.SetDisplayFormat(_clockDisplayFormat);
+ var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
+ Grid.SetColumnSpan(ClockWidget, columnSpan);
+ }
}
- if (WallpaperPreviewClockContainer is not null)
+ if (WallpaperPreviewClockWidget is not null)
{
- WallpaperPreviewClockContainer.IsVisible = showClock;
- }
-
- if (WallpaperPreviewClockTextBlock is not null && showClock)
- {
- WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
+ WallpaperPreviewClockWidget.IsVisible = showClock;
+ if (showClock)
+ {
+ WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
+ }
}
}
@@ -221,7 +277,7 @@ public partial class MainWindow
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
- var showDesktopEdit = true;
+ var showDesktopEdit = _isSettingsOpen;
BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
@@ -254,7 +310,7 @@ public partial class MainWindow
.Where(action => action.IsVisible)
.ToList();
var hasDynamicActions = dynamicActions.Count > 0;
- BuildDynamicTaskbarVisuals(dynamicActions);
+ BuildDynamicTaskbarVisuals(dynamicActions, _currentDesktopCellSize);
if (TaskbarDynamicActionsHost is not null)
{
@@ -304,6 +360,7 @@ public partial class MainWindow
ComponentLibraryWindow.IsVisible = true;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ RestoreComponentLibraryWindowPosition();
Dispatcher.UIThread.Post(() =>
{
@@ -326,6 +383,8 @@ public partial class MainWindow
_isComponentLibraryOpen = false;
CancelDesktopComponentDrag();
+ CancelDesktopComponentResize(restoreOriginalSpan: true);
+ ClearDesktopComponentSelection();
UpdateDesktopComponentHostEditState();
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
@@ -360,16 +419,50 @@ public partial class MainWindow
{
if (context == TaskbarContext.Desktop && _isComponentLibraryOpen)
{
+ var actions = new List();
+ if (_selectedDesktopComponentHost is not null)
+ {
+ actions.Add(new TaskbarActionItem(
+ TaskbarActionId.DeleteComponent,
+ L("component.delete", "Delete"),
+ "Delete",
+ IsVisible: true,
+ CommandKey: "component.delete"));
+
+ actions.Add(new TaskbarActionItem(
+ TaskbarActionId.EditComponent,
+ L("component.edit", "Edit"),
+ "Edit",
+ IsVisible: true,
+ CommandKey: "component.edit"));
+
+ return actions;
+ }
+
var canAddPage = _desktopPageCount < MaxDesktopPageCount;
- return
- [
- new TaskbarActionItem(
+ var canDeletePage = _desktopPageCount > MinDesktopPageCount;
+
+ if (canAddPage)
+ {
+ actions.Add(new TaskbarActionItem(
TaskbarActionId.AddDesktopPage,
L("desktop.add_page", "Add page"),
"Add",
- IsVisible: canAddPage,
- CommandKey: "desktop.add_page")
- ];
+ IsVisible: true,
+ CommandKey: "desktop.add_page"));
+ }
+
+ if (canDeletePage)
+ {
+ actions.Add(new TaskbarActionItem(
+ TaskbarActionId.DeleteDesktopPage,
+ L("desktop.delete_page", "Delete page"),
+ "Delete",
+ IsVisible: true,
+ CommandKey: "desktop.delete_page"));
+ }
+
+ return actions;
}
if (!_enableDynamicTaskbarActions)
@@ -377,12 +470,11 @@ public partial class MainWindow
return Array.Empty();
}
- // Reserved for page-specific actions. Disabled by default in this phase.
_ = context;
return Array.Empty();
}
- private void BuildDynamicTaskbarVisuals(IReadOnlyList actions)
+ private void BuildDynamicTaskbarVisuals(IReadOnlyList actions, double cellSize)
{
if (TaskbarDynamicActionsPanel is not null)
{
@@ -401,6 +493,36 @@ public partial class MainWindow
return;
}
+ // Match taskbar typographic scale to the current grid cell size.
+ var taskbarCellHeight = Math.Clamp(cellSize * 0.76, 36, 76);
+ var fontSize = Math.Clamp(taskbarCellHeight * 0.36, 11, 22);
+ var iconSize = Math.Clamp(taskbarCellHeight * 0.44, 12, 26);
+ var padding = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
+ var cornerRadius = Math.Clamp(taskbarCellHeight * 0.32, 8, 16);
+ var spacing = Math.Clamp(taskbarCellHeight * 0.18, 4, 10);
+
+ var pageCountText = $"{_currentDesktopSurfaceIndex + 1}/{_desktopPageCount}";
+ var pageCountBlock = new TextBlock
+ {
+ Text = pageCountText,
+ Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
+ FontSize = fontSize,
+ FontWeight = FontWeight.SemiBold,
+ VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, spacing, 0)
+ };
+
+ var pageCountContainer = new Border
+ {
+ Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
+ CornerRadius = new CornerRadius(cornerRadius),
+ Padding = new Thickness(padding),
+ Child = pageCountBlock,
+ Margin = new Thickness(0, 0, spacing, 0)
+ };
+
+ TaskbarDynamicActionsPanel.Children.Add(pageCountContainer);
+
foreach (var action in actions)
{
if (!action.IsVisible)
@@ -408,31 +530,91 @@ public partial class MainWindow
continue;
}
+ var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
+ action.Id == TaskbarActionId.DeleteComponent;
+ var isEditAction = action.Id == TaskbarActionId.EditComponent;
+
+ Symbol iconSymbol;
+ if (isDeleteAction)
+ {
+ iconSymbol = Symbol.Delete;
+ }
+ else if (isEditAction)
+ {
+ iconSymbol = Symbol.Edit;
+ }
+ else
+ {
+ iconSymbol = Symbol.Add;
+ }
+
+ Control icon = new SymbolIcon
+ {
+ Symbol = iconSymbol,
+ IconVariant = IconVariant.Regular,
+ FontSize = iconSize
+ };
+
+ var buttonContent = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = spacing * 0.6,
+ Children =
+ {
+ icon,
+ new TextBlock
+ {
+ Text = action.Title,
+ FontSize = fontSize,
+ VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
+ }
+ }
+ };
+
var button = new Button
{
- Content = action.Title,
+ Content = buttonContent,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
- Padding = new Thickness(12, 6),
- Foreground = Foreground,
+ Padding = new Thickness(padding),
+ Foreground = isDeleteAction
+ ? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
+ : Foreground,
Tag = action.CommandKey
};
button.Click += OnDynamicTaskbarActionClick;
TaskbarDynamicActionsPanel.Children.Add(button);
+ Control previewIcon = new SymbolIcon
+ {
+ Symbol = iconSymbol,
+ IconVariant = IconVariant.Regular,
+ FontSize = iconSize * 0.85
+ };
+
var previewText = new TextBlock
{
Text = action.Title,
- Foreground = Foreground,
- HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
+ FontSize = fontSize * 0.85,
+ Foreground = isDeleteAction
+ ? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
+ : Foreground,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
+
+ var previewContent = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = spacing * 0.5,
+ Children = { previewIcon, previewText }
+ };
+
var previewBorder = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
- Child = previewText
+ Child = previewContent
};
WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder);
}
@@ -450,9 +632,107 @@ public partial class MainWindow
case "desktop.add_page":
AddDesktopPage();
break;
+ case "desktop.delete_page":
+ DeleteCurrentDesktopPage();
+ break;
+ case "component.delete":
+ DeleteSelectedComponent();
+ break;
+ case "component.edit":
+ OpenComponentSettings();
+ break;
}
}
+ private void DeleteSelectedComponent()
+ {
+ if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
+ {
+ return;
+ }
+
+ var placement = _desktopComponentPlacements.FirstOrDefault(p =>
+ string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
+ if (placement is null)
+ {
+ return;
+ }
+
+ // 娴犲海缍夐弽闂磋厬缁夊娅庣紒鍕
+ if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
+ {
+ pageGrid.Children.Remove(_selectedDesktopComponentHost);
+ }
+
+ // Remove from persisted placement list as well.
+ _desktopComponentPlacements.Remove(placement);
+
+ ClearDesktopComponentSelection();
+
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+
+ // 娣囨繂鐡ㄧ拋鍓х枂
+ PersistSettings();
+ }
+
+ private void OpenComponentSettings()
+ {
+ if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
+ {
+ return;
+ }
+
+ var placement = _desktopComponentPlacements.FirstOrDefault(p =>
+ string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
+ if (placement is null)
+ {
+ return;
+ }
+
+ if (placement.ComponentId == BuiltInComponentIds.Date)
+ {
+ OpenDateComponentSettings();
+ }
+ }
+
+ private void OpenDateComponentSettings()
+ {
+ if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
+ {
+ return;
+ }
+
+ var settingsContent = new DateWidgetSettingsWindow();
+ ComponentSettingsContentHost.Content = settingsContent;
+
+ ComponentSettingsWindow.IsVisible = true;
+ ComponentSettingsWindow.Opacity = 0;
+
+ ComponentSettingsWindow.Opacity = 1;
+ }
+
+ private void CloseComponentSettingsWindow()
+ {
+ if (ComponentSettingsWindow is null)
+ {
+ return;
+ }
+
+ ComponentSettingsWindow.Opacity = 0;
+
+ DispatcherTimer.RunOnce(() =>
+ {
+ if (ComponentSettingsWindow is not null)
+ {
+ ComponentSettingsWindow.IsVisible = false;
+ }
+ if (ComponentSettingsContentHost is not null)
+ {
+ ComponentSettingsContentHost.Content = null;
+ }
+ }, TimeSpan.FromMilliseconds(200));
+ }
+
private void AddDesktopPage()
{
if (_desktopPageCount >= MaxDesktopPageCount)
@@ -464,6 +744,46 @@ public partial class MainWindow
_currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex);
RebuildDesktopGrid();
PersistSettings();
+
+ // 閺囧瓨鏌婇崝銊︹偓浣锋崲閸斺剝鐖弰鍓с仛
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ }
+
+ private void DeleteCurrentDesktopPage()
+ {
+ if (_desktopPageCount <= MinDesktopPageCount)
+ {
+ return;
+ }
+
+ var placementsToRemove = _desktopComponentPlacements
+ .Where(p => p.PageIndex == _currentDesktopSurfaceIndex)
+ .ToList();
+
+ foreach (var placement in placementsToRemove)
+ {
+ _desktopComponentPlacements.Remove(placement);
+ }
+
+ _desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
+
+ // 鐠嬪啯鏆hぐ鎾冲妞ょ敻娼扮槐銏犵穿
+ _currentDesktopSurfaceIndex = Math.Clamp(_currentDesktopSurfaceIndex, 0, _desktopPageCount - 1);
+
+ // Update remaining page indices after deletion.
+ foreach (var placement in _desktopComponentPlacements)
+ {
+ if (placement.PageIndex > _currentDesktopSurfaceIndex)
+ {
+ placement.PageIndex--;
+ }
+ }
+
+ RebuildDesktopGrid();
+ PersistSettings();
+
+ // 閺囧瓨鏌婇崝銊︹偓浣锋崲閸斺剝鐖弰鍓с仛
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void InitializeDesktopComponentPlacements(AppSettingsSnapshot snapshot)
@@ -491,10 +811,12 @@ public partial class MainWindow
continue;
}
- var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize(
- definition,
- placement.WidthCells,
- placement.HeightCells);
+ var (widthCells, heightCells) = NormalizeComponentCellSpan(
+ componentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ placement.WidthCells,
+ placement.HeightCells));
_desktopComponentPlacements.Add(new DesktopComponentPlacementSnapshot
{
@@ -532,10 +854,12 @@ public partial class MainWindow
continue;
}
- var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize(
- definition,
- placement.WidthCells,
- placement.HeightCells);
+ var (widthCells, heightCells) = NormalizeComponentCellSpan(
+ placement.ComponentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ placement.WidthCells,
+ placement.HeightCells));
var clampedColumn = Math.Clamp(placement.Column, 0, Math.Max(0, maxColumns - widthCells));
var clampedRow = Math.Clamp(placement.Row, 0, Math.Max(0, maxRows - heightCells));
@@ -571,10 +895,12 @@ public partial class MainWindow
return;
}
- var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize(
- definition,
- definition.MinWidthCells,
- definition.MinHeightCells);
+ var (widthCells, heightCells) = NormalizeComponentCellSpan(
+ componentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ definition.MinWidthCells,
+ definition.MinHeightCells));
var maxColumns = pageGrid.ColumnDefinitions.Count;
var maxRows = pageGrid.RowDefinitions.Count;
@@ -629,12 +955,90 @@ public partial class MainWindow
return null;
}
+ var componentCornerRadius = GetComponentCornerRadius(placement.ComponentId);
+
+ var visualInset = GetDesktopComponentVisualInset(
+ Math.Max(1, placement.WidthCells),
+ Math.Max(1, placement.HeightCells));
+
+ var contentHost = new Border
+ {
+ Tag = DesktopComponentContentHostTag,
+ Background = Brushes.Transparent,
+ CornerRadius = new CornerRadius(componentCornerRadius),
+ ClipToBounds = true,
+ Padding = visualInset,
+ Child = component
+ };
+
+ // Separate visual arc size from hit target size for better touch usability.
+ var handleTouchSize = Math.Clamp(_currentDesktopCellSize * 0.72, 30, 54);
+ var handleVisualSize = Math.Clamp(_currentDesktopCellSize * 0.56, 20, 40);
+ var handlePadding = Math.Max(2, (handleTouchSize - handleVisualSize) / 2);
+ var arcThickness = Math.Clamp(_currentDesktopCellSize * 0.17, 7, 14);
+ var arcData = Geometry.Parse("M 24,6 A 18,18 0 0 1 6,24");
+
+ var resizeHandleVisual = new Grid
+ {
+ Width = handleVisualSize,
+ Height = handleVisualSize,
+ IsHitTestVisible = false
+ };
+ resizeHandleVisual.Children.Add(new Path
+ {
+ Data = arcData,
+ Stretch = Stretch.Fill,
+ Stroke = GetThemeBrush("AdaptiveTextAccentBrush"),
+ StrokeThickness = arcThickness + 3,
+ StrokeLineCap = PenLineCap.Round
+ });
+ resizeHandleVisual.Children.Add(new Path
+ {
+ Data = arcData,
+ Stretch = Stretch.Fill,
+ Stroke = GetThemeBrush("AdaptiveAccentBrush"),
+ StrokeThickness = arcThickness,
+ StrokeLineCap = PenLineCap.Round
+ });
+
+ var resizeHandle = new Border
+ {
+ Tag = DesktopComponentResizeHandleTag,
+ Width = handleTouchSize,
+ Height = handleTouchSize,
+ Background = Brushes.Transparent,
+ BorderBrush = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ CornerRadius = new CornerRadius(handleTouchSize * 0.5),
+ Padding = new Thickness(handlePadding),
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ Margin = new Thickness(
+ 0,
+ 0,
+ -Math.Clamp(handleTouchSize * 0.42, 10, 24),
+ -Math.Clamp(handleTouchSize * 0.42, 10, 24)),
+ Child = resizeHandleVisual,
+ Opacity = 1,
+ IsVisible = false,
+ IsHitTestVisible = false
+ };
+ resizeHandle.PointerPressed += OnDesktopComponentResizeHandlePointerPressed;
+
+ var hostChrome = new Grid
+ {
+ ClipToBounds = false
+ };
+ hostChrome.Children.Add(contentHost);
+ hostChrome.Children.Add(resizeHandle);
+
var host = new Border
{
Tag = placement.PlacementId,
Background = Brushes.Transparent,
- ClipToBounds = true,
- Child = component
+ ClipToBounds = false,
+ CornerRadius = new CornerRadius(componentCornerRadius),
+ Child = hostChrome
};
host.Classes.Add(DesktopComponentHostClass);
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
@@ -642,6 +1046,112 @@ public partial class MainWindow
return host;
}
+ private static (int WidthCells, int HeightCells) NormalizeComponentCellSpan(
+ string componentId,
+ (int WidthCells, int HeightCells) span)
+ {
+ if (string.Equals(componentId, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase))
+ {
+ return (Math.Max(4, span.WidthCells), Math.Max(2, span.HeightCells));
+ }
+
+ if (string.Equals(componentId, BuiltInComponentIds.MonthCalendar, StringComparison.OrdinalIgnoreCase))
+ {
+ return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
+ }
+
+ if (string.Equals(componentId, BuiltInComponentIds.LunarCalendar, StringComparison.OrdinalIgnoreCase))
+ {
+ return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
+ }
+
+ return (Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells));
+ }
+
+ private double GetComponentCornerRadius(string componentId)
+ {
+ return componentId switch
+ {
+ BuiltInComponentIds.Date => 16,
+ BuiltInComponentIds.MonthCalendar => Math.Clamp(_currentDesktopCellSize * 0.26, 10, 22),
+ BuiltInComponentIds.LunarCalendar => Math.Clamp(_currentDesktopCellSize * 0.30, 12, 26),
+ _ => Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)
+ };
+ }
+
+ private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells)
+ {
+ // Keep the drop/selection bounds on grid cells while reducing visual footprint.
+ var baseInset = Math.Clamp(_currentDesktopCellSize * 0.08, 2, 10);
+ var horizontal = Math.Clamp(baseInset + Math.Max(0, widthCells - 1) * 0.25, 2, 12);
+ var vertical = Math.Clamp(baseInset * 0.85 + Math.Max(0, heightCells - 1) * 0.2, 2, 10);
+ return new Thickness(horizontal, vertical, horizontal, vertical);
+ }
+
+ private static Border? FindDesktopComponentHost(Visual? visual)
+ {
+ var current = visual;
+ while (current is not null)
+ {
+ if (current is Border border && border.Classes.Contains(DesktopComponentHostClass))
+ {
+ return border;
+ }
+
+ current = current.GetVisualParent();
+ }
+
+ return null;
+ }
+
+ private static Border? TryGetContentHost(Border host)
+ {
+ if (host.Child is Grid hostChrome)
+ {
+ return hostChrome.Children
+ .OfType()
+ .FirstOrDefault(child =>
+ string.Equals(child.Tag?.ToString(), DesktopComponentContentHostTag, StringComparison.Ordinal));
+ }
+
+ return null;
+ }
+
+ private static Border? TryGetResizeHandle(Border host)
+ {
+ if (host.Child is Grid hostChrome)
+ {
+ return hostChrome.Children
+ .OfType()
+ .FirstOrDefault(child =>
+ string.Equals(child.Tag?.ToString(), DesktopComponentResizeHandleTag, StringComparison.Ordinal));
+ }
+
+ return null;
+ }
+
+ private bool IsPointerOnSelectedFrameBorder(Border host, Point pointerInHost)
+ {
+ if (host != _selectedDesktopComponentHost || !_isComponentLibraryOpen)
+ {
+ return false;
+ }
+
+ var width = host.Bounds.Width;
+ var height = host.Bounds.Height;
+ if (width <= 1 || height <= 1)
+ {
+ return false;
+ }
+
+ var borderBand = Math.Clamp(_currentDesktopCellSize * 0.15, 8, 22);
+ var onLeft = pointerInHost.X <= borderBand;
+ var onRight = pointerInHost.X >= width - borderBand;
+ var onTop = pointerInHost.Y <= borderBand;
+ var onBottom = pointerInHost.Y >= height - borderBand;
+ return onLeft || onRight || onTop || onBottom;
+ }
+
private Control? CreateDesktopComponentControl(string componentId)
{
if (componentId == BuiltInComponentIds.Date)
@@ -653,6 +1163,24 @@ public partial class MainWindow
return widget;
}
+ if (componentId == BuiltInComponentIds.MonthCalendar)
+ {
+ var widget = new MonthCalendarWidget();
+ widget.SetTimeZoneService(_timeZoneService);
+ widget.ApplyCellSize(_currentDesktopCellSize);
+ widget.Classes.Add(DesktopComponentClass);
+ return widget;
+ }
+
+ if (componentId == BuiltInComponentIds.LunarCalendar)
+ {
+ var widget = new LunarCalendarWidget();
+ widget.SetTimeZoneService(_timeZoneService);
+ widget.ApplyCellSize(_currentDesktopCellSize);
+ widget.Classes.Add(DesktopComponentClass);
+ return widget;
+ }
+
return null;
}
@@ -667,6 +1195,8 @@ public partial class MainWindow
_isComponentLibraryOpen = false;
CancelDesktopComponentDrag();
+ CancelDesktopComponentResize(restoreOriginalSpan: true);
+ ClearDesktopComponentSelection();
UpdateDesktopComponentHostEditState();
UpdateComponentLibraryLayout(_currentDesktopCellSize);
}
@@ -687,30 +1217,25 @@ public partial class MainWindow
private void ApplyDesktopEditStateToHost(Border host, bool isEditMode)
{
- host.IsHitTestVisible = isEditMode;
- host.CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18));
+ host.IsHitTestVisible = true;
- if (isEditMode)
- {
- host.BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3));
- host.BorderBrush = GetThemeBrush("AdaptiveAccentBrush");
- }
- else
- {
- host.BorderThickness = new Thickness(0);
- host.BorderBrush = null;
- }
-
- if (host.Child is Control child)
+ if (TryGetContentHost(host) is Border contentHost)
{
// In edit mode, prefer drag interactions over component interactions.
- child.IsHitTestVisible = !isEditMode;
+ contentHost.IsHitTestVisible = !isEditMode;
+ if (contentHost.Child is Control componentControl)
+ {
+ componentControl.IsHitTestVisible = !isEditMode;
+ }
}
+
+ var isSelected = host == _selectedDesktopComponentHost;
+ ApplySelectionStateToHost(host, isSelected);
}
private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e)
{
- if (!_isComponentLibraryOpen || _isDesktopComponentDragActive)
+ if (!_isComponentLibraryOpen || _isDesktopComponentDragActive || _isDesktopComponentResizeActive)
{
return;
}
@@ -730,13 +1255,77 @@ public partial class MainWindow
return;
}
+ var wasSelected = host == _selectedDesktopComponentHost;
+ SetSelectedDesktopComponent(host);
+ if (!wasSelected)
+ {
+ e.Handled = true;
+ return;
+ }
+
+ var pointerInHost = e.GetPosition(host);
+ if (IsPointerOnSelectedFrameBorder(host, pointerInHost))
+ {
+ BeginDesktopComponentResizeDrag(host, placement, e);
+ if (_isDesktopComponentResizeActive)
+ {
+ e.Handled = true;
+ }
+
+ return;
+ }
+
BeginDesktopComponentMoveDrag(host, placement, e);
e.Handled = true;
}
+ private void SetSelectedDesktopComponent(Border? host)
+ {
+ // Clear previous selection
+ if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host)
+ {
+ ApplySelectionStateToHost(_selectedDesktopComponentHost, false);
+ }
+
+ // Set new selection
+ _selectedDesktopComponentHost = host;
+ if (host is not null)
+ {
+ ApplySelectionStateToHost(host, true);
+ }
+
+ // Refresh taskbar actions to show delete/edit buttons
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ }
+
+ private void ApplySelectionStateToHost(Border host, bool isSelected)
+ {
+ var showSelection = isSelected && _isComponentLibraryOpen;
+ host.BorderThickness = showSelection
+ ? new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3))
+ : new Thickness(0);
+ host.BorderBrush = showSelection ? GetThemeBrush("AdaptiveAccentBrush") : null;
+
+ if (TryGetResizeHandle(host) is Border resizeHandle)
+ {
+ resizeHandle.IsVisible = showSelection;
+ resizeHandle.IsHitTestVisible = showSelection;
+ }
+ }
+
+ private void ClearDesktopComponentSelection()
+ {
+ if (_selectedDesktopComponentHost is not null)
+ {
+ ApplySelectionStateToHost(_selectedDesktopComponentHost, false);
+ _selectedDesktopComponentHost = null;
+ }
+ }
+
private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e)
{
- if (DesktopEditDragLayer is null ||
+ if (_isDesktopComponentResizeActive ||
+ DesktopEditDragLayer is null ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition))
@@ -744,13 +1333,16 @@ public partial class MainWindow
return;
}
- var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize(
- definition,
- placement.WidthCells,
- placement.HeightCells);
+ var (widthCells, heightCells) = NormalizeComponentCellSpan(
+ placement.ComponentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ placement.WidthCells,
+ placement.HeightCells));
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
- var topLeft = new Point(placement.Column * _currentDesktopCellSize, placement.Row * _currentDesktopCellSize);
+ var pitch = CurrentDesktopPitch;
+ var topLeft = new Point(placement.Column * pitch, placement.Row * pitch);
var pointerOffset = pointerInViewport - topLeft;
sourceHost.Opacity = 0.35;
@@ -778,6 +1370,7 @@ public partial class MainWindow
{
if (!_isComponentLibraryOpen ||
_isDesktopComponentDragActive ||
+ _isDesktopComponentResizeActive ||
DesktopEditDragLayer is null ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
@@ -787,15 +1380,19 @@ public partial class MainWindow
return;
}
- var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize(
- definition,
- definition.MinWidthCells,
- definition.MinHeightCells);
+ var (widthCells, heightCells) = NormalizeComponentCellSpan(
+ componentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ definition.MinWidthCells,
+ definition.MinHeightCells));
// Center the component under the pointer while dragging from the library.
+ var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
+ var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
var pointerOffset = new Point(
- (widthCells * _currentDesktopCellSize) * 0.5,
- (heightCells * _currentDesktopCellSize) * 0.5);
+ ghostWidth * 0.5,
+ ghostHeight * 0.5);
_desktopComponentDrag = new DesktopComponentDragState
{
@@ -824,8 +1421,8 @@ public partial class MainWindow
DesktopEditDragLayer.Children.Clear();
- var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize);
- var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize);
+ var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
+ var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
var ghostContent = CreateDesktopComponentControl(componentId);
if (ghostContent is not null)
@@ -833,14 +1430,18 @@ public partial class MainWindow
ghostContent.IsHitTestVisible = false;
}
+ var visualInset = GetDesktopComponentVisualInset(widthCells, heightCells);
+
_desktopComponentDragGhost = new Border
{
Width = ghostWidth,
Height = ghostHeight,
- CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)),
+ CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.45, 16, 36)),
Background = new SolidColorBrush(Color.Parse("#331E40AF")),
BorderBrush = GetThemeBrush("AdaptiveAccentBrush"),
BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)),
+ Padding = visualInset,
+ ClipToBounds = true,
Child = ghostContent,
Opacity = 0.92,
IsHitTestVisible = false
@@ -849,9 +1450,217 @@ public partial class MainWindow
DesktopEditDragLayer.Children.Add(_desktopComponentDragGhost);
}
+ private void OnDesktopComponentResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (!_isComponentLibraryOpen ||
+ _isDesktopComponentDragActive ||
+ _isDesktopComponentResizeActive ||
+ DesktopPagesViewport is null ||
+ sender is not Border handle ||
+ !e.GetCurrentPoint(handle).Properties.IsLeftButtonPressed)
+ {
+ return;
+ }
+
+ var host = FindDesktopComponentHost(handle);
+ if (host?.Tag is not string placementId)
+ {
+ return;
+ }
+
+ var placement = _desktopComponentPlacements.FirstOrDefault(p =>
+ string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
+ if (placement is null)
+ {
+ return;
+ }
+
+ SetSelectedDesktopComponent(host);
+ BeginDesktopComponentResizeDrag(host, placement, e);
+ if (_isDesktopComponentResizeActive)
+ {
+ e.Handled = true;
+ }
+ }
+
+ private void BeginDesktopComponentResizeDrag(
+ Border sourceHost,
+ DesktopComponentPlacementSnapshot placement,
+ PointerPressedEventArgs e)
+ {
+ if (DesktopPagesViewport is null ||
+ _currentDesktopCellSize <= 0 ||
+ !_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) ||
+ !_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
+ {
+ return;
+ }
+
+ var startSpan = NormalizeComponentCellSpan(
+ placement.ComponentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ placement.WidthCells,
+ placement.HeightCells));
+
+ var minSpan = NormalizeComponentCellSpan(
+ placement.ComponentId,
+ ComponentPlacementRules.EnsureMinimumSize(
+ definition,
+ definition.MinWidthCells,
+ definition.MinHeightCells));
+
+ var maxWidthCells = Math.Max(startSpan.WidthCells, pageGrid.ColumnDefinitions.Count - placement.Column);
+ var maxHeightCells = Math.Max(startSpan.HeightCells, pageGrid.RowDefinitions.Count - placement.Row);
+ if (maxWidthCells <= 0 || maxHeightCells <= 0)
+ {
+ return;
+ }
+
+ var pointerInViewport = e.GetPosition(DesktopPagesViewport);
+ _desktopComponentResize = new DesktopComponentResizeState
+ {
+ PlacementId = placement.PlacementId,
+ ComponentId = placement.ComponentId,
+ SourceHost = sourceHost,
+ StartWidthCells = startSpan.WidthCells,
+ StartHeightCells = startSpan.HeightCells,
+ MinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells)),
+ MinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells)),
+ MaxWidthCells = maxWidthCells,
+ MaxHeightCells = maxHeightCells,
+ StartPointerInViewport = pointerInViewport,
+ CurrentWidthCells = startSpan.WidthCells,
+ CurrentHeightCells = startSpan.HeightCells
+ };
+
+ _isDesktopComponentResizeActive = true;
+ sourceHost.Opacity = 0.96;
+ e.Pointer.Capture(this);
+ }
+
+ private void UpdateDesktopComponentResizeVisual(Point pointerInViewport)
+ {
+ if (_desktopComponentResize is null)
+ {
+ return;
+ }
+
+ var pitch = CurrentDesktopPitch;
+ if (pitch <= 0 ||
+ _desktopComponentResize.StartWidthCells <= 0 ||
+ _desktopComponentResize.StartHeightCells <= 0)
+ {
+ return;
+ }
+
+ var deltaX = pointerInViewport.X - _desktopComponentResize.StartPointerInViewport.X;
+ var deltaY = pointerInViewport.Y - _desktopComponentResize.StartPointerInViewport.Y;
+ var widthScale = (_desktopComponentResize.StartWidthCells + deltaX / pitch) / _desktopComponentResize.StartWidthCells;
+ var heightScale = (_desktopComponentResize.StartHeightCells + deltaY / pitch) / _desktopComponentResize.StartHeightCells;
+
+ var proposedScale = Math.Max(widthScale, heightScale);
+ var minScale = Math.Max(
+ (double)_desktopComponentResize.MinWidthCells / _desktopComponentResize.StartWidthCells,
+ (double)_desktopComponentResize.MinHeightCells / _desktopComponentResize.StartHeightCells);
+ var maxScale = Math.Min(
+ (double)_desktopComponentResize.MaxWidthCells / _desktopComponentResize.StartWidthCells,
+ (double)_desktopComponentResize.MaxHeightCells / _desktopComponentResize.StartHeightCells);
+
+ if (double.IsNaN(proposedScale) || double.IsInfinity(proposedScale))
+ {
+ proposedScale = minScale;
+ }
+
+ if (maxScale < minScale)
+ {
+ maxScale = minScale;
+ }
+
+ var scale = Math.Clamp(proposedScale, minScale, maxScale);
+ var widthCells = Math.Clamp(
+ (int)Math.Round(_desktopComponentResize.StartWidthCells * scale),
+ _desktopComponentResize.MinWidthCells,
+ _desktopComponentResize.MaxWidthCells);
+ var heightCells = Math.Clamp(
+ (int)Math.Round(_desktopComponentResize.StartHeightCells * scale),
+ _desktopComponentResize.MinHeightCells,
+ _desktopComponentResize.MaxHeightCells);
+
+ var normalized = NormalizeComponentCellSpan(_desktopComponentResize.ComponentId, (widthCells, heightCells));
+ widthCells = Math.Clamp(normalized.WidthCells, _desktopComponentResize.MinWidthCells, _desktopComponentResize.MaxWidthCells);
+ heightCells = Math.Clamp(normalized.HeightCells, _desktopComponentResize.MinHeightCells, _desktopComponentResize.MaxHeightCells);
+
+ _desktopComponentResize.CurrentWidthCells = widthCells;
+ _desktopComponentResize.CurrentHeightCells = heightCells;
+ Grid.SetColumnSpan(_desktopComponentResize.SourceHost, widthCells);
+ Grid.SetRowSpan(_desktopComponentResize.SourceHost, heightCells);
+ }
+
+ private bool TryCompleteDesktopComponentResize(Point pointerInViewport)
+ {
+ if (_desktopComponentResize is null)
+ {
+ return false;
+ }
+
+ UpdateDesktopComponentResizeVisual(pointerInViewport);
+
+ var placement = _desktopComponentPlacements.FirstOrDefault(p =>
+ string.Equals(p.PlacementId, _desktopComponentResize.PlacementId, StringComparison.OrdinalIgnoreCase));
+ if (placement is null)
+ {
+ return false;
+ }
+
+ var widthCells = Math.Max(1, _desktopComponentResize.CurrentWidthCells);
+ var heightCells = Math.Max(1, _desktopComponentResize.CurrentHeightCells);
+ var changed = placement.WidthCells != widthCells || placement.HeightCells != heightCells;
+ placement.WidthCells = widthCells;
+ placement.HeightCells = heightCells;
+
+ ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
+ if (changed)
+ {
+ PersistSettings();
+ }
+
+ return true;
+ }
+
+ private void CancelDesktopComponentResize(bool restoreOriginalSpan)
+ {
+ if (!_isDesktopComponentResizeActive || _desktopComponentResize is null)
+ {
+ return;
+ }
+
+ if (restoreOriginalSpan)
+ {
+ Grid.SetColumnSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartWidthCells);
+ Grid.SetRowSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartHeightCells);
+ }
+
+ _desktopComponentResize.SourceHost.Opacity = 1;
+ ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
+ _desktopComponentResize = null;
+ _isDesktopComponentResizeActive = false;
+ }
+
private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e)
{
- if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null)
+ if (DesktopPagesViewport is null)
+ {
+ return;
+ }
+
+ if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
+ {
+ UpdateDesktopComponentResizeVisual(e.GetPosition(DesktopPagesViewport));
+ return;
+ }
+
+ if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
{
return;
}
@@ -861,7 +1670,26 @@ public partial class MainWindow
private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e)
{
- if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null)
+ if (DesktopPagesViewport is null)
+ {
+ return;
+ }
+
+ if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
+ {
+ var resizePointerInViewport = e.GetPosition(DesktopPagesViewport);
+ var resizeSuccess = TryCompleteDesktopComponentResize(resizePointerInViewport);
+ CancelDesktopComponentResize(restoreOriginalSpan: !resizeSuccess);
+ e.Pointer.Capture(null);
+ if (resizeSuccess)
+ {
+ e.Handled = true;
+ }
+
+ return;
+ }
+
+ if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
{
return;
}
@@ -878,6 +1706,12 @@ public partial class MainWindow
private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
+ if (_isDesktopComponentResizeActive)
+ {
+ CancelDesktopComponentResize(restoreOriginalSpan: true);
+ return;
+ }
+
if (!_isDesktopComponentDragActive)
{
return;
@@ -909,8 +1743,9 @@ public partial class MainWindow
_desktopComponentDragGhost.IsVisible = true;
_desktopComponentDrag.TargetRow = row;
_desktopComponentDrag.TargetColumn = column;
- Canvas.SetLeft(_desktopComponentDragGhost, column * _currentDesktopCellSize);
- Canvas.SetTop(_desktopComponentDragGhost, row * _currentDesktopCellSize);
+ var pitch = CurrentDesktopPitch;
+ Canvas.SetLeft(_desktopComponentDragGhost, column * pitch);
+ Canvas.SetTop(_desktopComponentDragGhost, row * pitch);
}
private bool TryGetDesktopComponentDropCell(
@@ -937,11 +1772,17 @@ public partial class MainWindow
return false;
}
+ var pitch = CurrentDesktopPitch;
+ if (pitch <= 0)
+ {
+ return false;
+ }
+
var x = pointerInViewport.X - state.PointerOffset.X;
var y = pointerInViewport.Y - state.PointerOffset.Y;
- column = (int)Math.Floor(x / _currentDesktopCellSize);
- row = (int)Math.Floor(y / _currentDesktopCellSize);
+ column = (int)Math.Floor(x / pitch);
+ row = (int)Math.Floor(y / pitch);
column = Math.Clamp(column, 0, Math.Max(0, maxColumns - state.WidthCells));
row = Math.Clamp(row, 0, Math.Max(0, maxRows - state.HeightCells));
@@ -1138,7 +1979,7 @@ public partial class MainWindow
Classes = { "glass-panel" },
Width = cardWidth,
Height = cardHeight,
- CornerRadius = new CornerRadius(18),
+ CornerRadius = new CornerRadius(36),
Padding = new Thickness(18),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
@@ -1234,7 +2075,7 @@ public partial class MainWindow
{
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
{
- return L("component_category.date", "Date");
+ return L("component_category.date", "Calendar");
}
return categoryId;
@@ -1341,29 +2182,49 @@ public partial class MainWindow
// Fit the preview to the page while preserving component cell span proportions.
var previewMaxWidth = _componentLibraryComponentPageWidth * 0.86;
var previewMaxHeight = viewportHeight * 0.72;
+ var previewSpan = NormalizeComponentCellSpan(
+ resolved.Id,
+ (resolved.MinWidthCells, resolved.MinHeightCells));
var previewCellSize = Math.Min(
- previewMaxWidth / Math.Max(1, resolved.MinWidthCells),
- previewMaxHeight / Math.Max(1, resolved.MinHeightCells));
- previewCellSize = Math.Clamp(previewCellSize, 18, 64);
+ previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
+ previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
+ previewCellSize = Math.Clamp(previewCellSize, 20, 72);
- var previewWidth = resolved.MinWidthCells * previewCellSize;
- var previewHeight = resolved.MinHeightCells * previewCellSize;
+ var previewWidth = previewSpan.WidthCells * previewCellSize;
+ var previewHeight = previewSpan.HeightCells * previewCellSize;
+ var renderCellSize = Math.Clamp(previewCellSize * 1.35, 28, 82);
- var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, previewCellSize);
+ var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, renderCellSize);
if (previewControl is null)
{
continue;
}
+ var previewSurface = new Border
+ {
+ Width = previewSpan.WidthCells * renderCellSize,
+ Height = previewSpan.HeightCells * renderCellSize,
+ Background = Brushes.Transparent,
+ Child = previewControl
+ };
+
+ var previewViewbox = new Viewbox
+ {
+ Width = previewWidth,
+ Height = previewHeight,
+ Stretch = Stretch.Uniform,
+ Child = previewSurface
+ };
+
var previewBorder = new Border
{
Width = previewWidth,
Height = previewHeight,
- CornerRadius = new CornerRadius(16),
+ CornerRadius = new CornerRadius(20),
ClipToBounds = true,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
- Child = previewControl,
+ Child = previewViewbox,
Tag = resolved.Id
};
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
@@ -1395,7 +2256,7 @@ public partial class MainWindow
new Border
{
Classes = { "glass-panel" },
- CornerRadius = new CornerRadius(18),
+ CornerRadius = new CornerRadius(28),
Padding = new Thickness(12),
Child = previewBorder
},
@@ -1431,6 +2292,22 @@ public partial class MainWindow
return widget;
}
+ if (componentId == BuiltInComponentIds.MonthCalendar)
+ {
+ var widget = new MonthCalendarWidget();
+ widget.SetTimeZoneService(_timeZoneService);
+ widget.ApplyCellSize(cellSize);
+ return widget;
+ }
+
+ if (componentId == BuiltInComponentIds.LunarCalendar)
+ {
+ var widget = new LunarCalendarWidget();
+ widget.SetTimeZoneService(_timeZoneService);
+ widget.ApplyCellSize(cellSize);
+ return widget;
+ }
+
return null;
}
@@ -1441,6 +2318,16 @@ public partial class MainWindow
return L("component.date", definition.DisplayName);
}
+ if (string.Equals(definition.Id, BuiltInComponentIds.MonthCalendar, StringComparison.OrdinalIgnoreCase))
+ {
+ return L("component.month_calendar", definition.DisplayName);
+ }
+
+ if (string.Equals(definition.Id, BuiltInComponentIds.LunarCalendar, StringComparison.OrdinalIgnoreCase))
+ {
+ return L("component.lunar_calendar", definition.DisplayName);
+ }
+
return definition.DisplayName;
}
@@ -1460,6 +2347,71 @@ public partial class MainWindow
}
}
+ private bool _isComponentLibraryWindowDragging;
+ private Point _componentLibraryWindowDragStartPoint;
+ private Thickness _componentLibraryWindowOriginalMargin;
+ private bool _isComponentLibraryWindowPositionCustomized;
+
+ private void OnComponentLibraryWindowPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (ComponentLibraryWindow is null || !_isComponentLibraryOpen)
+ {
+ return;
+ }
+
+ var point = e.GetPosition(ComponentLibraryWindow);
+ if (point.Y > 40) // 閺嶅洭顣介弽蹇涚彯鎼达妇瀹虫稉?0px
+ {
+ return;
+ }
+
+ _isComponentLibraryWindowDragging = true;
+ _componentLibraryWindowDragStartPoint = e.GetPosition(this);
+ _componentLibraryWindowOriginalMargin = ComponentLibraryWindow.Margin;
+
+ e.Pointer.Capture(ComponentLibraryWindow);
+ e.Handled = true;
+ }
+
+ private void OnComponentLibraryWindowPointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (!_isComponentLibraryWindowDragging || ComponentLibraryWindow is null)
+ {
+ return;
+ }
+
+ var currentPoint = e.GetPosition(this);
+ var delta = currentPoint - _componentLibraryWindowDragStartPoint;
+
+ var newMargin = new Thickness(
+ Math.Max(10, _componentLibraryWindowOriginalMargin.Left + delta.X),
+ Math.Max(10, _componentLibraryWindowOriginalMargin.Top + delta.Y),
+ Math.Max(10, _componentLibraryWindowOriginalMargin.Right - delta.X),
+ Math.Max(10, _componentLibraryWindowOriginalMargin.Bottom - delta.Y)
+ );
+
+ ComponentLibraryWindow.Margin = newMargin;
+ e.Handled = true;
+ }
+
+ private void OnComponentLibraryWindowPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (!_isComponentLibraryWindowDragging)
+ {
+ return;
+ }
+
+ _isComponentLibraryWindowDragging = false;
+ e.Pointer.Capture(null);
+
+ if (ComponentLibraryWindow is not null)
+ {
+ SaveComponentLibraryWindowPosition();
+ }
+
+ e.Handled = true;
+ }
+
private void OnComponentLibraryBackClick(object? sender, RoutedEventArgs e)
{
ShowComponentLibraryCategoryView();
@@ -1584,6 +2536,30 @@ public partial class MainWindow
_componentLibraryComponentHostTransform.X = Math.Clamp(tentative, minOffset, 0);
}
+ private void SaveComponentLibraryWindowPosition()
+ {
+ if (ComponentLibraryWindow is null)
+ {
+ return;
+ }
+
+ var margin = ComponentLibraryWindow.Margin;
+ _savedComponentLibraryMargin = margin;
+ _isComponentLibraryWindowPositionCustomized = true;
+ }
+
+ private void RestoreComponentLibraryWindowPosition()
+ {
+ if (ComponentLibraryWindow is null)
+ {
+ return;
+ }
+
+ ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
+ }
+
+ private Thickness _savedComponentLibraryMargin = new Thickness(24, 24, 24, 100);
+
private void OnComponentLibraryComponentViewportPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_isComponentLibraryComponentGestureActive ||
@@ -1622,3 +2598,4 @@ public partial class MainWindow
ApplyComponentLibraryComponentOffset();
}
}
+
diff --git a/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs
index 3ae828f..9ea81cd 100644
--- a/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs
+++ b/LanMontainDesktop/Views/MainWindow.DesktopPaging.cs
@@ -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 动态设置
diff --git a/LanMontainDesktop/Views/MainWindow.Localization.cs b/LanMontainDesktop/Views/MainWindow.Localization.cs
index f38bb4d..3208c4f 100644
--- a/LanMontainDesktop/Views/MainWindow.Localization.cs
+++ b/LanMontainDesktop/Views/MainWindow.Localization.cs
@@ -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");
diff --git a/LanMontainDesktop/Views/MainWindow.Settings.cs b/LanMontainDesktop/Views/MainWindow.Settings.cs
index 46acf3f..b95a627 100644
--- a/LanMontainDesktop/Views/MainWindow.Settings.cs
+++ b/LanMontainDesktop/Views/MainWindow.Settings.cs
@@ -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
diff --git a/LanMontainDesktop/Views/MainWindow.axaml b/LanMontainDesktop/Views/MainWindow.axaml
index 32b919f..4f85dfe 100644
--- a/LanMontainDesktop/Views/MainWindow.axaml
+++ b/LanMontainDesktop/Views/MainWindow.axaml
@@ -126,7 +126,7 @@
Grid.Column="1"
Classes="glass-panel"
ClipToBounds="False"
- CornerRadius="18"
+ CornerRadius="36"
Padding="18">
@@ -160,7 +160,7 @@
Margin="52"
MaxWidth="760"
MaxHeight="520"
- CornerRadius="18"
+ CornerRadius="36"
Padding="14">
@@ -232,12 +232,13 @@
@@ -374,14 +375,14 @@