From 469f7e11325854d980981be88065a854e7ecd761 Mon Sep 17 00:00:00 2001 From: lincube Date: Thu, 5 Mar 2026 12:34:39 +0800 Subject: [PATCH] 0.3.12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 --- .github/workflows/release.yml | 21 +- .gitignore | 1 + LanMountainDesktop/App.axaml.cs | 30 ++ .../Assets/Weather/HyperOS3/ATTRIBUTION.md | 17 ++ .../HyperOS3/Icons/icon_hero_moon_soft.png | Bin 0 -> 18222 bytes .../HyperOS3/Icons/icon_hero_sun_soft.png | Bin 0 -> 21454 bytes .../HyperOS3/Icons/icon_mini_cloudy_soft.png | Bin 0 -> 910 bytes .../HyperOS3/Icons/icon_mini_fog_soft.png | Bin 0 -> 988 bytes .../icon_mini_partly_cloudy_day_soft.png | Bin 0 -> 1898 bytes .../icon_mini_partly_cloudy_night_soft.png | Bin 0 -> 1566 bytes .../Icons/icon_mini_rain_heavy_soft.png | Bin 0 -> 1395 bytes .../Icons/icon_mini_rain_light_soft.png | Bin 0 -> 1253 bytes .../HyperOS3/Icons/icon_mini_snow_soft.png | Bin 0 -> 1126 bytes .../HyperOS3/Icons/icon_mini_storm_soft.png | Bin 0 -> 1314 bytes LanMountainDesktop/LanMountainDesktop.csproj | 21 +- LanMountainDesktop/Localization/en-US.json | 10 + LanMountainDesktop/Localization/zh-CN.json | 10 + .../Models/AppSettingsSnapshot.cs | 4 + .../Models/DailyArtworkMirrorSources.cs | 16 ++ .../Models/RecommendationDataModels.cs | 1 + .../Services/IRecommendationDataService.cs | 8 +- .../Services/RecommendationDataService.cs | 272 +++++++++++++++--- .../Services/WebView2RuntimeProbe.cs | 126 ++++++++ .../Services/WindowsStartupService.cs | 75 +++++ .../Views/Components/BrowserWidget.axaml | 18 +- .../Views/Components/BrowserWidget.axaml.cs | 61 +++- .../DailyArtworkSettingsWindow.axaml | 54 ++++ .../DailyArtworkSettingsWindow.axaml.cs | 97 +++++++ .../Components/DailyArtworkWidget.axaml.cs | 132 +++++++-- .../Components/ExtendedWeatherWidget.axaml | 2 +- .../Components/ExtendedWeatherWidget.axaml.cs | 157 ++++++---- .../Components/HourlyWeatherWidget.axaml | 2 +- .../Components/HourlyWeatherWidget.axaml.cs | 134 +++++---- .../Views/Components/HyperOS3WeatherTheme.cs | 46 ++- .../Components/MultiDayWeatherWidget.axaml | 2 +- .../Components/MultiDayWeatherWidget.axaml.cs | 137 +++++---- .../Views/Components/RecordingWidget.axaml.cs | 73 ++++- .../Views/Components/WeatherWidget.axaml | 2 +- .../Views/Components/WeatherWidget.axaml.cs | 134 +++++---- .../Views/MainWindow.ComponentSystem.cs | 55 ++++ .../Views/MainWindow.Localization.cs | 7 + .../Views/MainWindow.Settings.cs | 85 +++++- LanMountainDesktop/Views/MainWindow.axaml | 13 + LanMountainDesktop/Views/MainWindow.axaml.cs | 6 + .../installer/LanMountainDesktop.iss | 49 ++++ README.md | 1 + 46 files changed, 1535 insertions(+), 344 deletions(-) create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png create mode 100644 LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png create mode 100644 LanMountainDesktop/Models/DailyArtworkMirrorSources.cs create mode 100644 LanMountainDesktop/Services/WebView2RuntimeProbe.cs create mode 100644 LanMountainDesktop/Services/WindowsStartupService.cs create mode 100644 LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml create mode 100644 LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs 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 0000000000000000000000000000000000000000..a17b056ee7f88486126d1c5fce0dcecda01c9b7e GIT binary patch literal 18222 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 G&;$S(+Lc-W literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..764884df146cc66fc820118ffa31ffc9478f09bc GIT binary patch literal 988 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-49ta|E{-7;ac^($_n+)2bG*=OW{FW>o{C2g4cZvn#H#^tAt_oJ$W^Q}Yc$u>O{)00g@~wJZcJ_*||Du16Rc_}v z-Yqmd|0Z*H<}KaxD*sB~JpQ!F;P{&-FP7cBY7}WMR#_u^@d^}O` zjO1mL=&3&E{&(5Vvo^_#4&CZsJGtcX#K*hh8Mm)A`T9KQ`m6oN<+kTe?^;zCo3&Ya z`&-}TMrU^)nsHT6U$R~;_qKb~cJ0eQ|Gn89?|tzf=dA5Bo-Mvq^)o49$2^m-dkoHo zEqqSO7-vFY4&@kh;aNX z({SfGpMvs#b$654howpueL-Alik%}*sV@lbtp z=AtvFJ6DP9oMp0pTdeOxuO}wYrEMmrJY5D<=NY%QlHFfLFo)N_Xz!#G2`AT2jPqN3 zgGcJ5g78xR^P9XTdV7BDTdh&^c+IVE(*!c#2rFscRt@+6;g`8qAi= zxO{O*K;rT@F+Ef3&Gu?}o>J9(@pz?(XjaO^28hVSy_H-7h9zWK`xcMF*DKt3ws2b&9P`gxzopr0Ep|$ AZvX%Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3ee7efe732e00e78988981de8957be130af0a239 GIT binary patch literal 1898 zcmah~X*3%M7mXT>wbP~xu~soE_M#mmh&5EG5r$S$cc{>3BZ3T*7LS^=}e5^-M0sx>w{SK+Y zn1|s2fUE@W;^dDw4*#J81e<&QU z^plhP@9vp3M-;)q(ac4a#)v--_8pNbbF8TPyI<8!pM{$pe8G+53)7=33lUsBob7YF z@}Az7CGbPfH`|nT*5KkooBZA`|6VpwLPTtPsn%|)J%oAhOy!PV2^1n4I>BjR<3;|J zrS>~dwAAs#-&C1Hyta0qhK4>(T6wi1ZKDuTC-=A>i%|0k@>vRhgNF?^NXuB;w7UCj zIb1=jTt2ihDgZ`BVSMZ%{-}d9YH4Tjnm(!p^^I*|##(&-RDa*=t$C+YMKcS0C02|n zF>E7SAQZRB4DK$v@XVU%_w80f9Z%iVV#XU|RvO*pB+EL*>~{mx8voU{SSKCz4Gho5@aV@OQfNGZTr6I@2<)OjAG zx|bQ6v6=bTG578M8o{PUkt8uDJs-Y4 z(li|cSu-S}8i50;k&}wXBqn!u2yWIC*$}{N>>5L~WS5rIspEB9Z>MN?b2y!o1GEhw z1|jF010;V|&3`IE=Ad(DT*Q2JYQ3b^Un6+U{OqDtFs)|3Hvo%O;dcbE051v>hTk`G@cCtsUVojJg|b~O9B->UzsD2u?J!YHy5MPx|UttrHsi#jwU z=4FCJx@)NWYp|Ul$Pru|0u7l5Qeb9Hxp%5^u{QXsdEV#gYzaeLx0zQ{e(*Qt^8?aj zyiEnua;I~?-z1j(W0v+Q^U53qc zEPQn2Q~?A^G$9!EezAR#Hi1yI^3vlZtj#IM)6O3|hrC02n>nMau<%RISQKHqLp+Z) ziB1<33#_LfOrcsQ_Z_&hq#7uxeZ@U7*YBpr+A6s-KFN=SCvUhNCmZM8ePGi)BJ#oI zoaQQx_f9w8k9l?yA}SzCHqF)I_Yvxdz|(GXw7begy{-}L3~?T{NpHvL?u@52^K-}?)% zisR+31h`KG5XJ2avujUhRtuW`nZX5BX4vziVEA7li;GP z!5ws(LFVS&9MVsEI9=adf_L$j*C4}6He%o+gKFvx6%>n!mkJZjYXSME%O@hwS~=ur z(5Ok52-@qYN&^Tt8@G_0O-UgY)&_>l)rMU%yH&GmAwE;_d}Q*oe@l)n@p6>Go-W@uAFozpxZH}$EVtov7Y#DzrS@ZjN9g1Q4e&~N-~8_ zsagO>)Z#lz1r^$Xi&yzjMz^keL5A-N&J1?%QVm`6>b@>cb2zAiL{2Ihn?fyRgjCGb zgNAyMm^bR4wrWNs9XJx+f=Tc~xBfQ;zH7Y>kxFY>whm|o-FTdoLa6jR@liCvm7)ZG zZ1*bC9@gsRp>An-mR78n^cF&V(T+yGjuEGg-eC^9|Z z%Rh6CR+@eZeu#QEN=F$c_oF5e%K!dO{y#zf&t#ngfKlZ6@ z!k-)@5RDxkdXiYsN;|VU3Xsx0RY1F zwif2Du|CVp_jg^~Pl$wnLPu#U*dKQ}5VTU0ZQK_)W#k%Q9`Gu^x*%IJJjF>0;bu{& zukXh6&^mYFidbWwO%Uaa_~WcZv(KBC6SEV;Ui|#{6)V`bX==t65bC-7V)N2in=Occ z{XYT5dN)VfRQD%_6llxU;Zd9QGpOtxN({HP7E^p^{Uj+tI2 zc=VgG9Zq3#Yu!4NRF0{?gyzi7T*xnuSqUxf^;Mwpe0GCRSJ%xjd%t!$UY*+8oB;y56zyI%jQN_txe9slv{YkZ?{?$lFoLku5q{1 zG3>zO2c!|x!`&zVajA;e_uYh^DVXEl7`<*&!^5zR!H#8%bA{WS!qnD5XjKttVwnU^ zswITTMF6-NFnSAxH{$y?Zk;evK?gyJurY@2uW2YS?ny9HZg!#zH|sqj|9f;My3%ux zp?rO0wh^4vMj+4e!)gO{9U6Trp@GtsX=3=cz~^?O!$~Xo;{(Tvcy=8I^`lA#8U%Ni z+MZ4B;F+4hxdD~;7raWi4CMP*5YUe`*tb{c${TW+S)Bnm*>AHJSf?sv>8l^1!b4U! znCB=1BI-=p_@hp)vsJ1x71vi#>tjg8#kgfKBOP3?eDQ{1G#*`5w;N6KnN8s=uX+1)l%jONYbvEv2(t;Ul3 z^rNJEF_JRdD8nH6+4fPrcxDLf6xpoWB}F(4C#mH+>m%(20hbkrSHZWJ%^&ZGfbzD% zcvRU$o-O3~lSp@#kc?+Y?64rsksds6TV|3cPKB40Zk)?J`RUpBgfEzs#outlZktO} zG#|87qL{T{99VG}lV&HY8#UYt#pr#YKD2y8E=y7mk1?M^i%Q%y>v#$(G}ies1(fl+ z?J+;Os%7*bNixLvhxzsawGi9Z2Yw>9luO_>u zZxPa7=X3HaAQ-h7DKU;)e-O7P?+Y~a*2JR>LU_s|8cTSj>~P7#-c(d#Do-u=*9}G8 zyL1@{CHmk|8pRG{&^brEA3CJq41*+jUiB^N!fOAVR)--*P-Zt4N|YTrRB6n;p?}C4 zDb^24HHcsFjLvc@5=#^;wIJSts66Za_<~CMY7r3zA!;_RuqvN&?ktv<+5@k6vo0ow z;e>)Md{|w7dD*pKuB$W4DxT-9BmGKl zt2evczmSEv_E8jWYYg;a$NR=O6JrZoVq1$LOxT);IG!MyAoDX@_ysfG!atl vH9>99-BCYqHGzF~aEf;L!2cg=NP@t9AUWdL35u5i_%LH@3Ad=a>X-5_0=wa5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a9a4d039e8b9e5cc9a29a73cdef133c17c680a7 GIT binary patch literal 1395 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-46GcUE{-7;ac^(gXGn+3uov{-yrp}0j%JG1 zTrY7)ap!3c?R=6pB_{%21ic9QFd@m3WzQ5J#o!Mi`7gtAG4&@8a27tG@~yW;-vwp5i9b>He7YkV&b!{IQpl4FuV*tk|w-x<(;=%C|GwuZ1U? zU7guH{he8<%H@?`TqU}VyWR&aQu*0?`@yn?b-g7=zB6UD9WH#YojU#9yvz^nv(hXi zOWB@WyW!7k?{R5=X>Wn$LV@TzO1pnfv{=$Tzu%|5tjTYC_Rr#0g}&{j-R^3Ee;-Wj++)on;6M{wP}e?n;xiO73WZG86=}GXa91GmpzBs z)^68lMjw=a%Zc(gFERU)EFUSOUEbg*kpEh&`^@oE-VM(?b?+uk{25UxY|-)LFQYDB zq=fZ;wpHpe%=&iHJsA^EeEq0XCs6Y8=?}yfYPP}r#^z^Y0 zn-at7XIv@a-K@JOW=5&och=|47q;61*{!k%3bWXMIPh))@l5YA>M`H{_Ms?o+OMb| zbrM%{`1QrX33%BXv!{>u3Rjw(+xTZ{e4O;_xcz~XpFRC~^@{(~`G3Bx)i6E~x}tvV z)#y+AZ?audIJDgT^#6oQPZ(COxF7lT@}J*7pZRXeVfN+Q(DiHXw>K{Pu9>c{Q<}Z! zq>~zZgzMjPRkzOV4{5$+TCjfV|0A0a_#?J=GrMm4WU=wn`B*?xUF&dC;fX%CcI>vw)TGJ`Pn~h zb3bc*omO)K<`^l{|2x-JeyN&n9(e-nt;{Pc%6G9{i~cFU$5Zh#vrhE>%Vqta6 zM}B*D5~}2n`L1UdU;qE0#ywLeLG-J&x9JS2rL{S$*$%_am0h_1^(=Sa+U;kOqmM$g zeHFfW#kAn2eBKPOH<$mv=r+&(w|V`6*&;@^2R41R?q~Z@w6F0K@BY0I2Uot**!$}- zuW7*7x%2MFo%q6V`WnBF>8E0;?OM!#?)+M)!Xt3ZzyYNsYIvQSQ^UglsJFqppVby5 N<>~6@vd$@?2><}GmRkS- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..08d5e688b659e241a3fa5d5bdb4bb69adc5fe188 GIT binary patch literal 1253 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1bRtCK~#8N?b|EpE#2HHbSj-n7eg11fesxyc+61fQs`3fQaXAGfr2Y%FrMnMqqdA51;v)P zXmg>lWJ{;}e|oRat2{38`OP0ix?1<%_X9tOb++X9ecpTTxx={!000000000000000 z000000000000000000000000$%_3tqilTTtGv=t6&3cm1k>h+eJ1mUZHpV>AA4QYd zVoK5bj$*%0y0Q>9^{vn(d7eKiW~QGPvvAoI#jKlU*_Zm(SZ8Hf-j{8B*Hy?RGfd?Y zq922GWUt$D)<1U?Z*|FMhjx+aM`B$_Q9jCx;yBBTuBI+R%w^Qo2c!Kbj8^H`z`; zPK(EJmLJMQy@+MP-jZ`kuku=Rk{8Ez=g^_In>~|dD}5TL`MD%)Q<5zENvENmBDpro z89$6Ko3YPBbjtiP zjq-jF$tovrPms=^bb4hJMH|k+l3N*viGPoZCsDRJ3Dc0gJ6t&FHbWJjmSZ-c?$z6`eKnuNBdHO0R+8buqI)3ooJ zj8)%g5-jQ8k)mo307-EjnCOol9SEF^)suqr*iN3!vrf0(dDC0usG2W;t0?q6 zI$TuQ+rob)(WdXw;Q{~v0C;cvee>0`v+`^G5^d5sEq~eXm!(aoZw~b3rLdqHLW=** zb@8wBd*A%1hd)Ri3ko%nGxhoF#h(}Lr2JVAFU+-|_D{|CdKl|u|D^olRU%ERP@*QTV%hFp4=>E_9Jk-k#CE6AZ^627 zb?VC&%hmiz_y30;#yVMbv1tApq>fuZn!g5F^{pSxUxUqqm~AgjLhvuOSr#I;o~ zb?Zm-*BrJe)j}=1Rnz=6i0fLt%vMeF*DwdY@?+cW|5vP2GuS&TKh?tvtK(r3YBpa4 za?Q6{ruf#XcN8?ts&^DLkX;0}5tN0nNAUpw0000000000007{*d-whYBe5;-g}rq( P00000NkvXXu0mjf#W-FC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e8ae667f174ba0ab49da3997f6d567e010e3c045 GIT binary patch literal 1126 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-49vehT^vIy;@;lUFOYT@IbL{p!=aX$tYuw? zOoWpp!ZIW#%sk+<*=h5E1$}{Mqcqfvoed9iTq#VG^4?am%f7mL`>W_`>vd0jgWPxj z+x-3SnLlYlm7ku}zW?{ucN@@3JfOikP-634le4d{%{aU)w|&x`$;o%OvG*G8J~m_d zyZMg{Hy@s|u;1qJKl_)b8!r?u{V*eAZM^AP$(PH{n)_bf@^{y|_#KxjPE0%fRCs64 z``>Ik6V<%(6yIfk5V-k!+nle==RX{B-Om0^p*X&?-Kp(UbKb1SQRbCzPB<34*eAx{ zXsi76)17dmIlH}o->bEKH=8HP+mSx? zPXFp%C*Nx?jj1iVsdQ@oca>V{ol3oT&g=iz+yA(F>8!gw)4jI`OMc$7&Ntuo+nVk6 z`dvx4O@A3KjSu~G^(oWFQ+6k(ud0l__RB%)>%u8*wW`Y}J-n6PufEH2TR7kLFL|5O zH^q3}pXxQ=>yzHnRj=b?q<5{HP`!B0$H`8!EKp%dge_G+eI_{a zNlM@~%a7`-U+v2duM%_J`#jntvbR?LQPi|)t_$5G72Pd=RvDH4TxP6!dC`q;deiQl z&{=<}I@Cu0)jgYV%b#VFY#7p7Z++FBcE@MZ_6D8H*87f5n(g^Xu^?>bbK^*vpJ|uw zu-!6`+3Yj-oY*FhPft@g_jBwzaphmD&ZHW}NL!y@p&vrz^7e46hfjR2a(z|F-1Jx1 zKd(7;GHcoAtn*tnavy*E(Wz7RBHJna^2~?-v#oTuofpq~lI^y9Q-JW!_xnq8Uy4-= z_&j@BV5R?*=S522TjPn@?&dddH_EWQSE-!-sdC!q`-lF&*3m9M_bl}O^)(`or}dUz zuDtu%RA=jsr%88L+KA6Nd&=f3kS)vZaQr6ogUu!yAl}`lj0J&DRvy0V$LQ?Jzx#o|bZeDc%Jp0M_gC{?!fB$$a>8fjjyZW>K#r>zBhMu`lyJurWso;&T z6V^SlyBD!$7l+QZ1t29G*+f_CF?E}+0Ln(JaXh-J`#=(}R#@zvOJD11N)@)Be^qA} z7R$12m;8N`>%V^jRn5p`>uhn~Z z%s;*Ce8LhxD^_Wkq2Z;v;=QrhtPZ_SOr6>D~(FfEk; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3711dc9eaedb1c78ff470cab68e8d705c01e1157 GIT binary patch literal 1314 zcmV+-1>O3IP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1h+{#49z@;rZ3Ov+APOv0O{C?<_8%f8Z6W09Snp03Jee8)DBH%u>; zTZn!P7LmDc%c|eE4BqV}pX{1RrXPt#A%pTsUKFh?FT%TyOoBcASS$jwBIWYjNM&$7 zUM4T~<8j|)Fm~*NbhpX#@_YRia9`52{6a3vJ=*}?b>wqj^E`0R61g8ISrOh>V!8T+ zvPG1ywjxW=+*bFExT~AV$53{UXug0u;b3BToHSOqSN+U_~N7Dx4 zJo6+Ur^S;v%Xg)tUdPg5m*hy&uDrKA$%~e`a%j{2W>&IXN?*ijek2KfN|I$ywHy2t z$-Plld@qvD@HqZ$DZBFYECau1=?pJpu7KJ#?lXfQWyRmNj_^5go^NOuxh+icJ-PXX zk3htZrU18&SLsUyUIO5P(F}G_Qr7*5_^IqYiM`5XB=hQwnmflG4&Ab zGPjJQyc0w+%gN;l(rKmBDg7u~vo@Ao$S_RYD;1BTY<(1_A-OtEPJm&QZrU14F2>1Q zKoH~`w#Jf+aq=N>n3NBP(l~Nu+}sA53fC}5kA`DuB$=PU;X=?!knWsFBgvcuS=;GS za9@1sDt8Tn^w}UtLUKO+QL?RFdGv+!BRLA-H1 zN*cD#axvO$RP{&Mx~;=pi#8WkNAg^7Jd8uvqs>JX6Tow30=$(Day{ByR81$qNff#s zZ7!b_DWBWMfSerdz)$2+h#|hAAv>ozT|s*=~6dX5|xtgZDbI)wm#R>W06@( zzBh{5LYmu&o*s+NR`R`3i@DU z>S?hot|<9lBehrT^&L02M0{50`Bc9Ru9{OK?y}eSz?{`TFz@TPz-86dZyp@u=HqYl zFkEI^{n0`F2R#heRabx1to^Eo;JWJSH`_)%1Ru<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,介意勿用。 ## 项目定位 - 以网格化布局组织桌面组件,支持多页桌面与组件自由摆放。 - 提供顶部状态栏 + 底部任务栏的桌面框架,强调信息密度与可读性平衡。