mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8768fa1ed2 | ||
|
|
24f1b896e1 |
@@ -19,7 +19,7 @@
|
|||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<sty:FluentAvaloniaTheme />
|
<sty:FluentAvaloniaTheme />
|
||||||
<mi:MaterialIconStyles />
|
<mi:MaterialIconStyles />
|
||||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/MotionTokens.axaml" />
|
<StyleInclude Source="avares://LanMountainDesktop/Styles/FluttermotionToken.axaml" />
|
||||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
|
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
|
||||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
|
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public class PanelIntroAnimationBehavior
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
var timer = new DispatcherTimer(DispatcherPriority.Background)
|
var timer = new DispatcherTimer(DispatcherPriority.Background)
|
||||||
{
|
{
|
||||||
Interval = UiMotionTokens.StaggerStepInterval
|
Interval = FluttermotionToken.StaggerStepInterval
|
||||||
};
|
};
|
||||||
timer.Tick += (_, _) =>
|
timer.Tick += (_, _) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace LanMountainDesktop.Behaviors;
|
|||||||
|
|
||||||
public class PopupIntroAnimationBehavior
|
public class PopupIntroAnimationBehavior
|
||||||
{
|
{
|
||||||
private static readonly Easing StandardEasing = Easing.Parse(UiMotionTokens.StandardBezier);
|
private static readonly Easing StandardEasing = Easing.Parse(FluttermotionToken.StandardBezier);
|
||||||
|
|
||||||
public static readonly AttachedProperty<bool> IsEnabledProperty =
|
public static readonly AttachedProperty<bool> IsEnabledProperty =
|
||||||
AvaloniaProperty.RegisterAttached<PopupIntroAnimationBehavior, Control, bool>("IsEnabled");
|
AvaloniaProperty.RegisterAttached<PopupIntroAnimationBehavior, Control, bool>("IsEnabled");
|
||||||
@@ -97,14 +97,14 @@ public class PopupIntroAnimationBehavior
|
|||||||
|
|
||||||
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||||
opacityAnimation.Target = nameof(compositionVisual.Opacity);
|
opacityAnimation.Target = nameof(compositionVisual.Opacity);
|
||||||
opacityAnimation.Duration = UiMotionTokens.Standard;
|
opacityAnimation.Duration = FluttermotionToken.Standard;
|
||||||
opacityAnimation.InsertKeyFrame(0f, 0f);
|
opacityAnimation.InsertKeyFrame(0f, 0f);
|
||||||
opacityAnimation.InsertKeyFrame(1f, 1f, StandardEasing);
|
opacityAnimation.InsertKeyFrame(1f, 1f, StandardEasing);
|
||||||
compositionVisual.StartAnimation(nameof(compositionVisual.Opacity), opacityAnimation);
|
compositionVisual.StartAnimation(nameof(compositionVisual.Opacity), opacityAnimation);
|
||||||
|
|
||||||
var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation();
|
var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation();
|
||||||
scaleAnimation.Target = nameof(compositionVisual.Scale);
|
scaleAnimation.Target = nameof(compositionVisual.Scale);
|
||||||
scaleAnimation.Duration = UiMotionTokens.Standard;
|
scaleAnimation.Duration = FluttermotionToken.Standard;
|
||||||
scaleAnimation.InsertKeyFrame(0f, compositionVisual.Scale with { X = 0.94, Y = 0.94 });
|
scaleAnimation.InsertKeyFrame(0f, compositionVisual.Scale with { X = 0.94, Y = 0.94 });
|
||||||
scaleAnimation.InsertKeyFrame(1f, compositionVisual.Scale with { X = 1, Y = 1 }, StandardEasing);
|
scaleAnimation.InsertKeyFrame(1f, compositionVisual.Scale with { X = 1, Y = 1 }, StandardEasing);
|
||||||
compositionVisual.StartAnimation(nameof(compositionVisual.Scale), scaleAnimation);
|
compositionVisual.StartAnimation(nameof(compositionVisual.Scale), scaleAnimation);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public static class BuiltInComponentIds
|
|||||||
public const string Clock = "Clock";
|
public const string Clock = "Clock";
|
||||||
public const string DesktopClock = "DesktopClock";
|
public const string DesktopClock = "DesktopClock";
|
||||||
public const string DesktopWeatherClock = "DesktopWeatherClock";
|
public const string DesktopWeatherClock = "DesktopWeatherClock";
|
||||||
|
public const string DesktopWorldClock = "DesktopWorldClock";
|
||||||
public const string DesktopTimer = "DesktopTimer";
|
public const string DesktopTimer = "DesktopTimer";
|
||||||
public const string DesktopWeather = "DesktopWeather";
|
public const string DesktopWeather = "DesktopWeather";
|
||||||
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
|
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ public sealed class ComponentRegistry
|
|||||||
MinHeightCells: 1,
|
MinHeightCells: 1,
|
||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true),
|
AllowDesktopPlacement: true),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopWorldClock,
|
||||||
|
"World Clock",
|
||||||
|
"Clock",
|
||||||
|
"Clock",
|
||||||
|
MinWidthCells: 4,
|
||||||
|
MinHeightCells: 2,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true),
|
||||||
new DesktopComponentDefinition(
|
new DesktopComponentDefinition(
|
||||||
BuiltInComponentIds.DesktopTimer,
|
BuiltInComponentIds.DesktopTimer,
|
||||||
"Timer",
|
"Timer",
|
||||||
|
|||||||
@@ -162,6 +162,21 @@
|
|||||||
"schedule.settings.delete": "Delete",
|
"schedule.settings.delete": "Delete",
|
||||||
"schedule.settings.picker_title": "Select ClassIsland schedule file",
|
"schedule.settings.picker_title": "Select ClassIsland schedule file",
|
||||||
"schedule.settings.picker_file_type": "ClassIsland CSES schedule",
|
"schedule.settings.picker_file_type": "ClassIsland CSES schedule",
|
||||||
|
"worldclock.settings.title": "World Clock Settings",
|
||||||
|
"worldclock.settings.desc": "Choose a time zone for each of the four clocks.",
|
||||||
|
"worldclock.settings.clock_1": "Clock 1",
|
||||||
|
"worldclock.settings.clock_2": "Clock 2",
|
||||||
|
"worldclock.settings.clock_3": "Clock 3",
|
||||||
|
"worldclock.settings.clock_4": "Clock 4",
|
||||||
|
"worldclock.settings.second_mode_label": "Second Hand",
|
||||||
|
"worldclock.widget.today": "Today",
|
||||||
|
"worldclock.widget.yesterday": "Yesterday",
|
||||||
|
"worldclock.widget.tomorrow": "Tomorrow",
|
||||||
|
"worldclock.widget.offset_same": "0h",
|
||||||
|
"worldclock.widget.offset_ahead_hours": "Ahead {0}h",
|
||||||
|
"worldclock.widget.offset_behind_hours": "Behind {0}h",
|
||||||
|
"worldclock.widget.offset_ahead_hm": "Ahead {0}h {1}m",
|
||||||
|
"worldclock.widget.offset_behind_hm": "Behind {0}h {1}m",
|
||||||
"weather.widget.aqi_unknown": "AQI --",
|
"weather.widget.aqi_unknown": "AQI --",
|
||||||
"weather.widget.aqi_format": "AQI {0}",
|
"weather.widget.aqi_format": "AQI {0}",
|
||||||
"weather.widget.updated_format": "Updated {0:HH:mm}",
|
"weather.widget.updated_format": "Updated {0:HH:mm}",
|
||||||
@@ -222,6 +237,7 @@
|
|||||||
"component.lunar_calendar": "Lunar Calendar",
|
"component.lunar_calendar": "Lunar Calendar",
|
||||||
"component.desktop_clock": "Clock",
|
"component.desktop_clock": "Clock",
|
||||||
"component.weather_clock": "Weather Clock",
|
"component.weather_clock": "Weather Clock",
|
||||||
|
"component.world_clock": "World Clock",
|
||||||
"component.desktop_timer": "Timer",
|
"component.desktop_timer": "Timer",
|
||||||
"component.desktop_weather": "Weather",
|
"component.desktop_weather": "Weather",
|
||||||
"component.hourly_weather": "Hourly Weather",
|
"component.hourly_weather": "Hourly Weather",
|
||||||
@@ -244,6 +260,12 @@
|
|||||||
"component.study_score_overview": "Study Score Overview",
|
"component.study_score_overview": "Study Score Overview",
|
||||||
"component.study_deduction_reasons": "Deduction Reasons",
|
"component.study_deduction_reasons": "Deduction Reasons",
|
||||||
"component.study_interrupt_density": "Interrupt Density",
|
"component.study_interrupt_density": "Interrupt Density",
|
||||||
|
"desktop_clock.settings.title": "Clock Settings",
|
||||||
|
"desktop_clock.settings.desc": "Choose the time zone for the single clock.",
|
||||||
|
"desktop_clock.settings.timezone_label": "Time Zone",
|
||||||
|
"desktop_clock.settings.second_mode_label": "Second Hand",
|
||||||
|
"clock.second_mode.tick": "Tick",
|
||||||
|
"clock.second_mode.sweep": "Sweep",
|
||||||
"poetry.widget.loading_content": "Loading poetry...",
|
"poetry.widget.loading_content": "Loading poetry...",
|
||||||
"poetry.widget.loading_author": "Loading...",
|
"poetry.widget.loading_author": "Loading...",
|
||||||
"poetry.widget.fetch_failed": "Poetry fetch failed",
|
"poetry.widget.fetch_failed": "Poetry fetch failed",
|
||||||
|
|||||||
@@ -162,6 +162,21 @@
|
|||||||
"schedule.settings.delete": "删除",
|
"schedule.settings.delete": "删除",
|
||||||
"schedule.settings.picker_title": "选择 ClassIsland 课表文件",
|
"schedule.settings.picker_title": "选择 ClassIsland 课表文件",
|
||||||
"schedule.settings.picker_file_type": "ClassIsland CSES 课表",
|
"schedule.settings.picker_file_type": "ClassIsland CSES 课表",
|
||||||
|
"worldclock.settings.title": "世界时钟设置",
|
||||||
|
"worldclock.settings.desc": "分别为四个时钟选择时区。",
|
||||||
|
"worldclock.settings.clock_1": "时钟 1",
|
||||||
|
"worldclock.settings.clock_2": "时钟 2",
|
||||||
|
"worldclock.settings.clock_3": "时钟 3",
|
||||||
|
"worldclock.settings.clock_4": "时钟 4",
|
||||||
|
"worldclock.settings.second_mode_label": "秒针方式",
|
||||||
|
"worldclock.widget.today": "今天",
|
||||||
|
"worldclock.widget.yesterday": "昨天",
|
||||||
|
"worldclock.widget.tomorrow": "明天",
|
||||||
|
"worldclock.widget.offset_same": "0 小时",
|
||||||
|
"worldclock.widget.offset_ahead_hours": "早 {0} 小时",
|
||||||
|
"worldclock.widget.offset_behind_hours": "晚 {0} 小时",
|
||||||
|
"worldclock.widget.offset_ahead_hm": "早 {0} 小时 {1} 分",
|
||||||
|
"worldclock.widget.offset_behind_hm": "晚 {0} 小时 {1} 分",
|
||||||
"weather.widget.aqi_unknown": "AQI --",
|
"weather.widget.aqi_unknown": "AQI --",
|
||||||
"weather.widget.aqi_format": "AQI {0}",
|
"weather.widget.aqi_format": "AQI {0}",
|
||||||
"weather.widget.updated_format": "更新于 {0:HH:mm}",
|
"weather.widget.updated_format": "更新于 {0:HH:mm}",
|
||||||
@@ -222,6 +237,7 @@
|
|||||||
"component.lunar_calendar": "农历",
|
"component.lunar_calendar": "农历",
|
||||||
"component.desktop_clock": "时钟",
|
"component.desktop_clock": "时钟",
|
||||||
"component.weather_clock": "天气时钟",
|
"component.weather_clock": "天气时钟",
|
||||||
|
"component.world_clock": "世界时钟",
|
||||||
"component.desktop_timer": "计时器",
|
"component.desktop_timer": "计时器",
|
||||||
"component.desktop_weather": "天气",
|
"component.desktop_weather": "天气",
|
||||||
"component.hourly_weather": "小时天气",
|
"component.hourly_weather": "小时天气",
|
||||||
@@ -244,6 +260,12 @@
|
|||||||
"component.study_score_overview": "自习评分总览",
|
"component.study_score_overview": "自习评分总览",
|
||||||
"component.study_deduction_reasons": "扣分原因",
|
"component.study_deduction_reasons": "扣分原因",
|
||||||
"component.study_interrupt_density": "打断密度",
|
"component.study_interrupt_density": "打断密度",
|
||||||
|
"desktop_clock.settings.title": "时钟设置",
|
||||||
|
"desktop_clock.settings.desc": "为单时钟选择时区。",
|
||||||
|
"desktop_clock.settings.timezone_label": "时区",
|
||||||
|
"desktop_clock.settings.second_mode_label": "秒针方式",
|
||||||
|
"clock.second_mode.tick": "跳针",
|
||||||
|
"clock.second_mode.sweep": "扫针",
|
||||||
"poetry.widget.loading_content": "正在加载诗词",
|
"poetry.widget.loading_content": "正在加载诗词",
|
||||||
"poetry.widget.loading_author": "加载中",
|
"poetry.widget.loading_author": "加载中",
|
||||||
"poetry.widget.fetch_failed": "诗词获取失败",
|
"poetry.widget.fetch_failed": "诗词获取失败",
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool StudyEnvironmentShowDbfs { get; set; }
|
public bool StudyEnvironmentShowDbfs { get; set; }
|
||||||
|
|
||||||
|
public string DesktopClockTimeZoneId { get; set; } = "China Standard Time";
|
||||||
|
public string DesktopClockSecondHandMode { get; set; } = "Tick";
|
||||||
|
|
||||||
|
public List<string> WorldClockTimeZoneIds { get; set; } =
|
||||||
|
[
|
||||||
|
"China Standard Time",
|
||||||
|
"GMT Standard Time",
|
||||||
|
"AUS Eastern Standard Time",
|
||||||
|
"Eastern Standard Time"
|
||||||
|
];
|
||||||
|
public string WorldClockSecondHandMode { get; set; } = "Tick";
|
||||||
|
|
||||||
public AppSettingsSnapshot Clone()
|
public AppSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (AppSettingsSnapshot)MemberwiseClone();
|
var clone = (AppSettingsSnapshot)MemberwiseClone();
|
||||||
@@ -135,6 +147,10 @@ public sealed class AppSettingsSnapshot
|
|||||||
}
|
}
|
||||||
clone.ImportedClassSchedules = schedules;
|
clone.ImportedClassSchedules = schedules;
|
||||||
|
|
||||||
|
clone.WorldClockTimeZoneIds = WorldClockTimeZoneIds is { Count: > 0 }
|
||||||
|
? new List<string>(WorldClockTimeZoneIds)
|
||||||
|
: [];
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
LanMountainDesktop/Services/ClockSecondHandMode.cs
Normal file
21
LanMountainDesktop/Services/ClockSecondHandMode.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public static class ClockSecondHandMode
|
||||||
|
{
|
||||||
|
public const string Tick = "Tick";
|
||||||
|
public const string Sweep = "Sweep";
|
||||||
|
|
||||||
|
public static string Normalize(string? mode)
|
||||||
|
{
|
||||||
|
return string.Equals(mode?.Trim(), Sweep, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? Sweep
|
||||||
|
: Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSweep(string? mode)
|
||||||
|
{
|
||||||
|
return string.Equals(Normalize(mode), Sweep, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
187
LanMountainDesktop/Services/WorldClockTimeZoneCatalog.cs
Normal file
187
LanMountainDesktop/Services/WorldClockTimeZoneCatalog.cs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public static class WorldClockTimeZoneCatalog
|
||||||
|
{
|
||||||
|
public const int ClockCount = 4;
|
||||||
|
|
||||||
|
private static readonly string[][] DefaultTimeZoneCandidates =
|
||||||
|
[
|
||||||
|
["China Standard Time", "Asia/Shanghai"],
|
||||||
|
["GMT Standard Time", "Europe/London", "UTC"],
|
||||||
|
["AUS Eastern Standard Time", "Australia/Sydney"],
|
||||||
|
["Eastern Standard Time", "America/New_York"]
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string[]> CrossPlatformAliases =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = ["Asia/Shanghai"],
|
||||||
|
["Asia/Shanghai"] = ["China Standard Time"],
|
||||||
|
["GMT Standard Time"] = ["Europe/London", "UTC"],
|
||||||
|
["Europe/London"] = ["GMT Standard Time", "UTC"],
|
||||||
|
["AUS Eastern Standard Time"] = ["Australia/Sydney"],
|
||||||
|
["Australia/Sydney"] = ["AUS Eastern Standard Time"],
|
||||||
|
["Eastern Standard Time"] = ["America/New_York"],
|
||||||
|
["America/New_York"] = ["Eastern Standard Time"],
|
||||||
|
["UTC"] = ["Etc/UTC"],
|
||||||
|
["Etc/UTC"] = ["UTC"],
|
||||||
|
["Tokyo Standard Time"] = ["Asia/Tokyo"],
|
||||||
|
["Asia/Tokyo"] = ["Tokyo Standard Time"]
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizeTimeZoneIds(IEnumerable<string>? configuredIds)
|
||||||
|
{
|
||||||
|
var available = TimeZoneInfo.GetSystemTimeZones();
|
||||||
|
return NormalizeTimeZoneIds(configuredIds, available);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizeTimeZoneIds(
|
||||||
|
IEnumerable<string>? configuredIds,
|
||||||
|
IReadOnlyList<TimeZoneInfo> availableTimeZones)
|
||||||
|
{
|
||||||
|
var availableById = BuildAvailableTimeZoneLookup(availableTimeZones);
|
||||||
|
var requested = configuredIds?
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||||
|
.Select(id => id.Trim())
|
||||||
|
.ToList() ?? [];
|
||||||
|
|
||||||
|
var normalized = new List<string>(ClockCount);
|
||||||
|
for (var index = 0; index < ClockCount; index++)
|
||||||
|
{
|
||||||
|
var requestedId = index < requested.Count ? requested[index] : null;
|
||||||
|
var resolved = ResolveAvailableId(requestedId, availableById) ??
|
||||||
|
ResolveDefaultId(index, availableById) ??
|
||||||
|
TimeZoneInfo.Local.Id;
|
||||||
|
normalized.Add(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeZoneInfo ResolveTimeZoneOrLocal(string? timeZoneId)
|
||||||
|
{
|
||||||
|
if (TryResolveTimeZone(timeZoneId, out var resolved))
|
||||||
|
{
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeZoneInfo.Local;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, TimeZoneInfo> BuildAvailableTimeZoneLookup(
|
||||||
|
IReadOnlyList<TimeZoneInfo> availableTimeZones)
|
||||||
|
{
|
||||||
|
return availableTimeZones
|
||||||
|
.Where(zone => !string.IsNullOrWhiteSpace(zone.Id))
|
||||||
|
.GroupBy(zone => zone.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveDefaultId(
|
||||||
|
int slotIndex,
|
||||||
|
IReadOnlyDictionary<string, TimeZoneInfo> availableById)
|
||||||
|
{
|
||||||
|
var clampedIndex = Math.Clamp(slotIndex, 0, ClockCount - 1);
|
||||||
|
foreach (var candidateId in DefaultTimeZoneCandidates[clampedIndex])
|
||||||
|
{
|
||||||
|
var resolved = ResolveAvailableId(candidateId, availableById);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
|
{
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveAvailableId(
|
||||||
|
string? candidateId,
|
||||||
|
IReadOnlyDictionary<string, TimeZoneInfo> availableById)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidateId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedCandidate = candidateId.Trim();
|
||||||
|
if (availableById.TryGetValue(normalizedCandidate, out var exact))
|
||||||
|
{
|
||||||
|
return exact.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryResolveTimeZone(normalizedCandidate, out var resolvedZone) &&
|
||||||
|
availableById.TryGetValue(resolvedZone.Id, out var resolved))
|
||||||
|
{
|
||||||
|
return resolved.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CrossPlatformAliases.TryGetValue(normalizedCandidate, out var aliases))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var alias in aliases)
|
||||||
|
{
|
||||||
|
if (availableById.TryGetValue(alias, out var aliasZone))
|
||||||
|
{
|
||||||
|
return aliasZone.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryResolveTimeZone(alias, out var aliasResolvedZone) &&
|
||||||
|
availableById.TryGetValue(aliasResolvedZone.Id, out var mappedAlias))
|
||||||
|
{
|
||||||
|
return mappedAlias.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveTimeZone(string? timeZoneId, out TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
timeZone = TimeZoneInfo.Local;
|
||||||
|
if (string.IsNullOrWhiteSpace(timeZoneId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedId = timeZoneId.Trim();
|
||||||
|
if (TryFindTimeZone(normalizedId, out timeZone))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CrossPlatformAliases.TryGetValue(normalizedId, out var aliases))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var alias in aliases)
|
||||||
|
{
|
||||||
|
if (TryFindTimeZone(alias, out timeZone))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindTimeZone(string timeZoneId, out TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
timeZone = TimeZoneInfo.Local;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
LanMountainDesktop/Styles/FluttermotionToken.axaml
Normal file
12
LanMountainDesktop/Styles/FluttermotionToken.axaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Styles.Resources>
|
||||||
|
<x:TimeSpan x:Key="FluttermotionToken.Duration.Fast">0:0:0.12</x:TimeSpan>
|
||||||
|
<x:TimeSpan x:Key="FluttermotionToken.Duration.Standard">0:0:0.16</x:TimeSpan>
|
||||||
|
<x:TimeSpan x:Key="FluttermotionToken.Duration.Slow">0:0:0.20</x:TimeSpan>
|
||||||
|
<x:TimeSpan x:Key="FluttermotionToken.Duration.Page">0:0:0.24</x:TimeSpan>
|
||||||
|
<x:TimeSpan x:Key="FluttermotionToken.Duration.Intro">0:0:0.32</x:TimeSpan>
|
||||||
|
|
||||||
|
<x:Double x:Key="FluttermotionToken.BackdropBlurRadiusStrong">30</x:Double>
|
||||||
|
</Styles.Resources>
|
||||||
|
</Styles>
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
<Setter Property="Padding" Value="16,10" />
|
<Setter Property="Padding" Value="16,10" />
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.12" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||||
<BrushTransition Property="Background" Duration="0:0:0.12" />
|
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
|
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<ThicknessTransition Property="Padding" Duration="0:0:0.2" Easing="QuarticEaseOut" />
|
<ThicknessTransition Property="Padding" Duration="{StaticResource FluttermotionToken.Duration.Slow}" Easing="QuarticEaseOut" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
||||||
<Styles.Resources>
|
|
||||||
<x:String x:Key="MotionEasingStandard">0.22,1,0.36,1</x:String>
|
|
||||||
|
|
||||||
<x:String x:Key="MotionDurationFast">0:0:0.12</x:String>
|
|
||||||
<x:String x:Key="MotionDurationStandard">0:0:0.16</x:String>
|
|
||||||
<x:String x:Key="MotionDurationSlow">0:0:0.20</x:String>
|
|
||||||
<x:String x:Key="MotionDurationPage">0:0:0.24</x:String>
|
|
||||||
<x:String x:Key="MotionDurationIntro">0:0:0.32</x:String>
|
|
||||||
|
|
||||||
<x:Double x:Key="MotionBackdropBlurRadiusStrong">30</x:Double>
|
|
||||||
</Styles.Resources>
|
|
||||||
</Styles>
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
<Style Selector="^[(behaviors|PanelIntroAnimationBehavior.IsAnimationPlayed)=True]">
|
<Style Selector="^[(behaviors|PanelIntroAnimationBehavior.IsAnimationPlayed)=True]">
|
||||||
<Style.Animations>
|
<Style.Animations>
|
||||||
<Animation Duration="0:0:0.32"
|
<Animation Duration="{StaticResource FluttermotionToken.Duration.Intro}"
|
||||||
FillMode="Both"
|
FillMode="Both"
|
||||||
Easing="0.22,1,0.36,1">
|
Easing="0.22,1,0.36,1">
|
||||||
<KeyFrame Cue="0%">
|
<KeyFrame Cue="0%">
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
<Setter Property="MinHeight" Value="34" />
|
<Setter Property="MinHeight" Value="34" />
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<BrushTransition Property="Background" Duration="0:0:0.16" Easing="0.22,1,0.36,1" />
|
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||||
<BrushTransition Property="BorderBrush" Duration="0:0:0.16" Easing="0.22,1,0.36,1" />
|
<BrushTransition Property="BorderBrush" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.16" Easing="0.22,1,0.36,1" />
|
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
<Style Selector="Grid.settings-scope ComboBox">
|
<Style Selector="Grid.settings-scope ComboBox">
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<BrushTransition Property="Background" Duration="0:0:0.12" Easing="0.22,1,0.36,1" />
|
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
|
||||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" Easing="0.22,1,0.36,1" />
|
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -87,8 +87,8 @@
|
|||||||
<Style Selector="Grid.settings-scope ToggleSwitch">
|
<Style Selector="Grid.settings-scope ToggleSwitch">
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.16" Easing="0.22,1,0.36,1" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.16" Easing="0.22,1,0.36,1" />
|
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Theme;
|
namespace LanMountainDesktop.Theme;
|
||||||
|
|
||||||
public static class UiMotionTokens
|
public static class FluttermotionToken
|
||||||
{
|
{
|
||||||
public static readonly TimeSpan Fast = TimeSpan.FromMilliseconds(120);
|
public static readonly TimeSpan Fast = TimeSpan.FromMilliseconds(120);
|
||||||
public static readonly TimeSpan Standard = TimeSpan.FromMilliseconds(160);
|
public static readonly TimeSpan Standard = TimeSpan.FromMilliseconds(160);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Shapes;
|
using Avalonia.Controls.Shapes;
|
||||||
@@ -12,6 +13,40 @@ namespace LanMountainDesktop.Views.Components;
|
|||||||
|
|
||||||
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||||
{
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> ZhCityNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "\u5317\u4EAC",
|
||||||
|
["Asia/Shanghai"] = "\u5317\u4EAC",
|
||||||
|
["GMT Standard Time"] = "\u4F26\u6566",
|
||||||
|
["Europe/London"] = "\u4F26\u6566",
|
||||||
|
["AUS Eastern Standard Time"] = "\u6089\u5C3C",
|
||||||
|
["Australia/Sydney"] = "\u6089\u5C3C",
|
||||||
|
["Eastern Standard Time"] = "\u7EBD\u7EA6",
|
||||||
|
["America/New_York"] = "\u7EBD\u7EA6",
|
||||||
|
["Tokyo Standard Time"] = "\u4E1C\u4EAC",
|
||||||
|
["Asia/Tokyo"] = "\u4E1C\u4EAC",
|
||||||
|
["UTC"] = "\u534F\u8C03\u4E16\u754C\u65F6",
|
||||||
|
["Etc/UTC"] = "\u534F\u8C03\u4E16\u754C\u65F6"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> EnCityNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "Beijing",
|
||||||
|
["Asia/Shanghai"] = "Beijing",
|
||||||
|
["GMT Standard Time"] = "London",
|
||||||
|
["Europe/London"] = "London",
|
||||||
|
["AUS Eastern Standard Time"] = "Sydney",
|
||||||
|
["Australia/Sydney"] = "Sydney",
|
||||||
|
["Eastern Standard Time"] = "New York",
|
||||||
|
["America/New_York"] = "New York",
|
||||||
|
["Tokyo Standard Time"] = "Tokyo",
|
||||||
|
["Asia/Tokyo"] = "Tokyo",
|
||||||
|
["UTC"] = "UTC",
|
||||||
|
["Etc/UTC"] = "UTC"
|
||||||
|
};
|
||||||
|
|
||||||
private readonly DispatcherTimer _timer = new()
|
private readonly DispatcherTimer _timer = new()
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromSeconds(1)
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
@@ -20,11 +55,16 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
private const double DialSize = 258;
|
private const double DialSize = 258;
|
||||||
private const double Center = DialSize / 2;
|
private const double Center = DialSize / 2;
|
||||||
|
|
||||||
|
private readonly AppSettingsService _settingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
private TimeZoneService? _timeZoneService;
|
private TimeZoneService? _timeZoneService;
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
private bool _dialInitialized;
|
private bool _dialInitialized;
|
||||||
private bool _handsInitialized;
|
private bool _handsInitialized;
|
||||||
private bool? _isNightModeApplied;
|
private bool? _isNightModeApplied;
|
||||||
|
private TimeZoneInfo _clockTimeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal("China Standard Time");
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
private string _secondHandMode = ClockSecondHandMode.Tick;
|
||||||
private readonly Line _hourHandLine = CreateHandLine("#1A2A46", 12);
|
private readonly Line _hourHandLine = CreateHandLine("#1A2A46", 12);
|
||||||
private readonly Line _minuteHandLine = CreateHandLine("#29406B", 8);
|
private readonly Line _minuteHandLine = CreateHandLine("#29406B", 8);
|
||||||
private readonly Line _secondHandLine = CreateHandLine("#1A74F2", 4);
|
private readonly Line _secondHandLine = CreateHandLine("#1A74F2", 4);
|
||||||
@@ -40,6 +80,8 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
|
|
||||||
InitializeDialIfNeeded();
|
InitializeDialIfNeeded();
|
||||||
InitializeHandsIfNeeded();
|
InitializeHandsIfNeeded();
|
||||||
|
LoadClockSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +104,19 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
_timeZoneService = null;
|
_timeZoneService = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RefreshFromSettings()
|
||||||
|
{
|
||||||
|
LoadClockSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
|
UpdateClock();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
InitializeDialIfNeeded();
|
InitializeDialIfNeeded();
|
||||||
InitializeHandsIfNeeded();
|
InitializeHandsIfNeeded();
|
||||||
|
LoadClockSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
_timer.Start();
|
_timer.Start();
|
||||||
}
|
}
|
||||||
@@ -187,17 +238,22 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
{
|
{
|
||||||
ApplyModeVisualIfNeeded();
|
ApplyModeVisualIfNeeded();
|
||||||
|
|
||||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _clockTimeZone);
|
||||||
var hourAngle = (now.Hour % 12 + now.Minute / 60d + now.Second / 3600d) * 30d;
|
var secondValue = ClockSecondHandMode.IsSweep(_secondHandMode)
|
||||||
var minuteAngle = (now.Minute + now.Second / 60d) * 6d;
|
? now.Second + now.Millisecond / 1000d
|
||||||
var secondAngle = (now.Second + now.Millisecond / 1000d) * 6d;
|
: now.Second;
|
||||||
|
var minuteValue = now.Minute + secondValue / 60d;
|
||||||
|
var hourValue = (now.Hour % 12) + minuteValue / 60d;
|
||||||
|
|
||||||
|
var hourAngle = hourValue * 30d;
|
||||||
|
var minuteAngle = minuteValue * 6d;
|
||||||
|
var secondAngle = secondValue * 6d;
|
||||||
|
|
||||||
SetHandGeometry(_hourHandLine, hourAngle, forwardLength: 52, backwardLength: 6);
|
SetHandGeometry(_hourHandLine, hourAngle, forwardLength: 52, backwardLength: 6);
|
||||||
SetHandGeometry(_minuteHandLine, minuteAngle, forwardLength: 76, backwardLength: 8);
|
SetHandGeometry(_minuteHandLine, minuteAngle, forwardLength: 76, backwardLength: 8);
|
||||||
SetHandGeometry(_secondHandLine, secondAngle, forwardLength: 94, backwardLength: 18);
|
SetHandGeometry(_secondHandLine, secondAngle, forwardLength: 94, backwardLength: 18);
|
||||||
|
|
||||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
CityTextBlock.Text = ResolveCityName(_clockTimeZone);
|
||||||
CityTextBlock.Text = isZh ? "\u5317\u4eac" : "Beijing";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyModeVisualIfNeeded()
|
private void ApplyModeVisualIfNeeded()
|
||||||
@@ -299,6 +355,53 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadClockSettings()
|
||||||
|
{
|
||||||
|
var snapshot = _settingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
|
||||||
|
var configuredTimeZoneId = string.IsNullOrWhiteSpace(snapshot.DesktopClockTimeZoneId)
|
||||||
|
? "China Standard Time"
|
||||||
|
: snapshot.DesktopClockTimeZoneId.Trim();
|
||||||
|
|
||||||
|
_clockTimeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(configuredTimeZoneId);
|
||||||
|
_secondHandMode = ClockSecondHandMode.Normalize(snapshot.DesktopClockSecondHandMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySecondHandTimerInterval()
|
||||||
|
{
|
||||||
|
_timer.Interval = ClockSecondHandMode.IsSweep(_secondHandMode)
|
||||||
|
? TimeSpan.FromMilliseconds(16)
|
||||||
|
: TimeSpan.FromSeconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveCityName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
var cityNames = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ZhCityNames
|
||||||
|
: EnCityNames;
|
||||||
|
if (cityNames.TryGetValue(timeZone.Id, out var cityName))
|
||||||
|
{
|
||||||
|
return cityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = timeZone.Id;
|
||||||
|
var slashIndex = normalized.LastIndexOf('/');
|
||||||
|
if (slashIndex >= 0 && slashIndex < normalized.Length - 1)
|
||||||
|
{
|
||||||
|
normalized = normalized[(slashIndex + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized = normalized.Replace('_', ' ').Trim();
|
||||||
|
normalized = normalized
|
||||||
|
.Replace("Standard Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Daylight Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(normalized) ? timeZone.Id : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
private bool ResolveIsNightMode()
|
private bool ResolveIsNightMode()
|
||||||
{
|
{
|
||||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="560"
|
||||||
|
d:DesignHeight="300"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.AnalogClockWidgetSettingsWindow">
|
||||||
|
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
|
||||||
|
Padding="16">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*"
|
||||||
|
RowSpacing="10">
|
||||||
|
<TextBlock x:Name="TitleTextBlock"
|
||||||
|
Text="时钟设置"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="为单时钟选择时区。"
|
||||||
|
FontSize="12"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="2"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="10"
|
||||||
|
Margin="0,0,6,0">
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="TimeZoneLabelTextBlock"
|
||||||
|
Text="时区"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<ComboBox x:Name="TimeZoneComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinWidth="0"
|
||||||
|
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="SecondHandModeLabelTextBlock"
|
||||||
|
Text="秒针方式"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="12">
|
||||||
|
<RadioButton x:Name="SecondHandTickRadioButton"
|
||||||
|
GroupName="desktop_clock_second_mode"
|
||||||
|
Content="跳针"
|
||||||
|
Checked="OnSecondHandModeChanged" />
|
||||||
|
<RadioButton x:Name="SecondHandSweepRadioButton"
|
||||||
|
GroupName="desktop_clock_second_mode"
|
||||||
|
Content="扫针"
|
||||||
|
Checked="OnSecondHandModeChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class AnalogClockWidgetSettingsWindow : UserControl
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "中国标准时间",
|
||||||
|
["Asia/Shanghai"] = "中国标准时间",
|
||||||
|
["GMT Standard Time"] = "格林威治标准时间",
|
||||||
|
["Europe/London"] = "格林威治标准时间",
|
||||||
|
["AUS Eastern Standard Time"] = "澳大利亚东部标准时间",
|
||||||
|
["Australia/Sydney"] = "澳大利亚东部标准时间",
|
||||||
|
["Eastern Standard Time"] = "美国东部标准时间",
|
||||||
|
["America/New_York"] = "美国东部标准时间",
|
||||||
|
["Tokyo Standard Time"] = "日本标准时间",
|
||||||
|
["Asia/Tokyo"] = "日本标准时间",
|
||||||
|
["UTC"] = "协调世界时",
|
||||||
|
["Etc/UTC"] = "协调世界时"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private readonly TimeZoneService _timeZoneService = new();
|
||||||
|
private bool _suppressEvents;
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
private IReadOnlyList<TimeZoneInfo> _allTimeZones = Array.Empty<TimeZoneInfo>();
|
||||||
|
private string _selectedTimeZoneId = string.Empty;
|
||||||
|
private string _secondHandMode = ClockSecondHandMode.Tick;
|
||||||
|
|
||||||
|
public event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
|
public AnalogClockWidgetSettingsWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
LoadState();
|
||||||
|
ApplyLocalization();
|
||||||
|
PopulateTimeZoneComboBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
_selectedTimeZoneId = string.IsNullOrWhiteSpace(snapshot.DesktopClockTimeZoneId)
|
||||||
|
? "China Standard Time"
|
||||||
|
: snapshot.DesktopClockTimeZoneId.Trim();
|
||||||
|
_secondHandMode = ClockSecondHandMode.Normalize(snapshot.DesktopClockSecondHandMode);
|
||||||
|
|
||||||
|
_allTimeZones = _timeZoneService
|
||||||
|
.GetAllTimeZones()
|
||||||
|
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||||
|
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
TitleTextBlock.Text = L("desktop_clock.settings.title", "时钟设置");
|
||||||
|
DescriptionTextBlock.Text = L("desktop_clock.settings.desc", "为单时钟选择时区。");
|
||||||
|
TimeZoneLabelTextBlock.Text = L("desktop_clock.settings.timezone_label", "时区");
|
||||||
|
SecondHandModeLabelTextBlock.Text = L("desktop_clock.settings.second_mode_label", "秒针方式");
|
||||||
|
SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针");
|
||||||
|
SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateTimeZoneComboBox()
|
||||||
|
{
|
||||||
|
_suppressEvents = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TimeZoneComboBox.Items.Clear();
|
||||||
|
foreach (var timeZone in _allTimeZones)
|
||||||
|
{
|
||||||
|
TimeZoneComboBox.Items.Add(new ComboBoxItem
|
||||||
|
{
|
||||||
|
Tag = timeZone.Id,
|
||||||
|
Content = GetLocalizedTimeZoneDisplayName(timeZone)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
|
||||||
|
new[] { _selectedTimeZoneId },
|
||||||
|
_allTimeZones)[0];
|
||||||
|
_selectedTimeZoneId = normalizedId;
|
||||||
|
|
||||||
|
var selected = TimeZoneComboBox.Items
|
||||||
|
.OfType<ComboBoxItem>()
|
||||||
|
.FirstOrDefault(item => string.Equals(item.Tag as string, normalizedId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
TimeZoneComboBox.SelectedItem = selected ?? TimeZoneComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||||
|
|
||||||
|
var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode);
|
||||||
|
SecondHandTickRadioButton.IsChecked = string.Equals(
|
||||||
|
normalizedMode,
|
||||||
|
ClockSecondHandMode.Tick,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
SecondHandSweepRadioButton.IsChecked = string.Equals(
|
||||||
|
normalizedMode,
|
||||||
|
ClockSecondHandMode.Sweep,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveState()
|
||||||
|
{
|
||||||
|
var selectedId = (TimeZoneComboBox.SelectedItem as ComboBoxItem)?.Tag as string;
|
||||||
|
var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
|
||||||
|
new[] { selectedId ?? _selectedTimeZoneId },
|
||||||
|
_allTimeZones)[0];
|
||||||
|
_selectedTimeZoneId = normalizedId;
|
||||||
|
_secondHandMode = GetSelectedSecondHandMode();
|
||||||
|
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
snapshot.DesktopClockTimeZoneId = normalizedId;
|
||||||
|
snapshot.DesktopClockSecondHandMode = _secondHandMode;
|
||||||
|
_appSettingsService.Save(snapshot);
|
||||||
|
|
||||||
|
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSelectedSecondHandMode()
|
||||||
|
{
|
||||||
|
return SecondHandSweepRadioButton.IsChecked == true
|
||||||
|
? ClockSecondHandMode.Sweep
|
||||||
|
: ClockSecondHandMode.Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
|
||||||
|
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||||
|
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
|
||||||
|
var hours = totalMinutes / 60;
|
||||||
|
var minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ResolveZhDisplayName(timeZone)
|
||||||
|
: ResolveEnDisplayName(timeZone);
|
||||||
|
|
||||||
|
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveZhDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
|
||||||
|
{
|
||||||
|
return localizedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(timeZone.StandardName)
|
||||||
|
? timeZone.DisplayName
|
||||||
|
: timeZone.StandardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveEnDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(timeZone.StandardName))
|
||||||
|
{
|
||||||
|
return timeZone.StandardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeZone.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
<Border x:Name="ArtworkPanel"
|
<Border x:Name="ArtworkPanel"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
Background="#B8AE9A">
|
Background="#B8AE9A"
|
||||||
|
PointerPressed="OnArtworkPanelPointerPressed">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image x:Name="ArtworkImage"
|
<Image x:Name="ArtworkImage"
|
||||||
Stretch="UniformToFill" />
|
Stretch="UniformToFill" />
|
||||||
@@ -34,12 +35,14 @@
|
|||||||
FontSize="44"
|
FontSize="44"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
FontFeatures="tnum"
|
FontFeatures="tnum"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
LineHeight="46" />
|
LineHeight="46" />
|
||||||
<TextBlock x:Name="WeekdayTextBlock"
|
<TextBlock x:Name="WeekdayTextBlock"
|
||||||
Text="星期二"
|
Text="星期二"
|
||||||
Foreground="#F9F9F9"
|
Foreground="#F9F9F9"
|
||||||
FontSize="44"
|
FontSize="44"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
LineHeight="46" />
|
LineHeight="46" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -48,7 +51,8 @@
|
|||||||
<Border Grid.Column="1"
|
<Border Grid.Column="1"
|
||||||
x:Name="InfoPanel"
|
x:Name="InfoPanel"
|
||||||
Background="#111418"
|
Background="#111418"
|
||||||
Padding="18,14,18,14">
|
Padding="18,14,18,14"
|
||||||
|
PointerPressed="OnInfoPanelPointerPressed">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Canvas x:Name="BrickPatternCanvas"
|
<Canvas x:Name="BrickPatternCanvas"
|
||||||
IsHitTestVisible="False"
|
IsHitTestVisible="False"
|
||||||
@@ -76,6 +80,7 @@
|
|||||||
FontSize="44"
|
FontSize="44"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="2"
|
MaxLines="2"
|
||||||
Margin="0,0,0,8" />
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@
|
|||||||
FontSize="26"
|
FontSize="26"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="2" />
|
MaxLines="2" />
|
||||||
<TextBlock x:Name="YearTextBlock"
|
<TextBlock x:Name="YearTextBlock"
|
||||||
Text="1754"
|
Text="1754"
|
||||||
@@ -104,6 +110,7 @@
|
|||||||
FontWeight="Medium"
|
FontWeight="Medium"
|
||||||
FontFeatures="tnum"
|
FontFeatures="tnum"
|
||||||
TextWrapping="NoWrap"
|
TextWrapping="NoWrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="1" />
|
MaxLines="1" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -8,6 +9,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@@ -62,6 +64,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private double _currentCellSize = BaseCellSize;
|
private double _currentCellSize = BaseCellSize;
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
|
private string? _currentArtworkSourceUrl;
|
||||||
|
private string? _currentArtworkImageUrl;
|
||||||
|
|
||||||
public DailyArtworkWidget()
|
public DailyArtworkWidget()
|
||||||
{
|
{
|
||||||
@@ -102,7 +106,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
Math.Clamp(16 * scale, 8, 26));
|
Math.Clamp(16 * scale, 8, 26));
|
||||||
DateInfoStack.Spacing = Math.Clamp(2 * scale, 1, 6);
|
DateInfoStack.Spacing = Math.Clamp(4 * scale, 2, 10);
|
||||||
|
|
||||||
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24);
|
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24);
|
||||||
|
|
||||||
@@ -154,6 +158,28 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
await RefreshArtworkAsync(forceRefresh: false);
|
await RefreshArtworkAsync(forceRefresh: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnArtworkPanelPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = RefreshArtworkAsync(forceRefresh: true);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInfoPanelPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryOpenArtworkSourceUrl();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RefreshArtworkAsync(bool forceRefresh)
|
private async Task RefreshArtworkAsync(bool forceRefresh)
|
||||||
{
|
{
|
||||||
if (!_isAttached || _isRefreshing)
|
if (!_isAttached || _isRefreshing)
|
||||||
@@ -222,6 +248,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
ArtistTextBlock.Text = NormalizeCompactText(artist);
|
ArtistTextBlock.Text = NormalizeCompactText(artist);
|
||||||
|
|
||||||
YearTextBlock.Text = ResolveYearText(snapshot);
|
YearTextBlock.Text = ResolveYearText(snapshot);
|
||||||
|
_currentArtworkSourceUrl = snapshot.ArtworkUrl;
|
||||||
|
_currentArtworkImageUrl = snapshot.ImageUrl;
|
||||||
StatusTextBlock.IsVisible = false;
|
StatusTextBlock.IsVisible = false;
|
||||||
|
|
||||||
UpdateAdaptiveLayout();
|
UpdateAdaptiveLayout();
|
||||||
@@ -352,6 +380,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private void ApplyLoadingState()
|
private void ApplyLoadingState()
|
||||||
{
|
{
|
||||||
|
_currentArtworkSourceUrl = null;
|
||||||
|
_currentArtworkImageUrl = null;
|
||||||
StatusTextBlock.IsVisible = true;
|
StatusTextBlock.IsVisible = true;
|
||||||
StatusTextBlock.Text = L("artwork.widget.loading", "Loading...");
|
StatusTextBlock.Text = L("artwork.widget.loading", "Loading...");
|
||||||
PaintingTitleTextBlock.Text = BuildQuotedTitle(L("artwork.widget.loading_title", "Daily Artwork"));
|
PaintingTitleTextBlock.Text = BuildQuotedTitle(L("artwork.widget.loading_title", "Daily Artwork"));
|
||||||
@@ -362,6 +392,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private void ApplyFailedState()
|
private void ApplyFailedState()
|
||||||
{
|
{
|
||||||
|
_currentArtworkSourceUrl = null;
|
||||||
|
_currentArtworkImageUrl = null;
|
||||||
StatusTextBlock.IsVisible = true;
|
StatusTextBlock.IsVisible = true;
|
||||||
StatusTextBlock.Text = L("artwork.widget.fetch_failed", "Artwork fetch failed");
|
StatusTextBlock.Text = L("artwork.widget.fetch_failed", "Artwork fetch failed");
|
||||||
PaintingTitleTextBlock.Text = BuildQuotedTitle(L("artwork.widget.fallback_title", "Daily Artwork"));
|
PaintingTitleTextBlock.Text = BuildQuotedTitle(L("artwork.widget.fallback_title", "Daily Artwork"));
|
||||||
@@ -384,71 +416,94 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right);
|
var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right);
|
||||||
var leftPanelWidth = Math.Max(84, totalWidth - rightPanelWidth);
|
var leftPanelWidth = Math.Max(84, totalWidth - rightPanelWidth);
|
||||||
var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10);
|
var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10);
|
||||||
|
var leftContentHeight = Math.Max(30, totalHeight - DateInfoStack.Margin.Bottom - 10);
|
||||||
|
|
||||||
|
var dateStackSpacing = Math.Clamp(4 * scale, 2, 10);
|
||||||
|
DateInfoStack.Spacing = dateStackSpacing;
|
||||||
|
DateInfoStack.MaxWidth = leftContentWidth;
|
||||||
|
var leftSingleLineHeight = Math.Max(12, (leftContentHeight - dateStackSpacing) / 2d);
|
||||||
|
|
||||||
var dateBase = Math.Clamp(44 * scale, 16, 62);
|
var dateBase = Math.Clamp(44 * scale, 16, 62);
|
||||||
DateTextBlock.FontSize = FitFontSize(
|
DateTextBlock.FontSize = FitFontSize(
|
||||||
DateTextBlock.Text,
|
DateTextBlock.Text,
|
||||||
leftContentWidth,
|
leftContentWidth,
|
||||||
Math.Max(18, totalHeight * 0.20),
|
leftSingleLineHeight,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
minFontSize: Math.Max(12, dateBase * 0.68),
|
minFontSize: Math.Max(12, dateBase * 0.68),
|
||||||
maxFontSize: dateBase,
|
maxFontSize: dateBase,
|
||||||
weight: FontWeight.Bold,
|
weight: FontWeight.Bold,
|
||||||
lineHeightFactor: 1.00);
|
lineHeightFactor: 1.10);
|
||||||
DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.00;
|
DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.10;
|
||||||
|
|
||||||
WeekdayTextBlock.FontSize = FitFontSize(
|
WeekdayTextBlock.FontSize = FitFontSize(
|
||||||
WeekdayTextBlock.Text,
|
WeekdayTextBlock.Text,
|
||||||
leftContentWidth,
|
leftContentWidth,
|
||||||
Math.Max(18, totalHeight * 0.21),
|
leftSingleLineHeight,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
minFontSize: Math.Max(12, dateBase * 0.68),
|
minFontSize: Math.Max(12, dateBase * 0.68),
|
||||||
maxFontSize: dateBase,
|
maxFontSize: dateBase,
|
||||||
weight: FontWeight.Bold,
|
weight: FontWeight.Bold,
|
||||||
lineHeightFactor: 1.00);
|
lineHeightFactor: 1.10);
|
||||||
WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.00;
|
WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10;
|
||||||
|
|
||||||
|
var rightContentHeight = Math.Max(42, totalHeight - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom);
|
||||||
|
var titleBottomMargin = Math.Clamp(8 * scale, 4, 14);
|
||||||
|
var separatorBottomMargin = Math.Clamp(10 * scale, 4, 14);
|
||||||
|
var bottomStackSpacing = Math.Clamp(3 * scale, 2, 8);
|
||||||
|
var reservedHeight = titleBottomMargin + separatorBottomMargin + bottomStackSpacing + 3;
|
||||||
|
var textHeightBudget = Math.Max(24, rightContentHeight - reservedHeight);
|
||||||
|
var titleHeightBudget = Math.Max(16, textHeightBudget * 0.54);
|
||||||
|
var bottomTextBudget = Math.Max(10, textHeightBudget - titleHeightBudget);
|
||||||
|
var artistHeightBudget = Math.Max(8, bottomTextBudget * 0.66);
|
||||||
|
var yearHeightBudget = Math.Max(8, bottomTextBudget - artistHeightBudget);
|
||||||
|
|
||||||
var titleBase = Math.Clamp(44 * scale, 16, 58);
|
var titleBase = Math.Clamp(44 * scale, 16, 58);
|
||||||
PaintingTitleTextBlock.MaxWidth = rightContentWidth;
|
PaintingTitleTextBlock.MaxWidth = rightContentWidth;
|
||||||
|
PaintingTitleTextBlock.Margin = new Thickness(0, 0, 0, titleBottomMargin);
|
||||||
PaintingTitleTextBlock.FontSize = FitFontSize(
|
PaintingTitleTextBlock.FontSize = FitFontSize(
|
||||||
PaintingTitleTextBlock.Text,
|
PaintingTitleTextBlock.Text,
|
||||||
rightContentWidth,
|
rightContentWidth,
|
||||||
Math.Max(20, totalHeight * 0.34),
|
titleHeightBudget,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
minFontSize: Math.Max(12, titleBase * 0.62),
|
minFontSize: Math.Max(12, titleBase * 0.62),
|
||||||
maxFontSize: titleBase,
|
maxFontSize: titleBase,
|
||||||
weight: FontWeight.Bold,
|
weight: FontWeight.Bold,
|
||||||
lineHeightFactor: 1.08);
|
lineHeightFactor: 1.12);
|
||||||
PaintingTitleTextBlock.LineHeight = PaintingTitleTextBlock.FontSize * 1.08;
|
PaintingTitleTextBlock.LineHeight = PaintingTitleTextBlock.FontSize * 1.12;
|
||||||
|
|
||||||
var artistBase = Math.Clamp(26 * scale, 11, 34);
|
var artistBase = Math.Clamp(26 * scale, 11, 34);
|
||||||
|
if (ArtistTextBlock.Parent is StackPanel artistInfoStack)
|
||||||
|
{
|
||||||
|
artistInfoStack.Spacing = bottomStackSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
ArtistTextBlock.MaxWidth = rightContentWidth;
|
ArtistTextBlock.MaxWidth = rightContentWidth;
|
||||||
ArtistTextBlock.FontSize = FitFontSize(
|
ArtistTextBlock.FontSize = FitFontSize(
|
||||||
ArtistTextBlock.Text,
|
ArtistTextBlock.Text,
|
||||||
rightContentWidth,
|
rightContentWidth,
|
||||||
Math.Max(18, totalHeight * 0.24),
|
artistHeightBudget,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
minFontSize: Math.Max(10, artistBase * 0.72),
|
minFontSize: Math.Max(10, artistBase * 0.72),
|
||||||
maxFontSize: artistBase,
|
maxFontSize: artistBase,
|
||||||
weight: FontWeight.SemiBold,
|
weight: FontWeight.SemiBold,
|
||||||
lineHeightFactor: 1.12);
|
lineHeightFactor: 1.14);
|
||||||
ArtistTextBlock.LineHeight = ArtistTextBlock.FontSize * 1.12;
|
ArtistTextBlock.LineHeight = ArtistTextBlock.FontSize * 1.14;
|
||||||
|
|
||||||
var yearBase = Math.Clamp(22 * scale, 10, 30);
|
var yearBase = Math.Clamp(22 * scale, 10, 30);
|
||||||
YearTextBlock.MaxWidth = rightContentWidth;
|
YearTextBlock.MaxWidth = rightContentWidth;
|
||||||
YearTextBlock.FontSize = FitFontSize(
|
YearTextBlock.FontSize = FitFontSize(
|
||||||
YearTextBlock.Text,
|
YearTextBlock.Text,
|
||||||
rightContentWidth,
|
rightContentWidth,
|
||||||
Math.Max(14, totalHeight * 0.12),
|
yearHeightBudget,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
minFontSize: Math.Max(9.5, yearBase * 0.78),
|
minFontSize: Math.Max(9.5, yearBase * 0.78),
|
||||||
maxFontSize: yearBase,
|
maxFontSize: yearBase,
|
||||||
weight: FontWeight.Medium,
|
weight: FontWeight.Medium,
|
||||||
lineHeightFactor: 1.04);
|
lineHeightFactor: 1.08);
|
||||||
YearTextBlock.LineHeight = YearTextBlock.FontSize * 1.04;
|
YearTextBlock.LineHeight = YearTextBlock.FontSize * 1.08;
|
||||||
|
|
||||||
RightPanelSeparator.Width = Math.Clamp(rightContentWidth * 0.58, 42, 136);
|
RightPanelSeparator.Width = Math.Clamp(rightContentWidth * 0.58, 42, 136);
|
||||||
RightPanelSeparator.Margin = new Thickness(0, 0, 0, Math.Clamp(10 * scale, 4, 14));
|
RightPanelSeparator.Margin = new Thickness(0, 0, 0, separatorBottomMargin);
|
||||||
|
|
||||||
BrickPatternCanvas.Opacity = totalWidth < _currentCellSize * 4.2
|
BrickPatternCanvas.Opacity = totalWidth < _currentCellSize * 4.2
|
||||||
? 0.34
|
? 0.34
|
||||||
@@ -478,6 +533,54 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_currentArtworkBitmap = null;
|
_currentArtworkBitmap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryOpenArtworkSourceUrl()
|
||||||
|
{
|
||||||
|
var candidate = _currentArtworkSourceUrl;
|
||||||
|
if (!TryNormalizeHttpUrl(candidate, out var normalizedUrl) &&
|
||||||
|
!TryNormalizeHttpUrl(_currentArtworkImageUrl, out normalizedUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = normalizedUrl,
|
||||||
|
UseShellExecute = true
|
||||||
|
};
|
||||||
|
Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore malformed URLs or shell launch failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeHttpUrl(string? rawUrl, out string normalizedUrl)
|
||||||
|
{
|
||||||
|
normalizedUrl = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(rawUrl))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = rawUrl.Trim();
|
||||||
|
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedUrl = uri.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateLanguageCode()
|
private void UpdateLanguageCode()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
"component.weather_clock",
|
"component.weather_clock",
|
||||||
() => new WeatherClockWidget(),
|
() => new WeatherClockWidget(),
|
||||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
BuiltInComponentIds.DesktopWorldClock,
|
||||||
|
"component.world_clock",
|
||||||
|
() => new WorldClockWidget(),
|
||||||
|
cellSize => Math.Clamp(cellSize * 0.30, 10, 24)),
|
||||||
new DesktopComponentRuntimeRegistration(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.DesktopTimer,
|
BuiltInComponentIds.DesktopTimer,
|
||||||
"component.desktop_timer",
|
"component.desktop_timer",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||||
|
|
||||||
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
|
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
|
||||||
private readonly DispatcherTimer _animationTimer = new() { Interval = UiMotionTokens.WeatherAnimationFrameInterval };
|
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
|
||||||
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
|
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
|
||||||
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
|
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
|
||||||
private readonly AppSettingsService _settingsService = new();
|
private readonly AppSettingsService _settingsService = new();
|
||||||
@@ -43,6 +43,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
private readonly TextBlock[] _dailyHighBlocks;
|
private readonly TextBlock[] _dailyHighBlocks;
|
||||||
private readonly TextBlock[] _dailyLowBlocks;
|
private readonly TextBlock[] _dailyLowBlocks;
|
||||||
private readonly Image[] _dailyIconBlocks;
|
private readonly Image[] _dailyIconBlocks;
|
||||||
|
private readonly HyperOS3WeatherVisualKind[] _dailyIconKinds;
|
||||||
|
|
||||||
public ExtendedWeatherWidget()
|
public ExtendedWeatherWidget()
|
||||||
{
|
{
|
||||||
@@ -76,6 +77,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
[
|
[
|
||||||
DailyIcon0, DailyIcon1, DailyIcon2, DailyIcon3, DailyIcon4
|
DailyIcon0, DailyIcon1, DailyIcon2, DailyIcon3, DailyIcon4
|
||||||
];
|
];
|
||||||
|
_dailyIconKinds = Enumerable.Repeat(HyperOS3WeatherVisualKind.CloudyDay, _dailyIconBlocks.Length).ToArray();
|
||||||
ConfigureTextOverflowGuards();
|
ConfigureTextOverflowGuards();
|
||||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||||
_animationTimer.Tick += OnAnimationTick;
|
_animationTimer.Tick += OnAnimationTick;
|
||||||
@@ -344,6 +346,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(date, i + 1)}·{dayText}";
|
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(date, i + 1)}·{dayText}";
|
||||||
_dailyHighBlocks[i].Text = FormatTemperatureValue(daily?.HighTemperatureC);
|
_dailyHighBlocks[i].Text = FormatTemperatureValue(daily?.HighTemperatureC);
|
||||||
_dailyLowBlocks[i].Text = FormatTemperatureValue(daily?.LowTemperatureC);
|
_dailyLowBlocks[i].Text = FormatTemperatureValue(daily?.LowTemperatureC);
|
||||||
|
_dailyIconKinds[i] = dayKind;
|
||||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(dayKind));
|
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(dayKind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,6 +374,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(DateOnly.FromDateTime(DateTime.Now).AddDays(i + 1), i + 1)}·{L("weather.widget.condition_cloudy", "Cloudy")}";
|
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(DateOnly.FromDateTime(DateTime.Now).AddDays(i + 1), i + 1)}·{L("weather.widget.condition_cloudy", "Cloudy")}";
|
||||||
_dailyHighBlocks[i].Text = "--";
|
_dailyHighBlocks[i].Text = "--";
|
||||||
_dailyLowBlocks[i].Text = "--";
|
_dailyLowBlocks[i].Text = "--";
|
||||||
|
_dailyIconKinds[i] = HyperOS3WeatherVisualKind.CloudyDay;
|
||||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
|
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,7 +527,9 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
3.80);
|
3.80);
|
||||||
var hourlyTempSize = Math.Clamp(19 * hourlyCellScale, 6, 72);
|
var hourlyTempSize = Math.Clamp(19 * hourlyCellScale, 6, 72);
|
||||||
var hourlyTimeSize = Math.Clamp(14 * hourlyCellScale, 6, 52);
|
var hourlyTimeSize = Math.Clamp(14 * hourlyCellScale, 6, 52);
|
||||||
var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 114);
|
var hourlyIconSize = Math.Clamp(42 * hourlyCellScale, 9, 140);
|
||||||
|
hourlyIconSize = Math.Min(hourlyIconSize, Math.Max(10, hourlyCellWidth * 0.86));
|
||||||
|
hourlyIconSize = Math.Min(hourlyIconSize, Math.Max(10, hourlyHeight * 0.56));
|
||||||
var hourlyStackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
var hourlyStackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
||||||
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
|
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -548,7 +554,9 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
|
|
||||||
var dailyLabelSize = Math.Clamp(18.5 * dailyRowScale, 6, 70);
|
var dailyLabelSize = Math.Clamp(18.5 * dailyRowScale, 6, 70);
|
||||||
var dailyTempSize = Math.Clamp(19 * dailyRowScale, 6, 72);
|
var dailyTempSize = Math.Clamp(19 * dailyRowScale, 6, 72);
|
||||||
var dailyIconSize = Math.Clamp(30 * dailyRowScale, 8, 102);
|
var dailyIconSize = Math.Clamp(43 * dailyRowScale, 9, 132);
|
||||||
|
dailyIconSize = Math.Min(dailyIconSize, Math.Max(10, dailyRowHeight * 0.92));
|
||||||
|
dailyIconSize = Math.Min(dailyIconSize, Math.Max(10, innerWidth * 0.14));
|
||||||
var dailyLabelMaxWidth = Math.Clamp(innerWidth * 0.52, 28, 460);
|
var dailyLabelMaxWidth = Math.Clamp(innerWidth * 0.52, 28, 460);
|
||||||
var dailyHighWidth = Math.Clamp(innerWidth * 0.14, 14, 140);
|
var dailyHighWidth = Math.Clamp(innerWidth * 0.14, 14, 140);
|
||||||
var dailyLowWidth = Math.Clamp(innerWidth * 0.11, 12, 120);
|
var dailyLowWidth = Math.Clamp(innerWidth * 0.11, 12, 120);
|
||||||
@@ -570,8 +578,21 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
_dailyLowBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
|
_dailyLowBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
|
||||||
_dailyHighBlocks[i].TextAlignment = TextAlignment.Right;
|
_dailyHighBlocks[i].TextAlignment = TextAlignment.Right;
|
||||||
_dailyLowBlocks[i].TextAlignment = TextAlignment.Right;
|
_dailyLowBlocks[i].TextAlignment = TextAlignment.Right;
|
||||||
_dailyIconBlocks[i].Width = dailyIconSize;
|
if (_dailyIconBlocks[i].Parent is Grid dailyRowGrid)
|
||||||
_dailyIconBlocks[i].Height = dailyIconSize;
|
{
|
||||||
|
dailyRowGrid.ColumnSpacing = Math.Clamp(9 * dailyRowScale, 4, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dailyKind = i < _dailyIconKinds.Length
|
||||||
|
? _dailyIconKinds[i]
|
||||||
|
: HyperOS3WeatherVisualKind.CloudyDay;
|
||||||
|
var dailyIconVisualSize = Math.Clamp(
|
||||||
|
dailyIconSize * ResolveDailyMiniIconScaleBoost(dailyKind),
|
||||||
|
8,
|
||||||
|
148);
|
||||||
|
dailyIconVisualSize = Math.Min(dailyIconVisualSize, Math.Max(10, dailyRowHeight * 0.94));
|
||||||
|
_dailyIconBlocks[i].Width = dailyIconVisualSize;
|
||||||
|
_dailyIconBlocks[i].Height = dailyIconVisualSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,6 +926,21 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
|
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
|
||||||
_ => 1.0
|
_ => 1.0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static double ResolveDailyMiniIconScaleBoost(HyperOS3WeatherVisualKind kind) =>
|
||||||
|
kind switch
|
||||||
|
{
|
||||||
|
HyperOS3WeatherVisualKind.CloudyDay => 1.30,
|
||||||
|
HyperOS3WeatherVisualKind.CloudyNight => 1.28,
|
||||||
|
HyperOS3WeatherVisualKind.ClearDay => 1.26,
|
||||||
|
HyperOS3WeatherVisualKind.ClearNight => 1.24,
|
||||||
|
HyperOS3WeatherVisualKind.Fog => 1.18,
|
||||||
|
HyperOS3WeatherVisualKind.RainLight => 1.14,
|
||||||
|
HyperOS3WeatherVisualKind.RainHeavy => 1.12,
|
||||||
|
HyperOS3WeatherVisualKind.Snow => 1.12,
|
||||||
|
HyperOS3WeatherVisualKind.Storm => 1.08,
|
||||||
|
_ => 1.18
|
||||||
|
};
|
||||||
private static FontWeight ToVariableWeight(double weight) => (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
private static FontWeight ToVariableWeight(double weight) => (FontWeight)(int)Math.Clamp(Math.Round(weight), 1, 1000);
|
||||||
private static IBrush CreateSolidBrush(string colorHex) => new SolidColorBrush(Color.Parse(colorHex));
|
private static IBrush CreateSolidBrush(string colorHex) => new SolidColorBrush(Color.Parse(colorHex));
|
||||||
private static IBrush CreateSolidBrush(string colorHex, byte alpha) { var c = Color.Parse(colorHex); return new SolidColorBrush(Color.FromArgb(alpha, c.R, c.G, c.B)); }
|
private static IBrush CreateSolidBrush(string colorHex, byte alpha) { var c = Color.Parse(colorHex); return new SolidColorBrush(Color.FromArgb(alpha, c.R, c.G, c.B)); }
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
||||||
{
|
{
|
||||||
Interval = UiMotionTokens.WeatherAnimationFrameInterval
|
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly AppSettingsService _settingsService = new();
|
private readonly AppSettingsService _settingsService = new();
|
||||||
@@ -1264,7 +1264,9 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
||||||
var hourlyTempSize = Math.Clamp(19.5 * hourlyCellScale, 6, 72);
|
var hourlyTempSize = Math.Clamp(19.5 * hourlyCellScale, 6, 72);
|
||||||
var hourlyTimeSize = Math.Clamp(14.5 * hourlyCellScale, 6, 50);
|
var hourlyTimeSize = Math.Clamp(14.5 * hourlyCellScale, 6, 50);
|
||||||
var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 108);
|
var hourlyIconSize = Math.Clamp(42 * hourlyCellScale, 9, 136);
|
||||||
|
hourlyIconSize = Math.Min(hourlyIconSize, Math.Max(10, hourlyCellWidth * 0.86));
|
||||||
|
hourlyIconSize = Math.Min(hourlyIconSize, Math.Max(10, bottomZoneHeight * 0.52));
|
||||||
|
|
||||||
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
|
|
||||||
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
||||||
{
|
{
|
||||||
Interval = UiMotionTokens.WeatherAnimationFrameInterval
|
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly AppSettingsService _settingsService = new();
|
private readonly AppSettingsService _settingsService = new();
|
||||||
@@ -1112,7 +1112,9 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
|
||||||
var forecastRangeSize = Math.Clamp(18.0 * hourlyCellScale, 6, 62);
|
var forecastRangeSize = Math.Clamp(18.0 * hourlyCellScale, 6, 62);
|
||||||
var forecastLabelSize = Math.Clamp(13.8 * hourlyCellScale, 6, 48);
|
var forecastLabelSize = Math.Clamp(13.8 * hourlyCellScale, 6, 48);
|
||||||
var forecastIconSize = Math.Clamp(32 * hourlyCellScale, 8, 100);
|
var forecastIconSize = Math.Clamp(40 * hourlyCellScale, 9, 124);
|
||||||
|
forecastIconSize = Math.Min(forecastIconSize, Math.Max(10, hourlyCellWidth * 0.88));
|
||||||
|
forecastIconSize = Math.Min(forecastIconSize, Math.Max(10, bottomZoneHeight * 0.50));
|
||||||
|
|
||||||
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
Opacity="0.62"
|
Opacity="0.62"
|
||||||
Stretch="UniformToFill">
|
Stretch="UniformToFill">
|
||||||
<Image.Effect>
|
<Image.Effect>
|
||||||
<BlurEffect Radius="{DynamicResource MotionBackdropBlurRadiusStrong}" />
|
<BlurEffect Radius="{DynamicResource FluttermotionToken.BackdropBlurRadiusStrong}" />
|
||||||
</Image.Effect>
|
</Image.Effect>
|
||||||
</Image>
|
</Image>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
|||||||
|
|
||||||
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
private readonly DispatcherTimer _backgroundAnimationTimer = new()
|
||||||
{
|
{
|
||||||
Interval = UiMotionTokens.WeatherAnimationFrameInterval
|
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly AppSettingsService _settingsService = new();
|
private readonly AppSettingsService _settingsService = new();
|
||||||
|
|||||||
21
LanMountainDesktop/Views/Components/WorldClockWidget.axaml
Normal file
21
LanMountainDesktop/Views/Components/WorldClockWidget.axaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="420"
|
||||||
|
d:DesignHeight="210"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.WorldClockWidget">
|
||||||
|
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
Background="#F4F5F7"
|
||||||
|
BorderBrush="#16000000"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="26"
|
||||||
|
ClipToBounds="True"
|
||||||
|
Padding="10,8">
|
||||||
|
<Grid x:Name="ClockHostGrid"
|
||||||
|
ColumnDefinitions="*,*,*,*"
|
||||||
|
ColumnSpacing="8" />
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
671
LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
Normal file
671
LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||||
|
{
|
||||||
|
private const int BaseWidthCells = 4;
|
||||||
|
private const int BaseHeightCells = 2;
|
||||||
|
private const double BaseCellSize = 48;
|
||||||
|
private const double DialDesignSize = 100;
|
||||||
|
private const double DialCenter = DialDesignSize / 2d;
|
||||||
|
|
||||||
|
private static readonly FontFamily MiSansFontFamily =
|
||||||
|
new("MiSans VF, avares://LanMountainDesktop/Assets/Fonts#MiSans");
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> ZhCityNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "北京",
|
||||||
|
["Asia/Shanghai"] = "北京",
|
||||||
|
["GMT Standard Time"] = "伦敦",
|
||||||
|
["Europe/London"] = "伦敦",
|
||||||
|
["AUS Eastern Standard Time"] = "悉尼",
|
||||||
|
["Australia/Sydney"] = "悉尼",
|
||||||
|
["Eastern Standard Time"] = "纽约",
|
||||||
|
["America/New_York"] = "纽约",
|
||||||
|
["Tokyo Standard Time"] = "东京",
|
||||||
|
["Asia/Tokyo"] = "东京",
|
||||||
|
["UTC"] = "协调世界时",
|
||||||
|
["Etc/UTC"] = "协调世界时"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> EnCityNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "Beijing",
|
||||||
|
["Asia/Shanghai"] = "Beijing",
|
||||||
|
["GMT Standard Time"] = "London",
|
||||||
|
["Europe/London"] = "London",
|
||||||
|
["AUS Eastern Standard Time"] = "Sydney",
|
||||||
|
["Australia/Sydney"] = "Sydney",
|
||||||
|
["Eastern Standard Time"] = "New York",
|
||||||
|
["America/New_York"] = "New York",
|
||||||
|
["Tokyo Standard Time"] = "Tokyo",
|
||||||
|
["Asia/Tokyo"] = "Tokyo",
|
||||||
|
["UTC"] = "UTC",
|
||||||
|
["Etc/UTC"] = "UTC"
|
||||||
|
};
|
||||||
|
|
||||||
|
private sealed class ClockEntryVisual
|
||||||
|
{
|
||||||
|
public required StackPanel Host { get; init; }
|
||||||
|
|
||||||
|
public required Border DialBorder { get; init; }
|
||||||
|
|
||||||
|
public required Canvas TickCanvas { get; init; }
|
||||||
|
|
||||||
|
public required Canvas NumberCanvas { get; init; }
|
||||||
|
|
||||||
|
public required Line HourHand { get; init; }
|
||||||
|
|
||||||
|
public required Line MinuteHand { get; init; }
|
||||||
|
|
||||||
|
public required Line SecondHand { get; init; }
|
||||||
|
|
||||||
|
public required Ellipse CenterOuter { get; init; }
|
||||||
|
|
||||||
|
public required TextBlock CityTextBlock { get; init; }
|
||||||
|
|
||||||
|
public required TextBlock DayTextBlock { get; init; }
|
||||||
|
|
||||||
|
public required TextBlock OffsetTextBlock { get; init; }
|
||||||
|
|
||||||
|
public bool? IsNightApplied { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly DispatcherTimer _clockTimer = new()
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly AppSettingsService _settingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private readonly ClockEntryVisual[] _entryVisuals = new ClockEntryVisual[WorldClockTimeZoneCatalog.ClockCount];
|
||||||
|
private readonly TimeZoneInfo[] _entryTimeZones = new TimeZoneInfo[WorldClockTimeZoneCatalog.ClockCount];
|
||||||
|
|
||||||
|
private TimeZoneService? _timeZoneService;
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
private double _currentCellSize = BaseCellSize;
|
||||||
|
private DateTime _nextLanguageProbeUtc = DateTime.MinValue;
|
||||||
|
private string _secondHandMode = ClockSecondHandMode.Tick;
|
||||||
|
|
||||||
|
public WorldClockWidget()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
BuildClockEntryVisuals();
|
||||||
|
LoadFromSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
|
ApplyCellSize(_currentCellSize);
|
||||||
|
UpdateClockVisuals();
|
||||||
|
|
||||||
|
_clockTimer.Tick += OnClockTimerTick;
|
||||||
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
|
SizeChanged += OnSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||||
|
{
|
||||||
|
ClearTimeZoneService();
|
||||||
|
_timeZoneService = timeZoneService;
|
||||||
|
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||||
|
UpdateClockVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearTimeZoneService()
|
||||||
|
{
|
||||||
|
if (_timeZoneService is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||||
|
_timeZoneService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshFromSettings()
|
||||||
|
{
|
||||||
|
LoadFromSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
|
UpdateClockVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCellSize(double cellSize)
|
||||||
|
{
|
||||||
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
|
var scale = ResolveScale();
|
||||||
|
|
||||||
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
|
var horizontalPadding = Math.Clamp(10 * scale, 4, 26);
|
||||||
|
var verticalPadding = Math.Clamp(8 * scale, 3, 22);
|
||||||
|
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||||
|
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 10, 46));
|
||||||
|
|
||||||
|
var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);
|
||||||
|
var usableHeight = Math.Max(28, totalHeight - verticalPadding * 2);
|
||||||
|
|
||||||
|
var columnSpacing = Math.Clamp(usableWidth * 0.015, 2, 14);
|
||||||
|
ClockHostGrid.ColumnSpacing = columnSpacing;
|
||||||
|
var widthPerClock = Math.Max(18, (usableWidth - columnSpacing * 3) / WorldClockTimeZoneCatalog.ClockCount);
|
||||||
|
|
||||||
|
var secondaryFont = Math.Clamp(10.5 * scale * (widthPerClock / 46d), 7, 18);
|
||||||
|
var cityFont = Math.Clamp(secondaryFont * 1.42, 9, 24);
|
||||||
|
var textSpacing = Math.Clamp(2.8 * scale, 1, 7);
|
||||||
|
|
||||||
|
var estimatedTextHeight = cityFont * 1.2 + secondaryFont * 2.35 + textSpacing * 3;
|
||||||
|
var dialSize = Math.Clamp(Math.Min(widthPerClock, usableHeight - estimatedTextHeight), 18, 108);
|
||||||
|
if (dialSize < 18)
|
||||||
|
{
|
||||||
|
dialSize = Math.Clamp(Math.Min(widthPerClock, usableHeight * 0.56), 16, 108);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in _entryVisuals)
|
||||||
|
{
|
||||||
|
entry.Host.Spacing = textSpacing;
|
||||||
|
entry.DialBorder.Width = dialSize;
|
||||||
|
entry.DialBorder.Height = dialSize;
|
||||||
|
entry.DialBorder.CornerRadius = new CornerRadius(dialSize / 2d);
|
||||||
|
|
||||||
|
entry.CityTextBlock.FontSize = cityFont;
|
||||||
|
entry.DayTextBlock.FontSize = secondaryFont;
|
||||||
|
entry.OffsetTextBlock.FontSize = secondaryFont;
|
||||||
|
|
||||||
|
var maxTextWidth = Math.Max(16, widthPerClock + 10);
|
||||||
|
entry.CityTextBlock.MaxWidth = maxTextWidth;
|
||||||
|
entry.DayTextBlock.MaxWidth = maxTextWidth;
|
||||||
|
entry.OffsetTextBlock.MaxWidth = maxTextWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
LoadFromSettings();
|
||||||
|
ApplySecondHandTimerInterval();
|
||||||
|
UpdateClockVisuals();
|
||||||
|
_clockTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
_clockTimer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
ApplyCellSize(_currentCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
UpdateClockVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
UpdateClockVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildClockEntryVisuals()
|
||||||
|
{
|
||||||
|
ClockHostGrid.Children.Clear();
|
||||||
|
for (var index = 0; index < WorldClockTimeZoneCatalog.ClockCount; index++)
|
||||||
|
{
|
||||||
|
var entry = CreateClockEntryVisual();
|
||||||
|
_entryVisuals[index] = entry;
|
||||||
|
ClockHostGrid.Children.Add(entry.Host);
|
||||||
|
Grid.SetColumn(entry.Host, index);
|
||||||
|
Grid.SetRow(entry.Host, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClockEntryVisual CreateClockEntryVisual()
|
||||||
|
{
|
||||||
|
var tickCanvas = new Canvas
|
||||||
|
{
|
||||||
|
Width = DialDesignSize,
|
||||||
|
Height = DialDesignSize,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
var numberCanvas = new Canvas
|
||||||
|
{
|
||||||
|
Width = DialDesignSize,
|
||||||
|
Height = DialDesignSize,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
var handsCanvas = new Canvas
|
||||||
|
{
|
||||||
|
Width = DialDesignSize,
|
||||||
|
Height = DialDesignSize,
|
||||||
|
IsHitTestVisible = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var hourHand = CreateHandLine("#2B3242", 5.0);
|
||||||
|
var minuteHand = CreateHandLine("#40495E", 3.2);
|
||||||
|
var secondHand = CreateHandLine("#1A74F2", 2.2);
|
||||||
|
handsCanvas.Children.Add(hourHand);
|
||||||
|
handsCanvas.Children.Add(minuteHand);
|
||||||
|
handsCanvas.Children.Add(secondHand);
|
||||||
|
|
||||||
|
var centerOuter = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 11,
|
||||||
|
Height = 11,
|
||||||
|
Fill = CreateBrush("#4F7BC0"),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
var centerInner = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 4.5,
|
||||||
|
Height = 4.5,
|
||||||
|
Fill = CreateBrush("#1A74F2"),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialRoot = new Grid
|
||||||
|
{
|
||||||
|
Width = DialDesignSize,
|
||||||
|
Height = DialDesignSize
|
||||||
|
};
|
||||||
|
dialRoot.Children.Add(tickCanvas);
|
||||||
|
dialRoot.Children.Add(numberCanvas);
|
||||||
|
dialRoot.Children.Add(handsCanvas);
|
||||||
|
dialRoot.Children.Add(centerOuter);
|
||||||
|
dialRoot.Children.Add(centerInner);
|
||||||
|
|
||||||
|
var dialBorder = new Border
|
||||||
|
{
|
||||||
|
Width = 56,
|
||||||
|
Height = 56,
|
||||||
|
CornerRadius = new CornerRadius(28),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Background = CreateBrush("#FAFBFD"),
|
||||||
|
BorderBrush = CreateBrush("#DADFE8"),
|
||||||
|
ClipToBounds = true,
|
||||||
|
Child = new Viewbox
|
||||||
|
{
|
||||||
|
Stretch = Stretch.Uniform,
|
||||||
|
Child = dialRoot
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var cityTextBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.Empty,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = 13,
|
||||||
|
FontWeight = FontWeight.SemiBold,
|
||||||
|
Foreground = CreateBrush("#20232A"),
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
TextWrapping = TextWrapping.NoWrap,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var dayTextBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.Empty,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontWeight = FontWeight.Medium,
|
||||||
|
Foreground = CreateBrush("#646C79"),
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
TextWrapping = TextWrapping.NoWrap,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var offsetTextBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.Empty,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontWeight = FontWeight.Medium,
|
||||||
|
Foreground = CreateBrush("#7A7F89"),
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
TextWrapping = TextWrapping.NoWrap,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var host = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Vertical,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Spacing = 3,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
dialBorder,
|
||||||
|
cityTextBlock,
|
||||||
|
dayTextBlock,
|
||||||
|
offsetTextBlock
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var entry = new ClockEntryVisual
|
||||||
|
{
|
||||||
|
Host = host,
|
||||||
|
DialBorder = dialBorder,
|
||||||
|
TickCanvas = tickCanvas,
|
||||||
|
NumberCanvas = numberCanvas,
|
||||||
|
HourHand = hourHand,
|
||||||
|
MinuteHand = minuteHand,
|
||||||
|
SecondHand = secondHand,
|
||||||
|
CenterOuter = centerOuter,
|
||||||
|
CityTextBlock = cityTextBlock,
|
||||||
|
DayTextBlock = dayTextBlock,
|
||||||
|
OffsetTextBlock = offsetTextBlock
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplyDialTheme(entry, isNight: false);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildDialTicks(ClockEntryVisual entry, bool isNight)
|
||||||
|
{
|
||||||
|
entry.TickCanvas.Children.Clear();
|
||||||
|
var majorColor = isNight ? "#E3E7F2" : "#2D3341";
|
||||||
|
var minorColor = isNight ? "#9EA7B8" : "#9AA4B3";
|
||||||
|
|
||||||
|
for (var i = 0; i < 60; i++)
|
||||||
|
{
|
||||||
|
var isMajor = i % 5 == 0;
|
||||||
|
var angle = (i * 6 - 90) * Math.PI / 180d;
|
||||||
|
var outerRadius = DialCenter - 6.5;
|
||||||
|
var innerRadius = outerRadius - (isMajor ? 9 : 4.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;
|
||||||
|
|
||||||
|
entry.TickCanvas.Children.Add(new Line
|
||||||
|
{
|
||||||
|
StartPoint = new Point(x1, y1),
|
||||||
|
EndPoint = new Point(x2, y2),
|
||||||
|
Stroke = CreateBrush(isMajor ? majorColor : minorColor),
|
||||||
|
StrokeThickness = isMajor ? 1.9 : 0.8,
|
||||||
|
StrokeLineCap = PenLineCap.Round
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildDialNumbers(ClockEntryVisual entry, bool isNight)
|
||||||
|
{
|
||||||
|
entry.NumberCanvas.Children.Clear();
|
||||||
|
var numberColor = isNight ? "#F2F5FB" : "#1B202A";
|
||||||
|
var radius = 36;
|
||||||
|
for (var number = 1; number <= 12; number++)
|
||||||
|
{
|
||||||
|
var angle = (number * 30 - 90) * Math.PI / 180d;
|
||||||
|
var x = DialCenter + Math.Cos(angle) * radius;
|
||||||
|
var y = DialCenter + Math.Sin(angle) * radius;
|
||||||
|
var text = number.ToString(CultureInfo.InvariantCulture);
|
||||||
|
var isDoubleDigit = number >= 10;
|
||||||
|
var width = isDoubleDigit ? 14 : 10;
|
||||||
|
var height = 12;
|
||||||
|
var numberText = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
FontFamily = MiSansFontFamily,
|
||||||
|
FontSize = 9,
|
||||||
|
FontWeight = FontWeight.SemiBold,
|
||||||
|
Foreground = CreateBrush(numberColor),
|
||||||
|
TextAlignment = TextAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
Canvas.SetLeft(numberText, x - width / 2d);
|
||||||
|
Canvas.SetTop(numberText, y - height / 2d);
|
||||||
|
entry.NumberCanvas.Children.Add(numberText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFromSettings()
|
||||||
|
{
|
||||||
|
var snapshot = _settingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
|
||||||
|
var ids = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(snapshot.WorldClockTimeZoneIds);
|
||||||
|
for (var index = 0; index < WorldClockTimeZoneCatalog.ClockCount; index++)
|
||||||
|
{
|
||||||
|
var resolvedId = ids[index];
|
||||||
|
_entryTimeZones[index] = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(resolvedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_secondHandMode = ClockSecondHandMode.Normalize(snapshot.WorldClockSecondHandMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySecondHandTimerInterval()
|
||||||
|
{
|
||||||
|
_clockTimer.Interval = ClockSecondHandMode.IsSweep(_secondHandMode)
|
||||||
|
? TimeSpan.FromMilliseconds(16)
|
||||||
|
: TimeSpan.FromSeconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateClockVisuals()
|
||||||
|
{
|
||||||
|
var utcNow = DateTime.UtcNow;
|
||||||
|
ProbeLanguageCodeIfNeeded(utcNow);
|
||||||
|
|
||||||
|
var baseZone = _timeZoneService?.CurrentTimeZone ?? TimeZoneInfo.Local;
|
||||||
|
var baseNow = TimeZoneInfo.ConvertTimeFromUtc(utcNow, baseZone);
|
||||||
|
var baseOffset = baseZone.GetUtcOffset(utcNow);
|
||||||
|
|
||||||
|
for (var index = 0; index < WorldClockTimeZoneCatalog.ClockCount; index++)
|
||||||
|
{
|
||||||
|
var entry = _entryVisuals[index];
|
||||||
|
var zone = _entryTimeZones[index] ?? TimeZoneInfo.Local;
|
||||||
|
var zonedNow = TimeZoneInfo.ConvertTimeFromUtc(utcNow, zone);
|
||||||
|
var isNight = IsNightForLocalTime(zonedNow);
|
||||||
|
ApplyDialTheme(entry, isNight);
|
||||||
|
|
||||||
|
var secondValue = ClockSecondHandMode.IsSweep(_secondHandMode)
|
||||||
|
? zonedNow.Second + zonedNow.Millisecond / 1000d
|
||||||
|
: zonedNow.Second;
|
||||||
|
var minuteValue = zonedNow.Minute + secondValue / 60d;
|
||||||
|
var hourValue = (zonedNow.Hour % 12) + minuteValue / 60d;
|
||||||
|
|
||||||
|
var hourAngle = hourValue * 30d;
|
||||||
|
var minuteAngle = minuteValue * 6d;
|
||||||
|
var secondAngle = secondValue * 6d;
|
||||||
|
|
||||||
|
SetHandGeometry(entry.HourHand, hourAngle, forwardLength: 24, backwardLength: 4.8);
|
||||||
|
SetHandGeometry(entry.MinuteHand, minuteAngle, forwardLength: 33, backwardLength: 6);
|
||||||
|
SetHandGeometry(entry.SecondHand, secondAngle, forwardLength: 37, backwardLength: 8.5);
|
||||||
|
|
||||||
|
entry.CityTextBlock.Text = ResolveCityName(zone);
|
||||||
|
entry.DayTextBlock.Text = ResolveRelativeDayLabel((zonedNow.Date - baseNow.Date).Days);
|
||||||
|
|
||||||
|
var offsetDelta = zone.GetUtcOffset(utcNow) - baseOffset;
|
||||||
|
entry.OffsetTextBlock.Text = ResolveOffsetLabel(offsetDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyDialTheme(ClockEntryVisual entry, bool isNight)
|
||||||
|
{
|
||||||
|
if (entry.IsNightApplied.HasValue && entry.IsNightApplied.Value == isNight)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.IsNightApplied = isNight;
|
||||||
|
entry.DialBorder.Background = CreateBrush(isNight ? "#2D313A" : "#FAFBFD");
|
||||||
|
entry.DialBorder.BorderBrush = CreateBrush(isNight ? "#262A33" : "#DADFE8");
|
||||||
|
entry.HourHand.Stroke = CreateBrush(isNight ? "#F5F8FF" : "#2B3242");
|
||||||
|
entry.MinuteHand.Stroke = CreateBrush(isNight ? "#DDE4F0" : "#40495E");
|
||||||
|
entry.SecondHand.Stroke = CreateBrush("#1A74F2");
|
||||||
|
entry.CenterOuter.Fill = CreateBrush(isNight ? "#97B4EA" : "#4F7BC0");
|
||||||
|
|
||||||
|
BuildDialTicks(entry, isNight);
|
||||||
|
BuildDialNumbers(entry, isNight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProbeLanguageCodeIfNeeded(DateTime utcNow)
|
||||||
|
{
|
||||||
|
if (utcNow < _nextLanguageProbeUtc)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextLanguageProbeUtc = utcNow.AddSeconds(25);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_languageCode = "zh-CN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveCityName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
var cityNames = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ZhCityNames
|
||||||
|
: EnCityNames;
|
||||||
|
if (cityNames.TryGetValue(timeZone.Id, out var cityName))
|
||||||
|
{
|
||||||
|
return cityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = timeZone.Id;
|
||||||
|
var slashIndex = normalized.LastIndexOf('/');
|
||||||
|
if (slashIndex >= 0 && slashIndex < normalized.Length - 1)
|
||||||
|
{
|
||||||
|
normalized = normalized[(slashIndex + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized = normalized.Replace('_', ' ').Trim();
|
||||||
|
normalized = normalized
|
||||||
|
.Replace("Standard Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Daylight Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(normalized) ? timeZone.Id : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveRelativeDayLabel(int dayDelta)
|
||||||
|
{
|
||||||
|
if (dayDelta < 0)
|
||||||
|
{
|
||||||
|
return L("worldclock.widget.yesterday", "昨天");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayDelta > 0)
|
||||||
|
{
|
||||||
|
return L("worldclock.widget.tomorrow", "明天");
|
||||||
|
}
|
||||||
|
|
||||||
|
return L("worldclock.widget.today", "今天");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveOffsetLabel(TimeSpan delta)
|
||||||
|
{
|
||||||
|
var totalMinutes = (int)Math.Round(delta.TotalMinutes);
|
||||||
|
if (totalMinutes == 0)
|
||||||
|
{
|
||||||
|
return L("worldclock.widget.offset_same", "0 小时");
|
||||||
|
}
|
||||||
|
|
||||||
|
var absMinutes = Math.Abs(totalMinutes);
|
||||||
|
var hours = absMinutes / 60;
|
||||||
|
var minutes = absMinutes % 60;
|
||||||
|
var isAhead = totalMinutes > 0;
|
||||||
|
|
||||||
|
if (minutes == 0)
|
||||||
|
{
|
||||||
|
return isAhead
|
||||||
|
? Lf("worldclock.widget.offset_ahead_hours", "早 {0} 小时", hours)
|
||||||
|
: Lf("worldclock.widget.offset_behind_hours", "晚 {0} 小时", hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAhead
|
||||||
|
? Lf("worldclock.widget.offset_ahead_hm", "早 {0} 小时 {1} 分", hours, minutes)
|
||||||
|
: Lf("worldclock.widget.offset_behind_hm", "晚 {0} 小时 {1} 分", hours, minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Lf(string key, string fallback, params object[] args)
|
||||||
|
{
|
||||||
|
var template = L(key, fallback);
|
||||||
|
return string.Format(template, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double ResolveScale()
|
||||||
|
{
|
||||||
|
var cellScale = Math.Clamp(_currentCellSize / BaseCellSize, 0.56, 2.5);
|
||||||
|
var widthScale = Bounds.Width > 1
|
||||||
|
? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * BaseWidthCells), 0.52, 2.4)
|
||||||
|
: 1;
|
||||||
|
var heightScale = Bounds.Height > 1
|
||||||
|
? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * BaseHeightCells), 0.52, 2.4)
|
||||||
|
: 1;
|
||||||
|
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.50, 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNightForLocalTime(DateTime localTime)
|
||||||
|
{
|
||||||
|
var hour = localTime.Hour + localTime.Minute / 60d;
|
||||||
|
return hour < 6 || hour >= 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="560"
|
||||||
|
d:DesignHeight="380"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.WorldClockWidgetSettingsWindow">
|
||||||
|
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
|
||||||
|
Padding="16">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*"
|
||||||
|
RowSpacing="10">
|
||||||
|
<TextBlock x:Name="TitleTextBlock"
|
||||||
|
Text="世界时钟设置"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="分别为四个时钟选择时区。"
|
||||||
|
FontSize="12"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="2"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="10"
|
||||||
|
Margin="0,0,6,0">
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="SecondHandModeLabelTextBlock"
|
||||||
|
Text="秒针方式"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="12">
|
||||||
|
<RadioButton x:Name="SecondHandTickRadioButton"
|
||||||
|
GroupName="world_clock_second_mode"
|
||||||
|
Content="跳针"
|
||||||
|
Checked="OnSecondHandModeChanged" />
|
||||||
|
<RadioButton x:Name="SecondHandSweepRadioButton"
|
||||||
|
GroupName="world_clock_second_mode"
|
||||||
|
Content="扫针"
|
||||||
|
Checked="OnSecondHandModeChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="ClockOneLabelTextBlock"
|
||||||
|
Text="时钟 1"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<ComboBox x:Name="ClockOneTimeZoneComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinWidth="0"
|
||||||
|
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="ClockTwoLabelTextBlock"
|
||||||
|
Text="时钟 2"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<ComboBox x:Name="ClockTwoTimeZoneComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinWidth="0"
|
||||||
|
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="ClockThreeLabelTextBlock"
|
||||||
|
Text="时钟 3"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<ComboBox x:Name="ClockThreeTimeZoneComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinWidth="0"
|
||||||
|
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="12"
|
||||||
|
Padding="12">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock x:Name="ClockFourLabelTextBlock"
|
||||||
|
Text="时钟 4"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<ComboBox x:Name="ClockFourTimeZoneComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinWidth="0"
|
||||||
|
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class WorldClockWidgetSettingsWindow : UserControl
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["China Standard Time"] = "中国标准时间",
|
||||||
|
["Asia/Shanghai"] = "中国标准时间",
|
||||||
|
["GMT Standard Time"] = "格林威治标准时间",
|
||||||
|
["Europe/London"] = "格林威治标准时间",
|
||||||
|
["AUS Eastern Standard Time"] = "澳大利亚东部标准时间",
|
||||||
|
["Australia/Sydney"] = "澳大利亚东部标准时间",
|
||||||
|
["Eastern Standard Time"] = "美国东部标准时间",
|
||||||
|
["America/New_York"] = "美国东部标准时间",
|
||||||
|
["Tokyo Standard Time"] = "日本标准时间",
|
||||||
|
["Asia/Tokyo"] = "日本标准时间",
|
||||||
|
["UTC"] = "协调世界时",
|
||||||
|
["Etc/UTC"] = "协调世界时"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private readonly TimeZoneService _timeZoneService = new();
|
||||||
|
private readonly ComboBox[] _timeZoneComboBoxes;
|
||||||
|
private bool _suppressEvents;
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
private IReadOnlyList<TimeZoneInfo> _allTimeZones = Array.Empty<TimeZoneInfo>();
|
||||||
|
private IReadOnlyList<string> _selectedTimeZoneIds = Array.Empty<string>();
|
||||||
|
private string _secondHandMode = ClockSecondHandMode.Tick;
|
||||||
|
|
||||||
|
public event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
|
public WorldClockWidgetSettingsWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_timeZoneComboBoxes =
|
||||||
|
[
|
||||||
|
ClockOneTimeZoneComboBox,
|
||||||
|
ClockTwoTimeZoneComboBox,
|
||||||
|
ClockThreeTimeZoneComboBox,
|
||||||
|
ClockFourTimeZoneComboBox
|
||||||
|
];
|
||||||
|
|
||||||
|
LoadState();
|
||||||
|
ApplyLocalization();
|
||||||
|
PopulateTimeZoneComboBoxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
|
||||||
|
_allTimeZones = _timeZoneService
|
||||||
|
.GetAllTimeZones()
|
||||||
|
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||||
|
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_selectedTimeZoneIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
|
||||||
|
snapshot.WorldClockTimeZoneIds,
|
||||||
|
_allTimeZones);
|
||||||
|
_secondHandMode = ClockSecondHandMode.Normalize(snapshot.WorldClockSecondHandMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
TitleTextBlock.Text = L("worldclock.settings.title", "世界时钟设置");
|
||||||
|
DescriptionTextBlock.Text = L("worldclock.settings.desc", "分别为四个时钟选择时区。");
|
||||||
|
|
||||||
|
ClockOneLabelTextBlock.Text = L("worldclock.settings.clock_1", "时钟 1");
|
||||||
|
ClockTwoLabelTextBlock.Text = L("worldclock.settings.clock_2", "时钟 2");
|
||||||
|
ClockThreeLabelTextBlock.Text = L("worldclock.settings.clock_3", "时钟 3");
|
||||||
|
ClockFourLabelTextBlock.Text = L("worldclock.settings.clock_4", "时钟 4");
|
||||||
|
SecondHandModeLabelTextBlock.Text = L("worldclock.settings.second_mode_label", "秒针方式");
|
||||||
|
SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针");
|
||||||
|
SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateTimeZoneComboBoxes()
|
||||||
|
{
|
||||||
|
_suppressEvents = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var comboBox in _timeZoneComboBoxes)
|
||||||
|
{
|
||||||
|
comboBox.Items.Clear();
|
||||||
|
foreach (var timeZone in _allTimeZones)
|
||||||
|
{
|
||||||
|
comboBox.Items.Add(new ComboBoxItem
|
||||||
|
{
|
||||||
|
Tag = timeZone.Id,
|
||||||
|
Content = GetLocalizedTimeZoneDisplayName(timeZone)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < _timeZoneComboBoxes.Length; index++)
|
||||||
|
{
|
||||||
|
var comboBox = _timeZoneComboBoxes[index];
|
||||||
|
var targetId = index < _selectedTimeZoneIds.Count
|
||||||
|
? _selectedTimeZoneIds[index]
|
||||||
|
: TimeZoneInfo.Local.Id;
|
||||||
|
|
||||||
|
var selected = comboBox.Items
|
||||||
|
.OfType<ComboBoxItem>()
|
||||||
|
.FirstOrDefault(item => string.Equals(item.Tag as string, targetId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
comboBox.SelectedItem = selected ?? comboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode);
|
||||||
|
SecondHandTickRadioButton.IsChecked = string.Equals(
|
||||||
|
normalizedMode,
|
||||||
|
ClockSecondHandMode.Tick,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
SecondHandSweepRadioButton.IsChecked = string.Equals(
|
||||||
|
normalizedMode,
|
||||||
|
ClockSecondHandMode.Sweep,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveState()
|
||||||
|
{
|
||||||
|
var selectedIds = GetSelectedTimeZoneIds();
|
||||||
|
var normalizedIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(selectedIds, _allTimeZones);
|
||||||
|
_secondHandMode = GetSelectedSecondHandMode();
|
||||||
|
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
snapshot.WorldClockTimeZoneIds = normalizedIds.ToList();
|
||||||
|
snapshot.WorldClockSecondHandMode = _secondHandMode;
|
||||||
|
_appSettingsService.Save(snapshot);
|
||||||
|
|
||||||
|
_selectedTimeZoneIds = normalizedIds;
|
||||||
|
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSelectedSecondHandMode()
|
||||||
|
{
|
||||||
|
return SecondHandSweepRadioButton.IsChecked == true
|
||||||
|
? ClockSecondHandMode.Sweep
|
||||||
|
: ClockSecondHandMode.Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetSelectedTimeZoneIds()
|
||||||
|
{
|
||||||
|
var selectedIds = new List<string>(_timeZoneComboBoxes.Length);
|
||||||
|
foreach (var comboBox in _timeZoneComboBoxes)
|
||||||
|
{
|
||||||
|
if (comboBox.SelectedItem is ComboBoxItem item &&
|
||||||
|
item.Tag is string timeZoneId &&
|
||||||
|
!string.IsNullOrWhiteSpace(timeZoneId))
|
||||||
|
{
|
||||||
|
selectedIds.Add(timeZoneId.Trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds.Add(TimeZoneInfo.Local.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
|
||||||
|
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||||
|
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
|
||||||
|
var hours = totalMinutes / 60;
|
||||||
|
var minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ResolveZhDisplayName(timeZone)
|
||||||
|
: ResolveEnDisplayName(timeZone);
|
||||||
|
|
||||||
|
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveZhDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
|
||||||
|
{
|
||||||
|
return localizedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(timeZone.StandardName)
|
||||||
|
? timeZone.DisplayName
|
||||||
|
: timeZone.StandardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveEnDisplayName(TimeZoneInfo timeZone)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(timeZone.StandardName))
|
||||||
|
{
|
||||||
|
return timeZone.StandardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeZone.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -409,7 +409,7 @@ public partial class MainWindow
|
|||||||
{
|
{
|
||||||
OpenSettingsPage();
|
OpenSettingsPage();
|
||||||
}
|
}
|
||||||
}, UiMotionTokens.Slow);
|
}, FluttermotionToken.Slow);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeDesktopComponentDragHandlers()
|
private void InitializeDesktopComponentDragHandlers()
|
||||||
@@ -701,12 +701,24 @@ public partial class MainWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (placement.ComponentId == BuiltInComponentIds.DesktopClock)
|
||||||
|
{
|
||||||
|
OpenDesktopClockComponentSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule)
|
if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule)
|
||||||
{
|
{
|
||||||
OpenClassScheduleComponentSettings();
|
OpenClassScheduleComponentSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (placement.ComponentId == BuiltInComponentIds.DesktopWorldClock)
|
||||||
|
{
|
||||||
|
OpenWorldClockComponentSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
|
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
|
||||||
{
|
{
|
||||||
OpenDailyArtworkComponentSettings();
|
OpenDailyArtworkComponentSettings();
|
||||||
@@ -751,6 +763,38 @@ public partial class MainWindow
|
|||||||
ComponentSettingsWindow.Opacity = 1;
|
ComponentSettingsWindow.Opacity = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenDesktopClockComponentSettings()
|
||||||
|
{
|
||||||
|
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsContent = new AnalogClockWidgetSettingsWindow();
|
||||||
|
settingsContent.SettingsChanged += OnDesktopClockSettingsChanged;
|
||||||
|
ComponentSettingsContentHost.Content = settingsContent;
|
||||||
|
|
||||||
|
ComponentSettingsWindow.IsVisible = true;
|
||||||
|
ComponentSettingsWindow.Opacity = 0;
|
||||||
|
ComponentSettingsWindow.Opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenWorldClockComponentSettings()
|
||||||
|
{
|
||||||
|
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsContent = new WorldClockWidgetSettingsWindow();
|
||||||
|
settingsContent.SettingsChanged += OnWorldClockSettingsChanged;
|
||||||
|
ComponentSettingsContentHost.Content = settingsContent;
|
||||||
|
|
||||||
|
ComponentSettingsWindow.IsVisible = true;
|
||||||
|
ComponentSettingsWindow.Opacity = 0;
|
||||||
|
ComponentSettingsWindow.Opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenStudyEnvironmentComponentSettings()
|
private void OpenStudyEnvironmentComponentSettings()
|
||||||
{
|
{
|
||||||
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||||
@@ -796,6 +840,30 @@ public partial class MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDesktopClockSettingsChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
|
||||||
|
foreach (var pageGrid in _desktopPageComponentGrids.Values)
|
||||||
|
{
|
||||||
|
foreach (var host in pageGrid.Children.OfType<Border>())
|
||||||
|
{
|
||||||
|
if (!host.Classes.Contains(DesktopComponentHostClass))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetContentHost(host)?.Child is AnalogClockWidget widget)
|
||||||
|
{
|
||||||
|
widget.RefreshFromSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistSettings();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnStudyEnvironmentSettingsChanged(object? sender, EventArgs e)
|
private void OnStudyEnvironmentSettingsChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_ = sender;
|
_ = sender;
|
||||||
@@ -839,6 +907,30 @@ public partial class MainWindow
|
|||||||
PersistSettings();
|
PersistSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnWorldClockSettingsChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
|
||||||
|
foreach (var pageGrid in _desktopPageComponentGrids.Values)
|
||||||
|
{
|
||||||
|
foreach (var host in pageGrid.Children.OfType<Border>())
|
||||||
|
{
|
||||||
|
if (!host.Classes.Contains(DesktopComponentHostClass))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetContentHost(host)?.Child is WorldClockWidget widget)
|
||||||
|
{
|
||||||
|
widget.RefreshFromSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistSettings();
|
||||||
|
}
|
||||||
|
|
||||||
private void CloseComponentSettingsWindow()
|
private void CloseComponentSettingsWindow()
|
||||||
{
|
{
|
||||||
if (ComponentSettingsWindow is null)
|
if (ComponentSettingsWindow is null)
|
||||||
@@ -851,6 +943,11 @@ public partial class MainWindow
|
|||||||
classScheduleSettingsWindow.SettingsChanged -= OnClassScheduleSettingsChanged;
|
classScheduleSettingsWindow.SettingsChanged -= OnClassScheduleSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ComponentSettingsContentHost?.Content is AnalogClockWidgetSettingsWindow analogClockSettingsWindow)
|
||||||
|
{
|
||||||
|
analogClockSettingsWindow.SettingsChanged -= OnDesktopClockSettingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
if (ComponentSettingsContentHost?.Content is StudyEnvironmentWidgetSettingsWindow studyEnvironmentSettingsWindow)
|
if (ComponentSettingsContentHost?.Content is StudyEnvironmentWidgetSettingsWindow studyEnvironmentSettingsWindow)
|
||||||
{
|
{
|
||||||
studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged;
|
studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged;
|
||||||
@@ -861,6 +958,11 @@ public partial class MainWindow
|
|||||||
dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged;
|
dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ComponentSettingsContentHost?.Content is WorldClockWidgetSettingsWindow worldClockSettingsWindow)
|
||||||
|
{
|
||||||
|
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
ComponentSettingsWindow.Opacity = 0;
|
ComponentSettingsWindow.Opacity = 0;
|
||||||
|
|
||||||
DispatcherTimer.RunOnce(() =>
|
DispatcherTimer.RunOnce(() =>
|
||||||
@@ -873,7 +975,7 @@ public partial class MainWindow
|
|||||||
{
|
{
|
||||||
ComponentSettingsContentHost.Content = null;
|
ComponentSettingsContentHost.Content = null;
|
||||||
}
|
}
|
||||||
}, UiMotionTokens.Slow);
|
}, FluttermotionToken.Slow);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDesktopPage()
|
private void AddDesktopPage()
|
||||||
@@ -1272,6 +1374,14 @@ public partial class MainWindow
|
|||||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Keep world clock widget at 2:1 ratio: 4x2, 6x3, 8x4...
|
||||||
|
return SnapSpanToScaleRules(
|
||||||
|
span,
|
||||||
|
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyScoreOverview, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyScoreOverview, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Keep score overview widget square: 4x4, 5x5, 6x6...
|
// Keep score overview widget square: 4x4, 5x5, 6x6...
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Grid.Transitions>
|
<Grid.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.24" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Grid.Transitions>
|
</Grid.Transitions>
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<TranslateTransform>
|
<TranslateTransform>
|
||||||
<TranslateTransform.Transitions>
|
<TranslateTransform.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="X" Duration="0:0:0.24" />
|
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</TranslateTransform.Transitions>
|
</TranslateTransform.Transitions>
|
||||||
</TranslateTransform>
|
</TranslateTransform>
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Grid.Transitions>
|
<Grid.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.24" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Grid.Transitions>
|
</Grid.Transitions>
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@
|
|||||||
<TranslateTransform Y="30">
|
<TranslateTransform Y="30">
|
||||||
<TranslateTransform.Transitions>
|
<TranslateTransform.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Y" Duration="0:0:0.24" />
|
<DoubleTransition Property="Y" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</TranslateTransform.Transitions>
|
</TranslateTransform.Transitions>
|
||||||
</TranslateTransform>
|
</TranslateTransform>
|
||||||
@@ -1471,7 +1471,7 @@
|
|||||||
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
||||||
<Border.Transitions>
|
<Border.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Slow}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Border.Transitions>
|
</Border.Transitions>
|
||||||
|
|
||||||
@@ -1582,7 +1582,7 @@
|
|||||||
<TranslateTransform>
|
<TranslateTransform>
|
||||||
<TranslateTransform.Transitions>
|
<TranslateTransform.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="X" Duration="0:0:0.22" />
|
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</TranslateTransform.Transitions>
|
</TranslateTransform.Transitions>
|
||||||
</TranslateTransform>
|
</TranslateTransform>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public partial class MainWindow : Window
|
|||||||
private const int MinEdgeInsetPercent = 0;
|
private const int MinEdgeInsetPercent = 0;
|
||||||
private const int MaxEdgeInsetPercent = 30;
|
private const int MaxEdgeInsetPercent = 30;
|
||||||
private const int DefaultEdgeInsetPercent = 18;
|
private const int DefaultEdgeInsetPercent = 18;
|
||||||
private static readonly int SettingsTransitionDurationMs = (int)UiMotionTokens.Page.TotalMilliseconds;
|
private static readonly int SettingsTransitionDurationMs = (int)FluttermotionToken.Page.TotalMilliseconds;
|
||||||
private const double WallpaperPreviewMaxWidth = 520;
|
private const double WallpaperPreviewMaxWidth = 520;
|
||||||
private const double LightBackgroundLuminanceThreshold = 0.57;
|
private const double LightBackgroundLuminanceThreshold = 0.57;
|
||||||
private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";
|
private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";
|
||||||
|
|||||||
Reference in New Issue
Block a user