Compare commits

...

2 Commits

Author SHA1 Message Date
lincube
8768fa1ed2 0.4.1
修天气,时钟,每日图片....
2026-03-05 17:09:46 +08:00
lincube
24f1b896e1 0.4.0 2026-03-05 16:34:22 +08:00
33 changed files with 2057 additions and 77 deletions

View File

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

View File

@@ -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 += (_, _) =>
{ {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "诗词获取失败",

View File

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

View 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);
}
}

View 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;
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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)); }

View File

@@ -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++)
{ {

View File

@@ -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++)
{ {

View File

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

View File

@@ -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();

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

View 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));
}
}

View File

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

View File

@@ -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);
}
}

View File

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

View File

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

View File

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