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 @@