custom background image

This commit is contained in:
Lightczx
2024-02-06 17:31:19 +08:00
parent ef6d88ba2e
commit 3f6efe2e24
14 changed files with 233 additions and 15 deletions

View File

@@ -33,6 +33,16 @@ internal struct Bgra32
/// </summary>
public byte A;
public Bgra32(byte b, byte g, byte r, byte a)
{
B = b;
G = g;
R = r;
A = a;
}
public readonly double Luminance { get => ((0.299 * R) + (0.587 * G) + (0.114 * B)) / 255; }
/// <summary>
/// 从 Color 转换
/// </summary>
@@ -44,4 +54,11 @@ internal struct Bgra32
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
return bgra8;
}
public static unsafe implicit operator Color(Bgra32 bgra8)
{
Unsafe.SkipInit(out Color color);
*(uint*)&color = BinaryPrimitives.ReverseEndianness(*(uint*)&bgra8);
return color;
}
}

View File

@@ -38,4 +38,43 @@ internal static class SoftwareBitmapExtension
}
}
}
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double sum = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
sum += pixel.Luminance;
}
return sum / bytes.Length;
}
}
}
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double b = 0, g = 0, r = 0, a = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
b += pixel.B;
g += pixel.G;
r += pixel.R;
a += pixel.A;
}
return new((byte)(b / bytes.Length), (byte)(g / bytes.Length), (byte)(r / bytes.Length), (byte)(a / bytes.Length));
}
}
}
}

View File

@@ -18,4 +18,9 @@ internal static class RuntimeOptionsExtension
{
return Path.Combine(options.DataFolder, "ServerCache");
}
public static string GetDataFolderBackgroundFolder(this RuntimeOptions options)
{
return Path.Combine(options.DataFolder, "Background");
}
}

View File

@@ -2528,6 +2528,12 @@
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官网</value>
</data>
<data name="ViewPageSettingOpenBackgroundImageFolderDescription" xml:space="preserve">
<value>自定义背景图片,支持 bmp / gif / ico / jpg / jpeg / png / tiff / webp 格式</value>
</data>
<data name="ViewPageSettingOpenBackgroundImageFolderHeader" xml:space="preserve">
<value>打开背景图片文件夹</value>
</data>
<data name="ViewPageSettingResetAction" xml:space="preserve">
<value>重置</value>
</data>

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace Snap.Hutao.Service.BackgroundImage;
internal sealed class BackgroundImage
{
public BitmapImage ImageSource { get; set; } = default!;
public Color AccentColor { get; set; }
public double Luminance { get; set; }
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using System.IO;
using Windows.Graphics.Imaging;
namespace Snap.Hutao.Service.BackgroundImage;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IBackgroundImageService))]
internal sealed partial class BackgroundImageService : IBackgroundImageService
{
private static readonly HashSet<string> AllowedFormats = [".bmp", ".gif", ".ico", ".jpg", ".jpeg", ".png", ".tiff", ".webp"];
private readonly ITaskContext taskContext;
private readonly RuntimeOptions runtimeOptions;
private HashSet<string> backgroundPathMap;
public async ValueTask<ValueResult<bool, BackgroundImage>> GetNextBackgroundImageAsync()
{
HashSet<string> backgroundSet = SkipOrInitBackground();
if (backgroundSet.Count <= 0)
{
return new(false, default!);
}
string path = System.Random.Shared.GetItems(backgroundSet.ToArray(), 1)[0];
backgroundSet.Remove(path);
using (FileStream fileStream = File.OpenRead(path))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream.AsRandomAccessStream());
SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight);
Bgra32 accentColor = softwareBitmap.GetAccentColor();
await taskContext.SwitchToMainThreadAsync();
BackgroundImage background = new()
{
ImageSource = new(path.ToUri()),
AccentColor = accentColor,
Luminance = accentColor.Luminance,
};
return new(true, background);
}
}
private HashSet<string> SkipOrInitBackground()
{
if (backgroundPathMap is null || backgroundPathMap.Count <= 0)
{
string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder();
Directory.CreateDirectory(backgroundFolder);
backgroundPathMap = Directory
.GetFiles(backgroundFolder, "*.*", SearchOption.AllDirectories)
.Where(path => AllowedFormats.Contains(Path.GetExtension(path)))
.ToHashSet();
}
return backgroundPathMap;
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.BackgroundImage;
internal interface IBackgroundImageService
{
ValueTask<ValueResult<bool, BackgroundImage>> GetNextBackgroundImageAsync();
}

View File

@@ -321,7 +321,7 @@
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231219000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.6.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.14.0">
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.15.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -12,14 +12,15 @@
mc:Ignorable="d">
<UserControl.Resources>
<Thickness x:Key="NavigationViewContentMargin">0,44,0,0</Thickness>
<Thickness x:Key="NavigationViewContentGridBorderThickness">0,1,0,0</Thickness>
<x:Double x:Key="NavigationViewItemOnLeftIconBoxHeight">24</x:Double>
<SolidColorBrush x:Key="NavigationViewExpandedPaneBackground" Color="Transparent"/>
<Thickness x:Key="NavigationViewContentGridBorderThickness">0,1,0,0</Thickness>
</UserControl.Resources>
<Grid Transitions="{ThemeResource EntranceThemeTransitions}">
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}" Transitions="{ThemeResource EntranceThemeTransitions}">
<Image
Opacity="0.65"
Source="ms-appx:///Resource/TestBackground.jpg"
x:Name="BackdroundImagePresenter"
VerticalAlignment="Center"
Opacity="0"
Stretch="UniformToFill"/>
<NavigationView
@@ -109,4 +110,4 @@
<shv:InfoBarView/>
</Grid>
</UserControl>
</UserControl>

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Service.BackgroundImage;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Page;
@@ -14,6 +16,7 @@ namespace Snap.Hutao.View;
internal sealed partial class MainView : UserControl
{
private readonly INavigationService navigationService;
private readonly IBackgroundImageService backgroundImageService;
/// <summary>
/// 构造一个新的主视图
@@ -24,6 +27,9 @@ internal sealed partial class MainView : UserControl
IServiceProvider serviceProvider = Ioc.Default;
backgroundImageService = serviceProvider.GetRequiredService<IBackgroundImageService>();
RunBackgroundImageLoopAsync(serviceProvider.GetRequiredService<ITaskContext>()).SafeForget();
navigationService = serviceProvider.GetRequiredService<INavigationService>();
navigationService
.As<INavigationInitialization>()?
@@ -31,4 +37,35 @@ internal sealed partial class MainView : UserControl
navigationService.Navigate<AnnouncementPage>(INavigationAwaiter.Default, true);
}
private async ValueTask RunBackgroundImageLoopAsync(ITaskContext taskContext)
{
using (PeriodicTimer timer = new(TimeSpan.FromMinutes(5)))
{
do
{
(bool isOk, BackgroundImage backgroundImage) = await backgroundImageService.GetNextBackgroundImageAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
await AnimationBuilder
.Create()
.Opacity(to: 0D, duration: TimeSpan.FromMilliseconds(300), easingType: EasingType.Sine)
.StartAsync(BackdroundImagePresenter)
.ConfigureAwait(true);
BackdroundImagePresenter.Source = backgroundImage.ImageSource;
double targetOpacity = (1 - backgroundImage.Luminance) * 0.8;
await AnimationBuilder
.Create()
.Opacity(to: targetOpacity, duration: TimeSpan.FromMilliseconds(300), easingType: EasingType.Sine)
.StartAsync(BackdroundImagePresenter)
.ConfigureAwait(true);
}
} while (await timer.WaitForNextTickAsync().ConfigureAwait(false));
}
}
}

