diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6991b5a..3e6b45d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -89,13 +89,12 @@ jobs:
-o ./publish/windows-${{ matrix.arch }} `
--self-contained `
-r win-${{ matrix.arch }} `
- -p:PublishSingleFile=true `
+ -p:PublishSingleFile=false `
-p:SelfContained=true `
-p:DebugType=none `
-p:DebugSymbols=false `
- -p:PublishTrimmed=true `
- -p:TrimMode=partial `
- -p:PublishReadyToRun=true
+ -p:PublishTrimmed=false `
+ -p:PublishReadyToRun=false
shell: pwsh
- name: Install Inno Setup
@@ -221,13 +220,12 @@ jobs:
-o ./publish/linux-x64 \
--self-contained \
-r linux-x64 \
- -p:PublishSingleFile=true \
+ -p:PublishSingleFile=false \
-p:SelfContained=true \
-p:DebugType=none \
-p:DebugSymbols=false \
- -p:PublishTrimmed=true \
- -p:TrimMode=partial \
- -p:PublishReadyToRun=true
+ -p:PublishTrimmed=false \
+ -p:PublishReadyToRun=false
- name: Package as DEB
run: |
@@ -331,13 +329,12 @@ EOF
-o ./publish/macos-${{ matrix.arch }} \
--self-contained \
-r osx-${{ matrix.arch }} \
- -p:PublishSingleFile=true \
+ -p:PublishSingleFile=false \
-p:SelfContained=true \
-p:DebugType=none \
-p:DebugSymbols=false \
- -p:PublishTrimmed=true \
- -p:TrimMode=partial \
- -p:PublishReadyToRun=true
+ -p:PublishTrimmed=false \
+ -p:PublishReadyToRun=false
- name: Package as DMG
run: |
diff --git a/.gitignore b/.gitignore
index d8a9d9a..7814091 100644
--- a/.gitignore
+++ b/.gitignore
@@ -481,3 +481,4 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
nul
+/publish-test
diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index b8a5e70..50ce6a6 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -2,8 +2,10 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
+using System;
using System.Linq;
using Avalonia.Markup.Xaml;
+using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
using AvaloniaWebView;
@@ -14,6 +16,7 @@ public partial class App : Application
{
public override void Initialize()
{
+ ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
}
@@ -46,4 +49,31 @@ public partial class App : Application
BindingPlugins.DataValidators.Remove(plugin);
}
}
+
+ private static void ConfigureWebViewUserDataFolder()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ const string userDataFolderEnvVar = "WEBVIEW2_USER_DATA_FOLDER";
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(userDataFolderEnvVar)))
+ {
+ return;
+ }
+
+ var userDataFolder = WebView2RuntimeProbe.ResolveUserDataFolder();
+ Environment.SetEnvironmentVariable(
+ userDataFolderEnvVar,
+ userDataFolder,
+ EnvironmentVariableTarget.Process);
+ }
+ catch
+ {
+ // Keep startup resilient if user profile folders are unavailable.
+ }
+ }
}
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/ATTRIBUTION.md b/LanMountainDesktop/Assets/Weather/HyperOS3/ATTRIBUTION.md
index c3c7ace..001170d 100644
--- a/LanMountainDesktop/Assets/Weather/HyperOS3/ATTRIBUTION.md
+++ b/LanMountainDesktop/Assets/Weather/HyperOS3/ATTRIBUTION.md
@@ -36,3 +36,20 @@ Extracted weather icon paths inside APK (`res/*.webp`):
- `res/Mg.webp` -> `Icons/icon_windy.webp`
Use only according to Xiaomi's applicable license and usage terms.
+
+## Soft Widget Icon Set (2026-03-05)
+
+To better match the Xiaomi weather time-card visual hierarchy, an additional local icon set was generated for this project:
+
+- `Icons/icon_hero_sun_soft.png`
+- `Icons/icon_hero_moon_soft.png`
+- `Icons/icon_mini_partly_cloudy_day_soft.png`
+- `Icons/icon_mini_partly_cloudy_night_soft.png`
+- `Icons/icon_mini_cloudy_soft.png`
+- `Icons/icon_mini_rain_light_soft.png`
+- `Icons/icon_mini_rain_heavy_soft.png`
+- `Icons/icon_mini_storm_soft.png`
+- `Icons/icon_mini_snow_soft.png`
+- `Icons/icon_mini_fog_soft.png`
+
+These files are original derivative assets generated in-repo with local tooling, using the extracted Xiaomi package visual direction as reference (soft glow hero icon + lightweight forecast icons).
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png
new file mode 100644
index 0000000..a17b056
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png
new file mode 100644
index 0000000..0b95e2d
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png
new file mode 100644
index 0000000..9d752f9
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png
new file mode 100644
index 0000000..764884d
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png
new file mode 100644
index 0000000..3ee7efe
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png
new file mode 100644
index 0000000..3ba9cbe
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png
new file mode 100644
index 0000000..2a9a4d0
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png
new file mode 100644
index 0000000..08d5e68
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png
new file mode 100644
index 0000000..e8ae667
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png differ
diff --git a/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png
new file mode 100644
index 0000000..3711dc9
Binary files /dev/null and b/LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png differ
diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj
index 1d37bb0..15b7826 100644
--- a/LanMountainDesktop/LanMountainDesktop.csproj
+++ b/LanMountainDesktop/LanMountainDesktop.csproj
@@ -6,30 +6,17 @@
1.0.0
app.manifest
true
-
-
- true
- true
- partial
- true
- false
true
-
+
- true
- true
- partial
- true
+ false
+ false
+ false
false
none
-
-
-
- true
-
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index 66f7d19..075aec3 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -184,6 +184,9 @@
"settings.about.version_format": "Version: {0}",
"settings.about.codename_format": "Code Name: {0}",
"settings.about.font_format": "Font: {0}",
+ "settings.about.startup_header": "Windows Startup",
+ "settings.about.startup_desc": "Launch the app automatically when signing in to Windows.",
+ "settings.about.startup_toggle": "Launch at Windows sign-in",
"settings.footer": "LanMountainDesktop Settings",
"filepicker.title": "Select wallpaper",
"filepicker.image_files": "Image files",
@@ -255,6 +258,13 @@
"artwork.widget.fallback_artist": "Recommendation service unavailable",
"artwork.widget.fallback_year": "Try again later",
"artwork.widget.unknown_artist": "Unknown artist",
+ "artwork.settings.title": "Daily Artwork Settings",
+ "artwork.settings.desc": "Switch the data source used by Daily Artwork.",
+ "artwork.settings.source_label": "Mirror Source",
+ "artwork.settings.source_domestic": "Domestic Mirror",
+ "artwork.settings.source_overseas": "Overseas Mirror",
+ "artwork.settings.source_status_domestic": "Current source: Domestic mirror (optimized for China network)",
+ "artwork.settings.source_status_overseas": "Current source: Overseas mirror (art museum recommendations)",
"music.widget.unsupported": "Music control is not supported on this platform",
"music.widget.unsupported_hint": "This widget requires Windows SMTC",
"music.widget.no_session": "No music source",
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index 75ad4a4..2ffcb4f 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -184,6 +184,9 @@
"settings.about.version_format": "版本号: {0}",
"settings.about.codename_format": "版本代号: {0}",
"settings.about.font_format": "字体: {0}",
+ "settings.about.startup_header": "Windows 自启动",
+ "settings.about.startup_desc": "在登录 Windows 时自动启动应用。",
+ "settings.about.startup_toggle": "登录 Windows 时启动",
"settings.footer": "LanMountainDesktop 设置",
"filepicker.title": "选择壁纸",
"filepicker.image_files": "图片文件",
@@ -255,6 +258,13 @@
"artwork.widget.fallback_artist": "推荐服务不可用",
"artwork.widget.fallback_year": "稍后重试",
"artwork.widget.unknown_artist": "未知作者",
+ "artwork.settings.title": "每日图片设置",
+ "artwork.settings.desc": "切换每日图片的数据源。",
+ "artwork.settings.source_label": "镜像源",
+ "artwork.settings.source_domestic": "国内镜像",
+ "artwork.settings.source_overseas": "国外镜像",
+ "artwork.settings.source_status_domestic": "当前源:国内镜像(优先中国网络)",
+ "artwork.settings.source_status_overseas": "当前源:国外镜像(艺术馆推荐)",
"music.widget.unsupported": "当前平台不支持音乐控制",
"music.widget.unsupported_hint": "该组件仅支持 Windows SMTC",
"music.widget.no_session": "暂无音源",
diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
index fd5a35c..d35b9b5 100644
--- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
@@ -44,6 +44,10 @@ public sealed class AppSettingsSnapshot
public bool WeatherNoTlsRequests { get; set; }
+ public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
+
+ public bool AutoStartWithWindows { get; set; }
+
public List TopStatusComponentIds { get; set; } = [];
public List PinnedTaskbarActions { get; set; } =
diff --git a/LanMountainDesktop/Models/DailyArtworkMirrorSources.cs b/LanMountainDesktop/Models/DailyArtworkMirrorSources.cs
new file mode 100644
index 0000000..102538f
--- /dev/null
+++ b/LanMountainDesktop/Models/DailyArtworkMirrorSources.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace LanMountainDesktop.Models;
+
+public static class DailyArtworkMirrorSources
+{
+ public const string Domestic = "Domestic";
+ public const string Overseas = "Overseas";
+
+ public static string Normalize(string? value)
+ {
+ return string.Equals(value, Domestic, StringComparison.OrdinalIgnoreCase)
+ ? Domestic
+ : Overseas;
+ }
+}
diff --git a/LanMountainDesktop/Models/RecommendationDataModels.cs b/LanMountainDesktop/Models/RecommendationDataModels.cs
index 0dfd2ae..40b6a7c 100644
--- a/LanMountainDesktop/Models/RecommendationDataModels.cs
+++ b/LanMountainDesktop/Models/RecommendationDataModels.cs
@@ -10,6 +10,7 @@ public sealed record DailyArtworkSnapshot(
string? Museum,
string? ArtworkUrl,
string? ImageUrl,
+ string? ThumbnailDataUrl,
DateTimeOffset FetchedAt);
public sealed record DailyPoetrySnapshot(
diff --git a/LanMountainDesktop/Services/IRecommendationDataService.cs b/LanMountainDesktop/Services/IRecommendationDataService.cs
index 148d2e9..04ad90f 100644
--- a/LanMountainDesktop/Services/IRecommendationDataService.cs
+++ b/LanMountainDesktop/Services/IRecommendationDataService.cs
@@ -7,6 +7,7 @@ namespace LanMountainDesktop.Services;
public sealed record DailyArtworkQuery(
string? Locale = null,
+ string? MirrorSource = null,
bool ForceRefresh = false);
public sealed record DailyPoetryQuery(
@@ -35,11 +36,16 @@ public sealed record RecommendationApiOptions
public string JinriShiciPoetryUrl { get; init; } = "https://v1.jinrishici.com/all.json";
public string ArtInstituteArtworkApiTemplate { get; init; } =
- "https://api.artic.edu/api/v1/artworks?page={0}&limit={1}&fields=id,title,artist_title,artist_display,date_display,image_id,api_link";
+ "https://api.artic.edu/api/v1/artworks?page={0}&limit={1}&fields=id,title,artist_title,artist_display,date_display,image_id,api_link,thumbnail";
public string ArtInstituteImageUrlTemplate { get; init; } =
"https://www.artic.edu/iiif/2/{0}/full/843,/0/default.jpg";
+ public string DomesticArtworkApiUrl { get; init; } =
+ "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=8&mkt=zh-CN";
+
+ public string DomesticArtworkHost { get; init; } = "https://cn.bing.com";
+
public TimeSpan CacheDuration { get; init; } = TimeSpan.FromMinutes(20);
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(8);
diff --git a/LanMountainDesktop/Services/RecommendationDataService.cs b/LanMountainDesktop/Services/RecommendationDataService.cs
index 14022c6..dd21425 100644
--- a/LanMountainDesktop/Services/RecommendationDataService.cs
+++ b/LanMountainDesktop/Services/RecommendationDataService.cs
@@ -12,6 +12,8 @@ namespace LanMountainDesktop.Services;
public sealed class RecommendationDataService : IRecommendationInfoService, IDisposable
{
+ private const string UserAgent = "Mozilla/5.0";
+
private sealed record DailyArtworkCacheEntry(DailyArtworkSnapshot Snapshot, DateTimeOffset ExpireAt);
private sealed record DailyPoetryCacheEntry(DailyPoetrySnapshot Snapshot, DateTimeOffset ExpireAt);
private sealed record ArtworkCandidate(
@@ -19,13 +21,16 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
string? Artist,
string? Year,
string? ArtworkUrl,
- string? ImageId);
+ string? ImageId,
+ string? ThumbnailDataUrl);
private readonly RecommendationApiOptions _options;
private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient;
+ private readonly AppSettingsService _appSettingsService = new();
private readonly object _cacheGate = new();
- private DailyArtworkCacheEntry? _dailyArtworkCache;
+ private readonly Dictionary _dailyArtworkCacheBySource =
+ new(StringComparer.OrdinalIgnoreCase);
private DailyPoetryCacheEntry? _dailyPoetryCache;
public RecommendationDataService(
@@ -60,7 +65,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
{
lock (_cacheGate)
{
- _dailyArtworkCache = null;
+ _dailyArtworkCacheBySource.Clear();
_dailyPoetryCache = null;
}
}
@@ -79,7 +84,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, _options.JinriShiciPoetryUrl);
- request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
+ request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
using var response = await _httpClient.SendAsync(request, cancellationToken);
responseText = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
@@ -132,45 +137,25 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
CancellationToken cancellationToken = default)
{
var normalizedQuery = query ?? new DailyArtworkQuery();
- if (!normalizedQuery.ForceRefresh && TryGetDailyArtworkFromCache(out var cached))
+ var mirrorSource = ResolveArtworkMirrorSource(normalizedQuery);
+ if (!normalizedQuery.ForceRefresh && TryGetDailyArtworkFromCache(mirrorSource, out var cached))
{
return RecommendationQueryResult.Ok(cached);
}
- var candidateCount = Math.Clamp(_options.DefaultArtworkCandidateCount, 10, 100);
+ return string.Equals(mirrorSource, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
+ ? await GetDailyArtworkFromDomesticSourceAsync(mirrorSource, cancellationToken)
+ : await GetDailyArtworkFromOverseasSourceAsync(mirrorSource, cancellationToken);
+ }
+
+ private async Task> GetDailyArtworkFromOverseasSourceAsync(
+ string mirrorSource,
+ CancellationToken cancellationToken)
+ {
var localDate = GetChinaLocalDate();
- var page = Math.Clamp((localDate.DayOfYear % 100) + 1, 1, 100);
- var requestUrl = string.Format(
- CultureInfo.InvariantCulture,
- _options.ArtInstituteArtworkApiTemplate,
- page,
- candidateCount);
-
- string responseText;
- try
- {
- using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
- request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
- using var response = await _httpClient.SendAsync(request, cancellationToken);
- responseText = await response.Content.ReadAsStringAsync(cancellationToken);
- if (!response.IsSuccessStatusCode)
- {
- return RecommendationQueryResult.Fail(
- "upstream_http_error",
- $"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
- }
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
- {
- return RecommendationQueryResult.Fail("upstream_network_error", ex.Message);
- }
-
try
{
+ var responseText = await FetchOverseasArtworkPayloadAsync(localDate, cancellationToken);
using var document = JsonDocument.Parse(responseText);
var root = document.RootElement;
if (!root.TryGetProperty("data", out var dataArray) || dataArray.ValueKind != JsonValueKind.Array)
@@ -183,7 +168,9 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
{
var title = ReadString(item, "title");
var imageId = ReadString(item, "image_id");
- if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(imageId))
+ var thumbnailDataUrl = ReadString(item, "thumbnail", "lqip");
+ if (string.IsNullOrWhiteSpace(title) ||
+ (string.IsNullOrWhiteSpace(imageId) && string.IsNullOrWhiteSpace(thumbnailDataUrl)))
{
continue;
}
@@ -199,7 +186,8 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
artist,
ReadString(item, "date_display"),
ReadString(item, "api_link"),
- imageId.Trim()));
+ string.IsNullOrWhiteSpace(imageId) ? null : imageId.Trim(),
+ string.IsNullOrWhiteSpace(thumbnailDataUrl) ? null : thumbnailDataUrl.Trim()));
}
if (candidates.Count == 0)
@@ -217,24 +205,121 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
Museum: "The Art Institute of Chicago",
ArtworkUrl: selected.ArtworkUrl,
ImageUrl: BuildArtworkImageUrl(selected.ImageId),
+ ThumbnailDataUrl: selected.ThumbnailDataUrl,
FetchedAt: DateTimeOffset.UtcNow);
- SetDailyArtworkCache(snapshot);
+ SetDailyArtworkCache(mirrorSource, snapshot);
return RecommendationQueryResult.Ok(snapshot);
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (HttpRequestException ex)
+ {
+ return RecommendationQueryResult.Fail("upstream_network_error", ex.Message);
+ }
catch (Exception ex)
{
return RecommendationQueryResult.Fail("upstream_parse_error", ex.Message);
}
}
- private bool TryGetDailyArtworkFromCache(out DailyArtworkSnapshot snapshot)
+ private async Task> GetDailyArtworkFromDomesticSourceAsync(
+ string mirrorSource,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, _options.DomesticArtworkApiUrl);
+ request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
+ using var response = await _httpClient.SendAsync(request, cancellationToken);
+ var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ return RecommendationQueryResult.Fail(
+ "upstream_http_error",
+ $"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
+ }
+
+ using var document = JsonDocument.Parse(responseText);
+ var root = document.RootElement;
+ if (!root.TryGetProperty("images", out var images) || images.ValueKind != JsonValueKind.Array)
+ {
+ return RecommendationQueryResult.Fail("upstream_parse_error", "Daily image list is missing.");
+ }
+
+ var candidates = images.EnumerateArray().ToArray();
+ if (candidates.Length == 0)
+ {
+ return RecommendationQueryResult.Fail("upstream_empty_result", "No daily image candidates were returned.");
+ }
+
+ var localDate = GetChinaLocalDate();
+ var indexSeed = localDate.Year * 1000 + localDate.DayOfYear;
+ var selected = candidates[Math.Abs(indexSeed) % candidates.Length];
+
+ var imageUrl = BuildDomesticImageUrl(
+ ReadString(selected, "url"),
+ _options.DomesticArtworkHost);
+ if (string.IsNullOrWhiteSpace(imageUrl))
+ {
+ return RecommendationQueryResult.Fail("upstream_parse_error", "Daily image URL is missing.");
+ }
+
+ var title = ReadString(selected, "title");
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ title = ExtractDomesticTitle(ReadString(selected, "copyright"));
+ }
+
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ title = "Bing Daily Image";
+ }
+
+ var dateText = ParseDomesticDateText(ReadString(selected, "startdate"));
+ var artworkUrl = BuildDomesticImageUrl(
+ ReadString(selected, "copyrightlink"),
+ _options.DomesticArtworkHost);
+ if (string.IsNullOrWhiteSpace(artworkUrl) ||
+ artworkUrl.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
+ {
+ artworkUrl = null;
+ }
+
+ var snapshot = new DailyArtworkSnapshot(
+ Provider: "BingCN",
+ Title: title.Trim(),
+ Artist: "Bing China",
+ Year: dateText,
+ Museum: "Bing China",
+ ArtworkUrl: artworkUrl,
+ ImageUrl: imageUrl,
+ ThumbnailDataUrl: null,
+ FetchedAt: DateTimeOffset.UtcNow);
+
+ SetDailyArtworkCache(mirrorSource, snapshot);
+ return RecommendationQueryResult.Ok(snapshot);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ return RecommendationQueryResult.Fail("upstream_network_error", ex.Message);
+ }
+ }
+
+ private bool TryGetDailyArtworkFromCache(string mirrorSource, out DailyArtworkSnapshot snapshot)
{
lock (_cacheGate)
{
- if (_dailyArtworkCache is not null && _dailyArtworkCache.ExpireAt > DateTimeOffset.UtcNow)
+ if (_dailyArtworkCacheBySource.TryGetValue(mirrorSource, out var cacheEntry) &&
+ cacheEntry.ExpireAt > DateTimeOffset.UtcNow)
{
- snapshot = _dailyArtworkCache.Snapshot;
+ snapshot = cacheEntry.Snapshot;
return true;
}
}
@@ -243,11 +328,11 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
return false;
}
- private void SetDailyArtworkCache(DailyArtworkSnapshot snapshot)
+ private void SetDailyArtworkCache(string mirrorSource, DailyArtworkSnapshot snapshot)
{
lock (_cacheGate)
{
- _dailyArtworkCache = new DailyArtworkCacheEntry(
+ _dailyArtworkCacheBySource[mirrorSource] = new DailyArtworkCacheEntry(
snapshot,
DateTimeOffset.UtcNow.Add(_options.CacheDuration));
}
@@ -325,6 +410,105 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
imageId.Trim());
}
+ private string ResolveArtworkMirrorSource(DailyArtworkQuery query)
+ {
+ if (!string.IsNullOrWhiteSpace(query.MirrorSource))
+ {
+ return DailyArtworkMirrorSources.Normalize(query.MirrorSource);
+ }
+
+ try
+ {
+ var snapshot = _appSettingsService.Load();
+ return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
+ }
+ catch
+ {
+ return DailyArtworkMirrorSources.Overseas;
+ }
+ }
+
+ private async Task FetchOverseasArtworkPayloadAsync(DateOnly localDate, CancellationToken cancellationToken)
+ {
+ var candidateCount = Math.Clamp(_options.DefaultArtworkCandidateCount, 10, 100);
+ var page = Math.Clamp((localDate.DayOfYear % 100) + 1, 1, 100);
+ var requestUrl = string.Format(
+ CultureInfo.InvariantCulture,
+ _options.ArtInstituteArtworkApiTemplate,
+ page,
+ candidateCount);
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+ request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
+ using var response = await _httpClient.SendAsync(request, cancellationToken);
+ var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
+ }
+
+ return responseText;
+ }
+
+ private static string? BuildDomesticImageUrl(string? rawValue, string fallbackHost)
+ {
+ if (string.IsNullOrWhiteSpace(rawValue))
+ {
+ return null;
+ }
+
+ var candidate = rawValue.Trim();
+ if (Uri.TryCreate(candidate, UriKind.Absolute, out var absoluteUri))
+ {
+ return absoluteUri.ToString();
+ }
+
+ if (!Uri.TryCreate(fallbackHost, UriKind.Absolute, out var hostUri))
+ {
+ return null;
+ }
+
+ var normalizedPath = candidate.StartsWith("/", StringComparison.Ordinal) ? candidate : $"/{candidate}";
+ return new Uri(hostUri, normalizedPath).ToString();
+ }
+
+ private static string ExtractDomesticTitle(string? copyrightText)
+ {
+ if (string.IsNullOrWhiteSpace(copyrightText))
+ {
+ return string.Empty;
+ }
+
+ var compact = copyrightText.Trim();
+ var bracketIndex = compact.IndexOf('(');
+ if (bracketIndex <= 0)
+ {
+ return compact;
+ }
+
+ return compact[..bracketIndex].Trim();
+ }
+
+ private static string? ParseDomesticDateText(string? rawDate)
+ {
+ if (string.IsNullOrWhiteSpace(rawDate) || rawDate.Length < 8)
+ {
+ return null;
+ }
+
+ if (DateTime.TryParseExact(
+ rawDate[..8],
+ "yyyyMMdd",
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out var date))
+ {
+ return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ }
+
+ return null;
+ }
+
private static string? ReadFirstNonEmptyLine(string? text)
{
if (string.IsNullOrWhiteSpace(text))
diff --git a/LanMountainDesktop/Services/WebView2RuntimeProbe.cs b/LanMountainDesktop/Services/WebView2RuntimeProbe.cs
new file mode 100644
index 0000000..3abe548
--- /dev/null
+++ b/LanMountainDesktop/Services/WebView2RuntimeProbe.cs
@@ -0,0 +1,126 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Versioning;
+using Microsoft.Win32;
+
+namespace LanMountainDesktop.Services;
+
+public sealed record WebView2RuntimeAvailability(
+ bool IsAvailable,
+ string? Version,
+ string Message);
+
+public static class WebView2RuntimeProbe
+{
+ private const string WebView2RuntimeClientId = "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
+ private const string WebView2RuntimeKeyPath = @"SOFTWARE\Microsoft\EdgeUpdate\Clients\" + WebView2RuntimeClientId;
+ public const string RuntimeDownloadUrl = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
+
+ public static WebView2RuntimeAvailability GetAvailability()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return new WebView2RuntimeAvailability(
+ IsAvailable: true,
+ Version: null,
+ Message: string.Empty);
+ }
+
+ try
+ {
+ var version = TryGetVersionFromWebView2Api();
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ version = TryGetVersionFromRegistry();
+ }
+
+ if (!string.IsNullOrWhiteSpace(version))
+ {
+ return new WebView2RuntimeAvailability(
+ IsAvailable: true,
+ Version: version.Trim(),
+ Message: string.Empty);
+ }
+
+ return new WebView2RuntimeAvailability(
+ IsAvailable: false,
+ Version: null,
+ Message: $"WebView2 Runtime is missing. Install it from {RuntimeDownloadUrl} and restart the app.");
+ }
+ catch (Exception ex)
+ {
+ return new WebView2RuntimeAvailability(
+ IsAvailable: false,
+ Version: null,
+ Message: $"WebView2 runtime check failed: {ex.Message}");
+ }
+ }
+
+ public static string ResolveUserDataFolder()
+ {
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (string.IsNullOrWhiteSpace(localAppData))
+ {
+ localAppData = AppContext.BaseDirectory;
+ }
+
+ var userDataFolder = Path.Combine(localAppData, "LanMountainDesktop", "WebView2");
+ Directory.CreateDirectory(userDataFolder);
+ return userDataFolder;
+ }
+
+ private static string? TryGetVersionFromWebView2Api()
+ {
+ var type = Type.GetType(
+ "Microsoft.Web.WebView2.Core.CoreWebView2Environment, Microsoft.Web.WebView2.Core",
+ throwOnError: false);
+ if (type is null)
+ {
+ return null;
+ }
+
+ var method = type.GetMethod(
+ "GetAvailableBrowserVersionString",
+ BindingFlags.Public | BindingFlags.Static,
+ binder: null,
+ types: Type.EmptyTypes,
+ modifiers: null);
+ if (method is null)
+ {
+ return null;
+ }
+
+ return method.Invoke(null, null) as string;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static string? TryGetVersionFromRegistry()
+ {
+ return TryReadVersionFromRegistry(RegistryHive.LocalMachine, RegistryView.Registry64)
+ ?? TryReadVersionFromRegistry(RegistryHive.LocalMachine, RegistryView.Registry32)
+ ?? TryReadVersionFromRegistry(RegistryHive.CurrentUser, RegistryView.Registry64)
+ ?? TryReadVersionFromRegistry(RegistryHive.CurrentUser, RegistryView.Registry32);
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static string? TryReadVersionFromRegistry(RegistryHive hive, RegistryView view)
+ {
+ try
+ {
+ using var baseKey = RegistryKey.OpenBaseKey(hive, view);
+ using var runtimeKey = baseKey.OpenSubKey(WebView2RuntimeKeyPath, writable: false);
+ if (runtimeKey is null)
+ {
+ return null;
+ }
+
+ var value = runtimeKey.GetValue("pv") as string;
+ return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/LanMountainDesktop/Services/WindowsStartupService.cs b/LanMountainDesktop/Services/WindowsStartupService.cs
new file mode 100644
index 0000000..58813c8
--- /dev/null
+++ b/LanMountainDesktop/Services/WindowsStartupService.cs
@@ -0,0 +1,75 @@
+using System;
+using Microsoft.Win32;
+
+namespace LanMountainDesktop.Services;
+
+public sealed class WindowsStartupService
+{
+ private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
+ private const string ValueName = "LanMountainDesktop";
+ private readonly string _startupCommand;
+
+ public WindowsStartupService()
+ {
+ var processPath = Environment.ProcessPath;
+ _startupCommand = string.IsNullOrWhiteSpace(processPath)
+ ? string.Empty
+ : $"\"{processPath}\"";
+ }
+
+ public bool IsEnabled()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return false;
+ }
+
+ try
+ {
+ using var runKey = Registry.CurrentUser.OpenSubKey(RunKeyPath, writable: false);
+ return runKey?.GetValue(ValueName) is string value &&
+ !string.IsNullOrWhiteSpace(value);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public bool SetEnabled(bool enabled)
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return false;
+ }
+
+ if (enabled && string.IsNullOrWhiteSpace(_startupCommand))
+ {
+ return false;
+ }
+
+ try
+ {
+ using var runKey = Registry.CurrentUser.CreateSubKey(RunKeyPath);
+ if (runKey is null)
+ {
+ return false;
+ }
+
+ if (enabled)
+ {
+ runKey.SetValue(ValueName, _startupCommand, RegistryValueKind.String);
+ }
+ else
+ {
+ runKey.DeleteValue(ValueName, throwOnMissingValue: false);
+ }
+
+ return IsEnabled() == enabled;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml b/LanMountainDesktop/Views/Components/BrowserWidget.axaml
index e27d47e..86e998d 100644
--- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml
+++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml
@@ -23,7 +23,23 @@
Background="#FFFFFFFF"
BorderBrush="#22000000"
BorderThickness="1">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs
new file mode 100644
index 0000000..695f5f8
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs
@@ -0,0 +1,97 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services;
+
+namespace LanMountainDesktop.Views.Components;
+
+public partial class DailyArtworkSettingsWindow : UserControl
+{
+ private readonly AppSettingsService _appSettingsService = new();
+ private readonly LocalizationService _localizationService = new();
+ private string _languageCode = "zh-CN";
+ private bool _suppressEvents;
+
+ public event EventHandler? SettingsChanged;
+
+ public string CurrentSource => GetSelectedSource();
+
+ public DailyArtworkSettingsWindow()
+ {
+ InitializeComponent();
+ LoadState();
+ ApplyLocalization();
+ }
+
+ private void LoadState()
+ {
+ var snapshot = _appSettingsService.Load();
+ _languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
+
+ var source = DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
+ _suppressEvents = true;
+ MirrorSourceComboBox.SelectedIndex = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
+ ? 0
+ : 1;
+ _suppressEvents = false;
+ UpdateSourceStatus(source);
+ }
+
+ private void ApplyLocalization()
+ {
+ TitleTextBlock.Text = L("artwork.settings.title", "每日图片设置");
+ DescriptionTextBlock.Text = L("artwork.settings.desc", "切换每日图片的数据源。");
+ MirrorSourceLabelTextBlock.Text = L("artwork.settings.source_label", "镜像源");
+ MirrorSourceDomesticItem.Content = L("artwork.settings.source_domestic", "国内镜像");
+ MirrorSourceOverseasItem.Content = L("artwork.settings.source_overseas", "国外镜像");
+ UpdateSourceStatus(GetSelectedSource());
+ }
+
+ private void OnMirrorSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ var source = GetSelectedSource();
+ var snapshot = _appSettingsService.Load();
+ snapshot.DailyArtworkMirrorSource = source;
+ _appSettingsService.Save(snapshot);
+
+ UpdateSourceStatus(source);
+ SettingsChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private string GetSelectedSource()
+ {
+ if (MirrorSourceComboBox.SelectedItem is ComboBoxItem comboBoxItem &&
+ comboBoxItem.Tag is string tagValue)
+ {
+ return DailyArtworkMirrorSources.Normalize(tagValue);
+ }
+
+ return DailyArtworkMirrorSources.Overseas;
+ }
+
+ private void UpdateSourceStatus(string source)
+ {
+ if (StatusTextBlock is null)
+ {
+ return;
+ }
+
+ StatusTextBlock.Text = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
+ ? L("artwork.settings.source_status_domestic", "当前源:国内镜像(优先中国网络)")
+ : L("artwork.settings.source_status_overseas", "当前源:国外镜像(艺术馆推荐)");
+ }
+
+ private string L(string key, string fallback)
+ {
+ return _localizationService.GetString(_languageCode, key, fallback);
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
index b769003..3afddee 100644
--- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
@@ -122,6 +122,15 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
}
}
+ public void RefreshFromSettings()
+ {
+ _recommendationService.ClearCache();
+ if (_isAttached)
+ {
+ _ = RefreshArtworkAsync(forceRefresh: true);
+ }
+ }
+
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
@@ -219,7 +228,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout();
- var bitmap = await TryLoadArtworkBitmapAsync(snapshot.ImageUrl, cancellationToken);
+ var bitmap = await TryLoadArtworkBitmapAsync(snapshot.ImageUrl, snapshot.ThumbnailDataUrl, cancellationToken);
if (cancellationToken.IsCancellationRequested || !_isAttached)
{
bitmap?.Dispose();
@@ -229,35 +238,118 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
SetArtworkBitmap(bitmap);
}
- private static async Task TryLoadArtworkBitmapAsync(string? imageUrl, CancellationToken cancellationToken)
+ private static async Task TryLoadArtworkBitmapAsync(
+ string? imageUrl,
+ string? thumbnailDataUrl,
+ CancellationToken cancellationToken)
+ {
+ foreach (var candidateUrl in BuildImageUrlCandidates(imageUrl))
+ {
+ var remoteBitmap = await TryDownloadBitmapAsync(candidateUrl, cancellationToken);
+ if (remoteBitmap is not null)
+ {
+ return remoteBitmap;
+ }
+ }
+
+ return TryDecodeBitmapFromDataUrl(thumbnailDataUrl);
+ }
+
+ private static IEnumerable BuildImageUrlCandidates(string? imageUrl)
{
if (string.IsNullOrWhiteSpace(imageUrl))
+ {
+ yield break;
+ }
+
+ var normalizedUrl = imageUrl.Trim();
+ yield return normalizedUrl;
+
+ const string preferredSizeSegment = "/full/843,/0/default.jpg";
+ if (normalizedUrl.Contains(preferredSizeSegment, StringComparison.OrdinalIgnoreCase))
+ {
+ yield return normalizedUrl.Replace(
+ preferredSizeSegment,
+ "/full/1024,/0/default.jpg",
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ private static async Task TryDownloadBitmapAsync(string imageUrl, CancellationToken cancellationToken)
+ {
+ var withReferrer = await SendImageRequestAsync(imageUrl, includeReferrer: true, cancellationToken);
+ if (withReferrer is not null)
+ {
+ return withReferrer;
+ }
+
+ return await SendImageRequestAsync(imageUrl, includeReferrer: false, cancellationToken);
+ }
+
+ private static async Task SendImageRequestAsync(
+ string imageUrl,
+ bool includeReferrer,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, imageUrl);
+ request.Headers.TryAddWithoutValidation("User-Agent", BrowserUserAgent);
+ request.Headers.TryAddWithoutValidation("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
+ if (includeReferrer && Uri.TryCreate(imageUrl, UriKind.Absolute, out var imageUri))
+ {
+ request.Headers.Referrer = new Uri($"{imageUri.Scheme}://{imageUri.Host}/", UriKind.Absolute);
+ }
+
+ using var response = await ImageHttpClient.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ return null;
+ }
+
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+ var memory = new MemoryStream();
+ await stream.CopyToAsync(memory, cancellationToken);
+ memory.Position = 0;
+ return new Bitmap(memory);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch
{
return null;
}
+ }
- using var request = new HttpRequestMessage(HttpMethod.Get, imageUrl.Trim());
- request.Headers.TryAddWithoutValidation("User-Agent", BrowserUserAgent);
- request.Headers.TryAddWithoutValidation("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
- if (Uri.TryCreate(imageUrl.Trim(), UriKind.Absolute, out var imageUri))
- {
- request.Headers.Referrer = new Uri($"{imageUri.Scheme}://{imageUri.Host}/", UriKind.Absolute);
- }
-
- using var response = await ImageHttpClient.SendAsync(
- request,
- HttpCompletionOption.ResponseHeadersRead,
- cancellationToken);
- if (!response.IsSuccessStatusCode)
+ private static Bitmap? TryDecodeBitmapFromDataUrl(string? dataUrl)
+ {
+ if (string.IsNullOrWhiteSpace(dataUrl))
{
return null;
}
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
- var memory = new MemoryStream();
- await stream.CopyToAsync(memory, cancellationToken);
- memory.Position = 0;
- return new Bitmap(memory);
+ var trimmed = dataUrl.Trim();
+ var markerIndex = trimmed.IndexOf("base64,", StringComparison.OrdinalIgnoreCase);
+ if (markerIndex < 0 || markerIndex + 7 >= trimmed.Length)
+ {
+ return null;
+ }
+
+ var base64Payload = trimmed[(markerIndex + 7)..];
+ try
+ {
+ var bytes = Convert.FromBase64String(base64Payload);
+ return new Bitmap(new MemoryStream(bytes));
+ }
+ catch
+ {
+ return null;
+ }
}
private void ApplyLoadingState()
diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml
index fa96f7d..5d426c8 100644
--- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml
+++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml
@@ -88,7 +88,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
- TextTrimming="CharacterEllipsis"
+ TextTrimming="None"
MaxLines="1" />
= 4)
+ {
+ LayoutRoot.RowDefinitions[0].Height = new GridLength(summaryHeight, GridUnitType.Pixel);
+ LayoutRoot.RowDefinitions[1].Height = new GridLength(hourlyHeight, GridUnitType.Pixel);
+ LayoutRoot.RowDefinitions[2].Height = new GridLength(separatorBandHeight, GridUnitType.Pixel);
+ LayoutRoot.RowDefinitions[3].Height = new GridLength(1, GridUnitType.Star);
+ }
+
+ var topScale = Math.Clamp(((summaryHeight / 118d) * 0.44) + (visualScale * 0.84), 0.24, 4.00);
+ var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
+ var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
+ var iconSize = Math.Clamp(Lerp(90, 122, iconGrowth) * topScale * iconScaleBoost, 14, 360);
+ iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.18, 0.26, iconGrowth)));
+ var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
+ ? "00°"
+ : TemperatureTextBlock.Text.Trim();
+ var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
+ var temperatureMaxWidth = Math.Max(30, innerWidth - iconSize - SummaryGrid.ColumnSpacing - 6);
+ var rawTemperatureSize = Math.Clamp(Lerp(72, 102, iconGrowth) * topScale, 14, 340);
+ var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
+ TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340);
+ TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 380, emphasis));
+ TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 30, Math.Max(300, innerWidth * 0.66));
+ TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -12, 0), 0, 0);
+
+ var cityFontSize = Math.Clamp(18.5 * topScale, 7, 86);
+ var conditionFontSize = Math.Clamp(20 * topScale, 7, 90);
+ var rangeFontSize = Math.Clamp(20 * topScale, 7, 90);
CityTextBlock.FontSize = cityFontSize;
ConditionTextBlock.FontSize = conditionFontSize;
RangeTextBlock.FontSize = rangeFontSize;
- CityTextBlock.FontWeight = ToVariableWeight(540);
- ConditionTextBlock.FontWeight = ToVariableWeight(600);
- RangeTextBlock.FontWeight = ToVariableWeight(620);
+ CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
+ ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
+ RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
CityTextBlock.LineHeight = cityFontSize * 1.08;
ConditionTextBlock.LineHeight = conditionFontSize * 1.06;
RangeTextBlock.LineHeight = rangeFontSize * 1.06;
- var iconSize = Math.Clamp(height * 0.116, 36, 102);
+
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
- ConditionTextBlock.MaxWidth = Math.Clamp(width * 0.24, 58, 220);
- RangeTextBlock.MaxWidth = Math.Clamp(width * 0.30, 88, 270);
- CityTextBlock.MaxWidth = Math.Clamp(width * 0.36, 112, 300);
+ WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.4 * topScale, -12, 0), 0, 0);
+ ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.25, 28, 340);
+ RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 34, 380);
+ CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 34, 420);
HourlyPanelBorder.Padding = new Thickness(0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
+ HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(4 * fitScale, 0, 18), 0, 0);
- var hourlyBandHeight = Math.Clamp(height * 0.195, 74, 160);
- var hourlyCellWidth = Math.Max(34, (width - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * 5)) / 6d);
- var hourlyTempSize = Math.Clamp(hourlyBandHeight * 0.24, 10, 32);
- var hourlyTimeSize = Math.Clamp(hourlyBandHeight * 0.18, 8, 22);
- var hourlyIconSize = Math.Clamp(hourlyBandHeight * 0.20, 12, 30);
- var hourlyStackSpacing = Math.Clamp(hourlyBandHeight * 0.03, 1, 4);
+ var hourlyCellWidth = Math.Max(12, (innerWidth - (HourlyGrid.ColumnSpacing * 5)) / 6d);
+ var hourlyCellScale = Math.Clamp(
+ Math.Min((visualScale * 0.44) + ((hourlyHeight / 120d) * 0.62), hourlyCellWidth / 76d),
+ 0.22,
+ 3.80);
+ var hourlyTempSize = Math.Clamp(19 * hourlyCellScale, 6, 72);
+ var hourlyTimeSize = Math.Clamp(14 * hourlyCellScale, 6, 52);
+ var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 114);
+ var hourlyStackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
for (var i = 0; i < _hourlyTempBlocks.Length; i++)
{
_hourlyTempBlocks[i].FontSize = hourlyTempSize;
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
- _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 610, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
- _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(450, 530, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
- _hourlyTempBlocks[i].MaxWidth = hourlyCellWidth;
- _hourlyTimeBlocks[i].MaxWidth = hourlyCellWidth;
+ _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 650, emphasis));
+ _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(450, 560, emphasis));
+ _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
+ _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
if (_hourlyTempBlocks[i].Parent is StackPanel stack) stack.Spacing = hourlyStackSpacing;
}
- var dailyLabelSize = Math.Clamp(height * 0.041, 10, 30);
- var dailyTempSize = Math.Clamp(height * 0.043, 10, 33);
- var dailyIconSize = Math.Clamp(height * 0.040, 12, 30);
- var dailyLabelMaxWidth = Math.Clamp(width * (compactness > 0.3 ? 0.48 : 0.56), 120, 380);
- var dailyHighWidth = Math.Clamp(width * 0.11, 34, 72);
- var dailyLowWidth = Math.Clamp(width * 0.10, 30, 68);
+ SeparatorLine.Margin = new Thickness(0, Math.Clamp(separatorBandHeight * 0.45, 1, 16), 0, 0);
+ DailyGrid.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 0.5, 24), 0, 0);
+ var dailyAreaHeight = Math.Max(50, innerHeight - summaryHeight - hourlyHeight - separatorBandHeight - (LayoutRoot.RowSpacing * 3) - DailyGrid.Margin.Top);
+ var dailyRowSpacing = Math.Clamp(dailyAreaHeight * 0.028, 1, 22);
+ DailyGrid.RowSpacing = dailyRowSpacing;
+ var dailyRowHeight = Math.Max(8, (dailyAreaHeight - (dailyRowSpacing * 4)) / 5d);
+ var dailyRowScale = Math.Clamp(((dailyRowHeight / 40d) * 0.62) + (visualScale * 0.44), 0.22, 3.80);
+
+ var dailyLabelSize = Math.Clamp(18.5 * dailyRowScale, 6, 70);
+ var dailyTempSize = Math.Clamp(19 * dailyRowScale, 6, 72);
+ var dailyIconSize = Math.Clamp(30 * dailyRowScale, 8, 102);
+ var dailyLabelMaxWidth = Math.Clamp(innerWidth * 0.52, 28, 460);
+ var dailyHighWidth = Math.Clamp(innerWidth * 0.14, 14, 140);
+ var dailyLowWidth = Math.Clamp(innerWidth * 0.11, 12, 120);
+ var dailyHighRightGap = Math.Clamp(innerWidth * 0.018, 1, 28);
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
{
_dailyLabelBlocks[i].FontSize = dailyLabelSize;
_dailyHighBlocks[i].FontSize = dailyTempSize;
_dailyLowBlocks[i].FontSize = dailyTempSize;
- _dailyLabelBlocks[i].FontWeight = ToVariableWeight(Lerp(520, 600, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
- _dailyHighBlocks[i].FontWeight = ToVariableWeight(Lerp(560, 640, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
- _dailyLowBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 560, Math.Clamp((scale - 0.50) / 1.2, 0, 1)));
+ _dailyLabelBlocks[i].FontWeight = ToVariableWeight(Lerp(520, 620, emphasis));
+ _dailyHighBlocks[i].FontWeight = ToVariableWeight(Lerp(560, 680, emphasis));
+ _dailyLowBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 590, emphasis));
_dailyLabelBlocks[i].MaxWidth = dailyLabelMaxWidth;
_dailyHighBlocks[i].Width = dailyHighWidth;
_dailyLowBlocks[i].Width = dailyLowWidth;
+ _dailyHighBlocks[i].Margin = new Thickness(0, 0, dailyHighRightGap, 0);
+ _dailyLowBlocks[i].Margin = new Thickness(0);
_dailyHighBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyLowBlocks[i].HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right;
_dailyHighBlocks[i].TextAlignment = TextAlignment.Right;
@@ -784,6 +830,13 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private static double ResolveScale(double width, double height) => Math.Clamp(Math.Min(Math.Clamp(width / 620d, 0.42, 2.4), Math.Clamp(height / 620d, 0.42, 2.4)), 0.42, 2.4);
private static double Lerp(double from, double to, double t) => from + ((to - from) * t);
+ private static double ResolveHeroIconScaleBoost(HyperOS3WeatherVisualKind kind) =>
+ kind switch
+ {
+ HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm or HyperOS3WeatherVisualKind.Snow => 1.16,
+ HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
+ _ => 1.0
+ };
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, byte alpha) { var c = Color.Parse(colorHex); return new SolidColorBrush(Color.FromArgb(alpha, c.R, c.G, c.B)); }
diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml
index fbf15f1..b434230 100644
--- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml
+++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml
@@ -63,7 +63,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
- TextTrimming="CharacterEllipsis"
+ TextTrimming="None"
MaxLines="1" />
items)
{
var fallbackIcon = HyperOS3WeatherAssetLoader.LoadImage(
- HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(_activeVisualKind)));
+ HyperOS3WeatherTheme.ResolveMiniIconAsset(ToThemeKind(_activeVisualKind)));
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
if (i >= items.Count)
@@ -836,7 +836,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var item = items[i];
_hourlyTimeBlocks[i].Text = item.TimeLabel;
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(
- HyperOS3WeatherTheme.ResolveIconAsset(item.IconKind));
+ HyperOS3WeatherTheme.ResolveMiniIconAsset(item.IconKind));
_hourlyTempBlocks[i].Text = item.TemperatureText;
}
}
@@ -1168,68 +1168,84 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyAdaptiveTypography()
{
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
- var scaleX = Math.Clamp(layoutWidth / 608d, 0.58, 1.90);
- var scaleY = Math.Clamp(layoutHeight / 288d, 0.58, 1.90);
var innerWidth = Math.Max(120, layoutWidth);
- var innerHeight = Math.Max(72, layoutHeight);
- var compactness = Math.Clamp((1.0 - scaleY) / 0.55, 0, 1);
+ var innerHeight = Math.Max(56, layoutHeight);
+ var fitScale = Math.Clamp(Math.Min(innerWidth / 592d, innerHeight / 284d), 0.30, 3.20);
+ var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.60);
+ var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.60);
+ var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
- ContentGrid.RowSpacing = Math.Clamp((4.2 - (compactness * 0.7)) * scaleY, 2, 8);
- TopRowGrid.ColumnSpacing = Math.Clamp(8 * scaleX, 6, 13);
- BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp((1.0 - (compactness * 0.4)) * scaleY, 0, 2));
+ ContentGrid.RowSpacing = Math.Clamp(8 * fitScale, 1, 20);
+ TopRowGrid.ColumnSpacing = Math.Clamp(11 * fitScale, 3, 30);
+ BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * fitScale, 0, 7));
- var contentHeight = Math.Max(60, innerHeight - ContentGrid.RowSpacing);
- var topZoneRatio = Math.Clamp(0.38 + (compactness * 0.09), 0.36, 0.50);
- var topZoneHeight = Math.Clamp(contentHeight * topZoneRatio, 60, 170);
- var bottomZoneHeight = Math.Max(42, contentHeight - topZoneHeight);
- var topScaleH = Math.Clamp(topZoneHeight / 102d, 0.62, 2.0);
- var topScaleW = Math.Clamp(innerWidth / 620d, 0.62, 2.0);
- var topScale = Math.Clamp((topScaleH * 0.68) + (topScaleW * 0.32), 0.62, 2.0);
- var bottomScaleH = Math.Clamp(bottomZoneHeight / 122d, 0.56, 2.0);
- var bottomScale = Math.Clamp((bottomScaleH * 0.74) + (scaleX * 0.26), 0.56, 1.95);
- var bodyHeight = bottomZoneHeight;
+ var contentHeight = Math.Max(36, innerHeight - ContentGrid.RowSpacing);
+ var topZoneHeight = Math.Clamp(contentHeight * 0.47, 24, Math.Max(24, contentHeight - 12));
+ var bottomZoneHeight = Math.Max(10, contentHeight - topZoneHeight);
+ if (ContentGrid.RowDefinitions.Count >= 2)
+ {
+ ContentGrid.RowDefinitions[0].Height = new GridLength(topZoneHeight, GridUnitType.Pixel);
+ ContentGrid.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
+ }
- TemperatureTextBlock.FontSize = Math.Clamp(88 * topScale, 56, 132);
- TemperatureTextBlock.FontWeight = ToVariableWeight(315);
- TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-1.2 * topScale, -4, 0), 0, 0);
- TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 88, 196);
+ var topScale = Math.Clamp(((topZoneHeight / 116d) * 0.42) + (visualScale * 0.86), 0.24, 3.90);
+ var bottomScale = Math.Clamp(((bottomZoneHeight / 156d) * 0.44) + (visualScale * 0.72), 0.24, 3.80);
+ var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
+ var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
+ var iconSize = Math.Clamp(Lerp(88, 116, iconGrowth) * topScale * iconScaleBoost, 14, 360);
+ iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.22, 0.32, iconGrowth)));
+ var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
+ ? "00°"
+ : TemperatureTextBlock.Text.Trim();
+ var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
+ var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4);
+ var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320);
+ var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
+ TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320);
+ TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
+ TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0);
+ TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68));
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
- LocationIcon.FontSize = Math.Clamp(12 * topScale, 9, 17);
- CityTextBlock.FontSize = Math.Clamp(18 * topScale, 11, 26);
- CityTextBlock.FontWeight = ToVariableWeight(540);
- CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 112, 300);
+ LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52);
+ CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88);
+ CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
+ CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(0);
- ConditionRangeStack.Spacing = Math.Clamp(7 * topScale, 4, 13);
- ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 12, 27);
- RangeTextBlock.FontSize = Math.Clamp(20 * topScale, 12, 30);
- ConditionTextBlock.FontWeight = ToVariableWeight(600);
- RangeTextBlock.FontWeight = ToVariableWeight(620);
- ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 58, 220);
- RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.30, 88, 270);
- BottomInfoStack.Spacing = Math.Clamp(2.2 * topScale, 1, 6);
+ ConditionRangeStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24);
+ ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78);
+ RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84);
+ ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
+ RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
+ ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320);
+ RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360);
+ BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14);
- var iconSize = Math.Clamp(68 * topScale, 42, 98);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
+ WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -10, 0), 0, 0);
- HourlyPanelBorder.Padding = new Thickness(0, Math.Clamp(1 * scaleY, 0, 2), 0, 0);
- HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(1.2 * scaleY, 0, 3), 0, 0);
+ HourlyPanelBorder.Padding = new Thickness(0);
+ HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 1, 24), 0, 0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
- HourlyGrid.ColumnSpacing = Math.Clamp(7 * scaleX, 4, 11);
+ HourlyGrid.ColumnSpacing = Math.Clamp(4 * fitScale, 0.5, 24);
var hourlyColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var hourlyInnerWidth = Math.Max(
- 96,
- innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
- var hourlyCellWidth = Math.Max(34, hourlyInnerWidth / hourlyColumnCount);
- var stackSpacing = Math.Clamp((1.6 + (bottomScale * 0.8)) * scaleY, 1, 4);
- var hourlyTempSize = Math.Clamp(Math.Max(13, bodyHeight * 0.22) * (0.76 + (bottomScale * 0.24)), 13, 31);
- var hourlyTimeSize = Math.Clamp(Math.Max(10, bodyHeight * 0.17) * (0.78 + (bottomScale * 0.22)), 10, 23);
- var hourlyIconSize = Math.Clamp(Math.Max(14, bodyHeight * 0.25) * (0.78 + (bottomScale * 0.22)), 14, 35);
+ 32,
+ innerWidth - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
+ var hourlyCellWidth = Math.Max(12, hourlyInnerWidth / hourlyColumnCount);
+ var hourlyCellScale = Math.Clamp(
+ Math.Min((bottomScale * 0.66) + (visualScale * 0.44), hourlyCellWidth / 74d),
+ 0.22,
+ 3.60);
+ var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
+ var hourlyTempSize = Math.Clamp(19.5 * hourlyCellScale, 6, 72);
+ var hourlyTimeSize = Math.Clamp(14.5 * hourlyCellScale, 6, 50);
+ var hourlyIconSize = Math.Clamp(34 * hourlyCellScale, 8, 108);
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
@@ -1237,10 +1253,10 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
_hourlyTimeBlocks[i].FontSize = hourlyTimeSize;
_hourlyIconBlocks[i].Width = hourlyIconSize;
_hourlyIconBlocks[i].Height = hourlyIconSize;
- _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
- _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
- _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
- _hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
+ _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240);
+ _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240);
+ _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis));
+ _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack)
{
hourlyStack.Spacing = stackSpacing;
@@ -1253,10 +1269,20 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
return from + ((to - from) * t);
}
+ private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
+ {
+ return kind switch
+ {
+ WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
+ WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
+ _ => 1.0
+ };
+ }
+
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
- HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
+ HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
diff --git a/LanMountainDesktop/Views/Components/HyperOS3WeatherTheme.cs b/LanMountainDesktop/Views/Components/HyperOS3WeatherTheme.cs
index f8f59ea..34f7ea3 100644
--- a/LanMountainDesktop/Views/Components/HyperOS3WeatherTheme.cs
+++ b/LanMountainDesktop/Views/Components/HyperOS3WeatherTheme.cs
@@ -102,18 +102,32 @@ public static class HyperOS3WeatherTheme
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
};
- private static readonly IReadOnlyDictionary IconAssets =
+ private static readonly IReadOnlyDictionary HeroIconAssets =
new Dictionary
{
- [HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sunny_day.webp",
- [HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_moon_clear.webp",
- [HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
- [HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
- [HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_light.webp",
- [HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_heavy.webp",
- [HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_thunder.webp",
- [HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_snow.webp",
- [HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp"
+ [HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
+ [HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
+ [HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
+ [HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
+ [HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
+ [HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
+ [HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
+ [HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
+ [HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png"
+ };
+
+ private static readonly IReadOnlyDictionary MiniIconAssets =
+ new Dictionary
+ {
+ [HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
+ [HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
+ [HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
+ [HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
+ [HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
+ [HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
+ [HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
+ [HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
+ [HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png"
};
private static readonly IReadOnlyDictionary Palettes =
@@ -319,7 +333,17 @@ public static class HyperOS3WeatherTheme
public static string? ResolveIconAsset(HyperOS3WeatherVisualKind kind)
{
- return IconAssets.TryGetValue(kind, out var asset) ? asset : null;
+ return ResolveMiniIconAsset(kind);
+ }
+
+ public static string? ResolveHeroIconAsset(HyperOS3WeatherVisualKind kind)
+ {
+ return HeroIconAssets.TryGetValue(kind, out var asset) ? asset : null;
+ }
+
+ public static string? ResolveMiniIconAsset(HyperOS3WeatherVisualKind kind)
+ {
+ return MiniIconAssets.TryGetValue(kind, out var asset) ? asset : null;
}
public static string ResolveSunCoreAsset()
diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml
index 62a42a2..28d9490 100644
--- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml
+++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml
@@ -63,7 +63,7 @@
FontFeatures="tnum"
VerticalAlignment="Center"
Margin="0,-2,0,0"
- TextTrimming="CharacterEllipsis"
+ TextTrimming="None"
MaxLines="1" />
= 3)
+ {
+ ContentGrid.RowDefinitions[0].Height = new GridLength(topZoneHeight, GridUnitType.Pixel);
+ ContentGrid.RowDefinitions[1].Height = new GridLength(separatorHeight, GridUnitType.Pixel);
+ ContentGrid.RowDefinitions[2].Height = new GridLength(1, GridUnitType.Star);
+ }
- TemperatureTextBlock.FontSize = Math.Clamp(88 * topScale, 56, 132);
- TemperatureTextBlock.FontWeight = ToVariableWeight(315);
- TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-1.2 * topScale, -4, 0), 0, 0);
- TemperatureTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 88, 196);
+ var topScale = Math.Clamp(((topZoneHeight / 116d) * 0.42) + (visualScale * 0.86), 0.24, 3.90);
+ var bottomScale = Math.Clamp(((bottomZoneHeight / 156d) * 0.44) + (visualScale * 0.72), 0.24, 3.80);
+ var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
+ var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
+ var iconSize = Math.Clamp(Lerp(88, 116, iconGrowth) * topScale * iconScaleBoost, 14, 360);
+ iconSize = Math.Min(iconSize, Math.Max(14, innerWidth * Lerp(0.22, 0.32, iconGrowth)));
+ var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
+ ? "00°"
+ : TemperatureTextBlock.Text.Trim();
+ var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
+ var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4);
+ var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320);
+ var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
+ TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320);
+ TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
+ TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0);
+ TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68));
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
- LocationIcon.FontSize = Math.Clamp(12 * topScale, 9, 17);
- CityTextBlock.FontSize = Math.Clamp(18 * topScale, 11, 26);
- CityTextBlock.FontWeight = ToVariableWeight(540);
- CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 112, 300);
+ LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52);
+ CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88);
+ CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis));
+ CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460);
ConditionInfoBadge.Padding = new Thickness(0);
ConditionInfoBadge.CornerRadius = new CornerRadius(0);
- ConditionIconStack.Spacing = Math.Clamp(7 * topScale, 4, 13);
- ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 12, 27);
- RangeTextBlock.FontSize = Math.Clamp(20 * topScale, 12, 30);
- ConditionTextBlock.FontWeight = ToVariableWeight(600);
- RangeTextBlock.FontWeight = ToVariableWeight(620);
- ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 58, 220);
- RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.30, 88, 270);
- BottomInfoStack.Spacing = Math.Clamp(2.2 * topScale, 1, 6);
+ ConditionIconStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24);
+ ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78);
+ RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84);
+ ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis));
+ RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis));
+ ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320);
+ RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360);
+ BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14);
- var iconSize = Math.Clamp(68 * topScale, 42, 98);
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
+ WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -10, 0), 0, 0);
- HourlyPanelBorder.Padding = new Thickness(0, Math.Clamp(1 * scaleY, 0, 2), 0, 0);
- HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(1.2 * scaleY, 0, 3), 0, 0);
+ HourlyPanelBorder.Padding = new Thickness(0);
+ HourlyPanelBorder.Margin = new Thickness(0, Math.Clamp(6 * fitScale, 1, 24), 0, 0);
HourlyPanelBorder.CornerRadius = new CornerRadius(0);
- HourlyGrid.ColumnSpacing = Math.Clamp(7 * scaleX, 4, 11);
+ HourlyGrid.ColumnSpacing = Math.Clamp(5 * fitScale, 0.5, 28);
var hourlyColumnCount = Math.Max(1, _hourlyTimeBlocks.Length);
var hourlyInnerWidth = Math.Max(
- 96,
- innerWidth - HourlyPanelBorder.Padding.Left - HourlyPanelBorder.Padding.Right - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
- var hourlyCellWidth = Math.Max(34, hourlyInnerWidth / hourlyColumnCount);
- var stackSpacing = Math.Clamp((1.6 + (bottomScale * 0.8)) * scaleY, 1, 4);
- var forecastRangeSize = Math.Clamp(Math.Max(13, bodyHeight * 0.22) * (0.76 + (bottomScale * 0.24)), 13, 31);
- var forecastLabelSize = Math.Clamp(Math.Max(10, bodyHeight * 0.17) * (0.78 + (bottomScale * 0.22)), 10, 23);
- var forecastIconSize = Math.Clamp(Math.Max(14, bodyHeight * 0.25) * (0.78 + (bottomScale * 0.22)), 14, 35);
+ 32,
+ innerWidth - (HourlyGrid.ColumnSpacing * (hourlyColumnCount - 1)));
+ var hourlyCellWidth = Math.Max(12, hourlyInnerWidth / hourlyColumnCount);
+ var hourlyCellScale = Math.Clamp(
+ Math.Min((bottomScale * 0.66) + (visualScale * 0.44), hourlyCellWidth / 78d),
+ 0.22,
+ 3.60);
+ var stackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10);
+ var forecastRangeSize = Math.Clamp(18.0 * hourlyCellScale, 6, 62);
+ var forecastLabelSize = Math.Clamp(13.8 * hourlyCellScale, 6, 48);
+ var forecastIconSize = Math.Clamp(32 * hourlyCellScale, 8, 100);
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
{
@@ -1084,10 +1101,10 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_hourlyTempBlocks[i].FontSize = forecastRangeSize;
_hourlyIconBlocks[i].Width = forecastIconSize;
_hourlyIconBlocks[i].Height = forecastIconSize;
- _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
- _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 34, 112);
- _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(500);
- _hourlyTempBlocks[i].FontWeight = ToVariableWeight(590);
+ _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
+ _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260);
+ _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis));
+ _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
_hourlyTimeBlocks[i].TextAlignment = TextAlignment.Center;
_hourlyTempBlocks[i].TextAlignment = TextAlignment.Center;
if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack)
@@ -1102,10 +1119,20 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
return from + ((to - from) * t);
}
+ private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
+ {
+ return kind switch
+ {
+ WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
+ WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
+ _ => 1.0
+ };
+ }
+
private void SetMainWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
- HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
+ HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
index 309cffe..9bd2081 100644
--- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
@@ -9,6 +9,7 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
@@ -23,6 +24,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
};
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateRecorder();
+ private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly List _waveBars = [];
@@ -32,6 +34,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
private string _lastSavedFilePath = string.Empty;
private double _currentCellSize = 48;
private bool _isAttached;
+ private bool _pausedStudyMonitoringForRecording;
public RecordingWidget()
{
@@ -115,6 +118,12 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
{
_isAttached = false;
_uiTimer.Stop();
+
+ var snapshot = _audioRecorderService.GetSnapshot();
+ if (snapshot.State is not AudioRecorderRuntimeState.Recording and not AudioRecorderRuntimeState.Paused)
+ {
+ ResumeStudyMonitoringIfNeeded();
+ }
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -140,6 +149,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
_audioRecorderService.Discard();
+ ResumeStudyMonitoringIfNeeded();
RefreshVisual();
e.Handled = true;
}
@@ -165,7 +175,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
else
{
- _audioRecorderService.StartOrResume();
+ _ = TryStartRecordingWithMonitoringHandoff();
}
RefreshVisual();
@@ -201,6 +211,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
}
_ = _audioRecorderService.StopAndSave(outputPath);
+ ResumeStudyMonitoringIfNeeded();
RefreshVisual();
e.Handled = true;
}
@@ -208,6 +219,12 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
private void RefreshVisual()
{
var snapshot = _audioRecorderService.GetSnapshot();
+ if (_pausedStudyMonitoringForRecording &&
+ snapshot.State is AudioRecorderRuntimeState.Ready or AudioRecorderRuntimeState.Error or AudioRecorderRuntimeState.Unsupported)
+ {
+ ResumeStudyMonitoringIfNeeded();
+ snapshot = _audioRecorderService.GetSnapshot();
+ }
TitleTextBlock.Text = L("recording.widget.title", "Recorder");
TimerTextBlock.Text = FormatDuration(snapshot.Duration);
@@ -300,6 +317,60 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget
HintTextBlock.Text = L("recording.widget.hint.ready", "Tap red button to record");
}
+ private bool TryStartRecordingWithMonitoringHandoff()
+ {
+ if (_audioRecorderService.StartOrResume())
+ {
+ return true;
+ }
+
+ if (!TryPauseStudyMonitoringForRecording())
+ {
+ return false;
+ }
+
+ if (_audioRecorderService.StartOrResume())
+ {
+ return true;
+ }
+
+ ResumeStudyMonitoringIfNeeded();
+ return false;
+ }
+
+ private bool TryPauseStudyMonitoringForRecording()
+ {
+ if (_pausedStudyMonitoringForRecording)
+ {
+ return true;
+ }
+
+ var snapshot = _studyAnalyticsService.GetSnapshot();
+ if (snapshot.State != StudyAnalyticsRuntimeState.Running)
+ {
+ return false;
+ }
+
+ if (!_studyAnalyticsService.PauseMonitoring())
+ {
+ return false;
+ }
+
+ _pausedStudyMonitoringForRecording = true;
+ return true;
+ }
+
+ private void ResumeStudyMonitoringIfNeeded()
+ {
+ if (!_pausedStudyMonitoringForRecording)
+ {
+ return;
+ }
+
+ _pausedStudyMonitoringForRecording = false;
+ _ = _studyAnalyticsService.StartOrResumeMonitoring();
+ }
+
private void InitializeWaveBars()
{
if (_waveBars.Count > 0)
diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml b/LanMountainDesktop/Views/Components/WeatherWidget.axaml
index 3eddda9..b150a8b 100644
--- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml
+++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml
@@ -91,7 +91,7 @@
FontFeatures="tnum"
VerticalAlignment="Top"
Margin="-1,-7,0,0"
- TextTrimming="CharacterEllipsis"
+ TextTrimming="None"
MaxLines="1" />
1 ? Bounds.Width : Math.Max(80, _currentCellSize * 2);
+ var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(80, _currentCellSize * 2);
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 26, 46);
var horizontalPadding = Math.Clamp(_currentCellSize * metrics.HorizontalPaddingScale, 10, 24);
var verticalPadding = Math.Clamp(_currentCellSize * metrics.VerticalPaddingScale, 10, 24);
@@ -165,8 +167,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
ContentPaddingBorder.Padding = new Thickness(
- Math.Clamp(horizontalPadding * scale, 10, 24),
- Math.Clamp(verticalPadding * scale, 10, 24));
+ Math.Clamp(Math.Min(horizontalPadding * scale, hostWidth * 0.12), 3, 24),
+ Math.Clamp(Math.Min(verticalPadding * scale, hostHeight * 0.12), 3, 24));
ApplyAdaptiveTypography();
ResetParticles();
}
@@ -472,7 +474,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
palette.TertiaryText,
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast,
- isNightVisual ? (byte)0xD6 : (byte)0xC2);
+ isNightVisual ? (byte)0xC4 : (byte)0xAE);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
LocationIcon.Foreground = tertiary;
CityTextBlock.Foreground = tertiary;
@@ -815,25 +817,19 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
{
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 2;
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 2;
- var innerWidth = Math.Max(90, width - ContentPaddingBorder.Padding.Left - ContentPaddingBorder.Padding.Right);
- var innerHeight = Math.Max(90, height - ContentPaddingBorder.Padding.Top - ContentPaddingBorder.Padding.Bottom);
- var scaleX = Math.Clamp(innerWidth / 288d, 0.56, 2.2);
- var scaleY = Math.Clamp(innerHeight / 288d, 0.56, 2.2);
- var compactness = Math.Clamp((1.0 - scaleY) / 0.60, 0, 1);
+ var innerWidth = Math.Max(56, width - ContentPaddingBorder.Padding.Left - ContentPaddingBorder.Padding.Right);
+ var innerHeight = Math.Max(56, height - ContentPaddingBorder.Padding.Top - ContentPaddingBorder.Padding.Bottom);
+ var fitScale = Math.Clamp(Math.Min(innerWidth / 288d, innerHeight / 288d), 0.30, 3.20);
+ var cellScale = Math.Clamp(_currentCellSize / 44d, 0.34, 3.80);
+ var visualScale = Math.Clamp((fitScale * 0.72) + (cellScale * 0.28), 0.30, 3.80);
+ var emphasis = Math.Clamp((visualScale - 0.82) / 1.90, 0, 1);
- ContentGrid.RowSpacing = Math.Clamp((2.8 - (compactness * 0.5)) * scaleY, 1, 6);
- TopRowGrid.ColumnSpacing = Math.Clamp(7.5 * scaleX, 4, 13);
+ ContentGrid.RowSpacing = Math.Clamp(2.2 * fitScale, 0.5, 9);
+ TopRowGrid.ColumnSpacing = Math.Clamp(6.0 * fitScale, 2, 20);
- var availableHeight = Math.Max(80, innerHeight - (ContentGrid.RowSpacing * 2));
- var topZoneRatio = Math.Clamp(0.52 + ((1 - compactness) * 0.03), 0.48, 0.56);
- var bottomZoneRatio = Math.Clamp(0.36 - (compactness * 0.02), 0.32, 0.40);
- var topZoneHeight = Math.Clamp(availableHeight * topZoneRatio, 44, availableHeight - 30);
- var bottomZoneHeight = Math.Clamp(availableHeight * bottomZoneRatio, 34, availableHeight - topZoneHeight - 6);
- if (topZoneHeight + bottomZoneHeight > availableHeight - 6)
- {
- bottomZoneHeight = Math.Max(24, availableHeight - topZoneHeight - 6);
- topZoneHeight = Math.Max(42, availableHeight - bottomZoneHeight - 6);
- }
+ var availableHeight = Math.Max(40, innerHeight - (ContentGrid.RowSpacing * 2));
+ var topZoneHeight = Math.Clamp(availableHeight * 0.60, 22, Math.Max(22, availableHeight - 16));
+ var bottomZoneHeight = Math.Max(12, availableHeight - topZoneHeight - 2);
if (ContentGrid.RowDefinitions.Count >= 3)
{
@@ -842,46 +838,38 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
ContentGrid.RowDefinitions[2].Height = new GridLength(bottomZoneHeight, GridUnitType.Pixel);
}
- var topScaleH = Math.Clamp(topZoneHeight / 112d, 0.58, 2.2);
- var topScaleW = Math.Clamp(innerWidth / 288d, 0.60, 2.2);
- var topScale = Math.Clamp((topScaleH * 0.70) + (topScaleW * 0.30), 0.58, 2.2);
- var bottomScaleH = Math.Clamp(bottomZoneHeight / 80d, 0.62, 2.2);
- var bottomScale = Math.Clamp((bottomScaleH * 0.80) + (scaleX * 0.20), 0.62, 2.2);
-
- var iconSize = Math.Clamp(
- Math.Max(52, topZoneHeight * 0.50) * (0.76 + (topScale * 0.24)),
- 52,
- 136);
+ var topScale = Math.Clamp(((topZoneHeight / 170d) * 0.42) + (visualScale * 0.84), 0.24, 4.00);
+ var bottomScale = Math.Clamp(((bottomZoneHeight / 84d) * 0.46) + (visualScale * 0.66), 0.24, 3.90);
+ var iconGrowth = Math.Clamp((visualScale - 0.88) / 1.70, 0, 1);
+ var iconScaleBoost = ResolveHeroIconScaleBoost(_activeVisualKind);
+ var iconSize = Math.Clamp(Lerp(96, 124, iconGrowth) * topScale * iconScaleBoost, 18, 360);
+ iconSize = Math.Min(iconSize, Math.Max(18, innerWidth * Lerp(0.34, 0.44, iconGrowth)));
WeatherIconImage.Width = iconSize;
WeatherIconImage.Height = iconSize;
- WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-5 * topScale, -12, 0), 0, 0);
+ WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-4.2 * topScale, -14, 0), 0, 0);
- TemperatureTextBlock.FontSize = Math.Clamp(
- Math.Max(52, topZoneHeight * 0.69) * (0.74 + (topScale * 0.24)),
- 50,
- 146);
- TemperatureTextBlock.FontWeight = ToVariableWeight(310);
- TemperatureTextBlock.Margin = new Thickness(Math.Clamp(-2 * topScale, -5, 0), Math.Clamp(-8 * topScale, -14, -3), 0, 0);
- var temperatureMaxWidthLimit = Math.Max(90, innerWidth * 0.70);
- TemperatureTextBlock.MaxWidth = Math.Clamp(
- innerWidth - iconSize - TopRowGrid.ColumnSpacing - 8,
- 90,
- temperatureMaxWidthLimit);
+ var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text)
+ ? "00°"
+ : TemperatureTextBlock.Text.Trim();
+ var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6);
+ var temperatureMaxWidth = Math.Max(34, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 2);
+ var rawTemperatureSize = Math.Clamp(Lerp(94, 118, iconGrowth) * topScale, 22, 340);
+ var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62);
+ TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340);
+ TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis));
+ TemperatureTextBlock.Margin = new Thickness(Math.Clamp(-1.4 * topScale, -6, 0), Math.Clamp(-7.6 * topScale, -16, -1), 0, 0);
+ TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 34, Math.Max(34, innerWidth * 0.76));
- var bottomStackSpacing = Math.Clamp(1.2 * bottomScale, 1, 4);
+ var bottomStackSpacing = Math.Clamp(1.2 * bottomScale, 0.6, 8);
BottomInfoStack.Spacing = bottomStackSpacing;
- BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.8 * scaleY, 0, 4));
- BottomInfoStack.MaxHeight = Math.Max(32, bottomZoneHeight);
+ BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(1.4 * fitScale, 0, 6));
+ BottomInfoStack.MaxHeight = Math.Max(10, bottomZoneHeight);
- var bottomTextMaxWidth = Math.Min(innerWidth, Math.Max(56, innerWidth * 0.84));
- var conditionStackSpacing = Math.Clamp(1.4 + (2.1 * bottomScale), 1.2, 7);
+ var bottomTextMaxWidth = Math.Min(innerWidth, Math.Max(36, innerWidth * 0.86));
+ var conditionStackSpacing = Math.Clamp(1.2 + (2.0 * bottomScale), 0.5, 12);
ConditionStack.Spacing = conditionStackSpacing;
ConditionStack.Margin = new Thickness(0);
- var infoFontSizeRaw = Math.Clamp(
- Math.Max(14, bottomZoneHeight * 0.38) * (0.82 + (bottomScale * 0.24)),
- 15,
- 42);
- var infoFontSize = infoFontSizeRaw;
+ var infoFontSize = Math.Clamp(27 * bottomScale, 7, 86);
const double infoLineHeightFactor = 1.10;
var estimatedBottomUsedHeight =
(infoFontSize * infoLineHeightFactor * 3) +
@@ -890,35 +878,35 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
2;
if (estimatedBottomUsedHeight > bottomZoneHeight)
{
- var shrink = Math.Clamp(bottomZoneHeight / estimatedBottomUsedHeight, 0.58, 1.0);
- infoFontSize = Math.Max(11, infoFontSize * shrink);
- conditionStackSpacing = Math.Max(0.8, conditionStackSpacing * shrink);
- bottomStackSpacing = Math.Max(0.8, bottomStackSpacing * shrink);
+ var shrink = Math.Clamp(bottomZoneHeight / estimatedBottomUsedHeight, 0.36, 1.0);
+ infoFontSize = Math.Max(6, infoFontSize * shrink);
+ conditionStackSpacing = Math.Max(0.3, conditionStackSpacing * shrink);
+ bottomStackSpacing = Math.Max(0.3, bottomStackSpacing * shrink);
ConditionStack.Spacing = conditionStackSpacing;
BottomInfoStack.Spacing = bottomStackSpacing;
}
- var infoFontWeight = ToVariableWeight(590);
- ConditionTextBlock.FontSize = infoFontSize;
+ var infoFontWeight = ToVariableWeight(Lerp(580, 690, emphasis));
+ ConditionTextBlock.FontSize = Math.Max(6, infoFontSize * 0.96);
ConditionTextBlock.FontWeight = infoFontWeight;
- ConditionTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
+ ConditionTextBlock.LineHeight = ConditionTextBlock.FontSize * infoLineHeightFactor;
ConditionTextBlock.MaxWidth = bottomTextMaxWidth;
- RangeTextBlock.FontSize = infoFontSize;
+ RangeTextBlock.FontSize = Math.Max(6, infoFontSize * 1.03);
RangeTextBlock.FontWeight = infoFontWeight;
- RangeTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
+ RangeTextBlock.LineHeight = RangeTextBlock.FontSize * infoLineHeightFactor;
RangeTextBlock.MaxWidth = bottomTextMaxWidth;
CityInfoBadge.Padding = new Thickness(0);
CityInfoBadge.CornerRadius = new CornerRadius(0);
CityInfoBadge.MaxWidth = bottomTextMaxWidth;
LocationIcon.FontSize = Math.Clamp(
- Math.Max(9, bottomZoneHeight * 0.16) * (0.76 + (bottomScale * 0.22)),
- 9,
- 18);
+ 12 * bottomScale,
+ 6,
+ 34);
LocationIcon.FontSize = Math.Min(LocationIcon.FontSize, infoFontSize * 0.72);
- CityTextBlock.FontSize = infoFontSize;
- CityTextBlock.FontWeight = infoFontWeight;
- CityTextBlock.LineHeight = infoFontSize * infoLineHeightFactor;
+ CityTextBlock.FontSize = Math.Max(6, infoFontSize * 0.84);
+ CityTextBlock.FontWeight = ToVariableWeight(Lerp(500, 620, emphasis));
+ CityTextBlock.LineHeight = CityTextBlock.FontSize * infoLineHeightFactor;
CityTextBlock.MaxWidth = bottomTextMaxWidth;
}
@@ -927,10 +915,20 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
return from + ((to - from) * t);
}
+ private static double ResolveHeroIconScaleBoost(WeatherVisualKind kind)
+ {
+ return kind switch
+ {
+ WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
+ WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
+ _ => 1.0
+ };
+ }
+
private void SetWeatherIcon(WeatherVisualKind kind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
- HyperOS3WeatherTheme.ResolveIconAsset(ToThemeKind(kind)));
+ HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
}
private void SetLoadingSkeleton(bool isLoading)
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index 6a19049..93fc29d 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -706,6 +706,12 @@ public partial class MainWindow
return;
}
+ if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
+ {
+ OpenDailyArtworkComponentSettings();
+ return;
+ }
+
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{
OpenStudyEnvironmentComponentSettings();
@@ -760,6 +766,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1;
}
+ private void OpenDailyArtworkComponentSettings()
+ {
+ if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
+ {
+ return;
+ }
+
+ var settingsContent = new DailyArtworkSettingsWindow();
+ settingsContent.SettingsChanged += OnDailyArtworkSettingsChanged;
+ ComponentSettingsContentHost.Content = settingsContent;
+
+ ComponentSettingsWindow.IsVisible = true;
+ ComponentSettingsWindow.Opacity = 0;
+ ComponentSettingsWindow.Opacity = 1;
+ }
+
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
{
if (_selectedDesktopComponentHost is null)
@@ -788,6 +810,34 @@ public partial class MainWindow
}
}
+ private void OnDailyArtworkSettingsChanged(object? sender, EventArgs e)
+ {
+ _ = sender;
+ _ = e;
+
+ _dailyArtworkMirrorSource = sender is DailyArtworkSettingsWindow settingsWindow
+ ? DailyArtworkMirrorSources.Normalize(settingsWindow.CurrentSource)
+ : DailyArtworkMirrorSources.Normalize(_appSettingsService.Load().DailyArtworkMirrorSource);
+
+ foreach (var pageGrid in _desktopPageComponentGrids.Values)
+ {
+ foreach (var host in pageGrid.Children.OfType())
+ {
+ if (!host.Classes.Contains(DesktopComponentHostClass))
+ {
+ continue;
+ }
+
+ if (TryGetContentHost(host)?.Child is DailyArtworkWidget widget)
+ {
+ widget.RefreshFromSettings();
+ }
+ }
+ }
+
+ PersistSettings();
+ }
+
private void CloseComponentSettingsWindow()
{
if (ComponentSettingsWindow is null)
@@ -805,6 +855,11 @@ public partial class MainWindow
studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged;
}
+ if (ComponentSettingsContentHost?.Content is DailyArtworkSettingsWindow dailyArtworkSettingsWindow)
+ {
+ dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged;
+ }
+
ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() =>
diff --git a/LanMountainDesktop/Views/MainWindow.Localization.cs b/LanMountainDesktop/Views/MainWindow.Localization.cs
index e6100a8..88cedb6 100644
--- a/LanMountainDesktop/Views/MainWindow.Localization.cs
+++ b/LanMountainDesktop/Views/MainWindow.Localization.cs
@@ -262,6 +262,13 @@ public partial class MainWindow
"settings.about.font_format",
"Font: {0}",
AppFontName);
+ AboutStartupSettingsExpander.Header = L("settings.about.startup_header", "Windows Startup");
+ AboutStartupSettingsExpander.Description = L(
+ "settings.about.startup_desc",
+ "Launch the app automatically when signing in to Windows.");
+ AutoStartWithWindowsToggleSwitch.Content = L(
+ "settings.about.startup_toggle",
+ "Launch at Windows sign-in");
if (WallpaperPlacementComboBox?.ItemCount >= 5)
{
diff --git a/LanMountainDesktop/Views/MainWindow.Settings.cs b/LanMountainDesktop/Views/MainWindow.Settings.cs
index 103cd96..81a1296 100644
--- a/LanMountainDesktop/Views/MainWindow.Settings.cs
+++ b/LanMountainDesktop/Views/MainWindow.Settings.cs
@@ -658,6 +658,8 @@ public partial class MainWindow
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
WeatherIconPackId = _weatherIconPackId,
WeatherNoTlsRequests = _weatherNoTlsRequests,
+ DailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(_dailyArtworkMirrorSource),
+ AutoStartWithWindows = _autoStartWithWindows,
TopStatusComponentIds = _topStatusComponentIds.ToList(),
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
@@ -790,14 +792,37 @@ public partial class MainWindow
weatherCode: null,
temperatureText: "--",
updatedAt: null);
-
- UpdateWeatherLocationModePanels();
- UpdateWeatherLocationStatusText();
}
finally
{
_suppressWeatherLocationEvents = false;
}
+
+ UpdateWeatherLocationModePanels();
+ UpdateWeatherLocationStatusText();
+ }
+
+ private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot)
+ {
+ _autoStartWithWindows = OperatingSystem.IsWindows()
+ ? _windowsStartupService.IsEnabled()
+ : snapshot.AutoStartWithWindows;
+
+ if (AutoStartWithWindowsToggleSwitch is null)
+ {
+ return;
+ }
+
+ _suppressAutoStartToggleEvents = true;
+ try
+ {
+ AutoStartWithWindowsToggleSwitch.IsEnabled = OperatingSystem.IsWindows();
+ AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
+ }
+ finally
+ {
+ _suppressAutoStartToggleEvents = false;
+ }
}
private static WeatherLocationMode ParseWeatherLocationMode(string? value)
@@ -1022,6 +1047,51 @@ public partial class MainWindow
PersistSettings();
}
+ private void OnAutoStartWithWindowsToggled(object? sender, RoutedEventArgs e)
+ {
+ if (_suppressAutoStartToggleEvents || AutoStartWithWindowsToggleSwitch is null)
+ {
+ return;
+ }
+
+ var requested = AutoStartWithWindowsToggleSwitch.IsChecked == true;
+ if (!OperatingSystem.IsWindows())
+ {
+ _autoStartWithWindows = false;
+ _suppressAutoStartToggleEvents = true;
+ try
+ {
+ AutoStartWithWindowsToggleSwitch.IsEnabled = false;
+ AutoStartWithWindowsToggleSwitch.IsChecked = false;
+ }
+ finally
+ {
+ _suppressAutoStartToggleEvents = false;
+ }
+
+ PersistSettings();
+ return;
+ }
+
+ var applied = _windowsStartupService.SetEnabled(requested);
+ _autoStartWithWindows = _windowsStartupService.IsEnabled();
+
+ if (!applied || _autoStartWithWindows != requested)
+ {
+ _suppressAutoStartToggleEvents = true;
+ try
+ {
+ AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
+ }
+ finally
+ {
+ _suppressAutoStartToggleEvents = false;
+ }
+ }
+
+ PersistSettings();
+ }
+
private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e)
{
if (_isWeatherSearchInProgress || WeatherCitySearchTextBox is null || WeatherCityResultsComboBox is null)
@@ -1942,6 +2012,15 @@ public partial class MainWindow
};
}
+ if (AboutStartupSettingsExpander is not null)
+ {
+ AboutStartupSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
+ {
+ Symbol = Symbol.Play,
+ IconVariant = variant
+ };
+ }
+
UpdateThemeModeIcon();
}
diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml
index 2f760f4..d37f7a3 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml
+++ b/LanMountainDesktop/Views/MainWindow.axaml
@@ -1390,6 +1390,19 @@
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs
index 3ea0c65..8d1957e 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml.cs
+++ b/LanMountainDesktop/Views/MainWindow.axaml.cs
@@ -90,6 +90,7 @@ public partial class MainWindow : Window
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
+ private readonly WindowsStartupService _windowsStartupService = new();
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ComponentRegistry _componentRegistry = ComponentRegistry
@@ -151,6 +152,9 @@ public partial class MainWindow : Window
private string _weatherExcludedAlertsRaw = string.Empty;
private string _weatherIconPackId = "FluentRegular";
private bool _weatherNoTlsRequests;
+ private string _dailyArtworkMirrorSource = DailyArtworkMirrorSources.Overseas;
+ private bool _autoStartWithWindows;
+ private bool _suppressAutoStartToggleEvents;
private string _weatherSearchKeyword = string.Empty;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
@@ -225,6 +229,8 @@ public partial class MainWindow : Window
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
+ _dailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
+ InitializeAutoStartWithWindowsSetting(snapshot);
InitializeDesktopSurfaceState(snapshot);
InitializeDesktopComponentPlacements(snapshot);
InitializeSettingsIcons();
diff --git a/LanMountainDesktop/installer/LanMountainDesktop.iss b/LanMountainDesktop/installer/LanMountainDesktop.iss
index e1b1da2..8f8bc9b 100644
--- a/LanMountainDesktop/installer/LanMountainDesktop.iss
+++ b/LanMountainDesktop/installer/LanMountainDesktop.iss
@@ -48,6 +48,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
+Name: "startup"; Description: "Launch LanMountainDesktop when you sign in to Windows"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
[Files]
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
@@ -56,5 +57,53 @@ Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+[Registry]
+Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Tasks: startup; Flags: uninsdeletevalue
+
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+
+[Code]
+const
+ WebView2RuntimeKeyPath = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
+ WebView2RuntimeDownloadUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
+
+function IsWebView2RuntimeInstalled(): Boolean;
+var
+ VersionValue: string;
+begin
+ Result :=
+ RegQueryStringValue(HKLM64, WebView2RuntimeKeyPath, 'pv', VersionValue) or
+ RegQueryStringValue(HKLM32, WebView2RuntimeKeyPath, 'pv', VersionValue) or
+ RegQueryStringValue(HKCU64, WebView2RuntimeKeyPath, 'pv', VersionValue) or
+ RegQueryStringValue(HKCU32, WebView2RuntimeKeyPath, 'pv', VersionValue);
+end;
+
+function InitializeSetup(): Boolean;
+var
+ ErrorCode: Integer;
+begin
+ if IsWebView2RuntimeInstalled() then
+ begin
+ Result := True;
+ exit;
+ end;
+
+ if MsgBox(
+ 'Microsoft Edge WebView2 Runtime is required for the browser component.'#13#10#13#10 +
+ 'Click "Yes" to open the official download page. Install it first, then run this installer again.',
+ mbConfirmation,
+ MB_YESNO) = IDYES then
+ begin
+ if not ShellExec('open', WebView2RuntimeDownloadUrl, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode) then
+ begin
+ MsgBox(
+ 'Unable to open the download page automatically.'#13#10 +
+ 'Please open this URL manually:'#13#10 + WebView2RuntimeDownloadUrl,
+ mbError,
+ MB_OK);
+ end;
+ end;
+
+ Result := False;
+end;
diff --git a/README.md b/README.md
index ff59934..9b2fad8 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
`LanMountainDesktop` 是一个基于 Avalonia 的桌面壳层项目,目标不是“做一个启动器”,而是把桌面变成可编排的信息与交互空间。
+> ⚠️ **注意**:该项目使用 Vibe Coding,介意勿用。
## 项目定位
- 以网格化布局组织桌面组件,支持多页桌面与组件自由摆放。
- 提供顶部状态栏 + 底部任务栏的桌面框架,强调信息密度与可读性平衡。