0.2.3
小白板,天气,时钟
1
.gitignore
vendored
@@ -480,3 +480,4 @@ $RECYCLE.BIN/
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
nul
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Resources>
|
||||
<FontFamily x:Key="AppFontFamily">avares://LanMontainDesktop/Assets/Fonts#MiSans</FontFamily>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
@@ -15,6 +19,14 @@
|
||||
<sty:FluentAvaloniaTheme />
|
||||
<StyleInclude Source="avares://LanMontainDesktop/Styles/GlassModule.axaml" />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="UserControl">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="fi|SymbolIcon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
|
||||
BIN
LanMontainDesktop/Assets/Fonts/MiSans-Bold.ttf
Normal file
22
LanMontainDesktop/Assets/Fonts/MiSans-NOTICE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# MiSans Font Notice
|
||||
|
||||
This app bundles MiSans fonts for consistent cross-device rendering.
|
||||
|
||||
## Included files
|
||||
|
||||
- `MiSans-Regular.ttf`
|
||||
- `MiSans-Semibold.ttf`
|
||||
- `MiSans-Bold.ttf`
|
||||
|
||||
## Source
|
||||
|
||||
- Upstream package repository: https://github.com/dsrkafuu/misans
|
||||
- Original font source referenced by upstream: https://hyperos.mi.com/font/zh/
|
||||
|
||||
## License and usage notes
|
||||
|
||||
- Script/package license in upstream repository: Apache-2.0
|
||||
- MiSans font copyright and additional usage terms:
|
||||
https://hyperos.mi.com/font-download/MiSans%E5%AD%97%E4%BD%93%E7%9F%A5%E8%AF%86%E4%BA%A7%E6%9D%83%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE.pdf
|
||||
|
||||
Please review and comply with the MiSans font terms when distributing this app.
|
||||
BIN
LanMontainDesktop/Assets/Fonts/MiSans-Regular.ttf
Normal file
BIN
LanMontainDesktop/Assets/Fonts/MiSans-Semibold.ttf
Normal file
29
LanMontainDesktop/Assets/Weather/ATTRIBUTION.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Weather Background Assets
|
||||
|
||||
Weather card background images are sourced from **Pexels** and used under the Pexels license:
|
||||
https://www.pexels.com/license/
|
||||
|
||||
## Sources
|
||||
|
||||
- `clear_sky.jpg`
|
||||
- https://www.pexels.com/photo/a-clear-blue-sky-with-few-clouds-on-a-sunny-day-29390199/
|
||||
- `rain.jpg`
|
||||
- https://www.pexels.com/photo/rain-on-window-with-bokeh-lights-35075853/
|
||||
- `snow.jpg`
|
||||
- https://www.pexels.com/photo/mountain-covered-with-snow-209955/
|
||||
- `storm.jpg`
|
||||
- https://www.pexels.com/photo/sea-under-a-stormy-sky-4609228/
|
||||
|
||||
## Derived Variants (for widget scene mapping)
|
||||
|
||||
The following files are generated from the above base assets by color grading/brightness adjustments to match the ColorOS-like weather card style:
|
||||
|
||||
- `clear_day.jpg` (from `clear_sky.jpg`)
|
||||
- `clear_night.jpg` (from `clear_sky.jpg`)
|
||||
- `cloudy_day.jpg` (from `clear_sky.jpg`)
|
||||
- `cloudy_night.jpg` (from `clear_sky.jpg`)
|
||||
- `rain_light.jpg` (from `rain.jpg`)
|
||||
- `rain_heavy.jpg` (from `rain.jpg`)
|
||||
- `storm_dark.jpg` (from `storm.jpg`)
|
||||
- `fog_haze.jpg` (from `storm.jpg`)
|
||||
- `snow_soft.jpg` (from `snow.jpg`)
|
||||
BIN
LanMontainDesktop/Assets/Weather/clear_day.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
LanMontainDesktop/Assets/Weather/clear_night.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
LanMontainDesktop/Assets/Weather/clear_sky.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
LanMontainDesktop/Assets/Weather/cloudy_day.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
LanMontainDesktop/Assets/Weather/cloudy_night.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
LanMontainDesktop/Assets/Weather/fog_haze.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
LanMontainDesktop/Assets/Weather/rain.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
LanMontainDesktop/Assets/Weather/rain_heavy.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
LanMontainDesktop/Assets/Weather/rain_light.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
LanMontainDesktop/Assets/Weather/snow.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
LanMontainDesktop/Assets/Weather/snow_soft.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
LanMontainDesktop/Assets/Weather/storm.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
LanMontainDesktop/Assets/Weather/storm_dark.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
@@ -4,10 +4,16 @@ public static class BuiltInComponentIds
|
||||
{
|
||||
public const string Clock = "Clock";
|
||||
public const string DesktopClock = "DesktopClock";
|
||||
public const string DesktopWeatherClock = "DesktopWeatherClock";
|
||||
public const string DesktopTimer = "DesktopTimer";
|
||||
public const string DesktopWeather = "DesktopWeather";
|
||||
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
|
||||
public const string DesktopMultiDayWeather = "DesktopMultiDayWeather";
|
||||
public const string Blank2x4 = "Blank2x4";
|
||||
public const string Date = "Date";
|
||||
public const string MonthCalendar = "MonthCalendar";
|
||||
public const string LunarCalendar = "LunarCalendar";
|
||||
public const string HolidayCalendar = "HolidayCalendar";
|
||||
public const string DesktopWhiteboard = "DesktopWhiteboard";
|
||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||
}
|
||||
|
||||
@@ -39,6 +39,15 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWeatherClock,
|
||||
"Weather Clock",
|
||||
"Clock",
|
||||
"Clock",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"Timer",
|
||||
@@ -48,6 +57,51 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWeather,
|
||||
"Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopHourlyWeather,
|
||||
"Hourly Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopMultiDayWeather,
|
||||
"Multi-day Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWhiteboard,
|
||||
"Blackboard Portrait",
|
||||
"Edit",
|
||||
"Board",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||
"Blackboard Landscape",
|
||||
"Edit",
|
||||
"Board",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Date,
|
||||
"Calendar",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageReference Include="DotNetCampus.AvaloniaInkCanvas" Version="1.0.1" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" Version="2.0.319" />
|
||||
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.319" />
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"settings.nav.grid": "Grid",
|
||||
"settings.nav.color": "Color",
|
||||
"settings.nav.status_bar": "Status Bar",
|
||||
"settings.nav.weather": "Weather",
|
||||
"settings.nav.region": "Region",
|
||||
"settings.nav.about": "About",
|
||||
"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",
|
||||
@@ -74,13 +76,88 @@
|
||||
"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.weather.title": "Weather",
|
||||
"settings.weather.location_source_header": "Location Source",
|
||||
"settings.weather.location_source_desc": "Choose how weather widgets resolve location.",
|
||||
"settings.weather.mode_city_search": "City Search",
|
||||
"settings.weather.mode_coordinates": "Coordinates",
|
||||
"settings.weather.auto_refresh": "Auto refresh location on startup",
|
||||
"settings.weather.city_search_header": "City Search",
|
||||
"settings.weather.city_search_desc": "Search cities and apply one weather location.",
|
||||
"settings.weather.search_placeholder": "e.g. Beijing",
|
||||
"settings.weather.search_button": "Search",
|
||||
"settings.weather.apply_city_button": "Apply City",
|
||||
"settings.weather.search_hint": "Search by city name and apply one location.",
|
||||
"settings.weather.search_required": "Please enter a city keyword first.",
|
||||
"settings.weather.search_no_results": "No locations were found.",
|
||||
"settings.weather.search_failed_format": "Search failed: {0}",
|
||||
"settings.weather.search_result_count_format": "Found {0} locations.",
|
||||
"settings.weather.search_select_required": "Please select one location from search results.",
|
||||
"settings.weather.search_applied_format": "Location applied: {0}",
|
||||
"settings.weather.coordinates_header": "Coordinates",
|
||||
"settings.weather.coordinates_desc": "Set latitude/longitude and optional key/name.",
|
||||
"settings.weather.latitude_label": "Latitude",
|
||||
"settings.weather.longitude_label": "Longitude",
|
||||
"settings.weather.location_key_placeholder": "Location key (optional)",
|
||||
"settings.weather.location_name_placeholder": "Display name (optional)",
|
||||
"settings.weather.apply_coordinates_button": "Apply Coordinates",
|
||||
"settings.weather.coordinates_saved_format": "Coordinates saved: {0:F4}, {1:F4}",
|
||||
"settings.weather.coordinates_default_name_format": "Coordinate {0:F4}, {1:F4}",
|
||||
"settings.weather.preview_header": "Connection Test",
|
||||
"settings.weather.preview_desc": "Send one test request to verify current settings.",
|
||||
"settings.weather.preview_button": "Test Fetch",
|
||||
"settings.weather.preview_hint": "Use test fetch to verify your weather configuration.",
|
||||
"settings.weather.preview_missing_location": "Please apply one weather location before testing.",
|
||||
"settings.weather.preview_success_format": "Test success: {0} · {1} · {2}",
|
||||
"settings.weather.preview_failed_format": "Test fetch failed: {0}",
|
||||
"settings.weather.preview_unknown": "Unknown",
|
||||
"settings.weather.status_city_empty": "No city location is configured.",
|
||||
"settings.weather.status_city_format": "Mode: {0} | {1} | Key: {2}",
|
||||
"settings.weather.status_coordinates_format": "Mode: {0} | Lat {1:F4}, Lon {2:F4} | Key: {3}",
|
||||
"settings.weather.location_header": "Weather Location",
|
||||
"settings.weather.location_desc": "Set the location used by weather widgets.",
|
||||
"settings.weather.location_placeholder": "e.g. Beijing",
|
||||
"settings.weather.location_apply": "Save",
|
||||
"settings.weather.location_empty": "Weather location is not set.",
|
||||
"settings.weather.location_required": "Weather location cannot be empty.",
|
||||
"settings.weather.location_current_format": "Current weather location: {0}",
|
||||
"settings.weather.location_saved_format": "Weather location saved: {0}",
|
||||
"weather.widget.location_not_configured": "Weather location is not configured",
|
||||
"weather.widget.configure_hint": "Open Settings > Weather to configure",
|
||||
"weather.widget.loading": "Loading...",
|
||||
"weather.widget.fetch_failed": "Weather fetch failed",
|
||||
"weather.widget.retrying": "Retrying automatically",
|
||||
"weather.widget.location_unknown": "Unknown location",
|
||||
"weather.widget.condition_clear": "Clear",
|
||||
"weather.widget.condition_cloudy": "Cloudy",
|
||||
"weather.widget.condition_rain": "Rain",
|
||||
"weather.widget.condition_storm": "Thunderstorm",
|
||||
"weather.widget.condition_snow": "Snow",
|
||||
"weather.widget.condition_fog": "Fog",
|
||||
"weather.widget.condition_unknown": "Unknown",
|
||||
"weather.widget.range_unknown": "-- / --",
|
||||
"weather.widget.range_format": "{0} / {1}",
|
||||
"weather.widget.aqi_unknown": "AQI --",
|
||||
"weather.widget.aqi_format": "AQI {0}",
|
||||
"weather.widget.updated_format": "Updated {0:HH:mm}",
|
||||
"weather.hourly.now": "Now",
|
||||
"weather.multiday.today": "Today",
|
||||
"weather.multiday.tomorrow": "Tomorrow",
|
||||
"weather.multiday.aqi_format": "Air Quality {0}",
|
||||
"weather.multiday.aqi_unknown": "Air --",
|
||||
"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.timezone_header": "Time Zone",
|
||||
"settings.region.timezone_desc": "Select a time zone. Clock and calendar widgets will follow this zone.",
|
||||
"settings.region.applied_format": "Language switched to: {0}",
|
||||
"settings.about.title": "About",
|
||||
"settings.about.version_format": "Version: {0}",
|
||||
"settings.about.codename_format": "Code Name: {0}",
|
||||
"settings.about.font_format": "Font: {0}",
|
||||
"settings.footer": "LanMontainDesktop Settings",
|
||||
"filepicker.title": "Select wallpaper",
|
||||
"filepicker.image_files": "Image files",
|
||||
@@ -106,11 +183,19 @@
|
||||
"component.edit": "Edit",
|
||||
"component_category.clock": "Clock",
|
||||
"component_category.date": "Calendar",
|
||||
"component_category.weather": "Weather",
|
||||
"component_category.board": "Board",
|
||||
"component.date": "Calendar",
|
||||
"component.month_calendar": "Month Calendar",
|
||||
"component.lunar_calendar": "Lunar Calendar",
|
||||
"component.desktop_clock": "Clock",
|
||||
"component.weather_clock": "Weather Clock",
|
||||
"component.desktop_timer": "Timer",
|
||||
"component.desktop_weather": "Weather",
|
||||
"component.hourly_weather": "Hourly Weather",
|
||||
"component.multiday_weather": "Multi-day Weather",
|
||||
"component.whiteboard": "Blackboard (Portrait)",
|
||||
"component.blackboard_landscape": "Blackboard (Landscape)",
|
||||
"component.holiday_calendar": "Holiday Calendar",
|
||||
"desktop.add_page": "Add page",
|
||||
"desktop.delete_page": "Delete page",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"settings.nav.grid": "网格",
|
||||
"settings.nav.color": "颜色",
|
||||
"settings.nav.status_bar": "状态栏",
|
||||
"settings.nav.weather": "天气",
|
||||
"settings.nav.region": "地区",
|
||||
"settings.nav.about": "关于",
|
||||
"settings.wallpaper.title": "壁纸",
|
||||
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.current_label": "当前壁纸",
|
||||
@@ -74,13 +76,88 @@
|
||||
"settings.status_bar.spacing_mode_custom": "自定义",
|
||||
"settings.status_bar.spacing_custom_label": "自定义间距(%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.weather.title": "天气",
|
||||
"settings.weather.location_source_header": "位置来源",
|
||||
"settings.weather.location_source_desc": "选择天气组件如何解析当前位置。",
|
||||
"settings.weather.mode_city_search": "城市搜索",
|
||||
"settings.weather.mode_coordinates": "坐标输入",
|
||||
"settings.weather.auto_refresh": "启动时自动刷新位置",
|
||||
"settings.weather.city_search_header": "城市搜索",
|
||||
"settings.weather.city_search_desc": "搜索城市并应用一个天气位置。",
|
||||
"settings.weather.search_placeholder": "例如:北京",
|
||||
"settings.weather.search_button": "搜索",
|
||||
"settings.weather.apply_city_button": "应用城市",
|
||||
"settings.weather.search_hint": "输入城市名称进行搜索,然后应用一个结果。",
|
||||
"settings.weather.search_required": "请先输入城市关键字。",
|
||||
"settings.weather.search_no_results": "未找到匹配的位置。",
|
||||
"settings.weather.search_failed_format": "搜索失败:{0}",
|
||||
"settings.weather.search_result_count_format": "共找到 {0} 个位置。",
|
||||
"settings.weather.search_select_required": "请先从搜索结果中选择一个位置。",
|
||||
"settings.weather.search_applied_format": "已应用位置:{0}",
|
||||
"settings.weather.coordinates_header": "坐标输入",
|
||||
"settings.weather.coordinates_desc": "设置经纬度,并可选填写位置 key 和显示名。",
|
||||
"settings.weather.latitude_label": "纬度",
|
||||
"settings.weather.longitude_label": "经度",
|
||||
"settings.weather.location_key_placeholder": "位置 key(可选)",
|
||||
"settings.weather.location_name_placeholder": "显示名称(可选)",
|
||||
"settings.weather.apply_coordinates_button": "应用坐标",
|
||||
"settings.weather.coordinates_saved_format": "坐标已保存:{0:F4}, {1:F4}",
|
||||
"settings.weather.coordinates_default_name_format": "坐标 {0:F4}, {1:F4}",
|
||||
"settings.weather.preview_header": "连接测试",
|
||||
"settings.weather.preview_desc": "发送一次测试请求,验证当前配置是否可用。",
|
||||
"settings.weather.preview_button": "测试获取",
|
||||
"settings.weather.preview_hint": "可通过测试获取快速验证天气配置。",
|
||||
"settings.weather.preview_missing_location": "请先应用一个天气位置后再测试。",
|
||||
"settings.weather.preview_success_format": "测试成功:{0} · {1} · {2}",
|
||||
"settings.weather.preview_failed_format": "测试失败:{0}",
|
||||
"settings.weather.preview_unknown": "未知",
|
||||
"settings.weather.status_city_empty": "尚未配置城市位置。",
|
||||
"settings.weather.status_city_format": "模式:{0}|{1}|Key:{2}",
|
||||
"settings.weather.status_coordinates_format": "模式:{0}|纬度 {1:F4},经度 {2:F4}|Key:{3}",
|
||||
"settings.weather.location_header": "天气位置",
|
||||
"settings.weather.location_desc": "设置天气组件使用的位置。",
|
||||
"settings.weather.location_placeholder": "例如:北京",
|
||||
"settings.weather.location_apply": "保存",
|
||||
"settings.weather.location_empty": "尚未设置天气位置。",
|
||||
"settings.weather.location_required": "天气位置不能为空。",
|
||||
"settings.weather.location_current_format": "当前天气位置:{0}",
|
||||
"settings.weather.location_saved_format": "天气位置已保存:{0}",
|
||||
"weather.widget.location_not_configured": "尚未配置天气位置",
|
||||
"weather.widget.configure_hint": "请前往 设置 > 天气 完成配置",
|
||||
"weather.widget.loading": "加载中...",
|
||||
"weather.widget.fetch_failed": "天气获取失败",
|
||||
"weather.widget.retrying": "稍后会自动重试",
|
||||
"weather.widget.location_unknown": "未知位置",
|
||||
"weather.widget.condition_clear": "晴",
|
||||
"weather.widget.condition_cloudy": "多云",
|
||||
"weather.widget.condition_rain": "雨",
|
||||
"weather.widget.condition_storm": "雷暴",
|
||||
"weather.widget.condition_snow": "雪",
|
||||
"weather.widget.condition_fog": "雾",
|
||||
"weather.widget.condition_unknown": "未知天气",
|
||||
"weather.widget.range_unknown": "-- / --",
|
||||
"weather.widget.range_format": "{0} / {1}",
|
||||
"weather.widget.aqi_unknown": "AQI --",
|
||||
"weather.widget.aqi_format": "AQI {0}",
|
||||
"weather.widget.updated_format": "更新于 {0:HH:mm}",
|
||||
"weather.hourly.now": "现在",
|
||||
"weather.multiday.today": "今天",
|
||||
"weather.multiday.tomorrow": "明天",
|
||||
"weather.multiday.aqi_format": "空气优 {0}",
|
||||
"weather.multiday.aqi_unknown": "空气 --",
|
||||
"settings.region.title": "地区",
|
||||
"settings.region.description": "选择语言并立即应用到设置与主要界面。",
|
||||
"settings.region.language_header": "语言",
|
||||
"settings.region.language_label": "语言",
|
||||
"settings.region.language_zh": "中文",
|
||||
"settings.region.language_en": "英文",
|
||||
"settings.region.timezone_header": "时区",
|
||||
"settings.region.timezone_desc": "选择时区。时钟与日历组件会使用该时区。",
|
||||
"settings.region.applied_format": "语言已切换为:{0}",
|
||||
"settings.about.title": "关于",
|
||||
"settings.about.version_format": "版本号: {0}",
|
||||
"settings.about.codename_format": "版本代号: {0}",
|
||||
"settings.about.font_format": "字体: {0}",
|
||||
"settings.footer": "LanMontainDesktop 设置",
|
||||
"filepicker.title": "选择壁纸",
|
||||
"filepicker.image_files": "图片文件",
|
||||
@@ -106,11 +183,19 @@
|
||||
"component.edit": "编辑",
|
||||
"component_category.clock": "时钟",
|
||||
"component_category.date": "日历",
|
||||
"component_category.weather": "天气",
|
||||
"component_category.board": "白板",
|
||||
"component.date": "日历",
|
||||
"component.month_calendar": "月历",
|
||||
"component.lunar_calendar": "农历",
|
||||
"component.desktop_clock": "时钟",
|
||||
"component.weather_clock": "天气时钟",
|
||||
"component.desktop_timer": "计时器",
|
||||
"component.desktop_weather": "天气",
|
||||
"component.hourly_weather": "小时天气",
|
||||
"component.multiday_weather": "多日天气",
|
||||
"component.whiteboard": "竖向小黑板",
|
||||
"component.blackboard_landscape": "横向小黑板",
|
||||
"component.holiday_calendar": "节假日日历",
|
||||
"desktop.add_page": "新增页面",
|
||||
"desktop.delete_page": "删除页面",
|
||||
|
||||
@@ -24,6 +24,20 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public string? TimeZoneId { get; set; }
|
||||
|
||||
public string WeatherLocationMode { get; set; } = "CitySearch";
|
||||
|
||||
public string WeatherLocationKey { get; set; } = string.Empty;
|
||||
|
||||
public string WeatherLocationName { get; set; } = string.Empty;
|
||||
|
||||
public double WeatherLatitude { get; set; } = 39.9042;
|
||||
|
||||
public double WeatherLongitude { get; set; } = 116.4074;
|
||||
|
||||
public bool WeatherAutoRefreshLocation { get; set; }
|
||||
|
||||
public string WeatherLocationQuery { get; set; } = string.Empty;
|
||||
|
||||
public List<string> TopStatusComponentIds { get; set; } = [];
|
||||
|
||||
public List<string> PinnedTaskbarActions { get; set; } =
|
||||
|
||||
@@ -7,5 +7,6 @@ public enum TaskbarContext
|
||||
SettingsGrid,
|
||||
SettingsColor,
|
||||
SettingsStatusBar,
|
||||
SettingsWeather,
|
||||
SettingsRegion
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ public sealed record WeatherDailyForecast(
|
||||
string? SunsetTime,
|
||||
int? PrecipitationProbabilityPercent);
|
||||
|
||||
public sealed record WeatherHourlyForecast(
|
||||
DateTimeOffset Time,
|
||||
double? TemperatureC,
|
||||
int? WeatherCode,
|
||||
string? WeatherText);
|
||||
|
||||
public sealed record WeatherSnapshot(
|
||||
string Provider,
|
||||
string LocationKey,
|
||||
@@ -41,5 +47,5 @@ public sealed record WeatherSnapshot(
|
||||
DateTimeOffset FetchedAt,
|
||||
DateTimeOffset? ObservationTime,
|
||||
WeatherCurrentCondition Current,
|
||||
IReadOnlyList<WeatherDailyForecast> DailyForecasts);
|
||||
|
||||
IReadOnlyList<WeatherDailyForecast> DailyForecasts,
|
||||
IReadOnlyList<WeatherHourlyForecast> HourlyForecasts);
|
||||
|
||||
@@ -31,9 +31,13 @@ public sealed record WeatherQueryResult<T>(
|
||||
}
|
||||
}
|
||||
|
||||
public interface IWeatherDataService
|
||||
public interface IWeatherInfoService
|
||||
{
|
||||
Task<WeatherQueryResult<WeatherSnapshot>> GetWeatherAsync(WeatherQuery query, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IWeatherDataService : IWeatherInfoService
|
||||
{
|
||||
|
||||
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
|
||||
string keyword,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -310,6 +311,9 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
var currentNode = TryGetNode(payload, "current") ?? payload;
|
||||
var cityNode = TryGetNode(payload, "city");
|
||||
var dailyNode = TryGetNode(payload, "forecastDaily") ?? TryGetNode(payload, "daily");
|
||||
var hourlyNode = TryGetNode(payload, "forecastHourly") ??
|
||||
TryGetNode(payload, "hourly") ??
|
||||
TryGetNode(payload, "hourlyForecast");
|
||||
|
||||
var weatherCode = ReadInt(currentNode, "weather", "value") ??
|
||||
ReadInt(currentNode, "weatherCode") ??
|
||||
@@ -334,6 +338,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
WeatherText: weatherText);
|
||||
|
||||
var forecasts = ParseDailyForecasts(dailyNode, days, locale);
|
||||
var hourlyForecasts = ParseHourlyForecasts(hourlyNode, locale);
|
||||
|
||||
var locationName = ReadString(cityNode, "name") ??
|
||||
ReadString(payload, "cityName") ??
|
||||
@@ -351,7 +356,8 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
FetchedAt: DateTimeOffset.UtcNow,
|
||||
ObservationTime: observationTime,
|
||||
Current: current,
|
||||
DailyForecasts: forecasts);
|
||||
DailyForecasts: forecasts,
|
||||
HourlyForecasts: hourlyForecasts);
|
||||
}
|
||||
|
||||
private IReadOnlyList<WeatherDailyForecast> ParseDailyForecasts(JsonElement? dailyNode, int days, string locale)
|
||||
@@ -411,6 +417,151 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
return forecasts;
|
||||
}
|
||||
|
||||
private IReadOnlyList<WeatherHourlyForecast> ParseHourlyForecasts(JsonElement? hourlyNode, string locale)
|
||||
{
|
||||
var forecasts = new List<WeatherHourlyForecast>();
|
||||
if (!hourlyNode.HasValue)
|
||||
{
|
||||
return forecasts;
|
||||
}
|
||||
|
||||
var root = hourlyNode.Value;
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
ParseHourlyArray(root, locale, forecasts);
|
||||
return forecasts
|
||||
.OrderBy(item => item.Time)
|
||||
.Take(48)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return forecasts;
|
||||
}
|
||||
|
||||
var directArray =
|
||||
ReadArray(root, "value") ??
|
||||
ReadArray(root, "list") ??
|
||||
ReadArray(root, "hourly");
|
||||
|
||||
if (directArray.HasValue && directArray.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
ParseHourlyArray(directArray.Value, locale, forecasts);
|
||||
}
|
||||
|
||||
var timeArray =
|
||||
ReadArray(root, "time", "value") ??
|
||||
ReadArray(root, "datetime", "value") ??
|
||||
ReadArray(root, "date", "value") ??
|
||||
ReadArray(root, "pubTime", "value");
|
||||
var tempArray =
|
||||
ReadArray(root, "temperature", "value") ??
|
||||
ReadArray(root, "temp", "value") ??
|
||||
ReadArray(root, "temperature");
|
||||
var weatherArray =
|
||||
ReadArray(root, "weather", "value") ??
|
||||
ReadArray(root, "weatherCode", "value") ??
|
||||
ReadArray(root, "weather");
|
||||
|
||||
var count = Math.Max(
|
||||
timeArray?.GetArrayLength() ?? 0,
|
||||
Math.Max(
|
||||
tempArray?.GetArrayLength() ?? 0,
|
||||
weatherArray?.GetArrayLength() ?? 0));
|
||||
count = Math.Clamp(count, 0, 72);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var timeItem = GetArrayItem(timeArray, i);
|
||||
var tempItem = GetArrayItem(tempArray, i);
|
||||
var weatherItem = GetArrayItem(weatherArray, i);
|
||||
|
||||
var time = ParseTime(
|
||||
ReadString(timeItem, "value") ??
|
||||
ReadString(timeItem, "datetime") ??
|
||||
ReadString(timeItem, "time") ??
|
||||
ReadString(timeItem, "date") ??
|
||||
ReadString(timeItem));
|
||||
if (!time.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(weatherItem, "value") ??
|
||||
ReadInt(weatherItem, "code") ??
|
||||
ReadInt(weatherItem, "weatherCode") ??
|
||||
ReadInt(weatherItem, "from") ??
|
||||
ReadInt(weatherItem);
|
||||
var weatherText = ReadString(weatherItem, "text") ??
|
||||
ReadString(weatherItem, "desc") ??
|
||||
ResolveWeatherDescription(code, locale);
|
||||
|
||||
forecasts.Add(new WeatherHourlyForecast(
|
||||
Time: time.Value,
|
||||
TemperatureC: ReadDouble(tempItem, "value") ??
|
||||
ReadDouble(tempItem, "temperature") ??
|
||||
ReadDouble(tempItem, "temp") ??
|
||||
ReadDouble(tempItem),
|
||||
WeatherCode: code,
|
||||
WeatherText: weatherText));
|
||||
}
|
||||
|
||||
return forecasts
|
||||
.GroupBy(item => item.Time.ToUnixTimeSeconds())
|
||||
.Select(group => group.First())
|
||||
.OrderBy(item => item.Time)
|
||||
.Take(48)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void ParseHourlyArray(JsonElement array, string locale, ICollection<WeatherHourlyForecast> output)
|
||||
{
|
||||
foreach (var item in array.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind is not (JsonValueKind.Object or JsonValueKind.String or JsonValueKind.Number))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var time = ParseTime(
|
||||
ReadString(item, "datetime") ??
|
||||
ReadString(item, "time") ??
|
||||
ReadString(item, "date") ??
|
||||
ReadString(item, "forecastTime") ??
|
||||
ReadString(item, "pubTime") ??
|
||||
ReadString(item, "ts") ??
|
||||
ReadString(item));
|
||||
if (!time.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(item, "weatherCode") ??
|
||||
ReadInt(item, "code") ??
|
||||
ReadInt(item, "weather", "value") ??
|
||||
ReadInt(item, "weather") ??
|
||||
ReadInt(item, "from");
|
||||
var weatherText = ReadString(item, "weatherText") ??
|
||||
ReadString(item, "weather", "desc") ??
|
||||
ReadString(item, "weather", "text") ??
|
||||
ReadString(item, "desc") ??
|
||||
ResolveWeatherDescription(code, locale);
|
||||
|
||||
var temperature = ReadDouble(item, "temperature", "value") ??
|
||||
ReadDouble(item, "temperature") ??
|
||||
ReadDouble(item, "temp", "value") ??
|
||||
ReadDouble(item, "temp") ??
|
||||
ReadDouble(item, "value");
|
||||
|
||||
output.Add(new WeatherHourlyForecast(
|
||||
Time: time.Value,
|
||||
TemperatureC: temperature,
|
||||
WeatherCode: code,
|
||||
WeatherText: weatherText));
|
||||
}
|
||||
}
|
||||
|
||||
private static DateOnly? ResolveDateForIndex(JsonElement? dateArray, int index)
|
||||
{
|
||||
var item = GetArrayItem(dateArray, index);
|
||||
@@ -728,4 +879,3 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
: $"{text[..maxLength]}...";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls">
|
||||
|
||||
<Styles.Resources>
|
||||
<!-- Unified corner radius tokens used across settings and widget panels -->
|
||||
<CornerRadius x:Key="DesignCornerRadiusXl">32</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusLg">28</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusMd">20</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusSm">14</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusXs">12</CornerRadius>
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
@@ -55,6 +64,53 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope Border.settings-expander-shell">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope Button">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBoxItem">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||
<Setter Property="Padding" Value="10,6" />
|
||||
<Setter Property="Margin" Value="4,2" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ui|NumberBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||
<Setter Property="Padding" Value="10,6" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton:checked">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.swatch-button">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
|
||||
@@ -10,7 +10,7 @@ using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class AnalogClockWidget : UserControl
|
||||
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ public enum ClockDisplayFormat
|
||||
HourMinute // HH:mm
|
||||
}
|
||||
|
||||
public partial class ClockWidget : UserControl
|
||||
public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
ClipToBounds="True"
|
||||
Padding="12">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid Width="460"
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="460"
|
||||
Height="220"
|
||||
ColumnDefinitions="1.2*,1*"
|
||||
ColumnSpacing="12">
|
||||
@@ -24,85 +25,135 @@
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="GregorianHeadlineTextBlock"
|
||||
Grid.Row="0"
|
||||
FontSize="22"
|
||||
FontSize="30"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
|
||||
<UniformGrid Grid.Row="1"
|
||||
<UniformGrid x:Name="WeekdayHeaderGrid"
|
||||
Grid.Row="1"
|
||||
Columns="7">
|
||||
<TextBlock x:Name="WeekdayText0" Text="日" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText1" Text="一" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText2" Text="二" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText3" Text="三" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText4" Text="四" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText5" Text="五" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText6" Text="六" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText0"
|
||||
Text="S"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText1"
|
||||
Text="M"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText2"
|
||||
Text="T"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText3"
|
||||
Text="W"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText4"
|
||||
Text="T"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText5"
|
||||
Text="F"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="WeekdayText6"
|
||||
Text="S"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
FontWeight="SemiBold" />
|
||||
</UniformGrid>
|
||||
|
||||
<Grid x:Name="CalendarGrid"
|
||||
Grid.Row="2"
|
||||
RowDefinitions="*,*,*,*,*"
|
||||
RowDefinitions="*,*,*,*,*,*"
|
||||
ColumnDefinitions="*,*,*,*,*,*,*" />
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="LunarCardBorder"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource AdaptiveLayer2Brush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="24"
|
||||
BoxShadow="0 8 18 #1A000000"
|
||||
Padding="14">
|
||||
<Grid x:Name="RightPanelGrid"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="10">
|
||||
RowDefinitions="Auto,Auto,Auto,*,*"
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="LunarDateTextBlock"
|
||||
Grid.Row="0"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<TextBlock x:Name="LunarMetaTextBlock"
|
||||
Grid.Row="1"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Opacity="0.88"
|
||||
TextWrapping="Wrap" />
|
||||
Opacity="0.86"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<Border Grid.Row="2"
|
||||
<Border x:Name="DividerBorder"
|
||||
Grid.Row="2"
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Margin="0,1,0,1"
|
||||
Background="{DynamicResource AdaptiveStrokeBrush}" />
|
||||
|
||||
<Grid Grid.Row="3"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
ColumnSpacing="8"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock x:Name="YiLabelTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="宜"
|
||||
Text="Yi"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="#4E7D3A" />
|
||||
Foreground="#4E7D3A"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock x:Name="YiItemsTextBlock"
|
||||
Grid.Column="1"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
VerticalAlignment="Top" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="4"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
ColumnSpacing="8"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock x:Name="JiLabelTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="忌"
|
||||
Text="Ji"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="#A1473E" />
|
||||
Foreground="#A1473E"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock x:Name="JiItemsTextBlock"
|
||||
Grid.Column="1"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
VerticalAlignment="Top" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -4,26 +4,97 @@ using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class DateWidget : UserControl
|
||||
public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
private static readonly LunarCalendarService LunarCalendarService = new();
|
||||
|
||||
private static readonly string[] ZhWeekdayHeaders = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
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 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 = 64;
|
||||
private double _calendarDayFontSize = 14;
|
||||
private double _calendarTodayDotSize = 28;
|
||||
private double _weekdayFontSize = 17;
|
||||
private FontWeight _weekdayFontWeight = FontWeight.SemiBold;
|
||||
private double _calendarDayFontSize = 18;
|
||||
private FontWeight _calendarDayFontWeight = FontWeight.SemiBold;
|
||||
private double _calendarTodayDotSize = 32;
|
||||
private int _lunarItemCount = 3;
|
||||
private int _calendarVisibleRows = 6;
|
||||
private bool? _isNightModeApplied;
|
||||
private double _weekdayHeaderOpacity = 0.60;
|
||||
private double _weekdayNumberOpacity = 0.90;
|
||||
private double _weekendNumberOpacity = 0.58;
|
||||
|
||||
public DateWidget()
|
||||
{
|
||||
@@ -38,7 +109,7 @@ public partial class DateWidget : UserControl
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
if (_timeZoneService != null)
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
}
|
||||
@@ -82,56 +153,67 @@ public partial class DateWidget : UserControl
|
||||
var lunar = LunarCalendarService.GetLunarInfo(now);
|
||||
|
||||
GregorianHeadlineTextBlock.Text = isZh
|
||||
? $"{now.Month}月{now.Day}日 {ToChineseWeekday(now.DayOfWeek)}"
|
||||
: now.ToString("MMM d ddd", culture);
|
||||
? $"{now.Month}\u6708{now.Day}\u65e5"
|
||||
: now.ToString("MMM d", culture);
|
||||
|
||||
ApplyAdaptiveTypography();
|
||||
|
||||
if (isZh)
|
||||
{
|
||||
LunarDateTextBlock.Text = $"农历 {lunar.LunarDateZh}";
|
||||
LunarMetaTextBlock.Text = $"{lunar.GanzhiYearZh}年({lunar.ZodiacZh}年)";
|
||||
YiLabelTextBlock.Text = "宜";
|
||||
JiLabelTextBlock.Text = "忌";
|
||||
YiItemsTextBlock.Text = "祭祀 祈福 出行 会友";
|
||||
JiItemsTextBlock.Text = "动土 诉讼 远航 争执";
|
||||
LunarDateTextBlock.Text = lunar.LunarDateZh;
|
||||
LunarMetaTextBlock.Text = $"{lunar.GanzhiYearZh}\u5e74 {lunar.ZodiacZh}";
|
||||
YiLabelTextBlock.Text = "\u5b9c";
|
||||
JiLabelTextBlock.Text = "\u5fcc";
|
||||
}
|
||||
else
|
||||
{
|
||||
LunarDateTextBlock.Text = $"Lunar {lunar.LunarDateEn}";
|
||||
LunarMetaTextBlock.Text = $"Ganzhi year: {lunar.GanzhiYearEn} ({lunar.ZodiacEn})";
|
||||
LunarMetaTextBlock.Text = $"{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);
|
||||
}
|
||||
var itemCount = isZh ? _lunarItemCount : Math.Max(1, _lunarItemCount - 1);
|
||||
YiItemsTextBlock.Text = BuildDailySelection(
|
||||
now.Date,
|
||||
isZh ? ZhYiCandidates : EnYiCandidates,
|
||||
count: itemCount,
|
||||
salt: 17,
|
||||
useChineseSpacing: isZh);
|
||||
JiItemsTextBlock.Text = BuildDailySelection(
|
||||
now.Date,
|
||||
isZh ? ZhJiCandidates : EnJiCandidates,
|
||||
count: itemCount,
|
||||
salt: 29,
|
||||
useChineseSpacing: isZh);
|
||||
|
||||
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
|
||||
{
|
||||
return dayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Sunday => "周日",
|
||||
DayOfWeek.Monday => "周一",
|
||||
DayOfWeek.Tuesday => "周二",
|
||||
DayOfWeek.Wednesday => "周三",
|
||||
DayOfWeek.Thursday => "周四",
|
||||
DayOfWeek.Friday => "周五",
|
||||
_ => "周六"
|
||||
};
|
||||
UpdateWeekdayHeaders(isZh);
|
||||
ApplyModeVisualIfNeeded();
|
||||
GenerateCalendar(now);
|
||||
}
|
||||
|
||||
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];
|
||||
var blocks = GetWeekdayHeaderBlocks();
|
||||
for (var i = 0; i < blocks.Count; i++)
|
||||
{
|
||||
blocks[i].Text = headers[i];
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<TextBlock> GetWeekdayHeaderBlocks()
|
||||
{
|
||||
return
|
||||
[
|
||||
WeekdayText0,
|
||||
WeekdayText1,
|
||||
WeekdayText2,
|
||||
WeekdayText3,
|
||||
WeekdayText4,
|
||||
WeekdayText5,
|
||||
WeekdayText6
|
||||
];
|
||||
}
|
||||
|
||||
private void GenerateCalendar(DateTime currentDate)
|
||||
@@ -139,7 +221,8 @@ public partial class DateWidget : UserControl
|
||||
var removeList = new List<Control>();
|
||||
foreach (var child in CalendarGrid.Children)
|
||||
{
|
||||
if (child is Control control && control.Tag is string tag &&
|
||||
if (child is Control control &&
|
||||
control.Tag is string tag &&
|
||||
(tag == "day" || tag == "today-dot"))
|
||||
{
|
||||
removeList.Add(control);
|
||||
@@ -158,12 +241,19 @@ public partial class DateWidget : UserControl
|
||||
var firstDayOfMonth = new DateTime(year, month, 1);
|
||||
var daysInMonth = DateTime.DaysInMonth(year, month);
|
||||
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
|
||||
_calendarVisibleRows = GetCalendarRowCount(startDayOfWeek, daysInMonth);
|
||||
EnsureCalendarRows(_calendarVisibleRows);
|
||||
|
||||
// 4x2 widget has less vertical space than 2x2. Compress only on 6-row months.
|
||||
var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0;
|
||||
var dayFontSize = Math.Clamp(_calendarDayFontSize * rowDensity, 8, 24);
|
||||
var todayDotSize = Math.Clamp(_calendarTodayDotSize * rowDensity, 13.5, 32);
|
||||
|
||||
for (var day = 1; day <= daysInMonth; day++)
|
||||
{
|
||||
var row = (day + startDayOfWeek - 1) / 7;
|
||||
var col = (day + startDayOfWeek - 1) % 7;
|
||||
if (row > 4)
|
||||
if (row >= _calendarVisibleRows)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -173,8 +263,9 @@ public partial class DateWidget : UserControl
|
||||
Text = day.ToString(CultureInfo.CurrentCulture),
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = _calendarDayFontSize,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
FontSize = dayFontSize,
|
||||
FontWeight = _calendarDayFontWeight,
|
||||
LineHeight = dayFontSize * 1.04,
|
||||
Tag = "day"
|
||||
};
|
||||
|
||||
@@ -190,9 +281,9 @@ public partial class DateWidget : UserControl
|
||||
dayText.Foreground = onAccentBrush;
|
||||
var dot = new Border
|
||||
{
|
||||
Width = _calendarTodayDotSize,
|
||||
Height = _calendarTodayDotSize,
|
||||
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
|
||||
Width = todayDotSize,
|
||||
Height = todayDotSize,
|
||||
CornerRadius = new CornerRadius(todayDotSize * 0.5),
|
||||
Background = accentBrush,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
@@ -208,8 +299,8 @@ public partial class DateWidget : UserControl
|
||||
{
|
||||
var isWeekend = col is 0 or 6;
|
||||
dayText.Foreground = isWeekend
|
||||
? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.82)
|
||||
: GetThemeBrush("AdaptiveTextPrimaryBrush", 0.92);
|
||||
? GetThemeBrush("AdaptiveTextSecondaryBrush", _weekendNumberOpacity)
|
||||
: GetThemeBrush("AdaptiveTextPrimaryBrush", _weekdayNumberOpacity);
|
||||
Grid.SetRow(dayText, row);
|
||||
Grid.SetColumn(dayText, col);
|
||||
CalendarGrid.Children.Add(dayText);
|
||||
@@ -220,44 +311,155 @@ public partial class DateWidget : UserControl
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateDate();
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 16, 40));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(12 * scale, 8, 18));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
||||
|
||||
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
||||
LeftPanelGrid.RowSpacing = Math.Clamp(5.2 * scale, 2.5, 10);
|
||||
WeekdayHeaderGrid.Margin = new Thickness(
|
||||
0,
|
||||
Math.Clamp(0.5 * scale, 0, 2),
|
||||
0,
|
||||
Math.Clamp(2.4 * scale, 1, 4));
|
||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
||||
|
||||
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));
|
||||
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
||||
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
||||
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
||||
|
||||
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;
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var headerTextLength = Math.Max(1, GregorianHeadlineTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
||||
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
||||
|
||||
LunarDateTextBlock.FontSize = Math.Clamp(28 * scale, 17, 44);
|
||||
LunarMetaTextBlock.FontSize = Math.Clamp(14 * scale, 10, 22);
|
||||
YiLabelTextBlock.FontSize = Math.Clamp(18 * scale, 12, 28);
|
||||
GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 42);
|
||||
GregorianHeadlineTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
GregorianHeadlineTextBlock.LineHeight = GregorianHeadlineTextBlock.FontSize * 1.03;
|
||||
|
||||
_weekdayFontSize = Math.Clamp(14.8 * scale * densityBoost, 7, 20);
|
||||
_weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
foreach (var block in GetWeekdayHeaderBlocks())
|
||||
{
|
||||
block.FontSize = _weekdayFontSize;
|
||||
block.FontWeight = _weekdayFontWeight;
|
||||
block.LineHeight = _weekdayFontSize * 1.02;
|
||||
}
|
||||
|
||||
_calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 22);
|
||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.30, 13.5, 31);
|
||||
|
||||
var rightDensity = scale <= 0.72 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.38 ? 1.03 : 1.0;
|
||||
LunarDateTextBlock.FontSize = Math.Clamp(30 * scale * rightDensity, 14, 44);
|
||||
LunarMetaTextBlock.FontSize = Math.Clamp(12.5 * scale * rightDensity, 8.8, 18);
|
||||
YiLabelTextBlock.FontSize = Math.Clamp(16.5 * scale * rightDensity, 10, 23);
|
||||
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
|
||||
YiItemsTextBlock.FontSize = Math.Clamp(16 * scale, 11, 24);
|
||||
YiItemsTextBlock.FontSize = Math.Clamp(13.8 * scale * rightDensity, 8.5, 19);
|
||||
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
|
||||
YiItemsTextBlock.LineHeight = YiItemsTextBlock.FontSize * 1.15;
|
||||
JiItemsTextBlock.LineHeight = JiItemsTextBlock.FontSize * 1.15;
|
||||
|
||||
_calendarDayFontSize = Math.Clamp(14 * scale, 9, 22);
|
||||
_calendarTodayDotSize = Math.Clamp(28 * scale, 17, 38);
|
||||
LunarDateTextBlock.FontWeight = ToVariableWeight(Lerp(640, 760, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
LunarMetaTextBlock.FontWeight = ToVariableWeight(Lerp(500, 620, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
YiLabelTextBlock.FontWeight = ToVariableWeight(Lerp(620, 740, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
JiLabelTextBlock.FontWeight = YiLabelTextBlock.FontWeight;
|
||||
YiItemsTextBlock.FontWeight = ToVariableWeight(Lerp(520, 660, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
JiItemsTextBlock.FontWeight = YiItemsTextBlock.FontWeight;
|
||||
|
||||
UpdateDate();
|
||||
var maxLines = scale <= 0.82 ? 1 : 2;
|
||||
YiItemsTextBlock.MaxLines = maxLines;
|
||||
JiItemsTextBlock.MaxLines = maxLines;
|
||||
|
||||
_lunarItemCount = scale switch
|
||||
{
|
||||
<= 0.72 => 2,
|
||||
<= 0.96 => 3,
|
||||
<= 1.32 => 4,
|
||||
_ => 5
|
||||
};
|
||||
|
||||
if (maxLines == 1)
|
||||
{
|
||||
_lunarItemCount = Math.Min(_lunarItemCount, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
LunarCardBorder.BorderBrush = isNightMode
|
||||
? CreateBrush("#3FFFFFFF")
|
||||
: CreateBrush("#14000000");
|
||||
LunarCardBorder.BoxShadow = BoxShadows.Parse(isNightMode
|
||||
? "0 10 26 #42000000"
|
||||
: "0 8 20 #1A000000");
|
||||
|
||||
_weekdayHeaderOpacity = isNightMode ? 0.66 : 0.60;
|
||||
_weekdayNumberOpacity = isNightMode ? 0.93 : 0.90;
|
||||
_weekendNumberOpacity = isNightMode ? 0.68 : 0.58;
|
||||
|
||||
GregorianHeadlineTextBlock.Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush", isNightMode ? 0.97 : 0.95);
|
||||
LunarDateTextBlock.Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush", isNightMode ? 0.97 : 0.95);
|
||||
LunarMetaTextBlock.Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush", isNightMode ? 0.92 : 0.86);
|
||||
YiItemsTextBlock.Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush", isNightMode ? 0.95 : 0.92);
|
||||
JiItemsTextBlock.Foreground = YiItemsTextBlock.Foreground;
|
||||
|
||||
foreach (var block in GetWeekdayHeaderBlocks())
|
||||
{
|
||||
block.Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush", _weekdayHeaderOpacity);
|
||||
}
|
||||
|
||||
YiLabelTextBlock.Foreground = CreateBrush(isNightMode ? "#8CB57D" : "#4E7D3A");
|
||||
JiLabelTextBlock.Foreground = CreateBrush(isNightMode ? "#C98981" : "#A1473E");
|
||||
DividerBorder.Opacity = isNightMode ? 0.48 : 0.72;
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
var cellScale = Math.Clamp(_currentCellSize / 48d, 0.62, 1.8);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 220d, 0.62, 1.85) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 460d, 0.62, 1.85) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.08), 0.62, 1.8);
|
||||
}
|
||||
|
||||
private IBrush GetThemeBrush(string key, double opacity)
|
||||
@@ -274,4 +476,88 @@ public partial class DateWidget : UserControl
|
||||
|
||||
return new SolidColorBrush(Colors.Gray, opacity);
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static string BuildDailySelection(
|
||||
DateTime date,
|
||||
string[] pool,
|
||||
int count,
|
||||
int salt,
|
||||
bool useChineseSpacing)
|
||||
{
|
||||
if (pool.Length == 0 || count <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var target = Math.Min(count, pool.Length);
|
||||
var selected = new List<string>(target);
|
||||
var usedIndices = new HashSet<int>();
|
||||
var cursor = Math.Abs(date.Year * 1009 + date.DayOfYear * 37 + salt * 211);
|
||||
var step = (salt % Math.Max(1, pool.Length - 1)) + 1;
|
||||
|
||||
for (var i = 0; i < pool.Length * 3 && selected.Count < target; i++)
|
||||
{
|
||||
var index = (cursor + i * step) % pool.Length;
|
||||
if (usedIndices.Add(index))
|
||||
{
|
||||
selected.Add(pool[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return string.Join(useChineseSpacing ? " " : ", ", selected);
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static FontWeight ToVariableWeight(double weight)
|
||||
{
|
||||
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
||||
}
|
||||
|
||||
private static int GetCalendarRowCount(int startDayOfWeek, int daysInMonth)
|
||||
{
|
||||
return Math.Max(5, (int)Math.Ceiling((startDayOfWeek + daysInMonth) / 7d));
|
||||
}
|
||||
|
||||
private void EnsureCalendarRows(int rowCount)
|
||||
{
|
||||
if (CalendarGrid.RowDefinitions.Count == rowCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalendarGrid.RowDefinitions.Clear();
|
||||
for (var i = 0; i < rowCount; i++)
|
||||
{
|
||||
CalendarGrid.RowDefinitions.Add(new RowDefinition(GridLength.Star));
|
||||
}
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using LanMontainDesktop.ComponentSystem;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public sealed record DesktopComponentRuntimeRegistration(
|
||||
string ComponentId,
|
||||
string DisplayNameLocalizationKey,
|
||||
Func<Control> ControlFactory,
|
||||
Func<double, double>? CornerRadiusResolver = null);
|
||||
|
||||
public sealed class DesktopComponentRuntimeDescriptor
|
||||
{
|
||||
private static readonly Func<double, double> DefaultCornerRadiusResolver =
|
||||
cellSize => Math.Clamp(cellSize * 0.22, 8, 18);
|
||||
|
||||
private readonly Func<Control> _controlFactory;
|
||||
private readonly Func<double, double> _cornerRadiusResolver;
|
||||
|
||||
internal DesktopComponentRuntimeDescriptor(
|
||||
DesktopComponentDefinition definition,
|
||||
string displayNameLocalizationKey,
|
||||
Func<Control> controlFactory,
|
||||
Func<double, double>? cornerRadiusResolver)
|
||||
{
|
||||
Definition = definition;
|
||||
DisplayNameLocalizationKey = displayNameLocalizationKey;
|
||||
_controlFactory = controlFactory;
|
||||
_cornerRadiusResolver = cornerRadiusResolver ?? DefaultCornerRadiusResolver;
|
||||
}
|
||||
|
||||
public DesktopComponentDefinition Definition { get; }
|
||||
|
||||
public string DisplayNameLocalizationKey { get; }
|
||||
|
||||
public Control CreateControl(double cellSize, TimeZoneService timeZoneService, IWeatherInfoService weatherInfoService)
|
||||
{
|
||||
var control = _controlFactory();
|
||||
if (control is IDesktopComponentWidget sizedComponent)
|
||||
{
|
||||
sizedComponent.ApplyCellSize(cellSize);
|
||||
}
|
||||
|
||||
if (control is ITimeZoneAwareComponentWidget timeZoneAwareComponent)
|
||||
{
|
||||
timeZoneAwareComponent.SetTimeZoneService(timeZoneService);
|
||||
}
|
||||
|
||||
if (control is IWeatherInfoAwareComponentWidget weatherInfoAwareComponent)
|
||||
{
|
||||
weatherInfoAwareComponent.SetWeatherInfoService(weatherInfoService);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
public double ResolveCornerRadius(double cellSize)
|
||||
{
|
||||
return _cornerRadiusResolver(Math.Max(1, cellSize));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DesktopComponentRuntimeRegistry
|
||||
{
|
||||
private readonly Dictionary<string, DesktopComponentRuntimeDescriptor> _descriptors;
|
||||
|
||||
public DesktopComponentRuntimeRegistry(
|
||||
ComponentRegistry componentRegistry,
|
||||
IEnumerable<DesktopComponentRuntimeRegistration> registrations)
|
||||
{
|
||||
var registrationMap = registrations
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.ComponentId) && r.ControlFactory is not null)
|
||||
.GroupBy(r => r.ComponentId.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_descriptors = componentRegistry
|
||||
.GetAll()
|
||||
.Where(definition => registrationMap.ContainsKey(definition.Id))
|
||||
.ToDictionary(
|
||||
definition => definition.Id,
|
||||
definition =>
|
||||
{
|
||||
var registration = registrationMap[definition.Id];
|
||||
return new DesktopComponentRuntimeDescriptor(
|
||||
definition,
|
||||
registration.DisplayNameLocalizationKey,
|
||||
registration.ControlFactory,
|
||||
registration.CornerRadiusResolver);
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static DesktopComponentRuntimeRegistry CreateDefault(ComponentRegistry componentRegistry)
|
||||
{
|
||||
return new DesktopComponentRuntimeRegistry(
|
||||
componentRegistry,
|
||||
new[]
|
||||
{
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.Date,
|
||||
"component.date",
|
||||
() => new DateWidget(),
|
||||
_ => 16),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.MonthCalendar,
|
||||
"component.month_calendar",
|
||||
() => new MonthCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.26, 10, 22)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.LunarCalendar,
|
||||
"component.lunar_calendar",
|
||||
() => new LunarCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 26)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"component.desktop_clock",
|
||||
() => new AnalogClockWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 28)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWeatherClock,
|
||||
"component.weather_clock",
|
||||
() => new WeatherClockWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"component.desktop_timer",
|
||||
() => new TimerWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 28)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWeather,
|
||||
"component.desktop_weather",
|
||||
() => new WeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopHourlyWeather,
|
||||
"component.hourly_weather",
|
||||
() => new HourlyWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopMultiDayWeather,
|
||||
"component.multiday_weather",
|
||||
() => new MultiDayWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWhiteboard,
|
||||
"component.whiteboard",
|
||||
() => new WhiteboardWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||
"component.blackboard_landscape",
|
||||
() => new WhiteboardWidget(baseWidthCells: 4),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.HolidayCalendar,
|
||||
"component.holiday_calendar",
|
||||
() => new HolidayCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.32, 12, 28))
|
||||
});
|
||||
}
|
||||
|
||||
public bool TryGetDescriptor(string componentId, out DesktopComponentRuntimeDescriptor descriptor)
|
||||
{
|
||||
return _descriptors.TryGetValue(componentId, out descriptor!);
|
||||
}
|
||||
|
||||
public IReadOnlyList<DesktopComponentRuntimeDescriptor> GetDesktopComponents()
|
||||
{
|
||||
return _descriptors.Values
|
||||
.Where(descriptor => descriptor.Definition.AllowDesktopPlacement)
|
||||
.OrderBy(descriptor => descriptor.Definition.Category, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class HolidayCalendarWidget : UserControl
|
||||
public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
|
||||
296
LanMontainDesktop/Views/Components/HourlyWeatherWidget.axaml
Normal file
@@ -0,0 +1,296 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="640"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMontainDesktop.Views.Components.HourlyWeatherWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Background="#68A9EC">
|
||||
<Grid>
|
||||
<Border x:Name="BackgroundImageLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="BackgroundMotionLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.24"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Border.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1.05"
|
||||
ScaleY="1.05" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundTintLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.20" />
|
||||
|
||||
<Border x:Name="BackgroundLightLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.66">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="1,1">
|
||||
<GradientStop Color="#5BFFFFFF"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#1FFFFFFF"
|
||||
Offset="0.30" />
|
||||
<GradientStop Color="#00000000"
|
||||
Offset="0.55" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundShadeLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.78">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="0,1">
|
||||
<GradientStop Color="#00040A16"
|
||||
Offset="0.50" />
|
||||
<GradientStop Color="#2E0B1C34"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Canvas x:Name="ParticleLayer"
|
||||
IsHitTestVisible="False"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="ContentPaddingBorder"
|
||||
Padding="18"
|
||||
Background="Transparent">
|
||||
<Grid x:Name="LayoutRoot">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="10">
|
||||
<Grid x:Name="TopRowGrid"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<fi:SymbolIcon x:Name="LocationIcon"
|
||||
Symbol="Location"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<TextBlock x:Name="CityTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="北京"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<fi:SymbolIcon x:Name="WeatherIconSymbol"
|
||||
Grid.Column="2"
|
||||
Symbol="WeatherSunny"
|
||||
IconVariant="Regular"
|
||||
FontSize="40"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock x:Name="TemperatureTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="26°"
|
||||
FontSize="108"
|
||||
FontWeight="Bold"
|
||||
FontFeatures="tnum"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,4,0,10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<StackPanel x:Name="ConditionRangeStack"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="8"
|
||||
Margin="0,0,0,10">
|
||||
<TextBlock x:Name="ConditionTextBlock"
|
||||
Text="晴"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
<TextBlock x:Name="RangeTextBlock"
|
||||
Text="20° / 28°"
|
||||
FontSize="36"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel x:Name="BottomInfoStack"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="4"
|
||||
Margin="0,0,0,8">
|
||||
<Border x:Name="HourlyPanelBorder"
|
||||
Background="#1CFFFFFF"
|
||||
CornerRadius="18"
|
||||
ClipToBounds="True"
|
||||
Padding="12,8">
|
||||
<Grid x:Name="HourlyGrid"
|
||||
ColumnDefinitions="*,*,*,*,*,*">
|
||||
<StackPanel Grid.Column="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime0"
|
||||
Text="现在"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon0"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp0"
|
||||
Text="26°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime1"
|
||||
Text="09:00"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon1"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp1"
|
||||
Text="28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime2"
|
||||
Text="10:00"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon2"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp2"
|
||||
Text="26°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime3"
|
||||
Text="11:00"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon3"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp3"
|
||||
Text="24°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime4"
|
||||
Text="12:00"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon4"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp4"
|
||||
Text="24°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime5"
|
||||
Text="13:00"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon5"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp5"
|
||||
Text="23°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
1630
LanMontainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public interface IDesktopComponentWidget
|
||||
{
|
||||
void ApplyCellSize(double cellSize);
|
||||
}
|
||||
|
||||
public interface ITimeZoneAwareComponentWidget
|
||||
{
|
||||
void SetTimeZoneService(TimeZoneService timeZoneService);
|
||||
}
|
||||
|
||||
public interface IWeatherInfoAwareComponentWidget
|
||||
{
|
||||
void SetWeatherInfoService(IWeatherInfoService weatherInfoService);
|
||||
}
|
||||
@@ -3,12 +3,13 @@ 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 LunarCalendarWidget : UserControl
|
||||
public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
@@ -79,6 +80,11 @@ public partial class LunarCalendarWidget : UserControl
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private FontWeight _gregorianLineWeight = FontWeight.SemiBold;
|
||||
private FontWeight _lunarDateWeight = FontWeight.Bold;
|
||||
private FontWeight _labelWeight = FontWeight.Bold;
|
||||
private FontWeight _itemsWeight = FontWeight.SemiBold;
|
||||
private int _auspiciousItemCount = 4;
|
||||
|
||||
public LunarCalendarWidget()
|
||||
{
|
||||
@@ -131,6 +137,8 @@ public partial class LunarCalendarWidget : UserControl
|
||||
|
||||
private void UpdateContent()
|
||||
{
|
||||
ApplyAdaptiveTypography();
|
||||
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -146,13 +154,13 @@ public partial class LunarCalendarWidget : UserControl
|
||||
YiItemsTextBlock.Text = BuildDailySelection(
|
||||
now.Date,
|
||||
isZh ? ZhYiCandidates : EnYiCandidates,
|
||||
count: 4,
|
||||
count: _auspiciousItemCount,
|
||||
salt: 17,
|
||||
useChineseSpacing: isZh);
|
||||
JiItemsTextBlock.Text = BuildDailySelection(
|
||||
now.Date,
|
||||
isZh ? ZhJiCandidates : EnJiCandidates,
|
||||
count: 4,
|
||||
count: _auspiciousItemCount,
|
||||
salt: 29,
|
||||
useChineseSpacing: isZh);
|
||||
}
|
||||
@@ -160,6 +168,11 @@ public partial class LunarCalendarWidget : UserControl
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateContent();
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 16, 44));
|
||||
@@ -172,12 +185,33 @@ public partial class LunarCalendarWidget : UserControl
|
||||
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);
|
||||
var densityBoost = scale <= 0.72 ? 0.90 : scale <= 0.88 ? 0.95 : scale >= 1.42 ? 1.04 : 1.0;
|
||||
GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 38);
|
||||
LunarDateTextBlock.FontSize = Math.Clamp(88 * scale * densityBoost, 28, 134);
|
||||
YiLabelTextBlock.FontSize = Math.Clamp(30 * scale * densityBoost, 12, 46);
|
||||
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
|
||||
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
|
||||
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 36);
|
||||
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
|
||||
|
||||
_gregorianLineWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_lunarDateWeight = ToVariableWeight(Lerp(650, 780, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_labelWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
_itemsWeight = ToVariableWeight(Lerp(520, 670, Math.Clamp((scale - 0.58) / 1.2, 0, 1)));
|
||||
|
||||
GregorianLineTextBlock.FontWeight = _gregorianLineWeight;
|
||||
LunarDateTextBlock.FontWeight = _lunarDateWeight;
|
||||
YiLabelTextBlock.FontWeight = _labelWeight;
|
||||
JiLabelTextBlock.FontWeight = _labelWeight;
|
||||
YiItemsTextBlock.FontWeight = _itemsWeight;
|
||||
JiItemsTextBlock.FontWeight = _itemsWeight;
|
||||
|
||||
_auspiciousItemCount = scale switch
|
||||
{
|
||||
<= 0.72 => 2,
|
||||
<= 0.92 => 3,
|
||||
<= 1.30 => 4,
|
||||
_ => 5
|
||||
};
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
@@ -188,6 +222,16 @@ public partial class LunarCalendarWidget : UserControl
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static FontWeight ToVariableWeight(double weight)
|
||||
{
|
||||
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
||||
}
|
||||
|
||||
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
|
||||
{
|
||||
return dayOfWeek switch
|
||||
|
||||
@@ -9,7 +9,7 @@ using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class MonthCalendarWidget : UserControl
|
||||
public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
@@ -21,7 +21,10 @@ public partial class MonthCalendarWidget : UserControl
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private double _weekdayFontSize = 20;
|
||||
private FontWeight _weekdayFontWeight = FontWeight.SemiBold;
|
||||
private double _calendarDayFontSize = 22;
|
||||
private FontWeight _calendarDayFontWeight = FontWeight.SemiBold;
|
||||
private double _calendarTodayDotSize = 44;
|
||||
|
||||
public MonthCalendarWidget()
|
||||
@@ -83,6 +86,8 @@ public partial class MonthCalendarWidget : UserControl
|
||||
? $"{now.Month}\u6708{now.Day}\u65e5"
|
||||
: now.ToString("MMM d", culture);
|
||||
|
||||
// Locale changes the header width; re-balance typography on every refresh.
|
||||
ApplyAdaptiveTypography();
|
||||
UpdateWeekdayHeaders(isZh);
|
||||
GenerateCalendar(now);
|
||||
}
|
||||
@@ -152,7 +157,7 @@ public partial class MonthCalendarWidget : UserControl
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = _calendarDayFontSize,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
FontWeight = _calendarDayFontWeight,
|
||||
Tag = "day"
|
||||
};
|
||||
|
||||
@@ -198,24 +203,40 @@ public partial class MonthCalendarWidget : UserControl
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateCalendar();
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
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);
|
||||
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
||||
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
||||
|
||||
HeaderTextBlock.FontSize = Math.Clamp(42 * scale, 14, 58);
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var headerTextLength = Math.Max(1, HeaderTextBlock.Text?.Length ?? (isZh ? 5 : 6));
|
||||
var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0;
|
||||
var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0;
|
||||
|
||||
var weekdayFontSize = Math.Clamp(20 * scale, 8, 26);
|
||||
HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 62);
|
||||
HeaderTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.62) / 1.2, 0, 1)));
|
||||
HeaderTextBlock.LineHeight = HeaderTextBlock.FontSize * 1.05;
|
||||
|
||||
_weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 27);
|
||||
_weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||
foreach (var block in GetWeekdayHeaderBlocks())
|
||||
{
|
||||
block.FontSize = weekdayFontSize;
|
||||
block.FontSize = _weekdayFontSize;
|
||||
block.FontWeight = _weekdayFontWeight;
|
||||
block.LineHeight = _weekdayFontSize * 1.06;
|
||||
}
|
||||
|
||||
_calendarDayFontSize = Math.Clamp(22 * scale, 8, 30);
|
||||
_calendarTodayDotSize = Math.Clamp(44 * scale, 16, 58);
|
||||
|
||||
UpdateCalendar();
|
||||
_calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 32);
|
||||
_calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.3, 0, 1)));
|
||||
_calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.95, 16, 62);
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
@@ -240,5 +261,14 @@ public partial class MonthCalendarWidget : UserControl
|
||||
|
||||
return new SolidColorBrush(Colors.Gray, opacity);
|
||||
}
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static FontWeight ToVariableWeight(double weight)
|
||||
{
|
||||
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
276
LanMontainDesktop/Views/Components/MultiDayWeatherWidget.axaml
Normal file
@@ -0,0 +1,276 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="640"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMontainDesktop.Views.Components.MultiDayWeatherWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Background="#68A9EC">
|
||||
<Grid>
|
||||
<Border x:Name="BackgroundImageLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="BackgroundMotionLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.24"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Border.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1.05"
|
||||
ScaleY="1.05" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundTintLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.20" />
|
||||
|
||||
<Border x:Name="BackgroundLightLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.66">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="1,1">
|
||||
<GradientStop Color="#5BFFFFFF"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#1FFFFFFF"
|
||||
Offset="0.30" />
|
||||
<GradientStop Color="#00000000"
|
||||
Offset="0.55" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundShadeLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.78">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="0,1">
|
||||
<GradientStop Color="#00040A16"
|
||||
Offset="0.50" />
|
||||
<GradientStop Color="#2E0B1C34"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Canvas x:Name="ParticleLayer"
|
||||
IsHitTestVisible="False"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="ContentPaddingBorder"
|
||||
Padding="18"
|
||||
Background="Transparent">
|
||||
<Grid x:Name="LayoutRoot">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="10">
|
||||
<Grid x:Name="TopRowGrid"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<fi:SymbolIcon x:Name="LocationIcon"
|
||||
Symbol="Location"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<TextBlock x:Name="CityTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="北京"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<StackPanel x:Name="ConditionIconStack"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="ConditionTextBlock"
|
||||
Text="晴"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
<fi:SymbolIcon x:Name="WeatherIconSymbol"
|
||||
Symbol="WeatherSunny"
|
||||
IconVariant="Regular"
|
||||
FontSize="40"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock x:Name="TemperatureTextBlock"
|
||||
Grid.Column="0"
|
||||
Text="26°"
|
||||
FontSize="108"
|
||||
FontWeight="Bold"
|
||||
FontFeatures="tnum"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,4,0,10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<TextBlock x:Name="RangeTextBlock"
|
||||
Grid.Column="2"
|
||||
Text="空气优 22"
|
||||
FontSize="36"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel x:Name="BottomInfoStack"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="4"
|
||||
Margin="0,0,0,8">
|
||||
<Border x:Name="HourlyPanelBorder"
|
||||
Background="#1CFFFFFF"
|
||||
CornerRadius="18"
|
||||
ClipToBounds="True"
|
||||
Padding="12,8">
|
||||
<Grid x:Name="HourlyGrid"
|
||||
ColumnDefinitions="*,*,*,*,*">
|
||||
<StackPanel Grid.Column="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime0"
|
||||
Text="今天"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon0"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp0"
|
||||
Text="20° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime1"
|
||||
Text="明天"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon1"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp1"
|
||||
Text="20° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime2"
|
||||
Text="周六"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon2"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp2"
|
||||
Text="20° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime3"
|
||||
Text="周日"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon3"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp3"
|
||||
Text="20° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="HourlyTime4"
|
||||
Text="周一"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<fi:SymbolIcon x:Name="HourlyIcon4"
|
||||
Symbol="WeatherSunny"
|
||||
FontSize="28"
|
||||
IconVariant="Regular"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="HourlyTemp4"
|
||||
Text="20° / 28°"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
1572
LanMontainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
Normal file
@@ -10,7 +10,7 @@ using Avalonia.Threading;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class TimerWidget : UserControl
|
||||
public partial class TimerWidget : UserControl, IDesktopComponentWidget
|
||||
{
|
||||
private const int MaxTimerSeconds = 60;
|
||||
private const double DialSize = 224;
|
||||
|
||||
98
LanMontainDesktop/Views/Components/WeatherClockWidget.axaml
Normal file
@@ -0,0 +1,98 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="260"
|
||||
d:DesignHeight="120"
|
||||
x:Class="LanMontainDesktop.Views.Components.WeatherClockWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
Background="#FFFFFF"
|
||||
BorderBrush="#14000000"
|
||||
BorderThickness="1"
|
||||
CornerRadius="22"
|
||||
ClipToBounds="True"
|
||||
Padding="12,8">
|
||||
<Grid x:Name="ContentGrid"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="10">
|
||||
<StackPanel x:Name="LeftStack"
|
||||
ClipToBounds="True"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="TimeTextBlock"
|
||||
Text="15:07"
|
||||
FontSize="36"
|
||||
FontWeight="Bold"
|
||||
FontFeatures="tnum"
|
||||
Foreground="#10131A"
|
||||
MaxLines="1"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<StackPanel x:Name="DateWeatherStack"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="DateTextBlock"
|
||||
Text="8月14日"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#7A7E87"
|
||||
VerticalAlignment="Center"
|
||||
MaxLines="1"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<fi:SymbolIcon x:Name="WeatherIconSymbol"
|
||||
Symbol="WeatherPartlyCloudyDay"
|
||||
FontSize="18"
|
||||
Foreground="#5A9CFF"
|
||||
VerticalAlignment="Center"
|
||||
IconVariant="Regular" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border x:Name="AnalogDialBorder"
|
||||
Grid.Column="1"
|
||||
Width="52"
|
||||
Height="52"
|
||||
CornerRadius="26"
|
||||
Background="#F8FAFF"
|
||||
BorderBrush="#12000000"
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Center">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid Width="104"
|
||||
Height="104">
|
||||
<Canvas x:Name="TickCanvas"
|
||||
Width="104"
|
||||
Height="104"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Canvas x:Name="HandsCanvas"
|
||||
Width="104"
|
||||
Height="104"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Ellipse x:Name="CenterDotOuter"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Fill="#4F7CC0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Ellipse x:Name="CenterDotInner"
|
||||
Width="5"
|
||||
Height="5"
|
||||
Fill="#1A74F2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
619
LanMontainDesktop/Views/Components/WeatherClockWidget.axaml.cs
Normal file
@@ -0,0 +1,619 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using FluentIcons.Common;
|
||||
using LanMontainDesktop.Models;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
|
||||
{
|
||||
private sealed record WeatherClockConfig(
|
||||
string LanguageCode,
|
||||
string Locale,
|
||||
string LocationKey,
|
||||
double Latitude,
|
||||
double Longitude);
|
||||
|
||||
private const double DialDesignSize = 104;
|
||||
private const double DialCenter = DialDesignSize / 2d;
|
||||
|
||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||
|
||||
private readonly DispatcherTimer _clockTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private readonly DispatcherTimer _weatherRefreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(12)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
|
||||
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);
|
||||
private readonly Line _secondHandLine = CreateHandLine("#1A74F2", 1.9);
|
||||
|
||||
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private CancellationTokenSource? _refreshCts;
|
||||
private double _currentCellSize = 48;
|
||||
private bool _isAttached;
|
||||
private bool _dialInitialized;
|
||||
private bool _handsInitialized;
|
||||
private bool _isRefreshing;
|
||||
private bool? _isNightModeApplied;
|
||||
private string _languageCode = "zh-CN";
|
||||
private Symbol _activeWeatherSymbol = Symbol.WeatherPartlyCloudyDay;
|
||||
|
||||
public WeatherClockWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_clockTimer.Tick += OnClockTimerTick;
|
||||
_weatherRefreshTimer.Tick += OnWeatherRefreshTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
ApplyDefaultWeatherIcon();
|
||||
UpdateClockVisual();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
}
|
||||
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
UpdateClockVisual();
|
||||
}
|
||||
|
||||
public void SetWeatherInfoService(IWeatherInfoService weatherInfoService)
|
||||
{
|
||||
_weatherInfoService = weatherInfoService ?? DefaultWeatherInfoService;
|
||||
if (_isAttached)
|
||||
{
|
||||
_ = RefreshWeatherAsync(forceRefresh: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
var targetHeight = Bounds.Height > 1
|
||||
? Math.Clamp(Bounds.Height, 38, 160)
|
||||
: Math.Clamp(_currentCellSize * 0.92, 38, 120);
|
||||
var targetWidth = Bounds.Width > 1
|
||||
? Math.Clamp(Bounds.Width, 48, 520)
|
||||
: Math.Clamp(_currentCellSize * 2.15, 88, 260);
|
||||
var compactness = Math.Clamp((170 - targetWidth) / 78d, 0, 1);
|
||||
var compactFactor = Lerp(1, 0.72, compactness);
|
||||
var cornerRadius = Math.Clamp(targetHeight * 0.40, 15, 36);
|
||||
|
||||
var horizontalPadding = Math.Clamp(targetHeight * Lerp(0.18, 0.12, compactness), 5, 30);
|
||||
var verticalPadding = Math.Clamp(targetHeight * Lerp(0.14, 0.10, compactness), 3, 20);
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var columnSpacing = Math.Clamp(targetHeight * Lerp(0.16, 0.08, compactness), 3, 22);
|
||||
ContentGrid.ColumnSpacing = columnSpacing;
|
||||
LeftStack.Spacing = Math.Clamp(targetHeight * Lerp(0.06, 0.04, compactness), 1.5, 10);
|
||||
DateWeatherStack.Spacing = Math.Clamp(targetHeight * Lerp(0.10, 0.06, compactness), 3, 14);
|
||||
|
||||
TimeTextBlock.FontSize = Math.Clamp(31 * scale * compactFactor, 14, 62);
|
||||
DateTextBlock.FontSize = Math.Clamp(15.5 * scale * compactFactor, 9, 30);
|
||||
WeatherIconSymbol.FontSize = Math.Clamp(17 * scale * compactFactor, 10, 32);
|
||||
|
||||
TimeTextBlock.FontWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
|
||||
DateTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
|
||||
|
||||
var contentHeight = Math.Max(24, targetHeight - (verticalPadding * 2));
|
||||
var contentWidth = Math.Max(48, targetWidth - (horizontalPadding * 2));
|
||||
var minimumLeftWidth = Math.Clamp(contentWidth * Lerp(0.56, 0.64, compactness), 52, 360);
|
||||
var maxDialByWidth = Math.Max(18, contentWidth - minimumLeftWidth - columnSpacing);
|
||||
var dialByHeight = contentHeight * Lerp(0.94, 0.84, compactness);
|
||||
var dialSize = Math.Clamp(Math.Min(dialByHeight, maxDialByWidth), 20, 140);
|
||||
var leftContentWidth = Math.Max(26, contentWidth - dialSize - columnSpacing);
|
||||
|
||||
LeftStack.MaxWidth = leftContentWidth;
|
||||
DateWeatherStack.MaxWidth = leftContentWidth;
|
||||
TimeTextBlock.MaxWidth = leftContentWidth;
|
||||
DateTextBlock.MaxWidth = Math.Max(18, leftContentWidth - WeatherIconSymbol.FontSize - DateWeatherStack.Spacing);
|
||||
|
||||
AnalogDialBorder.Width = dialSize;
|
||||
AnalogDialBorder.Height = dialSize;
|
||||
AnalogDialBorder.CornerRadius = new CornerRadius(dialSize / 2d);
|
||||
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
UpdateClockVisual();
|
||||
_clockTimer.Start();
|
||||
_weatherRefreshTimer.Start();
|
||||
_ = RefreshWeatherAsync(forceRefresh: false);
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = false;
|
||||
_clockTimer.Stop();
|
||||
_weatherRefreshTimer.Stop();
|
||||
CancelRefreshRequest();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClockVisual();
|
||||
}
|
||||
|
||||
private async void OnWeatherRefreshTick(object? sender, EventArgs e)
|
||||
{
|
||||
await RefreshWeatherAsync(forceRefresh: false);
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClockVisual();
|
||||
}
|
||||
|
||||
private async Task RefreshWeatherAsync(bool forceRefresh)
|
||||
{
|
||||
if (!_isAttached || _isRefreshing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
var config = LoadConfig();
|
||||
_languageCode = config.LanguageCode;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.LocationKey))
|
||||
{
|
||||
ApplyDefaultWeatherIcon();
|
||||
_isRefreshing = false;
|
||||
UpdateClockVisual();
|
||||
return;
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var previous = Interlocked.Exchange(ref _refreshCts, cts);
|
||||
previous?.Cancel();
|
||||
previous?.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
var query = new WeatherQuery(
|
||||
LocationKey: config.LocationKey,
|
||||
Latitude: config.Latitude,
|
||||
Longitude: config.Longitude,
|
||||
ForecastDays: 1,
|
||||
Locale: config.Locale,
|
||||
ForceRefresh: forceRefresh);
|
||||
|
||||
var result = await _weatherInfoService.GetWeatherAsync(query, cts.Token);
|
||||
if (cts.IsCancellationRequested || !_isAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.Success || result.Data is null)
|
||||
{
|
||||
ApplyDefaultWeatherIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyWeatherSnapshot(result.Data);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignore canceled refresh requests.
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!cts.IsCancellationRequested && _isAttached)
|
||||
{
|
||||
ApplyDefaultWeatherIcon();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_refreshCts, cts))
|
||||
{
|
||||
_refreshCts = null;
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWeatherSnapshot(WeatherSnapshot snapshot)
|
||||
{
|
||||
var isNight = ResolveIsNight(snapshot);
|
||||
_activeWeatherSymbol = ResolveWeatherSymbol(snapshot.Current.WeatherCode, isNight);
|
||||
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNight));
|
||||
}
|
||||
|
||||
private void ApplyDefaultWeatherIcon()
|
||||
{
|
||||
var isNight = IsNightNow();
|
||||
_activeWeatherSymbol = isNight ? Symbol.WeatherMoon : Symbol.WeatherPartlyCloudyDay;
|
||||
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNight));
|
||||
}
|
||||
|
||||
private void UpdateClockVisual()
|
||||
{
|
||||
ApplyModeVisualIfNeeded();
|
||||
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
TimeTextBlock.Text = now.ToString("HH:mm", CultureInfo.CurrentCulture);
|
||||
DateTextBlock.Text = FormatDate(now);
|
||||
|
||||
var hourAngle = (now.Hour % 12 + now.Minute / 60d + now.Second / 3600d) * 30d;
|
||||
var minuteAngle = (now.Minute + now.Second / 60d) * 6d;
|
||||
var secondAngle = (now.Second + now.Millisecond / 1000d) * 6d;
|
||||
|
||||
SetHandGeometry(_hourHandLine, hourAngle, forwardLength: 23.5, backwardLength: 5.0);
|
||||
SetHandGeometry(_minuteHandLine, minuteAngle, forwardLength: 33.5, backwardLength: 6.5);
|
||||
SetHandGeometry(_secondHandLine, secondAngle, forwardLength: 39.0, backwardLength: 10.0);
|
||||
}
|
||||
|
||||
private void InitializeDialIfNeeded()
|
||||
{
|
||||
if (_dialInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTicks(isNightMode: false);
|
||||
_dialInitialized = true;
|
||||
}
|
||||
|
||||
private void InitializeHandsIfNeeded()
|
||||
{
|
||||
if (_handsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HandsCanvas.Children.Clear();
|
||||
HandsCanvas.Children.Add(_hourHandLine);
|
||||
HandsCanvas.Children.Add(_minuteHandLine);
|
||||
HandsCanvas.Children.Add(_secondHandLine);
|
||||
_handsInitialized = true;
|
||||
}
|
||||
|
||||
private void BuildTicks(bool isNightMode)
|
||||
{
|
||||
TickCanvas.Children.Clear();
|
||||
var tickColor = isNightMode ? "#CED7EA" : "#1C2333";
|
||||
|
||||
for (var i = 0; i < 12; i++)
|
||||
{
|
||||
var angle = (i * 30 - 90) * Math.PI / 180d;
|
||||
var isMajor = i % 3 == 0;
|
||||
var outerRadius = DialCenter - 8;
|
||||
var innerRadius = outerRadius - (isMajor ? 13.5 : 9.5);
|
||||
|
||||
var x1 = DialCenter + Math.Cos(angle) * innerRadius;
|
||||
var y1 = DialCenter + Math.Sin(angle) * innerRadius;
|
||||
var x2 = DialCenter + Math.Cos(angle) * outerRadius;
|
||||
var y2 = DialCenter + Math.Sin(angle) * outerRadius;
|
||||
|
||||
TickCanvas.Children.Add(new Line
|
||||
{
|
||||
StartPoint = new Point(x1, y1),
|
||||
EndPoint = new Point(x2, y2),
|
||||
Stroke = CreateBrush(tickColor),
|
||||
StrokeThickness = isMajor ? 2.8 : 1.9,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
RootBorder.Background = isNightMode
|
||||
? CreateGradientBrush("#2A3346", "#202A3B")
|
||||
: CreateGradientBrush("#FFFFFF", "#F6F8FC");
|
||||
RootBorder.BorderBrush = CreateBrush(isNightMode ? "#36F2F5FF" : "#14000000");
|
||||
|
||||
AnalogDialBorder.Background = isNightMode
|
||||
? CreateBrush("#1B2434")
|
||||
: CreateBrush("#F8FAFF");
|
||||
AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000");
|
||||
|
||||
TimeTextBlock.Foreground = CreateBrush(isNightMode ? "#F8FBFF" : "#10131A");
|
||||
DateTextBlock.Foreground = CreateBrush(isNightMode ? "#BCC8DD" : "#7A7E87");
|
||||
|
||||
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938");
|
||||
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749");
|
||||
_secondHandLine.Stroke = CreateBrush("#1A74F2");
|
||||
CenterDotOuter.Fill = CreateBrush(isNightMode ? "#7BAAE8" : "#4F7CC0");
|
||||
CenterDotInner.Fill = CreateBrush("#1A74F2");
|
||||
|
||||
BuildTicks(isNightMode);
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNightMode));
|
||||
}
|
||||
|
||||
private WeatherClockConfig LoadConfig()
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
var locale = string.Equals(languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||
? "zh_cn"
|
||||
: "en_us";
|
||||
|
||||
var latitude = NormalizeLatitude(snapshot.WeatherLatitude);
|
||||
var longitude = NormalizeLongitude(snapshot.WeatherLongitude);
|
||||
var locationKey = snapshot.WeatherLocationKey?.Trim() ?? string.Empty;
|
||||
|
||||
var modeIsCoordinates = string.Equals(
|
||||
snapshot.WeatherLocationMode,
|
||||
"Coordinates",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (modeIsCoordinates && string.IsNullOrWhiteSpace(locationKey))
|
||||
{
|
||||
locationKey = BuildCoordinateLocationKey(latitude, longitude);
|
||||
}
|
||||
|
||||
return new WeatherClockConfig(
|
||||
LanguageCode: languageCode,
|
||||
Locale: locale,
|
||||
LocationKey: locationKey,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude);
|
||||
}
|
||||
|
||||
private string FormatDate(DateTime dateTime)
|
||||
{
|
||||
var isZh = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase);
|
||||
if (isZh)
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"{dateTime.Month}\u6708{dateTime.Day}\u65e5");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.GetCultureInfo(_languageCode);
|
||||
return dateTime.ToString("MMM d", culture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return dateTime.ToString("MMM d", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 2.20);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 56d, 0.65, 2.80) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 180d, 0.65, 2.80) : 1;
|
||||
return Math.Clamp(Math.Min(heightScale, widthScale) * 1.02 * cellScale, 0.62, 2.40);
|
||||
}
|
||||
|
||||
private bool ResolveIsNight(WeatherSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
try
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
var zoned = TimeZoneInfo.ConvertTime(observed, _timeZoneService.CurrentTimeZone);
|
||||
return zoned.Hour < 6 || zoned.Hour >= 18;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to local observation.
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return IsNightNow();
|
||||
}
|
||||
|
||||
private bool IsNightNow()
|
||||
{
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
return now.Hour < 6 || now.Hour >= 18;
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherSymbol(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
{
|
||||
0 => isNight ? Symbol.WeatherMoon : Symbol.WeatherSunny,
|
||||
1 or 2 => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay,
|
||||
3 or 7 => Symbol.WeatherRainShowersDay,
|
||||
8 or 9 => Symbol.WeatherRain,
|
||||
4 => Symbol.WeatherThunderstorm,
|
||||
13 or 14 or 15 or 16 => Symbol.WeatherSnow,
|
||||
18 or 32 => Symbol.WeatherFog,
|
||||
_ => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveWeatherIconColor(Symbol symbol, bool isNightMode)
|
||||
{
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.WeatherSunny => isNightMode ? "#FFD978" : "#F7B500",
|
||||
Symbol.WeatherMoon => "#F6D98F",
|
||||
Symbol.WeatherPartlyCloudyDay => "#5A9CFF",
|
||||
Symbol.WeatherPartlyCloudyNight => "#8AB6FF",
|
||||
Symbol.WeatherRainShowersDay => "#5F96E8",
|
||||
Symbol.WeatherRain => "#4B84DA",
|
||||
Symbol.WeatherThunderstorm => "#F1C24D",
|
||||
Symbol.WeatherSnow => "#8EBFE5",
|
||||
_ => isNightMode ? "#A9BDD7" : "#93A2B8"
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetHandGeometry(Line hand, double angleDeg, double forwardLength, double backwardLength)
|
||||
{
|
||||
var radians = (angleDeg - 90) * Math.PI / 180d;
|
||||
var cos = Math.Cos(radians);
|
||||
var sin = Math.Sin(radians);
|
||||
|
||||
hand.StartPoint = new Point(
|
||||
DialCenter - (cos * backwardLength),
|
||||
DialCenter - (sin * backwardLength));
|
||||
hand.EndPoint = new Point(
|
||||
DialCenter + (cos * forwardLength),
|
||||
DialCenter + (sin * forwardLength));
|
||||
}
|
||||
|
||||
private static Line CreateHandLine(string colorHex, double thickness)
|
||||
{
|
||||
return new Line
|
||||
{
|
||||
StartPoint = new Point(DialCenter, DialCenter),
|
||||
EndPoint = new Point(DialCenter, DialCenter - 32),
|
||||
Stroke = CreateBrush(colorHex),
|
||||
StrokeThickness = thickness,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
};
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateGradientBrush(string fromHex, string toHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromHex), 0),
|
||||
new GradientStop(Color.Parse(toHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static FontWeight ToVariableWeight(double weight)
|
||||
{
|
||||
return (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
private static string BuildCoordinateLocationKey(double latitude, double longitude)
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"coord:{latitude:F4},{longitude:F4}");
|
||||
}
|
||||
|
||||
private static double NormalizeLatitude(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return 39.9042;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, -90, 90);
|
||||
}
|
||||
|
||||
private static double NormalizeLongitude(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return 116.4074;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, -180, 180);
|
||||
}
|
||||
|
||||
private void CancelRefreshRequest()
|
||||
{
|
||||
var cts = Interlocked.Exchange(ref _refreshCts, null);
|
||||
cts?.Cancel();
|
||||
cts?.Dispose();
|
||||
}
|
||||
}
|
||||
150
LanMontainDesktop/Views/Components/WeatherWidget.axaml
Normal file
@@ -0,0 +1,150 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="320"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMontainDesktop.Views.Components.WeatherWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Background="#68A9EC">
|
||||
<Grid>
|
||||
<Border x:Name="BackgroundImageLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="BackgroundMotionLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.24"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<Border.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1.05"
|
||||
ScaleY="1.05" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundTintLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.20" />
|
||||
|
||||
<Border x:Name="BackgroundLightLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.66">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="1,1">
|
||||
<GradientStop Color="#5BFFFFFF"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#1FFFFFFF"
|
||||
Offset="0.30" />
|
||||
<GradientStop Color="#00000000"
|
||||
Offset="0.55" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BackgroundShadeLayer"
|
||||
CornerRadius="28"
|
||||
ClipToBounds="True"
|
||||
Opacity="0.78">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="0,1">
|
||||
<GradientStop Color="#00040A16"
|
||||
Offset="0.50" />
|
||||
<GradientStop Color="#2E0B1C34"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<Canvas x:Name="ParticleLayer"
|
||||
IsHitTestVisible="False"
|
||||
ClipToBounds="True" />
|
||||
|
||||
<Border x:Name="ContentPaddingBorder"
|
||||
Padding="18"
|
||||
Background="Transparent">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="300"
|
||||
Height="300">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
RowSpacing="8">
|
||||
<Grid x:Name="TopRowGrid"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<fi:SymbolIcon x:Name="LocationIcon"
|
||||
Symbol="Location"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<TextBlock x:Name="CityTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Beijing"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<fi:SymbolIcon x:Name="WeatherIconSymbol"
|
||||
Grid.Column="2"
|
||||
Symbol="WeatherSunny"
|
||||
IconVariant="Regular"
|
||||
FontSize="40"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="TemperatureTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="26"
|
||||
FontSize="108"
|
||||
FontWeight="Bold"
|
||||
FontFeatures="tnum"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,4,0,10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<StackPanel x:Name="BottomInfoStack"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="4"
|
||||
Margin="0,0,0,10">
|
||||
<TextBlock x:Name="ConditionTextBlock"
|
||||
Text="Clear"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
|
||||
<TextBlock x:Name="RangeTextBlock"
|
||||
Text="20 / 28"
|
||||
FontSize="36"
|
||||
FontWeight="SemiBold"
|
||||
FontFeatures="tnum"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
1215
LanMontainDesktop/Views/Components/WeatherWidget.axaml.cs
Normal file
94
LanMontainDesktop/Views/Components/WhiteboardWidget.axaml
Normal file
@@ -0,0 +1,94 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:inking="using:DotNetCampus.Inking"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="240"
|
||||
d:DesignHeight="480"
|
||||
x:Class="LanMontainDesktop.Views.Components.WhiteboardWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
Background="#F1F4F9"
|
||||
CornerRadius="20"
|
||||
ClipToBounds="True"
|
||||
Padding="8">
|
||||
<Grid RowDefinitions="*,Auto"
|
||||
RowSpacing="8">
|
||||
<Border x:Name="CanvasBorder"
|
||||
Grid.Row="0"
|
||||
Background="#FFFFFF"
|
||||
BorderBrush="#24000000"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
ClipToBounds="True">
|
||||
<inking:InkCanvas x:Name="InkCanvas" />
|
||||
</Border>
|
||||
|
||||
<Border x:Name="ToolbarBorder"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Background="#E6FFFFFF"
|
||||
BorderBrush="#16000000"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="8,6">
|
||||
<StackPanel x:Name="ToolbarButtonsPanel"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button x:Name="PenButton"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="0"
|
||||
CornerRadius="15"
|
||||
ToolTip.Tip="Pen"
|
||||
Click="OnPenButtonClick">
|
||||
<fi:SymbolIcon x:Name="PenIcon"
|
||||
Symbol="Pen"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
<Button x:Name="EraserButton"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="0"
|
||||
CornerRadius="15"
|
||||
ToolTip.Tip="Eraser"
|
||||
Click="OnEraserButtonClick">
|
||||
<fi:SymbolIcon x:Name="EraserIcon"
|
||||
Symbol="EraserTool"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
<Button x:Name="ClearButton"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="0"
|
||||
CornerRadius="15"
|
||||
ToolTip.Tip="Clear"
|
||||
Click="OnClearButtonClick">
|
||||
<fi:SymbolIcon x:Name="ClearIcon"
|
||||
Symbol="Delete"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
<Button x:Name="ExportButton"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="0"
|
||||
CornerRadius="15"
|
||||
ToolTip.Tip="Export SVG"
|
||||
Click="OnExportButtonClick">
|
||||
<fi:SymbolIcon x:Name="ExportIcon"
|
||||
Symbol="ArrowExport"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
361
LanMontainDesktop/Views/Components/WhiteboardWidget.axaml.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using DotNetCampus.Inking;
|
||||
using FluentIcons.Avalonia;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget
|
||||
{
|
||||
private enum WhiteboardToolMode
|
||||
{
|
||||
Pen,
|
||||
Eraser
|
||||
}
|
||||
|
||||
private static readonly PropertyInfo? StrokeColorProperty = typeof(SkiaStroke).GetProperty(nameof(SkiaStroke.Color));
|
||||
private readonly int _baseWidthCells;
|
||||
private double _currentCellSize = 48;
|
||||
private WhiteboardToolMode _toolMode = WhiteboardToolMode.Pen;
|
||||
private bool? _isNightModeApplied;
|
||||
private SKColor _currentInkColor = SKColors.Black;
|
||||
|
||||
public WhiteboardWidget()
|
||||
: this(baseWidthCells: 2)
|
||||
{
|
||||
}
|
||||
|
||||
public WhiteboardWidget(int baseWidthCells)
|
||||
{
|
||||
_baseWidthCells = Math.Max(1, baseWidthCells);
|
||||
InitializeComponent();
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ConfigureInkCanvas();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
ApplyThemeVisual(force: true);
|
||||
SetToolMode(WhiteboardToolMode.Pen);
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
ApplyThemeVisual(force: true);
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
// Keep all state in-memory for lightweight re-attach scenarios.
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ApplyThemeVisual(force: false);
|
||||
}
|
||||
|
||||
private void ConfigureInkCanvas()
|
||||
{
|
||||
InkCanvas.EditingMode = InkCanvasEditingMode.Ink;
|
||||
var settings = InkCanvas.AvaloniaSkiaInkCanvas.Settings;
|
||||
settings.IgnorePressure = true;
|
||||
settings.InkThickness = 2.5f;
|
||||
settings.EraserSize = new Size(20, 20);
|
||||
settings.IsBitmapCacheEnabled = true;
|
||||
settings.MaxBitmapCacheSize = 2048;
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
var availableWidth = Bounds.Width > 1 ? Bounds.Width : (_currentCellSize * _baseWidthCells);
|
||||
var buttonSize = Math.Clamp(availableWidth * 0.15, 24, 40);
|
||||
var buttonCornerRadius = buttonSize * 0.5;
|
||||
var toolbarSpacing = Math.Clamp(buttonSize * 0.25, 4, 10);
|
||||
var toolbarPaddingHorizontal = Math.Clamp(buttonSize * 0.36, 6, 12);
|
||||
var toolbarPaddingVertical = Math.Clamp(buttonSize * 0.24, 4, 8);
|
||||
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.14, 6, 14));
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.34, 12, 28));
|
||||
CanvasBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.24, 10, 22));
|
||||
ToolbarBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.22, 10, 20));
|
||||
ToolbarBorder.Padding = new Thickness(toolbarPaddingHorizontal, toolbarPaddingVertical);
|
||||
ToolbarButtonsPanel.Spacing = toolbarSpacing;
|
||||
|
||||
foreach (var button in new[] { PenButton, EraserButton, ClearButton, ExportButton })
|
||||
{
|
||||
button.Width = buttonSize;
|
||||
button.Height = buttonSize;
|
||||
button.CornerRadius = new CornerRadius(buttonCornerRadius);
|
||||
}
|
||||
|
||||
var settings = InkCanvas.AvaloniaSkiaInkCanvas.Settings;
|
||||
settings.InkThickness = (float)Math.Clamp(_currentCellSize * 0.06, 2.0, 6.0);
|
||||
var eraserSize = Math.Clamp(_currentCellSize * 0.42, 12, 44);
|
||||
settings.EraserSize = new Size(eraserSize, eraserSize);
|
||||
}
|
||||
|
||||
private void ApplyThemeVisual(bool force)
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (!force && _isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
_currentInkColor = isNightMode ? SKColors.White : SKColors.Black;
|
||||
|
||||
RootBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#FF181B22") : Color.Parse("#FFF1F4F9"));
|
||||
CanvasBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#FF000000") : Color.Parse("#FFFFFFFF"));
|
||||
CanvasBorder.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#30FFFFFF") : Color.Parse("#24000000"));
|
||||
ToolbarBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#1AFFFFFF") : Color.Parse("#E6FFFFFF"));
|
||||
ToolbarBorder.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#26FFFFFF") : Color.Parse("#16000000"));
|
||||
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.Settings.InkColor = _currentInkColor;
|
||||
RecolorAllStrokes(_currentInkColor);
|
||||
RefreshToolButtonVisuals();
|
||||
}
|
||||
|
||||
private void RecolorAllStrokes(SKColor targetColor)
|
||||
{
|
||||
for (var i = 0; i < InkCanvas.Strokes.Count; i++)
|
||||
{
|
||||
TrySetStrokeColor(InkCanvas.Strokes[i], targetColor);
|
||||
}
|
||||
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
|
||||
InkCanvas.InvalidateVisual();
|
||||
}
|
||||
|
||||
private static void TrySetStrokeColor(SkiaStroke stroke, SKColor color)
|
||||
{
|
||||
if (StrokeColorProperty is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StrokeColorProperty.SetValue(stroke, color);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep current stroke color when reflection is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush brush)
|
||||
{
|
||||
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
|
||||
}
|
||||
|
||||
private void SetToolMode(WhiteboardToolMode mode)
|
||||
{
|
||||
_toolMode = mode;
|
||||
InkCanvas.EditingMode = mode == WhiteboardToolMode.Pen
|
||||
? InkCanvasEditingMode.Ink
|
||||
: InkCanvasEditingMode.EraseByPoint;
|
||||
|
||||
if (mode == WhiteboardToolMode.Pen)
|
||||
{
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.Settings.InkColor = _currentInkColor;
|
||||
}
|
||||
|
||||
RefreshToolButtonVisuals();
|
||||
}
|
||||
|
||||
private void RefreshToolButtonVisuals()
|
||||
{
|
||||
var isNightMode = _isNightModeApplied ?? ResolveIsNightMode();
|
||||
var activeBackground = ResolveThemeBrush("AdaptiveAccentBrush", isNightMode ? Color.Parse("#FF93C5FD") : Color.Parse("#FF3B82F6"));
|
||||
var activeForeground = ResolveThemeBrush("AdaptiveOnAccentBrush", Colors.White);
|
||||
var idleForeground = ResolveThemeBrush("AdaptiveTextPrimaryBrush", isNightMode ? Color.Parse("#FFE5E7EB") : Color.Parse("#FF0F172A"));
|
||||
var idleBackground = new SolidColorBrush(isNightMode ? Color.Parse("#33FFFFFF") : Color.Parse("#14000000"));
|
||||
|
||||
ApplyToolButtonVisual(PenButton, _toolMode == WhiteboardToolMode.Pen, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(EraserButton, _toolMode == WhiteboardToolMode.Eraser, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(ClearButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(ExportButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
}
|
||||
|
||||
private static void ApplyToolButtonVisual(
|
||||
Button button,
|
||||
bool isActive,
|
||||
IBrush activeBackground,
|
||||
IBrush activeForeground,
|
||||
IBrush idleBackground,
|
||||
IBrush idleForeground)
|
||||
{
|
||||
button.Background = isActive ? activeBackground : idleBackground;
|
||||
button.Foreground = isActive ? activeForeground : idleForeground;
|
||||
button.BorderThickness = new Thickness(0);
|
||||
|
||||
if (button.Content is SymbolIcon symbolIcon)
|
||||
{
|
||||
symbolIcon.Foreground = button.Foreground;
|
||||
}
|
||||
}
|
||||
|
||||
private IBrush ResolveThemeBrush(string key, Color fallback)
|
||||
{
|
||||
if (this.TryFindResource(key, out var resource) && resource is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
|
||||
return new SolidColorBrush(fallback);
|
||||
}
|
||||
|
||||
private void OnPenButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
SetToolMode(WhiteboardToolMode.Pen);
|
||||
}
|
||||
|
||||
private void OnEraserButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
SetToolMode(WhiteboardToolMode.Eraser);
|
||||
}
|
||||
|
||||
private void OnClearButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var strokeList = InkCanvas.Strokes.ToList();
|
||||
foreach (var stroke in strokeList)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ReferenceEquals(stroke.InkCanvas, InkCanvas.AvaloniaSkiaInkCanvas))
|
||||
{
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.RemoveStaticStroke(stroke);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep the widget alive even if one stroke removal fails.
|
||||
}
|
||||
}
|
||||
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.UseBitmapCache(false);
|
||||
InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
|
||||
InkCanvas.InvalidateVisual();
|
||||
}
|
||||
|
||||
private async void OnExportButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var fileName = $"whiteboard-{DateTime.Now:yyyyMMdd-HHmmss}.svg";
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is not null)
|
||||
{
|
||||
var saveFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Export Whiteboard SVG",
|
||||
SuggestedFileName = fileName,
|
||||
DefaultExtension = "svg",
|
||||
FileTypeChoices =
|
||||
[
|
||||
new FilePickerFileType("SVG image")
|
||||
{
|
||||
Patterns = ["*.svg"],
|
||||
MimeTypes = ["image/svg+xml"]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (saveFile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var saveStream = await saveFile.OpenWriteAsync();
|
||||
ExportSvgToStream(saveStream);
|
||||
return;
|
||||
}
|
||||
|
||||
var exportFolder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMontainDesktop",
|
||||
"Exports");
|
||||
Directory.CreateDirectory(exportFolder);
|
||||
var savePath = Path.Combine(exportFolder, fileName);
|
||||
await using var fileStream = File.Create(savePath);
|
||||
ExportSvgToStream(fileStream);
|
||||
}
|
||||
|
||||
private void ExportSvgToStream(Stream stream)
|
||||
{
|
||||
var width = Math.Max(1d, CanvasBorder.Bounds.Width);
|
||||
var height = Math.Max(1d, CanvasBorder.Bounds.Height);
|
||||
var bounds = SKRect.Create((float)width, (float)height);
|
||||
|
||||
using var svgCanvas = SKSvgCanvas.Create(bounds, stream);
|
||||
using var backgroundPaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
Color = (_isNightModeApplied ?? false) ? SKColors.Black : SKColors.White
|
||||
};
|
||||
svgCanvas.DrawRect(bounds, backgroundPaint);
|
||||
|
||||
using var strokePaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
foreach (var stroke in InkCanvas.Strokes)
|
||||
{
|
||||
strokePaint.Color = stroke.Color;
|
||||
svgCanvas.DrawPath(stroke.Path, strokePaint);
|
||||
}
|
||||
|
||||
svgCanvas.Flush();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public partial class MainWindow
|
||||
private TranslateTransform? _componentLibraryCategoryHostTransform;
|
||||
private TranslateTransform? _componentLibraryComponentHostTransform;
|
||||
private IReadOnlyList<ComponentLibraryCategory> _componentLibraryCategories = Array.Empty<ComponentLibraryCategory>();
|
||||
private IReadOnlyList<DesktopComponentDefinition> _componentLibraryActiveComponents = Array.Empty<DesktopComponentDefinition>();
|
||||
private IReadOnlyList<DesktopComponentRuntimeDescriptor> _componentLibraryActiveComponents = Array.Empty<DesktopComponentRuntimeDescriptor>();
|
||||
private bool _isComponentLibraryCategoryGestureActive;
|
||||
private bool _isComponentLibraryComponentGestureActive;
|
||||
private Point _componentLibraryCategoryGestureStartPoint;
|
||||
@@ -93,7 +93,9 @@ public partial class MainWindow
|
||||
string Id,
|
||||
Symbol Icon,
|
||||
string Title,
|
||||
IReadOnlyList<DesktopComponentDefinition> Components);
|
||||
IReadOnlyList<DesktopComponentRuntimeDescriptor> Components);
|
||||
|
||||
private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale);
|
||||
|
||||
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -258,7 +260,8 @@ public partial class MainWindow
|
||||
1 => TaskbarContext.SettingsGrid,
|
||||
2 => TaskbarContext.SettingsColor,
|
||||
3 => TaskbarContext.SettingsStatusBar,
|
||||
4 => TaskbarContext.SettingsRegion,
|
||||
4 => TaskbarContext.SettingsWeather,
|
||||
5 => TaskbarContext.SettingsRegion,
|
||||
_ => TaskbarContext.Desktop
|
||||
};
|
||||
}
|
||||
@@ -806,7 +809,8 @@ public partial class MainWindow
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: placement.PlacementId.Trim();
|
||||
var componentId = placement.ComponentId.Trim();
|
||||
if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement)
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -814,7 +818,7 @@ public partial class MainWindow
|
||||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||||
componentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
runtimeDescriptor.Definition,
|
||||
placement.WidthCells,
|
||||
placement.HeightCells));
|
||||
|
||||
@@ -849,7 +853,8 @@ public partial class MainWindow
|
||||
|
||||
foreach (var placement in _desktopComponentPlacements.Where(p => p.PageIndex == pageIndex))
|
||||
{
|
||||
if (!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) || !definition.AllowDesktopPlacement)
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) ||
|
||||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -857,7 +862,7 @@ public partial class MainWindow
|
||||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||||
placement.ComponentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
runtimeDescriptor.Definition,
|
||||
placement.WidthCells,
|
||||
placement.HeightCells));
|
||||
|
||||
@@ -890,7 +895,8 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement)
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -898,9 +904,9 @@ public partial class MainWindow
|
||||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||||
componentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
definition.MinWidthCells,
|
||||
definition.MinHeightCells));
|
||||
runtimeDescriptor.Definition,
|
||||
runtimeDescriptor.Definition.MinWidthCells,
|
||||
runtimeDescriptor.Definition.MinHeightCells));
|
||||
|
||||
var maxColumns = pageGrid.ColumnDefinitions.Count;
|
||||
var maxRows = pageGrid.RowDefinitions.Count;
|
||||
@@ -1046,55 +1052,110 @@ public partial class MainWindow
|
||||
return host;
|
||||
}
|
||||
|
||||
private static (int WidthCells, int HeightCells) NormalizeComponentCellSpan(
|
||||
private (int WidthCells, int HeightCells) NormalizeComponentCellSpan(
|
||||
string componentId,
|
||||
(int WidthCells, int HeightCells) span)
|
||||
{
|
||||
if (string.Equals(componentId, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase))
|
||||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||||
{
|
||||
return (Math.Max(4, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
var normalized = ComponentPlacementRules.EnsureMinimumSize(
|
||||
runtimeDescriptor.Definition,
|
||||
span.WidthCells,
|
||||
span.HeightCells);
|
||||
return NormalizeAspectRatioForComponent(componentId, normalized);
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.MonthCalendar, StringComparison.OrdinalIgnoreCase))
|
||||
return NormalizeAspectRatioForComponent(
|
||||
componentId,
|
||||
(Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells)));
|
||||
}
|
||||
|
||||
private static (int WidthCells, int HeightCells) NormalizeAspectRatioForComponent(
|
||||
string componentId,
|
||||
(int WidthCells, int HeightCells) span)
|
||||
{
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopWhiteboard, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
// Support both portrait ratios and snap to nearest viable scale tier.
|
||||
return SnapSpanToScaleRules(
|
||||
span,
|
||||
new ComponentScaleRule(WidthUnit: 1, HeightUnit: 2, MinScale: 2), // 2x4, 3x6, 4x8...
|
||||
new ComponentScaleRule(WidthUnit: 3, HeightUnit: 4, MinScale: 1)); // 3x4, 6x8...
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.LunarCalendar, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopBlackboardLandscape, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
// Support both landscape ratios and snap to nearest viable scale tier.
|
||||
return SnapSpanToScaleRules(
|
||||
span,
|
||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2), // 4x2, 6x3, 8x4...
|
||||
new ComponentScaleRule(WidthUnit: 4, HeightUnit: 3, MinScale: 1)); // 4x3, 8x6...
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopClock, StringComparison.OrdinalIgnoreCase))
|
||||
return span;
|
||||
}
|
||||
|
||||
private static (int WidthCells, int HeightCells) SnapSpanToScaleRules(
|
||||
(int WidthCells, int HeightCells) span,
|
||||
params ComponentScaleRule[] rules)
|
||||
{
|
||||
var targetWidth = Math.Max(1, span.WidthCells);
|
||||
var targetHeight = Math.Max(1, span.HeightCells);
|
||||
|
||||
var hasCandidate = false;
|
||||
var bestWidth = targetWidth;
|
||||
var bestHeight = targetHeight;
|
||||
var bestArea = -1;
|
||||
var bestDistance = double.MaxValue;
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
if (rule.WidthUnit <= 0 || rule.HeightUnit <= 0 || rule.MinScale <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var maxScale = Math.Min(targetWidth / rule.WidthUnit, targetHeight / rule.HeightUnit);
|
||||
if (maxScale < rule.MinScale)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var scale = rule.MinScale; scale <= maxScale; scale++)
|
||||
{
|
||||
var width = rule.WidthUnit * scale;
|
||||
var height = rule.HeightUnit * scale;
|
||||
var area = width * height;
|
||||
var dx = targetWidth - width;
|
||||
var dy = targetHeight - height;
|
||||
var distance = dx * dx + dy * dy;
|
||||
|
||||
if (!hasCandidate ||
|
||||
area > bestArea ||
|
||||
(area == bestArea && distance < bestDistance))
|
||||
{
|
||||
hasCandidate = true;
|
||||
bestWidth = width;
|
||||
bestHeight = height;
|
||||
bestArea = area;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopTimer, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
}
|
||||
|
||||
if (string.Equals(componentId, BuiltInComponentIds.HolidayCalendar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
|
||||
}
|
||||
|
||||
return (Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells));
|
||||
return hasCandidate
|
||||
? (bestWidth, bestHeight)
|
||||
: (targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
private double GetComponentCornerRadius(string componentId)
|
||||
{
|
||||
return componentId switch
|
||||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||||
{
|
||||
BuiltInComponentIds.Date => 16,
|
||||
BuiltInComponentIds.MonthCalendar => Math.Clamp(_currentDesktopCellSize * 0.26, 10, 22),
|
||||
BuiltInComponentIds.LunarCalendar => Math.Clamp(_currentDesktopCellSize * 0.30, 12, 26),
|
||||
BuiltInComponentIds.DesktopClock => Math.Clamp(_currentDesktopCellSize * 0.30, 12, 28),
|
||||
BuiltInComponentIds.DesktopTimer => Math.Clamp(_currentDesktopCellSize * 0.30, 12, 28),
|
||||
BuiltInComponentIds.HolidayCalendar => Math.Clamp(_currentDesktopCellSize * 0.32, 12, 28),
|
||||
_ => Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)
|
||||
};
|
||||
return runtimeDescriptor.ResolveCornerRadius(_currentDesktopCellSize);
|
||||
}
|
||||
|
||||
return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18);
|
||||
}
|
||||
|
||||
private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells)
|
||||
@@ -1172,60 +1233,14 @@ public partial class MainWindow
|
||||
|
||||
private Control? CreateDesktopComponentControl(string componentId)
|
||||
{
|
||||
if (componentId == BuiltInComponentIds.Date)
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||||
{
|
||||
var widget = new DateWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(_currentDesktopCellSize);
|
||||
widget.Classes.Add(DesktopComponentClass);
|
||||
return widget;
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.DesktopClock)
|
||||
{
|
||||
var widget = new AnalogClockWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(_currentDesktopCellSize);
|
||||
widget.Classes.Add(DesktopComponentClass);
|
||||
return widget;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.DesktopTimer)
|
||||
{
|
||||
var widget = new TimerWidget();
|
||||
widget.ApplyCellSize(_currentDesktopCellSize);
|
||||
widget.Classes.Add(DesktopComponentClass);
|
||||
return widget;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.HolidayCalendar)
|
||||
{
|
||||
var widget = new HolidayCalendarWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(_currentDesktopCellSize);
|
||||
widget.Classes.Add(DesktopComponentClass);
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
var component = runtimeDescriptor.CreateControl(_currentDesktopCellSize, _timeZoneService, _weatherDataService);
|
||||
component.Classes.Add(DesktopComponentClass);
|
||||
return component;
|
||||
}
|
||||
|
||||
private void CollapseComponentLibraryPanel()
|
||||
@@ -1372,7 +1387,7 @@ public partial class MainWindow
|
||||
DesktopEditDragLayer is null ||
|
||||
DesktopPagesViewport is null ||
|
||||
_currentDesktopCellSize <= 0 ||
|
||||
!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition))
|
||||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1380,7 +1395,7 @@ public partial class MainWindow
|
||||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||||
placement.ComponentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
runtimeDescriptor.Definition,
|
||||
placement.WidthCells,
|
||||
placement.HeightCells));
|
||||
|
||||
@@ -1418,8 +1433,8 @@ public partial class MainWindow
|
||||
DesktopEditDragLayer is null ||
|
||||
DesktopPagesViewport is null ||
|
||||
_currentDesktopCellSize <= 0 ||
|
||||
!_componentRegistry.TryGetDefinition(componentId, out var definition) ||
|
||||
!definition.AllowDesktopPlacement)
|
||||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
|
||||
!runtimeDescriptor.Definition.AllowDesktopPlacement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1427,9 +1442,9 @@ public partial class MainWindow
|
||||
var (widthCells, heightCells) = NormalizeComponentCellSpan(
|
||||
componentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
definition.MinWidthCells,
|
||||
definition.MinHeightCells));
|
||||
runtimeDescriptor.Definition,
|
||||
runtimeDescriptor.Definition.MinWidthCells,
|
||||
runtimeDescriptor.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);
|
||||
@@ -1534,7 +1549,7 @@ public partial class MainWindow
|
||||
{
|
||||
if (DesktopPagesViewport is null ||
|
||||
_currentDesktopCellSize <= 0 ||
|
||||
!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) ||
|
||||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) ||
|
||||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||||
{
|
||||
return;
|
||||
@@ -1543,16 +1558,16 @@ public partial class MainWindow
|
||||
var startSpan = NormalizeComponentCellSpan(
|
||||
placement.ComponentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
runtimeDescriptor.Definition,
|
||||
placement.WidthCells,
|
||||
placement.HeightCells));
|
||||
|
||||
var minSpan = NormalizeComponentCellSpan(
|
||||
placement.ComponentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
definition,
|
||||
definition.MinWidthCells,
|
||||
definition.MinHeightCells));
|
||||
runtimeDescriptor.Definition,
|
||||
runtimeDescriptor.Definition.MinWidthCells,
|
||||
runtimeDescriptor.Definition.MinHeightCells));
|
||||
|
||||
var maxWidthCells = Math.Max(startSpan.WidthCells, pageGrid.ColumnDefinitions.Count - placement.Column);
|
||||
var maxHeightCells = Math.Max(startSpan.HeightCells, pageGrid.RowDefinitions.Count - placement.Row);
|
||||
@@ -2052,24 +2067,20 @@ public partial class MainWindow
|
||||
|
||||
private IReadOnlyList<ComponentLibraryCategory> GetComponentLibraryCategories()
|
||||
{
|
||||
var definitions = _componentRegistry
|
||||
.GetAll()
|
||||
.Where(definition => definition.AllowDesktopPlacement)
|
||||
.ToList();
|
||||
|
||||
if (definitions.Count == 0)
|
||||
var descriptors = _componentRuntimeRegistry.GetDesktopComponents();
|
||||
if (descriptors.Count == 0)
|
||||
{
|
||||
return Array.Empty<ComponentLibraryCategory>();
|
||||
}
|
||||
|
||||
return definitions
|
||||
.GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
|
||||
return descriptors
|
||||
.GroupBy(descriptor => descriptor.Definition.Category, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group =>
|
||||
{
|
||||
var categoryId = string.IsNullOrWhiteSpace(group.Key) ? "Other" : group.Key.Trim();
|
||||
var components = group
|
||||
.OrderBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
return new ComponentLibraryCategory(
|
||||
categoryId,
|
||||
@@ -2092,6 +2103,16 @@ public partial class MainWindow
|
||||
return Symbol.CalendarDate;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.WeatherSunny;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Edit;
|
||||
}
|
||||
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
@@ -2107,6 +2128,16 @@ public partial class MainWindow
|
||||
return L("component_category.date", "Calendar");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component_category.weather", "Weather");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component_category.board", "Board");
|
||||
}
|
||||
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
@@ -2236,8 +2267,9 @@ public partial class MainWindow
|
||||
|
||||
for (var i = 0; i < componentCount; i++)
|
||||
{
|
||||
var definition = _componentLibraryActiveComponents[i];
|
||||
if (!_componentRegistry.TryGetDefinition(definition.Id, out var resolved) || !resolved.AllowDesktopPlacement)
|
||||
var descriptor = _componentLibraryActiveComponents[i];
|
||||
var definition = descriptor.Definition;
|
||||
if (!definition.AllowDesktopPlacement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2253,8 +2285,8 @@ public partial class MainWindow
|
||||
var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94;
|
||||
var previewMaxHeight = viewportHeight * 0.86;
|
||||
var previewSpan = NormalizeComponentCellSpan(
|
||||
resolved.Id,
|
||||
(resolved.MinWidthCells, resolved.MinHeightCells));
|
||||
definition.Id,
|
||||
(definition.MinWidthCells, definition.MinHeightCells));
|
||||
var previewCellSize = Math.Min(
|
||||
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
|
||||
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
|
||||
@@ -2264,11 +2296,7 @@ public partial class MainWindow
|
||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||
var renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
|
||||
|
||||
var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, renderCellSize);
|
||||
if (previewControl is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var previewControl = descriptor.CreateControl(renderCellSize, _timeZoneService, _weatherDataService);
|
||||
|
||||
var previewSurface = new Border
|
||||
{
|
||||
@@ -2294,13 +2322,13 @@ public partial class MainWindow
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Child = previewViewbox,
|
||||
Tag = resolved.Id
|
||||
Tag = definition.Id
|
||||
};
|
||||
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = GetLocalizedComponentDisplayName(resolved),
|
||||
Text = GetLocalizedComponentDisplayName(descriptor),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
|
||||
@@ -2346,91 +2374,9 @@ public partial class MainWindow
|
||||
UpdateComponentLibraryComponentNavigationButtons();
|
||||
}
|
||||
|
||||
private Control? CreateComponentLibraryPreviewControl(string componentId, double cellSize)
|
||||
private string GetLocalizedComponentDisplayName(DesktopComponentRuntimeDescriptor descriptor)
|
||||
{
|
||||
if (componentId == BuiltInComponentIds.Date)
|
||||
{
|
||||
var widget = new DateWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(cellSize);
|
||||
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;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.DesktopClock)
|
||||
{
|
||||
var widget = new AnalogClockWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(cellSize);
|
||||
return widget;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.DesktopTimer)
|
||||
{
|
||||
var widget = new TimerWidget();
|
||||
widget.ApplyCellSize(cellSize);
|
||||
return widget;
|
||||
}
|
||||
|
||||
if (componentId == BuiltInComponentIds.HolidayCalendar)
|
||||
{
|
||||
var widget = new HolidayCalendarWidget();
|
||||
widget.SetTimeZoneService(_timeZoneService);
|
||||
widget.ApplyCellSize(cellSize);
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetLocalizedComponentDisplayName(DesktopComponentDefinition definition)
|
||||
{
|
||||
if (string.Equals(definition.Id, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Id, BuiltInComponentIds.DesktopClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component.desktop_clock", definition.DisplayName);
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Id, BuiltInComponentIds.DesktopTimer, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component.desktop_timer", definition.DisplayName);
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Id, BuiltInComponentIds.HolidayCalendar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component.holiday_calendar", definition.DisplayName);
|
||||
}
|
||||
|
||||
return definition.DisplayName;
|
||||
return L(descriptor.DisplayNameLocalizationKey, descriptor.Definition.DisplayName);
|
||||
}
|
||||
|
||||
private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
@@ -9,6 +10,29 @@ namespace LanMontainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
{
|
||||
private const string AppCodeName = "Administrate";
|
||||
private const string AppFontName = "MiSans";
|
||||
private const string FallbackAppVersion = "1.0.0";
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["China Standard Time"] = "中国标准时间",
|
||||
["Asia/Shanghai"] = "中国标准时间",
|
||||
["Tokyo Standard Time"] = "日本标准时间",
|
||||
["Asia/Tokyo"] = "日本标准时间",
|
||||
["Pacific Standard Time"] = "太平洋标准时间",
|
||||
["America/Los_Angeles"] = "太平洋标准时间",
|
||||
["Eastern Standard Time"] = "美国东部标准时间",
|
||||
["America/New_York"] = "美国东部标准时间",
|
||||
["Central European Standard Time"] = "中欧标准时间",
|
||||
["Europe/Berlin"] = "中欧标准时间",
|
||||
["GMT Standard Time"] = "格林威治标准时间",
|
||||
["Europe/London"] = "格林威治标准时间",
|
||||
["UTC"] = "协调世界时",
|
||||
["Etc/UTC"] = "协调世界时"
|
||||
};
|
||||
|
||||
private void InitializeLocalization(string? languageCode)
|
||||
{
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(languageCode);
|
||||
@@ -82,6 +106,7 @@ public partial class MainWindow
|
||||
SettingsNavGridTextBlock.Text = L("settings.nav.grid", "Grid");
|
||||
SettingsNavColorTextBlock.Text = L("settings.nav.color", "Color");
|
||||
SettingsNavStatusBarTextBlock.Text = L("settings.nav.status_bar", "Status Bar");
|
||||
SettingsNavWeatherTextBlock.Text = L("settings.nav.weather", "Weather");
|
||||
SettingsNavRegionTextBlock.Text = L("settings.nav.region", "Region");
|
||||
|
||||
WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "个性化我们的背景");
|
||||
@@ -141,10 +166,78 @@ public partial class MainWindow
|
||||
StatusBarSpacingModeCustomItem.Content = L("settings.status_bar.spacing_mode_custom", "Custom");
|
||||
StatusBarSpacingCustomLabelTextBlock.Text = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
|
||||
|
||||
WeatherPanelTitleTextBlock.Text = L("settings.weather.title", "Weather");
|
||||
WeatherLocationSettingsExpander.Header = L("settings.weather.location_source_header", "Location Source");
|
||||
WeatherLocationSettingsExpander.Description = L(
|
||||
"settings.weather.location_source_desc",
|
||||
"Choose how weather widgets resolve location.");
|
||||
WeatherLocationModeCityItem.Content = L("settings.weather.mode_city_search", "City Search");
|
||||
WeatherLocationModeCoordinatesItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
|
||||
WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_refresh", "Auto refresh location on startup");
|
||||
|
||||
WeatherCitySearchSettingsExpander.Header = L("settings.weather.city_search_header", "City Search");
|
||||
WeatherCitySearchSettingsExpander.Description = L(
|
||||
"settings.weather.city_search_desc",
|
||||
"Search cities and apply one weather location.");
|
||||
WeatherCitySearchTextBox.Watermark = L("settings.weather.search_placeholder", "e.g. Beijing");
|
||||
WeatherSearchButton.Content = L("settings.weather.search_button", "Search");
|
||||
WeatherApplyCityButton.Content = L("settings.weather.apply_city_button", "Apply City");
|
||||
|
||||
WeatherCoordinateSettingsExpander.Header = L("settings.weather.coordinates_header", "Coordinates");
|
||||
WeatherCoordinateSettingsExpander.Description = L(
|
||||
"settings.weather.coordinates_desc",
|
||||
"Set latitude/longitude and optional key/name.");
|
||||
WeatherLatitudeNumberBox.Header = L("settings.weather.latitude_label", "Latitude");
|
||||
WeatherLongitudeNumberBox.Header = L("settings.weather.longitude_label", "Longitude");
|
||||
WeatherLocationKeyTextBox.Watermark = L("settings.weather.location_key_placeholder", "Location key (optional)");
|
||||
WeatherLocationNameTextBox.Watermark = L("settings.weather.location_name_placeholder", "Display name (optional)");
|
||||
WeatherApplyCoordinatesButton.Content = L("settings.weather.apply_coordinates_button", "Apply Coordinates");
|
||||
|
||||
WeatherPreviewSettingsExpander.Header = L("settings.weather.preview_header", "Connection Test");
|
||||
WeatherPreviewSettingsExpander.Description = L(
|
||||
"settings.weather.preview_desc",
|
||||
"Send one test request to verify current settings.");
|
||||
WeatherPreviewButton.Content = L("settings.weather.preview_button", "Test Fetch");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_weatherSearchKeyword))
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = L(
|
||||
"settings.weather.search_hint",
|
||||
"Search by city name and apply one location.");
|
||||
}
|
||||
|
||||
if (!_isWeatherPreviewInProgress)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = L(
|
||||
"settings.weather.preview_hint",
|
||||
"Use test fetch to verify your weather configuration.");
|
||||
}
|
||||
|
||||
UpdateWeatherLocationStatusText();
|
||||
|
||||
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
|
||||
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");
|
||||
LanguageChineseItem.Content = L("settings.region.language_zh", "Chinese");
|
||||
LanguageEnglishItem.Content = L("settings.region.language_en", "English");
|
||||
TimeZoneSettingsExpander.Header = L("settings.region.timezone_header", "Time Zone");
|
||||
TimeZoneSettingsExpander.Description = L(
|
||||
"settings.region.timezone_desc",
|
||||
"Select a time zone. Clock and calendar widgets will follow this zone.");
|
||||
|
||||
SettingsNavAboutTextBlock.Text = L("settings.nav.about", "About");
|
||||
AboutPanelTitleTextBlock.Text = L("settings.about.title", "About");
|
||||
VersionTextBlock.Text = Lf(
|
||||
"settings.about.version_format",
|
||||
"Version: {0}",
|
||||
GetAppVersionText());
|
||||
CodeNameTextBlock.Text = Lf(
|
||||
"settings.about.codename_format",
|
||||
"Code Name: {0}",
|
||||
AppCodeName);
|
||||
FontInfoTextBlock.Text = Lf(
|
||||
"settings.about.font_format",
|
||||
"Font: {0}",
|
||||
AppFontName);
|
||||
|
||||
if (WallpaperPlacementComboBox?.ItemCount >= 5)
|
||||
{
|
||||
@@ -163,12 +256,48 @@ public partial class MainWindow
|
||||
DesktopGrid.RowDefinitions.Count,
|
||||
DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d);
|
||||
|
||||
InitializeTimeZoneSettings();
|
||||
BuildComponentLibraryCategoryPages();
|
||||
RenderLauncherRootTiles();
|
||||
UpdateOpenSettingsActionVisualState();
|
||||
UpdateWallpaperDisplay();
|
||||
}
|
||||
|
||||
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
|
||||
{
|
||||
var offset = timeZone.BaseUtcOffset;
|
||||
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||
var hours = Math.Abs(offset.Hours);
|
||||
var minutes = Math.Abs(offset.Minutes);
|
||||
var name = string.IsNullOrWhiteSpace(timeZone.StandardName)
|
||||
? timeZone.DisplayName
|
||||
: timeZone.StandardName;
|
||||
|
||||
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) &&
|
||||
ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
|
||||
{
|
||||
name = localizedName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = timeZone.Id;
|
||||
}
|
||||
|
||||
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {name}";
|
||||
}
|
||||
|
||||
private static string GetAppVersionText()
|
||||
{
|
||||
var version = typeof(MainWindow).Assembly.GetName().Version;
|
||||
if (version is null || version.Major < 0 || version.Minor < 0 || version.Build < 0)
|
||||
{
|
||||
return FallbackAppVersion;
|
||||
}
|
||||
|
||||
return $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
|
||||
private void OnLanguageSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressLanguageSelectionEvents || LanguageComboBox?.SelectedItem is not ComboBoxItem item)
|
||||
@@ -185,4 +314,48 @@ public partial class MainWindow
|
||||
GetLanguageDisplayName(_languageCode));
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void UpdateWeatherLocationStatusText()
|
||||
{
|
||||
if (WeatherLocationStatusTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modeText = _weatherLocationMode == WeatherLocationMode.Coordinates
|
||||
? L("settings.weather.mode_coordinates", "Coordinates")
|
||||
: L("settings.weather.mode_city_search", "City Search");
|
||||
|
||||
if (_weatherLocationMode == WeatherLocationMode.CitySearch)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_weatherLocationKey))
|
||||
{
|
||||
WeatherLocationStatusTextBlock.Text = L(
|
||||
"settings.weather.status_city_empty",
|
||||
"No city location is configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
var locationName = string.IsNullOrWhiteSpace(_weatherLocationName)
|
||||
? _weatherLocationKey
|
||||
: _weatherLocationName;
|
||||
WeatherLocationStatusTextBlock.Text = Lf(
|
||||
"settings.weather.status_city_format",
|
||||
"Mode: {0} | {1} | Key: {2}",
|
||||
modeText,
|
||||
locationName,
|
||||
_weatherLocationKey);
|
||||
return;
|
||||
}
|
||||
|
||||
WeatherLocationStatusTextBlock.Text = Lf(
|
||||
"settings.weather.status_coordinates_format",
|
||||
"Mode: {0} | Lat {1:F4}, Lon {2:F4} | Key: {3}",
|
||||
modeText,
|
||||
_weatherLatitude,
|
||||
_weatherLongitude,
|
||||
string.IsNullOrWhiteSpace(_weatherLocationKey)
|
||||
? BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude)
|
||||
: _weatherLocationKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using FluentIcons.Common;
|
||||
using LanMontainDesktop.Views.Components;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -61,8 +62,9 @@ public partial class MainWindow
|
||||
WallpaperSettingsPanel is null ||
|
||||
ColorSettingsPanel is null ||
|
||||
StatusBarSettingsPanel is null ||
|
||||
WeatherSettingsPanel is null ||
|
||||
RegionSettingsPanel is null ||
|
||||
AboutSettingsPanel is null)
|
||||
AboutSettingsPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -72,8 +74,9 @@ public partial class MainWindow
|
||||
GridSettingsPanel.IsVisible = selectedIndex == 1;
|
||||
ColorSettingsPanel.IsVisible = selectedIndex == 2;
|
||||
StatusBarSettingsPanel.IsVisible = selectedIndex == 3;
|
||||
RegionSettingsPanel.IsVisible = selectedIndex == 4;
|
||||
AboutSettingsPanel.IsVisible = selectedIndex == 5;
|
||||
WeatherSettingsPanel.IsVisible = selectedIndex == 4;
|
||||
RegionSettingsPanel.IsVisible = selectedIndex == 5;
|
||||
AboutSettingsPanel.IsVisible = selectedIndex == 6;
|
||||
|
||||
if (selectedIndex == 1)
|
||||
{
|
||||
@@ -645,6 +648,13 @@ public partial class MainWindow
|
||||
SettingsTabIndex = Math.Max(0, SettingsNavListBox?.SelectedIndex ?? 0),
|
||||
LanguageCode = _languageCode,
|
||||
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
|
||||
WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode),
|
||||
WeatherLocationKey = _weatherLocationKey,
|
||||
WeatherLocationName = _weatherLocationName,
|
||||
WeatherLatitude = _weatherLatitude,
|
||||
WeatherLongitude = _weatherLongitude,
|
||||
WeatherAutoRefreshLocation = _weatherAutoRefreshLocation,
|
||||
WeatherLocationQuery = BuildLegacyWeatherLocationQuery(),
|
||||
TopStatusComponentIds = _topStatusComponentIds.ToList(),
|
||||
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
|
||||
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
|
||||
@@ -677,6 +687,560 @@ public partial class MainWindow
|
||||
}, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
|
||||
}
|
||||
|
||||
private void InitializeWeatherSettings(AppSettingsSnapshot snapshot)
|
||||
{
|
||||
_suppressWeatherLocationEvents = true;
|
||||
try
|
||||
{
|
||||
_weatherLocationMode = ParseWeatherLocationMode(snapshot.WeatherLocationMode);
|
||||
_weatherLocationKey = snapshot.WeatherLocationKey?.Trim() ?? string.Empty;
|
||||
_weatherLocationName = snapshot.WeatherLocationName?.Trim() ?? string.Empty;
|
||||
_weatherLatitude = NormalizeLatitude(snapshot.WeatherLatitude);
|
||||
_weatherLongitude = NormalizeLongitude(snapshot.WeatherLongitude);
|
||||
_weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation;
|
||||
_weatherSearchKeyword = string.Empty;
|
||||
|
||||
var legacyQuery = snapshot.WeatherLocationQuery?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(_weatherLocationKey) && !string.IsNullOrWhiteSpace(legacyQuery))
|
||||
{
|
||||
_weatherLocationKey = legacyQuery;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_weatherLocationName) && !string.IsNullOrWhiteSpace(legacyQuery))
|
||||
{
|
||||
_weatherLocationName = legacyQuery;
|
||||
}
|
||||
|
||||
SelectWeatherLocationModeInUi(_weatherLocationMode);
|
||||
if (WeatherAutoRefreshToggleSwitch is not null)
|
||||
{
|
||||
WeatherAutoRefreshToggleSwitch.IsChecked = _weatherAutoRefreshLocation;
|
||||
}
|
||||
|
||||
if (WeatherCitySearchTextBox is not null)
|
||||
{
|
||||
WeatherCitySearchTextBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
if (WeatherCityResultsComboBox is not null)
|
||||
{
|
||||
WeatherCityResultsComboBox.Items.Clear();
|
||||
}
|
||||
|
||||
if (WeatherLocationKeyTextBox is not null)
|
||||
{
|
||||
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
|
||||
}
|
||||
|
||||
if (WeatherLocationNameTextBox is not null)
|
||||
{
|
||||
WeatherLocationNameTextBox.Text = _weatherLocationName;
|
||||
}
|
||||
|
||||
if (WeatherLatitudeNumberBox is not null)
|
||||
{
|
||||
WeatherLatitudeNumberBox.Value = _weatherLatitude;
|
||||
}
|
||||
|
||||
if (WeatherLongitudeNumberBox is not null)
|
||||
{
|
||||
WeatherLongitudeNumberBox.Value = _weatherLongitude;
|
||||
}
|
||||
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = L(
|
||||
"settings.weather.search_hint",
|
||||
"Search by city name and apply one location.");
|
||||
}
|
||||
|
||||
if (WeatherCoordinateStatusTextBlock is not null)
|
||||
{
|
||||
WeatherCoordinateStatusTextBlock.Text = string.Empty;
|
||||
}
|
||||
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = L(
|
||||
"settings.weather.preview_hint",
|
||||
"Use test fetch to verify your weather configuration.");
|
||||
}
|
||||
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressWeatherLocationEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static WeatherLocationMode ParseWeatherLocationMode(string? value)
|
||||
{
|
||||
return string.Equals(value, "Coordinates", StringComparison.OrdinalIgnoreCase)
|
||||
? WeatherLocationMode.Coordinates
|
||||
: WeatherLocationMode.CitySearch;
|
||||
}
|
||||
|
||||
private static string ToWeatherLocationModeTag(WeatherLocationMode mode)
|
||||
{
|
||||
return mode == WeatherLocationMode.Coordinates ? "Coordinates" : "CitySearch";
|
||||
}
|
||||
|
||||
private static double NormalizeLatitude(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return 39.9042;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, -90, 90);
|
||||
}
|
||||
|
||||
private static double NormalizeLongitude(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return 116.4074;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, -180, 180);
|
||||
}
|
||||
|
||||
private string BuildLegacyWeatherLocationQuery()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_weatherLocationName))
|
||||
{
|
||||
return _weatherLocationName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_weatherLocationKey))
|
||||
{
|
||||
return _weatherLocationKey;
|
||||
}
|
||||
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{_weatherLatitude:F4},{_weatherLongitude:F4}");
|
||||
}
|
||||
|
||||
private void SelectWeatherLocationModeInUi(WeatherLocationMode mode)
|
||||
{
|
||||
if (WeatherLocationModeComboBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in WeatherLocationModeComboBox.Items.OfType<ComboBoxItem>())
|
||||
{
|
||||
if (string.Equals(item.Tag?.ToString(), ToWeatherLocationModeTag(mode), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WeatherLocationModeComboBox.SelectedItem = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WeatherLocationModeComboBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
|
||||
}
|
||||
|
||||
private void UpdateWeatherLocationModePanels()
|
||||
{
|
||||
if (WeatherCitySearchSettingsExpander is not null)
|
||||
{
|
||||
WeatherCitySearchSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.CitySearch;
|
||||
}
|
||||
|
||||
if (WeatherCoordinateSettingsExpander is not null)
|
||||
{
|
||||
WeatherCoordinateSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.Coordinates;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWeatherLocationModeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressWeatherLocationEvents || WeatherLocationModeComboBox?.SelectedItem is not ComboBoxItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString());
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnWeatherAutoRefreshToggled(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppressWeatherLocationEvents || WeatherAutoRefreshToggleSwitch is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherAutoRefreshLocation = WeatherAutoRefreshToggleSwitch.IsChecked == true;
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isWeatherSearchInProgress || WeatherCitySearchTextBox is null || WeatherCityResultsComboBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keyword = WeatherCitySearchTextBox.Text?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = L(
|
||||
"settings.weather.search_required",
|
||||
"Please enter a city keyword first.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherSearchKeyword = keyword;
|
||||
_isWeatherSearchInProgress = true;
|
||||
SetWeatherSearchBusy(isBusy: true);
|
||||
try
|
||||
{
|
||||
var result = await _weatherDataService.SearchLocationsAsync(keyword, ResolveWeatherApiLocale());
|
||||
if (!result.Success || result.Data is null)
|
||||
{
|
||||
WeatherCityResultsComboBox.Items.Clear();
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = Lf(
|
||||
"settings.weather.search_failed_format",
|
||||
"Search failed: {0}",
|
||||
result.ErrorMessage ?? result.ErrorCode ?? "Unknown error");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var locations = result.Data
|
||||
.Where(location => !string.IsNullOrWhiteSpace(location.LocationKey))
|
||||
.Take(80)
|
||||
.ToList();
|
||||
|
||||
WeatherCityResultsComboBox.Items.Clear();
|
||||
foreach (var location in locations)
|
||||
{
|
||||
WeatherCityResultsComboBox.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Content = FormatWeatherLocationDisplayName(location),
|
||||
Tag = location
|
||||
});
|
||||
}
|
||||
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = locations.Count == 0
|
||||
? L("settings.weather.search_no_results", "No locations were found.")
|
||||
: Lf(
|
||||
"settings.weather.search_result_count_format",
|
||||
"Found {0} locations.",
|
||||
locations.Count);
|
||||
}
|
||||
|
||||
if (locations.Count > 0)
|
||||
{
|
||||
WeatherCityResultsComboBox.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = Lf(
|
||||
"settings.weather.search_failed_format",
|
||||
"Search failed: {0}",
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isWeatherSearchInProgress = false;
|
||||
SetWeatherSearchBusy(isBusy: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatWeatherLocationDisplayName(WeatherLocation location)
|
||||
{
|
||||
var affiliation = string.IsNullOrWhiteSpace(location.Affiliation)
|
||||
? string.Empty
|
||||
: $" ({location.Affiliation})";
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{location.Name}{affiliation} · {location.LocationKey}");
|
||||
}
|
||||
|
||||
private static string BuildWeatherLocationName(WeatherLocation location)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(location.Affiliation))
|
||||
{
|
||||
return location.Name;
|
||||
}
|
||||
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{location.Name} ({location.Affiliation})");
|
||||
}
|
||||
|
||||
private void OnApplyWeatherCitySelectionClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WeatherCityResultsComboBox?.SelectedItem is not ComboBoxItem item ||
|
||||
item.Tag is not WeatherLocation location)
|
||||
{
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = L(
|
||||
"settings.weather.search_select_required",
|
||||
"Please select one location from search results.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherLocationMode = WeatherLocationMode.CitySearch;
|
||||
_weatherLocationKey = location.LocationKey.Trim();
|
||||
_weatherLocationName = BuildWeatherLocationName(location);
|
||||
_weatherLatitude = NormalizeLatitude(location.Latitude);
|
||||
_weatherLongitude = NormalizeLongitude(location.Longitude);
|
||||
|
||||
_suppressWeatherLocationEvents = true;
|
||||
try
|
||||
{
|
||||
SelectWeatherLocationModeInUi(_weatherLocationMode);
|
||||
if (WeatherLocationKeyTextBox is not null)
|
||||
{
|
||||
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
|
||||
}
|
||||
|
||||
if (WeatherLocationNameTextBox is not null)
|
||||
{
|
||||
WeatherLocationNameTextBox.Text = _weatherLocationName;
|
||||
}
|
||||
|
||||
if (WeatherLatitudeNumberBox is not null)
|
||||
{
|
||||
WeatherLatitudeNumberBox.Value = _weatherLatitude;
|
||||
}
|
||||
|
||||
if (WeatherLongitudeNumberBox is not null)
|
||||
{
|
||||
WeatherLongitudeNumberBox.Value = _weatherLongitude;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressWeatherLocationEvents = false;
|
||||
}
|
||||
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = Lf(
|
||||
"settings.weather.search_applied_format",
|
||||
"Location applied: {0}",
|
||||
_weatherLocationName);
|
||||
}
|
||||
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnApplyWeatherCoordinatesClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WeatherLatitudeNumberBox is null || WeatherLongitudeNumberBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var latitude = NormalizeLatitude(WeatherLatitudeNumberBox.Value);
|
||||
var longitude = NormalizeLongitude(WeatherLongitudeNumberBox.Value);
|
||||
var keyInput = WeatherLocationKeyTextBox?.Text?.Trim() ?? string.Empty;
|
||||
var nameInput = WeatherLocationNameTextBox?.Text?.Trim() ?? string.Empty;
|
||||
|
||||
_weatherLocationMode = WeatherLocationMode.Coordinates;
|
||||
_weatherLatitude = latitude;
|
||||
_weatherLongitude = longitude;
|
||||
_weatherLocationKey = string.IsNullOrWhiteSpace(keyInput)
|
||||
? BuildCoordinateLocationKey(latitude, longitude)
|
||||
: keyInput;
|
||||
_weatherLocationName = string.IsNullOrWhiteSpace(nameInput)
|
||||
? Lf(
|
||||
"settings.weather.coordinates_default_name_format",
|
||||
"Coordinate {0:F4}, {1:F4}",
|
||||
latitude,
|
||||
longitude)
|
||||
: nameInput;
|
||||
|
||||
_suppressWeatherLocationEvents = true;
|
||||
try
|
||||
{
|
||||
SelectWeatherLocationModeInUi(_weatherLocationMode);
|
||||
if (WeatherLocationKeyTextBox is not null && string.IsNullOrWhiteSpace(keyInput))
|
||||
{
|
||||
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
|
||||
}
|
||||
|
||||
if (WeatherLocationNameTextBox is not null && string.IsNullOrWhiteSpace(nameInput))
|
||||
{
|
||||
WeatherLocationNameTextBox.Text = _weatherLocationName;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressWeatherLocationEvents = false;
|
||||
}
|
||||
|
||||
if (WeatherCoordinateStatusTextBlock is not null)
|
||||
{
|
||||
WeatherCoordinateStatusTextBlock.Text = Lf(
|
||||
"settings.weather.coordinates_saved_format",
|
||||
"Coordinates saved: {0:F4}, {1:F4}",
|
||||
_weatherLatitude,
|
||||
_weatherLongitude);
|
||||
}
|
||||
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private static string BuildCoordinateLocationKey(double latitude, double longitude)
|
||||
{
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"coord:{latitude:F4},{longitude:F4}");
|
||||
}
|
||||
|
||||
private async void OnTestWeatherRequestClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isWeatherPreviewInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_weatherLocationKey))
|
||||
{
|
||||
if (_weatherLocationMode == WeatherLocationMode.Coordinates)
|
||||
{
|
||||
_weatherLocationKey = BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = L(
|
||||
"settings.weather.preview_missing_location",
|
||||
"Please apply one weather location before testing.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_isWeatherPreviewInProgress = true;
|
||||
SetWeatherPreviewBusy(isBusy: true);
|
||||
try
|
||||
{
|
||||
var query = new WeatherQuery(
|
||||
LocationKey: _weatherLocationKey,
|
||||
Latitude: _weatherLatitude,
|
||||
Longitude: _weatherLongitude,
|
||||
ForecastDays: 3,
|
||||
Locale: ResolveWeatherApiLocale(),
|
||||
IsGlobal: false,
|
||||
ForceRefresh: true);
|
||||
|
||||
var result = await _weatherDataService.GetWeatherAsync(query);
|
||||
if (!result.Success || result.Data is null)
|
||||
{
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = Lf(
|
||||
"settings.weather.preview_failed_format",
|
||||
"Test fetch failed: {0}",
|
||||
result.ErrorMessage ?? result.ErrorCode ?? "Unknown error");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = result.Data;
|
||||
var location = string.IsNullOrWhiteSpace(snapshot.LocationName)
|
||||
? (!string.IsNullOrWhiteSpace(_weatherLocationName) ? _weatherLocationName : _weatherLocationKey)
|
||||
: snapshot.LocationName;
|
||||
var weather = snapshot.Current.WeatherText ??
|
||||
L("settings.weather.preview_unknown", "Unknown");
|
||||
var temperature = snapshot.Current.TemperatureC.HasValue
|
||||
? string.Create(CultureInfo.InvariantCulture, $"{snapshot.Current.TemperatureC.Value:F1}°C")
|
||||
: "--";
|
||||
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = Lf(
|
||||
"settings.weather.preview_success_format",
|
||||
"Test success: {0} · {1} · {2}",
|
||||
location,
|
||||
weather,
|
||||
temperature);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = Lf(
|
||||
"settings.weather.preview_failed_format",
|
||||
"Test fetch failed: {0}",
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isWeatherPreviewInProgress = false;
|
||||
SetWeatherPreviewBusy(isBusy: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWeatherSearchBusy(bool isBusy)
|
||||
{
|
||||
if (WeatherSearchButton is not null)
|
||||
{
|
||||
WeatherSearchButton.IsEnabled = !isBusy;
|
||||
}
|
||||
|
||||
if (WeatherSearchProgressRing is not null)
|
||||
{
|
||||
WeatherSearchProgressRing.IsVisible = isBusy;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWeatherPreviewBusy(bool isBusy)
|
||||
{
|
||||
if (WeatherPreviewButton is not null)
|
||||
{
|
||||
WeatherPreviewButton.IsEnabled = !isBusy;
|
||||
}
|
||||
|
||||
if (WeatherPreviewProgressRing is not null)
|
||||
{
|
||||
WeatherPreviewProgressRing.IsVisible = isBusy;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveWeatherApiLocale()
|
||||
{
|
||||
return string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||
? "zh_cn"
|
||||
: "en_us";
|
||||
}
|
||||
|
||||
private void UpdateAdaptiveTextSystem()
|
||||
{
|
||||
var isLightBackground = _isSettingsOpen
|
||||
@@ -1110,6 +1674,15 @@ public partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
if (WeatherLocationSettingsExpander is not null)
|
||||
{
|
||||
WeatherLocationSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.WeatherSunny,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (LanguageSettingsExpander is not null)
|
||||
{
|
||||
LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMontainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
@@ -133,10 +133,10 @@
|
||||
<TextBlock x:Name="LauncherTitleTextBlock"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Text="应用启动台" />
|
||||
Text="App Launcher" />
|
||||
<TextBlock x:Name="LauncherSubtitleTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="按 Windows 开始菜单结构显示所有应用与文件夹" />
|
||||
Text="Apps and folders from Windows Start Menu." />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
@@ -183,7 +183,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="文件夹" />
|
||||
Text="Folder" />
|
||||
<Button x:Name="LauncherFolderCloseButton"
|
||||
Grid.Column="2"
|
||||
Width="38"
|
||||
@@ -346,6 +346,7 @@
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="SettingsPage"
|
||||
Classes="settings-scope"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -375,14 +376,14 @@
|
||||
</Border.RenderTransform>
|
||||
|
||||
<Border Classes="mica-strong"
|
||||
CornerRadius="32"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusXl}"
|
||||
Padding="18">
|
||||
<Grid ColumnDefinitions="220,*"
|
||||
ColumnSpacing="16">
|
||||
<Border x:Name="SettingsNavPanelBorder"
|
||||
Classes="glass-panel"
|
||||
Grid.Column="0"
|
||||
CornerRadius="28"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||
Padding="10">
|
||||
<Border.Styles>
|
||||
<Style Selector="ListBox#SettingsNavListBox">
|
||||
@@ -395,7 +396,7 @@
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="Margin" Value="0,2" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox#SettingsNavListBox ListBoxItem:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemHoverBackgroundBrush}" />
|
||||
@@ -438,6 +439,12 @@
|
||||
<TextBlock x:Name="SettingsNavStatusBarTextBlock" Text="状态栏" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem x:Name="SettingsNavWeatherItem" ToolTip.Tip="天气">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<fi:SymbolIcon x:Name="SettingsNavWeatherIcon" Symbol="WeatherSunny" IconVariant="Regular" />
|
||||
<TextBlock x:Name="SettingsNavWeatherTextBlock" Text="天气" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem x:Name="SettingsNavRegionItem" ToolTip.Tip="地区">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<fi:SymbolIcon x:Name="SettingsNavRegionIcon" Symbol="Globe" IconVariant="Regular" />
|
||||
@@ -456,7 +463,7 @@
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Classes="glass-panel"
|
||||
CornerRadius="20"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="14">
|
||||
<Grid>
|
||||
<Grid x:Name="WallpaperSettingsPanel"
|
||||
@@ -469,7 +476,7 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Margin="0,0,0,24"
|
||||
Text="个性化您的背景" />
|
||||
Text="涓€у寲鎮ㄧ殑鑳屾櫙" />
|
||||
|
||||
<!-- Left Column: Monitor Preview -->
|
||||
<Border x:Name="WallpaperPreviewHost"
|
||||
@@ -520,7 +527,7 @@
|
||||
<Border x:Name="WallpaperPreviewTaskbarFixedActionsHost" Grid.Column="0">
|
||||
<StackPanel x:Name="WallpaperPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
|
||||
<fi:SymbolIcon Classes="icon-s" Symbol="Window" />
|
||||
<TextBlock x:Name="WallpaperPreviewBackButtonTextBlock" Text="回到Windows" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="WallpaperPreviewBackButtonTextBlock" Text="鍥炲埌Windows" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel x:Name="WallpaperPreviewTaskbarDynamicActionsHost"
|
||||
@@ -532,7 +539,7 @@
|
||||
<StackPanel Orientation="Horizontal" Spacing="3">
|
||||
<StackPanel x:Name="WallpaperPreviewComponentLibraryVisual" IsVisible="False" Orientation="Horizontal" Spacing="3">
|
||||
<fi:FluentIcon Classes="icon-s" Icon="Apps" />
|
||||
<TextBlock x:Name="WallpaperPreviewComponentLibraryTextBlock" Text="组件库" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="WallpaperPreviewComponentLibraryTextBlock" Text="Widget library" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<fi:SymbolIcon x:Name="WallpaperPreviewSettingsButtonIcon" Classes="icon-s" Symbol="Settings" />
|
||||
</StackPanel>
|
||||
@@ -550,22 +557,22 @@
|
||||
Margin="16,0,0,0"
|
||||
Spacing="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="预览状态" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<TextBlock Text="Preview status" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<TextBlock x:Name="WallpaperPathTextBlock"
|
||||
FontSize="14"
|
||||
FontWeight="Medium"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="未选择文件" />
|
||||
Text="鏈€夋嫨鏂囦欢" />
|
||||
<TextBlock x:Name="WallpaperStatusTextBlock"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
|
||||
Text="就绪" />
|
||||
Text="灏辩华" />
|
||||
</StackPanel>
|
||||
|
||||
<Separator Background="{DynamicResource SurfaceStrokeColorDefaultBrush}" Height="1" Margin="0,8" />
|
||||
|
||||
<TextBlock Text="选择图片或视频" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock Text="Choose image or video" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
|
||||
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
|
||||
<Button x:Name="PickWallpaperButton"
|
||||
@@ -574,17 +581,18 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="0,10"
|
||||
Click="OnPickWallpaperClick"
|
||||
Content="浏览照片" />
|
||||
Content="娴忚鐓х墖" />
|
||||
<Button x:Name="ClearWallpaperButton"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="0,10"
|
||||
Click="OnClearWallpaperClick"
|
||||
Content="重置" />
|
||||
Content="閲嶇疆" />
|
||||
</Grid>
|
||||
|
||||
<ui:SettingsExpander x:Name="WallpaperPlacementSettingsExpander"
|
||||
Header="选择契合度"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WallpaperPlacementSettingsExpander"
|
||||
Header="Placement"
|
||||
Padding="12,8">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox x:Name="WallpaperPlacementComboBox"
|
||||
@@ -598,6 +606,7 @@
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -611,7 +620,7 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Margin="0,0,0,24"
|
||||
Text="调整网格布局" />
|
||||
Text="璋冩暣缃戞牸甯冨眬" />
|
||||
|
||||
<!-- Left Column: Grid Preview -->
|
||||
<Border x:Name="GridPreviewHost"
|
||||
@@ -657,7 +666,7 @@
|
||||
<Border x:Name="GridPreviewTaskbarFixedActionsHost" Grid.Column="0">
|
||||
<StackPanel x:Name="GridPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
|
||||
<fi:SymbolIcon Classes="icon-s" Symbol="Window" />
|
||||
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="回到Windows" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="鍥炲埌Windows" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel x:Name="GridPreviewTaskbarDynamicActionsHost"
|
||||
@@ -669,7 +678,7 @@
|
||||
<StackPanel Orientation="Horizontal" Spacing="3">
|
||||
<StackPanel x:Name="GridPreviewComponentLibraryVisual" IsVisible="False" Orientation="Horizontal" Spacing="3">
|
||||
<fi:FluentIcon x:Name="GridPreviewComponentLibraryIcon" Classes="icon-s" Icon="Apps" />
|
||||
<TextBlock x:Name="GridPreviewComponentLibraryTextBlock" Text="组件库" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="GridPreviewComponentLibraryTextBlock" Text="Widget library" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<fi:SymbolIcon x:Name="GridPreviewSettingsButtonIcon" Classes="icon-s" Symbol="Settings" />
|
||||
</StackPanel>
|
||||
@@ -686,7 +695,7 @@
|
||||
<StackPanel Grid.Row="1" Grid.Column="1"
|
||||
Margin="16,0,0,0"
|
||||
Spacing="16">
|
||||
<TextBlock Text="竖排格数" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock Text="绔栨帓鏍兼暟" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<Slider x:Name="GridSizeSlider"
|
||||
@@ -745,7 +754,7 @@
|
||||
|
||||
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="≈ 0 px" />
|
||||
Text="鈮?0 px" />
|
||||
|
||||
<Button x:Name="ApplyGridButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -769,7 +778,8 @@
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Color" />
|
||||
|
||||
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
|
||||
Header="日夜模式"
|
||||
Description="切换应用的浅色或深色主题。">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
@@ -783,8 +793,10 @@
|
||||
Unchecked="OnNightModeUnchecked" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<ui:SettingsExpander x:Name="ThemeColorSettingsExpander"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="ThemeColorSettingsExpander"
|
||||
Header="主题色"
|
||||
Description="选择应用的主题点缀色。">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
@@ -961,6 +973,7 @@
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="ThemeColorStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
|
||||
@@ -976,7 +989,8 @@
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Status Bar" />
|
||||
|
||||
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
|
||||
Header="时间组件"
|
||||
Description="在顶部状态栏显示时钟。"
|
||||
IsExpanded="False">
|
||||
@@ -991,23 +1005,25 @@
|
||||
Unchecked="OnStatusBarClockUnchecked" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<StackPanel Margin="0,8,0,0" Spacing="12">
|
||||
<TextBlock Text="显示格式" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock Text="鏄剧ず鏍煎紡" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<RadioButton x:Name="ClockFormatHMSSRadio"
|
||||
Content="时分秒 (HH:mm:ss)"
|
||||
Content="鏃跺垎绉?(HH:mm:ss)"
|
||||
GroupName="ClockFormat"
|
||||
Checked="OnClockFormatChanged"
|
||||
Tag="Hms" />
|
||||
<RadioButton x:Name="ClockFormatHMRadio"
|
||||
Content="时分 (HH:mm)"
|
||||
Content="鏃跺垎 (HH:mm)"
|
||||
GroupName="ClockFormat"
|
||||
Checked="OnClockFormatChanged"
|
||||
Tag="Hm" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
|
||||
Header="Component spacing"
|
||||
Description="Adjust spacing between status bar components."
|
||||
IsExpanded="False">
|
||||
@@ -1054,11 +1070,168 @@
|
||||
|
||||
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="≈ 0 px" />
|
||||
Text="鈮?0 px" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel x:Name="WeatherSettingsPanel"
|
||||
IsVisible="False"
|
||||
Spacing="16">
|
||||
<TextBlock x:Name="WeatherPanelTitleTextBlock"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="天气" />
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherLocationSettingsExpander"
|
||||
Header="Location Source"
|
||||
Description="Choose how weather widgets resolve location.">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<ComboBox x:Name="WeatherLocationModeComboBox"
|
||||
Width="220"
|
||||
SelectionChanged="OnWeatherLocationModeSelectionChanged">
|
||||
<ComboBoxItem x:Name="WeatherLocationModeCityItem"
|
||||
Tag="CitySearch"
|
||||
Content="City Search" />
|
||||
<ComboBoxItem x:Name="WeatherLocationModeCoordinatesItem"
|
||||
Tag="Coordinates"
|
||||
Content="Coordinates" />
|
||||
</ComboBox>
|
||||
<ToggleSwitch x:Name="WeatherAutoRefreshToggleSwitch"
|
||||
Checked="OnWeatherAutoRefreshToggled"
|
||||
Unchecked="OnWeatherAutoRefreshToggled"
|
||||
Content="Auto refresh location on startup" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherCitySearchSettingsExpander"
|
||||
Header="City Search"
|
||||
Description="Search cities and apply one weather location.">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto"
|
||||
ColumnSpacing="8">
|
||||
<TextBox x:Name="WeatherCitySearchTextBox"
|
||||
Grid.Column="0"
|
||||
Watermark="e.g. Beijing" />
|
||||
<ui:ProgressRing x:Name="WeatherSearchProgressRing"
|
||||
Grid.Column="1"
|
||||
Width="24"
|
||||
Height="24"
|
||||
IsActive="True"
|
||||
IsVisible="False" />
|
||||
<Button x:Name="WeatherSearchButton"
|
||||
Grid.Column="2"
|
||||
Padding="12,8"
|
||||
Click="OnSearchWeatherCityClick"
|
||||
Content="Search" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox x:Name="WeatherCityResultsComboBox"
|
||||
MinWidth="320" />
|
||||
|
||||
<Button x:Name="WeatherApplyCityButton"
|
||||
HorizontalAlignment="Left"
|
||||
Padding="12,8"
|
||||
Click="OnApplyWeatherCitySelectionClick"
|
||||
Content="Apply City" />
|
||||
|
||||
<TextBlock x:Name="WeatherSearchStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="Search by city name and apply one location." />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherCoordinateSettingsExpander"
|
||||
Header="Coordinates"
|
||||
Description="Set latitude/longitude and optional key/name."
|
||||
IsVisible="False">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
ColumnSpacing="10">
|
||||
<ui:NumberBox x:Name="WeatherLatitudeNumberBox"
|
||||
Grid.Column="0"
|
||||
Header="Latitude"
|
||||
Minimum="-90"
|
||||
Maximum="90"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
SmallChange="0.1"
|
||||
LargeChange="1"
|
||||
Value="39.9042" />
|
||||
<ui:NumberBox x:Name="WeatherLongitudeNumberBox"
|
||||
Grid.Column="1"
|
||||
Header="Longitude"
|
||||
Minimum="-180"
|
||||
Maximum="180"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
SmallChange="0.1"
|
||||
LargeChange="1"
|
||||
Value="116.4074" />
|
||||
</Grid>
|
||||
|
||||
<TextBox x:Name="WeatherLocationKeyTextBox"
|
||||
Watermark="Location key (optional)" />
|
||||
|
||||
<TextBox x:Name="WeatherLocationNameTextBox"
|
||||
Watermark="Display name (optional)" />
|
||||
|
||||
<Button x:Name="WeatherApplyCoordinatesButton"
|
||||
HorizontalAlignment="Left"
|
||||
Padding="12,8"
|
||||
Click="OnApplyWeatherCoordinatesClick"
|
||||
Content="Apply Coordinates" />
|
||||
|
||||
<TextBlock x:Name="WeatherCoordinateStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherPreviewSettingsExpander"
|
||||
Header="Connection Test"
|
||||
Description="Send one test request to verify current settings.">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button x:Name="WeatherPreviewButton"
|
||||
Padding="12,8"
|
||||
Click="OnTestWeatherRequestClick"
|
||||
Content="Test Fetch" />
|
||||
<ui:ProgressRing x:Name="WeatherPreviewProgressRing"
|
||||
Width="24"
|
||||
Height="24"
|
||||
IsActive="True"
|
||||
IsVisible="False" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="WeatherPreviewResultTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="Use test fetch to verify your weather configuration." />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="WeatherLocationStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="No city location is configured." />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="RegionSettingsPanel"
|
||||
IsVisible="False"
|
||||
Spacing="16">
|
||||
@@ -1068,7 +1241,8 @@
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="地区" />
|
||||
|
||||
<ui:SettingsExpander x:Name="LanguageSettingsExpander"
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="LanguageSettingsExpander"
|
||||
Header="语言"
|
||||
Description="选择语言,立即应用到设置与主要界面。">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
@@ -1083,9 +1257,11 @@
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
<ui:SettingsExpander x:Name="TimeZoneSettingsExpander"
|
||||
Header="时区"
|
||||
Description="选择时区,时钟和日历将根据此时区显示时间。">
|
||||
</Border>
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="TimeZoneSettingsExpander"
|
||||
Header="Time Zone"
|
||||
Description="Select a time zone. Clock and calendar widgets will follow this zone.">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
|
||||
</ui:SettingsExpander.IconSource>
|
||||
@@ -1095,17 +1271,19 @@
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="AboutSettingsPanel" IsVisible="False" Spacing="20">
|
||||
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Text="关于" />
|
||||
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="20" Padding="20">
|
||||
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Text="鍏充簬" />
|
||||
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="LanMontainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock Text="现代化桌面壳层应用" FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
|
||||
<TextBlock x:Name="VersionTextBlock" Text="版本号: 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock x:Name="CodeNameTextBlock" Text="版本代号: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
|
||||
<TextBlock x:Name="VersionTextBlock" Text="鐗堟湰鍙? 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<TextBlock x:Name="CodeNameTextBlock" Text="鐗堟湰浠e彿: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
|
||||
<TextBlock x:Name="FontInfoTextBlock" Text="字体: MiSans" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -1136,7 +1314,7 @@
|
||||
CornerRadius="36,36,0,0"
|
||||
Padding="16,12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="组件设置"
|
||||
<TextBlock Text="缁勪欢璁剧疆"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
@@ -1188,7 +1366,7 @@
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="小组件" />
|
||||
Text="Widgets" />
|
||||
<Button x:Name="CloseComponentLibraryButton"
|
||||
Grid.Column="1"
|
||||
Padding="8"
|
||||
@@ -1320,3 +1498,4 @@
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ public partial class MainWindow : Window
|
||||
Video
|
||||
}
|
||||
|
||||
private enum WeatherLocationMode
|
||||
{
|
||||
CitySearch,
|
||||
Coordinates
|
||||
}
|
||||
|
||||
private const int StatusBarRowIndex = 0;
|
||||
private const int MinShortSideCells = 6;
|
||||
private const int MaxShortSideCells = 96;
|
||||
@@ -83,12 +89,14 @@ public partial class MainWindow : Window
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly TimeZoneService _timeZoneService = new();
|
||||
private readonly TimeZoneService _timeZoneService = new();
|
||||
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
|
||||
private readonly ComponentRegistry _componentRegistry = ComponentRegistry
|
||||
.CreateDefault()
|
||||
.RegisterExtensions(
|
||||
JsonComponentExtensionProvider.LoadProvidersFromDirectory(
|
||||
Path.Combine(AppContext.BaseDirectory, "Extensions", "Components")));
|
||||
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
|
||||
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
|
||||
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
|
||||
@@ -99,6 +107,8 @@ public partial class MainWindow : Window
|
||||
private bool _suppressThemeToggleEvents;
|
||||
private bool _suppressStatusBarToggleEvents;
|
||||
private bool _suppressLanguageSelectionEvents;
|
||||
private bool _suppressTimeZoneSelectionEvents;
|
||||
private bool _suppressWeatherLocationEvents;
|
||||
private bool _suppressSettingsPersistence;
|
||||
private bool _isUpdatingWallpaperPreviewLayout;
|
||||
private bool _isComponentLibraryOpen;
|
||||
@@ -131,6 +141,15 @@ public partial class MainWindow : Window
|
||||
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
|
||||
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
|
||||
private string _languageCode = "zh-CN";
|
||||
private WeatherLocationMode _weatherLocationMode = WeatherLocationMode.CitySearch;
|
||||
private string _weatherLocationKey = string.Empty;
|
||||
private string _weatherLocationName = string.Empty;
|
||||
private double _weatherLatitude = 39.9042;
|
||||
private double _weatherLongitude = 116.4074;
|
||||
private bool _weatherAutoRefreshLocation;
|
||||
private string _weatherSearchKeyword = string.Empty;
|
||||
private bool _isWeatherSearchInProgress;
|
||||
private bool _isWeatherPreviewInProgress;
|
||||
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
|
||||
|
||||
private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap;
|
||||
@@ -138,6 +157,7 @@ public partial class MainWindow : Window
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_componentRuntimeRegistry = DesktopComponentRuntimeRegistry.CreateDefault(_componentRegistry);
|
||||
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
|
||||
PropertyChanged += OnWindowPropertyChanged;
|
||||
InitializeDesktopComponentDragHandlers();
|
||||
@@ -192,13 +212,14 @@ public partial class MainWindow : Window
|
||||
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
|
||||
GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged;
|
||||
|
||||
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 5);
|
||||
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 6);
|
||||
UpdateSettingsTabContent();
|
||||
|
||||
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
|
||||
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
|
||||
ApplyTaskbarSettings(snapshot);
|
||||
InitializeLocalization(snapshot.LanguageCode);
|
||||
InitializeWeatherSettings(snapshot);
|
||||
InitializeDesktopSurfaceState(snapshot);
|
||||
InitializeDesktopComponentPlacements(snapshot);
|
||||
InitializeSettingsIcons();
|
||||
@@ -247,6 +268,10 @@ public partial class MainWindow : Window
|
||||
_videoWallpaperPlayer = null;
|
||||
_libVlc?.Dispose();
|
||||
_libVlc = null;
|
||||
if (_weatherDataService is IDisposable weatherServiceDisposable)
|
||||
{
|
||||
weatherServiceDisposable.Dispose();
|
||||
}
|
||||
_wallpaperBitmap?.Dispose();
|
||||
_wallpaperBitmap = null;
|
||||
PropertyChanged -= OnWindowPropertyChanged;
|
||||
@@ -1241,14 +1266,15 @@ public partial class MainWindow : Window
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeTimeZoneSettings()
|
||||
{
|
||||
private void InitializeTimeZoneSettings()
|
||||
{
|
||||
// Populate timezone dropdown items before selecting current timezone.
|
||||
_suppressTimeZoneSelectionEvents = true;
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
var timeZones = _timeZoneService.GetAllTimeZones();
|
||||
foreach (var tz in timeZones)
|
||||
{
|
||||
var displayText = _timeZoneService.GetTimeZoneDisplayName(tz);
|
||||
var displayText = GetLocalizedTimeZoneDisplayName(tz);
|
||||
var item = new ComboBoxItem
|
||||
{
|
||||
Content = displayText,
|
||||
@@ -1262,11 +1288,12 @@ public partial class MainWindow : Window
|
||||
TimeZoneComboBox.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
_suppressTimeZoneSelectionEvents = false;
|
||||
}
|
||||
|
||||
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressLanguageSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item)
|
||||
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressTimeZoneSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1279,6 +1306,6 @@ public partial class MainWindow : Window
|
||||
|
||||
_timeZoneService.SetTimeZoneById(timeZoneId);
|
||||
PersistSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
LanMontainDesktop/qodana.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
|
||||
#################################################################################
|
||||
# WARNING: Do not store sensitive information in this file, #
|
||||
# as its contents will be included in the Qodana report. #
|
||||
#################################################################################
|
||||
version: "1.0"
|
||||
|
||||
#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
|
||||
ide: QDNET
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||
# severityThresholds - configures maximum thresholds for different problem severities
|
||||
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||
#failureConditions:
|
||||
# severityThresholds:
|
||||
# any: 15
|
||||
# critical: 5
|
||||
# testCoverageThresholds:
|
||||
# fresh: 70
|
||||
# total: 50
|
||||