View File

@@ -455,7 +455,7 @@
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGachaLogHeader}"/>
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGachaLogHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleHeader}"
@@ -472,7 +472,7 @@
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingStorageHeader}"/>
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingStorageHeader}"/>
<cwc:SettingsExpander
Description="{Binding DataFolderView.Size}"
Header="{shcm:ResourceString Name=ViewPageSettingDataFolderHeader}"
@@ -510,6 +510,11 @@
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsExpander.Content>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
Command="{Binding OpenBackgroundImageFolderCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingOpenBackgroundImageFolderDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingOpenBackgroundImageFolderHeader}"
IsClickEnabled="True"/>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingResetAction}"

View File

@@ -272,6 +272,12 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
}
}
[Command("OpenBackgroundImageFolderCommand")]
private async Task OpenBackgroundImageFolderAsync()
{
await Launcher.LaunchFolderPathAsync(runtimeOptions.GetDataFolderBackgroundFolder());
}
[Command("DeleteUsersCommand")]
private async Task DangerousDeleteUsersAsync()
{

View File

@@ -385,8 +385,12 @@ internal static class ApiEndpoints
return $"{SdkStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel:D}&sub_channel_id={scheme.SubChannel:D}";
}
// https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?filter_adv=true&key=eYd89JmJ&language=zh-cn&launcher_id=18
// https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?key=eYd89JmJ&language=zh-cn&launcher_id=18
public static string SdkStaticLauncherContent(LaunchScheme scheme, string languageCode, bool advOnly = true)
{
return advOnly
? $"{SdkStaticLauncherApi}/content?filter_adv=true&key={scheme.Key}&launcher_id={scheme.LauncherId}&launguage={languageCode}"
: $"{SdkStaticLauncherApi}/content?key={scheme.Key}&launcher_id={scheme.LauncherId}&launguage={languageCode}";
}
#endregion
#region Hosts | Queries

View File

@@ -2,9 +2,10 @@
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Snap.Hutao.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
<application xmlns="urn:schemas-microsoft-com:asm.v3" xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:dpiAwareness>PerMonitorV2</ws2:dpiAwareness>
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly>
</assembly>