Compare commits

...

38 Commits

Author SHA1 Message Date
Zhe Fang
0e1f487ac1 chores: add .gif in readme.md 2025-10-24 18:21:19 -04:00
Zhe Fang
3c8775d2cd chores: reduce .gif fps to 24 2025-10-24 18:09:04 -04:00
Zhe Fang
7c39214f2d chores: replace .mp4 with .gif in docs 2025-10-24 18:03:27 -04:00
Zhe Fang
b7952f5eef chores: edit .csproj content to make it possible for publishing to ms store, update version code 2025-10-24 16:45:26 -04:00
Zhe Fang
8c0d3667e4 chores: add contributors section in settings page 2025-10-24 16:21:09 -04:00
Zhe Fang
e1f900a9e6 chores: add help text in switcher 2025-10-24 15:57:33 -04:00
Zhe Fang
212c1fbcdd chores: add lyrics window switcher entry for top command area ands system tray, add animation when open and close switcher 2025-10-24 15:43:36 -04:00
Zhe Fang
2cb04fa1b7 feature: add support for song title and artist font size automatic adjustment 2025-10-24 14:08:28 -04:00
Zhe Fang
8ab3a53a38 chores: add video width in docs 2025-10-24 13:15:09 -04:00
Zhe Fang
3b4d5b9668 chores: update docs 2025-10-24 13:12:29 -04:00
Zhe Fang
2409165b91 chores: update docs 2025-10-24 12:31:55 -04:00
Zhe Fang
fe1eb2882e chores: update docs 2025-10-24 12:31:15 -04:00
Zhe Fang
51dd58fc74 docs: update Google Drive link 2025-10-24 07:53:18 -04:00
Zhe Fang
0f5f663b32 chores: round value to 1 decimal place in ExtendedSlider control. 2025-10-24 07:48:53 -04:00
Zhe Fang
4815c32dba chores: default to hide in system env in docked mode, desktop mode and fullscreen mode templates 2025-10-24 07:17:49 -04:00
Zhe Fang
3f5a0a0d73 Merge pull request #120 from Storyteller-Studios/dev
chores: Force Dpi to 96
2025-10-24 07:10:48 -04:00
Raspberry-Monster
1b34b28fbe chores: Force Dpi to 96 2025-10-24 16:17:08 +08:00
Zhe Fang
fe4d67c1f1 chores: adjust SongInfoFontSize for fullscreen template mode 2025-10-23 22:42:51 -04:00
Zhe Fang
1ae00257a1 chores: update template mode config (to match with fluid layer enabled by defualt) 2025-10-23 22:26:58 -04:00
Zhe Fang
814de1a4a5 chore: enable fluid layer by default 2025-10-23 22:23:47 -04:00
Zhe Fang
28b568e7a4 chore: add accelerator Escape for close button in LyricsWindowSwitchControl 2025-10-23 22:19:51 -04:00
Zhe Fang
786d23b320 chores: add animation when switching color picker type, enable line fade by default, default to MedianCut, edit displayed text for color picker type for better direct understanding for users. 2025-10-23 21:47:37 -04:00
Zhe Fang
a394527f80 Merge pull request #119 from Storyteller-Studios/dev
Support Multiple Palette Generation Algorithm and Remove ImageSharp
2025-10-23 21:09:12 -04:00
Raspberry-Monster
80422376c3 chores: Use Bitmap's Dpi First 2025-10-24 00:31:24 +08:00
Raspberry-Monster
5114c83843 chores: Reduce Unnecessary Buffer Copies 2025-10-23 23:16:26 +08:00
Raspberry-Monster
835e0d34fc chores: Bump Dependencies, Remove ImageSharp 2025-10-23 22:38:31 +08:00
Raspberry-Monster
7ab833a53a chores: Support Multiple Palette Generation Method 2025-10-23 18:16:23 +08:00
Raspberry-Monster
d348a30237 Merge branch 'dev-upstream' into dev 2025-10-22 23:06:08 +08:00
Zhe Fang
d0b626c508 fix: issue related with docked mode 2025-10-22 10:52:57 -04:00
Raspberry-Monster
9debdc76f9 chores: Change ColorThief to Impressionist 2025-10-22 22:48:10 +08:00
Zhe Fang
67a45e90fa chore: update version code 2025-10-22 10:31:14 -04:00
Zhe Fang
b4c7655043 chores: move window color sampling settings item to general tab, remove useless debug writeline 2025-10-22 10:23:50 -04:00
Zhe Fang
2adc2aced2 fix: window color sampling incorrect behavior 2025-10-22 10:18:49 -04:00
Zhe Fang
e638739638 fix: improve docked mode window sizeing behaviors 2025-10-22 09:24:16 -04:00
Zhe Fang
c24213358e Merge pull request #118 from Storyteller-Studios/dev
Improve identification of NeteaseFamily and fix Lrc Parser unexpected behavior when parsing [Min:Sec:MillSec]
2025-10-22 07:58:28 -04:00
Raspberry-Monster
6e78f849c4 chores: Improve LrcParser 2025-10-22 14:10:23 +08:00
Raspberry-Monster
80444b69e0 chores: Improve identification of NeteaseFamily and fix Lrc Parser unexpected behavior when parsing [Min:Sec:MillSec] 2025-10-22 13:06:33 +08:00
Zhe Fang
78775e9bb3 Update README.md 2025-10-21 22:51:53 -04:00
96 changed files with 3110 additions and 520 deletions

View File

@@ -1,150 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateKeyFile>BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx</PackageCertificateKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx" />
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />
<Content Include="Images\LargeTile.scale-200.png" />
<Content Include="Images\LargeTile.scale-400.png" />
<Content Include="Images\SmallTile.scale-100.png" />
<Content Include="Images\SmallTile.scale-125.png" />
<Content Include="Images\SmallTile.scale-150.png" />
<Content Include="Images\SmallTile.scale-200.png" />
<Content Include="Images\SmallTile.scale-400.png" />
<Content Include="Images\SplashScreen.scale-100.png" />
<Content Include="Images\SplashScreen.scale-125.png" />
<Content Include="Images\SplashScreen.scale-150.png" />
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\SplashScreen.scale-400.png" />
<Content Include="Images\Square150x150Logo.scale-100.png" />
<Content Include="Images\Square150x150Logo.scale-125.png" />
<Content Include="Images\Square150x150Logo.scale-150.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.scale-100.png" />
<Content Include="Images\Square44x44Logo.scale-125.png" />
<Content Include="Images\Square44x44Logo.scale-150.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
<Content Include="Images\StoreLogo.scale-100.png" />
<Content Include="Images\StoreLogo.scale-125.png" />
<Content Include="Images\StoreLogo.scale-150.png" />
<Content Include="Images\StoreLogo.scale-200.png" />
<Content Include="Images\StoreLogo.scale-400.png" />
<Content Include="Images\Wide310x150Logo.scale-100.png" />
<Content Include="Images\Wide310x150Logo.scale-125.png" />
<Content Include="Images\Wide310x150Logo.scale-150.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<Content Include="Images\Wide310x150Logo.scale-400.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
<EnableMsixTooling>true</EnableMsixTooling>
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateKeyFile>BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx</PackageCertificateKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx" />
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />
<Content Include="Images\LargeTile.scale-200.png" />
<Content Include="Images\LargeTile.scale-400.png" />
<Content Include="Images\SmallTile.scale-100.png" />
<Content Include="Images\SmallTile.scale-125.png" />
<Content Include="Images\SmallTile.scale-150.png" />
<Content Include="Images\SmallTile.scale-200.png" />
<Content Include="Images\SmallTile.scale-400.png" />
<Content Include="Images\SplashScreen.scale-100.png" />
<Content Include="Images\SplashScreen.scale-125.png" />
<Content Include="Images\SplashScreen.scale-150.png" />
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\SplashScreen.scale-400.png" />
<Content Include="Images\Square150x150Logo.scale-100.png" />
<Content Include="Images\Square150x150Logo.scale-125.png" />
<Content Include="Images\Square150x150Logo.scale-150.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.scale-100.png" />
<Content Include="Images\Square44x44Logo.scale-125.png" />
<Content Include="Images\Square44x44Logo.scale-150.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
<Content Include="Images\StoreLogo.scale-100.png" />
<Content Include="Images\StoreLogo.scale-125.png" />
<Content Include="Images\StoreLogo.scale-150.png" />
<Content Include="Images\StoreLogo.scale-200.png" />
<Content Include="Images\StoreLogo.scale-400.png" />
<Content Include="Images\Wide310x150Logo.scale-100.png" />
<Content Include="Images\Wide310x150Logo.scale-125.png" />
<Content Include="Images\Wide310x150Logo.scale-150.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<Content Include="Images\Wide310x150Logo.scale-400.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
<EnableMsixTooling>true</EnableMsixTooling>
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
</Project>

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.82.0" />
Version="1.0.84.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -118,6 +118,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<LyricsSearchControlViewModel>()
.AddSingleton<LyricsWindowSettingsControlViewModel>()
.AddSingleton<LyricsWindowSwitchControlViewModel>()
.AddSingleton<LyricsWindowSwitchWindowViewModel>()
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()

View File

@@ -49,7 +49,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="3v.EvtSource" Version="2.0.0" />
<PackageReference Include="ColorThief.ImageSharp" Version="1.0.0" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
@@ -66,14 +65,14 @@
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.2" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
@@ -82,18 +81,21 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.8" />
<PackageReference Include="System.Drawing.Common" Version="9.0.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">

View File

@@ -9,7 +9,7 @@ namespace BetterLyrics.WinUI3.Constants
public static class Link
{
public const string GitHubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string FAQUrl = $"{GitHubUrl}/blob/dev/FAQ/FAQ.md";
public const string FAQUrl = $"{GitHubUrl}/blob/dev/FAQ/index.md";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";

View File

@@ -23,7 +23,7 @@
Glyph=&#xE744;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageAutoSize">
<controls:SettingsCard x:Uid="SettingsPageAutoAdjust">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.AutoAlbumArtSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard IsEnabled="{x:Bind AlbumArtLayoutSettings.AutoAlbumArtSize, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
@@ -67,14 +67,26 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<local:ExtendedSlider
Default="18"
Frequency="2"
Maximum="72"
Minimum="8"
Value="{x:Bind AlbumArtLayoutSettings.SongInfoFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLyricsFontSize"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8E9;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageAutoAdjust">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.IsAutoSongInfoFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard>
<local:ExtendedSlider
Default="18"
Frequency="2"
IsEnabled="{x:Bind AlbumArtLayoutSettings.IsAutoSongInfoFontSize, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
Maximum="72"
Minimum="8"
Value="{x:Bind AlbumArtLayoutSettings.SongInfoFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsExpander
x:Uid="SettingsPageShowTitle"

View File

@@ -43,7 +43,7 @@
Margin="3,-2,3,0"
Orientation="Horizontal"
Spacing="2">
<TextBlock VerticalAlignment="Center" Text="{x:Bind Value, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind RoundedValue, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind Unit, Mode=OneWay}" />
</StackPanel>

View File

@@ -68,6 +68,8 @@ namespace BetterLyrics.WinUI3.Controls
DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(ExtendedSlider), new PropertyMetadata(default));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(double), typeof(ExtendedSlider), new PropertyMetadata(default));
private static readonly DependencyProperty RoundedValueProperty =
DependencyProperty.Register(nameof(Value), typeof(string), typeof(ExtendedSlider), new PropertyMetadata(default));
public static readonly DependencyProperty DefaultProperty =
DependencyProperty.Register(nameof(Default), typeof(double), typeof(ExtendedSlider), new PropertyMetadata(default));
public static readonly DependencyProperty ResetButtonVisibilityProperty =
@@ -93,7 +95,16 @@ namespace BetterLyrics.WinUI3.Controls
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
set
{
SetValue(ValueProperty, value);
SetValue(RoundedValueProperty, value.ToString("F1").Replace(".0", ""));
}
}
private string RoundedValue
{
get => (string)GetValue(RoundedValueProperty);
set => SetValue(RoundedValueProperty, value);
}
public double Default
{

View File

@@ -5,6 +5,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
@@ -113,6 +114,13 @@
Value="{x:Bind LyricsBackgroundSettings.FluidOverlayOpacity, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPagePaletteGeneratorType">
<ComboBox SelectedIndex="{x:Bind LyricsBackgroundSettings.PaletteGeneratorType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageMedianCut" />
<ComboBoxItem x:Uid="SettingsPageOctTree" />
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>

View File

@@ -181,7 +181,7 @@
Glyph=&#xE8E9;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicLyricsFontSize">
<controls:SettingsCard x:Uid="SettingsPageAutoAdjust">
<ToggleSwitch IsOn="{x:Bind LyricsStyleSettings.IsDynamicLyricsFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard IsEnabled="{x:Bind LyricsStyleSettings.IsDynamicLyricsFontSize, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">

View File

@@ -137,6 +137,27 @@
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsExpander
x:Uid="SettingsPageAdaptEnvColor"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE88F;}"
IsExpanded="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard
x:Uid="SettingsPageEnvColorSample"
Header="Environment color sample mode"
IsEnabled="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.EnvironmentSampleMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleBelow" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleAbove" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleInner" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleEdge" />
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageDockMonitor" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7F4;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ComboBox ItemsSource="{x:Bind ViewModel.MonitorDeviceNames, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.MonitorDeviceName, Mode=TwoWay}" />
@@ -275,24 +296,6 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsBorderless, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageAdaptEnvColor"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE88F;}"
IsExpanded="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageEnvColorSample" Header="Environment color sample mode">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.EnvironmentSampleMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleBelow" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleAbove" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleInner" />
<ComboBoxItem x:Uid="SettingsPageEnvColorSampleEdge" />
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageDragArea" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB41;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.TitleBarArea, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageTitleBarAreaNone" />

View File

@@ -16,16 +16,27 @@
VerticalAlignment="Center"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
CornerRadius="12">
<FontIcon
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Top"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE8AB;" />
<Button
Margin="12"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="Button_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE653;}"
Style="{StaticResource GhostButtonStyle}" />
Glyph=&#xE73C;}"
Style="{StaticResource GhostButtonStyle}">
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Escape" />
</Button.KeyboardAccelerators>
</Button>
<ListView
Margin="48"
Margin="48,56"
ItemsSource="{x:Bind ViewModel.AppSettings.WindowBoundsRecords, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.LiveStates.LyricsWindowStatus, Mode=TwoWay}">
<ItemsControl.ItemsPanel>
@@ -46,5 +57,20 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Spacing="6">
<FontIcon
Margin="0,1,0,0"
FontFamily="{StaticResource IconFontFamily}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE946;" />
<TextBlock x:Uid="LyricsWindowSwitchWindowHelp" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
@@ -32,13 +33,21 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<LyricsWindowSwitchControlViewModel>();
}
private void Grid_Tapped(object sender, TappedRoutedEventArgs e)
private async void Grid_Tapped(object sender, TappedRoutedEventArgs e)
{
WindowHelper.HideWindow<LyricsWindowSwitchWindow>();
await HideAsync();
}
private void Button_Click(object sender, RoutedEventArgs e)
private async void Button_Click(object sender, RoutedEventArgs e)
{
await HideAsync();
}
private async Task HideAsync()
{
var lyricsWindowSwitchWindow = WindowHelper.GetWindowByWindowType<LyricsWindowSwitchWindow>();
lyricsWindowSwitchWindow?.ViewModel.RootGridOpacity = 0;
await Task.Delay(300);
WindowHelper.HideWindow<LyricsWindowSwitchWindow>();
}
}

View File

@@ -29,6 +29,11 @@
<Setter Property="MinWidth" Value="600" />
</Style>
</MenuFlyout.MenuFlyoutPresenterStyle>
<MenuFlyoutItem
x:Uid="SystemTraySwitch"
Command="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8AB;}" />
<MenuFlyoutItem
x:Uid="SystemTrayLyrics"
Command="{x:Bind ViewModel.OpenLyricsCommand}"
@@ -44,13 +49,11 @@
Command="{x:Bind ViewModel.OpenSettingsCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE713;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="SystemTrayResetWindowPosition"
Command="{x:Bind ViewModel.ResetWindowPositionCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE923;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="SystemTrayRestart"
Command="{x:Bind ViewModel.RestartAppCommand}"

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum PaletteGeneratorType
{
MedianCut,
OctTree
}
}

View File

@@ -131,7 +131,7 @@ namespace BetterLyrics.WinUI3.Helper
}
case WindowPixelSampleMode.AboveWindow:
{
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 3, screenWidth, 1);
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 2, screenWidth, 1);
}
case WindowPixelSampleMode.WindowArea:
{
@@ -149,49 +149,21 @@ namespace BetterLyrics.WinUI3.Helper
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
var edgeThickness = new Thickness(36, 36, 36, 36);
List<System.Drawing.Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
if (edgeThickness.Top > 0)
edgeColors.Add(GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - (int)edgeThickness.Top, width, (int)edgeThickness.Top));
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
if (edgeThickness.Bottom > 0)
edgeColors.Add(GetAverageColorFromScreenRegion(myRect.Left, myRect.Bottom, width, (int)edgeThickness.Bottom));
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
if (edgeThickness.Left > 0)
edgeColors.Add(GetAverageColorFromScreenRegion(myRect.Left - (int)edgeThickness.Left, myRect.Top, (int)edgeThickness.Left, height));
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
if (edgeThickness.Right > 0)
edgeColors.Add(GetAverageColorFromScreenRegion(myRect.Right, myRect.Top, (int)edgeThickness.Right, height));
// 合并四边平均色
if (edgeColors.Count == 0)

View File

@@ -1,15 +1,14 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models.Settings;
using CommunityToolkit.WinUI.Helpers;
using Impressionist.Abstractions;
using Impressionist.Implementations;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
using System.IO;
@@ -21,6 +20,7 @@ using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
using static Vanara.PInvoke.Ole32;
namespace BetterLyrics.WinUI3.Helper
{
@@ -46,7 +46,7 @@ namespace BetterLyrics.WinUI3.Helper
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
public static async Task<IRandomAccessStream> CreateTextPlaceholderBytesAsync(int width, int height)
{
using var device = CanvasDevice.GetSharedDevice();
using var renderTarget = new CanvasRenderTarget(device, width, height, 96);
@@ -76,31 +76,58 @@ namespace BetterLyrics.WinUI3.Helper
}
// 保存为 PNG 并转为 byte[]
using var stream = new InMemoryRandomAccessStream();
var stream = new InMemoryRandomAccessStream();
await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png);
var buffer = new byte[stream.Size];
using (var reader = new DataReader(stream.GetInputStreamAt(0)))
{
await reader.LoadAsync((uint)stream.Size);
reader.ReadBytes(buffer);
}
return buffer;
stream.Seek(0);
return stream;
}
public static List<Windows.UI.Color> GetAccentColorsFromByte(byte[] bytes, int count, bool? isDark = null)
public static Task<ThemeColorResult> GetAccentColorAsync(BitmapDecoder decoder, PaletteGeneratorType generatorType)
{
using var image = Image.Load<Rgba32>(bytes);
var colorThief = new ColorThief.ImageSharp.ColorThief();
var mainColor = colorThief.GetColor(image, 10, false);
var palette = colorThief.GetPalette(image, 255, 10, false);
var topColors = palette
.OrderByDescending(x => x.Population)
.Where(x => x.IsDark == (isDark ?? mainColor.IsDark))
.Select(x => Windows.UI.Color.FromArgb(x.Color.A, x.Color.R, x.Color.G, x.Color.B))
.Take(count)
.ToList();
return generatorType switch
{
PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorFromByteAsync(decoder),
PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorFromByteAsync(decoder),
_ => throw new ArgumentOutOfRangeException(nameof(generatorType)),
};
}
return topColors;
public static Task<PaletteResult> GetAccentColorsAsync(BitmapDecoder decoder, int count, PaletteGeneratorType generatorType, bool? isDark = null)
{
return generatorType switch
{
PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorsFromByteAsync(decoder, count, isDark),
PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorsFromByteAsync(decoder, count, isDark),
_ => throw new ArgumentOutOfRangeException(nameof(generatorType)),
};
}
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();
var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight;
var vector = new Dictionary<Vector3, int>();
for (int i = 0; i < count; i += 10)
{
var offset = i * 4;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
if (a == 0) continue;
var color = new Vector3(r, g, b);
if (vector.ContainsKey(color))
{
vector[color]++;
}
else
{
vector[color] = 1;
}
}
return vector;
}
//public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
@@ -129,13 +156,12 @@ namespace BetterLyrics.WinUI3.Helper
// return stream;
//}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
public static async Task<IBuffer> ToBufferAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var reader = new DataReader(stream);
await reader.LoadAsync((uint)stream.Size);
byte[] buffer = new byte[stream.Size];
reader.ReadBytes(buffer);
stream.Seek(0);
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
await stream.ReadAsync(buffer, (uint)stream.Size, InputStreamOptions.None);
return buffer;
}
@@ -156,53 +182,105 @@ namespace BetterLyrics.WinUI3.Helper
return (double)(sum / (pixels.Length / 4));
}
public static byte[] MakeSquareWithThemeColor(byte[] imageBytes)
public static async Task<BitmapDecoder> MakeSquareWithThemeColor(IBuffer buffer, PaletteGeneratorType generatorType)
{
using var image = Image.Load<Rgba32>(imageBytes);
if (image.Width == image.Height)
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(buffer);
var decoder = await BitmapDecoder.CreateAsync(stream);
if (decoder.PixelWidth == decoder.PixelHeight)
{
// 已经是正方形,直接返回
return imageBytes;
return decoder;
}
int size = Math.Max(image.Width, image.Height);
using var device = CanvasDevice.GetSharedDevice();
using var canvasBitmap = await CanvasBitmap.LoadAsync(device, stream);
var size = Math.Max(decoder.PixelWidth, decoder.PixelHeight);
var themeColor = Rgba32.ParseHex(GetAccentColorsFromByte(imageBytes, 1).FirstOrDefault().ToHex());
var result = await GetAccentColorAsync(decoder, generatorType);
var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z);
using var renderTarget = new CanvasRenderTarget(device, size, size, 96);
using var square = new Image<Rgba32>(size, size, themeColor);
int offsetX = (int)(size - decoder.PixelWidth) / 2;
int offsetY = (int)(size - decoder.PixelHeight) / 2;
using (var ds = renderTarget.CreateDrawingSession())
{
ds.FillRectangle(0, 0, size, size, color);
ds.DrawImage(canvasBitmap, offsetX, offsetY);
}
int offsetX = (size - image.Width) / 2;
int offsetY = (size - image.Height) / 2;
// 保存为 PNG 并转为 byte[]
stream.Seek(0);
stream.Size = 0;
await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png);
stream.Seek(0);
var newDecoder = await BitmapDecoder.CreateAsync(stream);
return newDecoder;
square.Mutate(ctx => ctx.DrawImage(image, new Point(offsetX, offsetY), 1f));
using var ms = new MemoryStream();
square.Save(ms, new PngEncoder());
return ms.ToArray();
}
public static byte[] Resize(byte[] imageBytes, int size)
public static async Task<IBuffer> Resize(IBuffer buffer, int size)
{
using (Image image = Image.Load(imageBytes))
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(buffer);
var decoder = await BitmapDecoder.CreateAsync(stream);
var factor = Math.Max((double)size / decoder.PixelWidth, (double)size / decoder.PixelHeight);
var width = (uint)(decoder.PixelWidth * factor);
var height = (uint)(decoder.PixelHeight * factor);
if (factor > 1)
{
var factor = Math.Max((double)size / image.Width, (double)size / image.Height);
int width = (int)(image.Width * factor);
int height = (int)(image.Height * factor);
if (factor > 1)
var transform = new BitmapTransform()
{
image.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
}
else
{
image.Mutate(x => x.Resize(width, height, KnownResamplers.NearestNeighbor));
}
ScaledWidth = width,
ScaledHeight = height,
InterpolationMode = BitmapInterpolationMode.Fant
};
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Rgba8,
BitmapAlphaMode.Straight,
transform, ExifOrientationMode.RespectExifOrientation,
ColorManagementMode.ColorManageToSRgb);
var pixels = pixelData.DetachPixelData();
using var ms = new MemoryStream();
image.Save(ms, new JpegEncoder());
return ms.ToArray();
stream.Seek(0);
stream.Size = 0;
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels);
await encoder.FlushAsync();
var output = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None);
return output;
}
else
{
var transform = new BitmapTransform()
{
ScaledWidth = (uint)width,
ScaledHeight = (uint)height,
InterpolationMode = BitmapInterpolationMode.NearestNeighbor
};
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Rgba8,
BitmapAlphaMode.Straight,
transform, ExifOrientationMode.RespectExifOrientation,
ColorManagementMode.ColorManageToSRgb);
var pixels = pixelData.DetachPixelData();
stream.Seek(0);
stream.Size = 0;
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels);
await encoder.FlushAsync();
var output = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None);
return output;
}
}

View File

@@ -4,6 +4,7 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Models;
using Lyricify.Lyrics.Parsers;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,7 +14,7 @@ using LyricsData = BetterLyrics.WinUI3.Models.LyricsData;
namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
public partial class LyricsParser
{
private List<LyricsData> _lyricsDataArr = [];
@@ -34,10 +35,10 @@ namespace BetterLyrics.WinUI3.Helper
ParseLrc(raw);
break;
case LyricsFormat.Qrc:
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines);
ParseQQNeteaseKugou(QrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Krc:
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines);
ParseQQNeteaseKugou(KrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
@@ -121,9 +122,7 @@ namespace BetterLyrics.WinUI3.Helper
new List<(int time, string text, List<(int time, string text)> syllables)>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = new Regex(
@"(\[|\<)(\d{2}):(\d{2})\.(\d{2,3})(\]|\>)([^\[\]\<\>]*)"
);
var syllableRegex = SyllableRegex();
foreach (var line in lines)
{
@@ -140,7 +139,7 @@ namespace BetterLyrics.WinUI3.Helper
syllables.Add((totalMs, text));
}
if (syllables.Count > 0)
if (syllables.Count > 1)
{
lrcLines.Add(
(
@@ -153,18 +152,19 @@ namespace BetterLyrics.WinUI3.Helper
else
{
// 普通LRC行
var bracketRegex = new Regex(@"\[(\d{2}):(\d{2})\.(\d{2,3})\]");
Regex? bracketRegex = LrcRegex();
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches[0];
var m = bracketMatches![0];
int min = int.Parse(m.Groups[1].Value);
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[3].Value.PadRight(3, '0'));
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex.Replace(line, "");
content = bracketRegex!.Replace(line, "");
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
@@ -450,5 +450,10 @@ namespace BetterLyrics.WinUI3.Helper
_lyricsDataArr.Add(new LyricsData(lyricsLines));
}
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
private static partial Regex LrcRegex();
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
private static partial Regex SyllableRegex();
}
}

View File

@@ -0,0 +1,83 @@
using ColorThiefDotNet;
using Impressionist.Abstractions;
using Impressionist.Implementations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Helper
{
public static class PaletteHelper
{
private static ColorThief colorThief = new();
public static async Task<PaletteResult> OctTreeGetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, bool? isDark = null)
{
var colors = await GetPixelColor(decoder);
var palette = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(colors, count, false, isDark);
return palette;
}
public static async Task<ThemeColorResult> OctTreeGetAccentColorFromByteAsync(BitmapDecoder decoder)
{
var colors = await GetPixelColor(decoder);
var theme = await PaletteGenerators.OctTreePaletteGenerator.CreateThemeColor(colors, false);
return theme;
}
public static async Task<ThemeColorResult> MedianCutGetAccentColorFromByteAsync(BitmapDecoder decoder)
{
var mainColor = await colorThief.GetColor(decoder, 10, false);
var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark);
return theme;
}
public static async Task<PaletteResult> MedianCutGetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, bool? isDark = null)
{
var mainColor = await colorThief.GetColor(decoder, 10, false);
var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark);
var palette = await colorThief.GetPalette(decoder, 255, 10, false);
var topColors = palette
.Where(x => x.IsDark == (isDark ?? mainColor.IsDark))
.OrderByDescending(x => x.Population)
.Select(x => new Vector3(x.Color.R, x.Color.G, x.Color.B))
.Take(count)
.ToList();
var paletteResult = new PaletteResult(topColors, mainColor.IsDark, theme);
return paletteResult;
}
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();
var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight;
var vector = new Dictionary<Vector3, int>();
for (int i = 0; i < count; i += 10)
{
var offset = i * 4;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
if (a == 0) continue;
var color = new Vector3(r, g, b);
if (vector.ContainsKey(color))
{
vector[color]++;
}
else
{
vector[color] = 1;
}
}
return vector;
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIdMatcher
{
private static readonly List<string> _neteaseFamilyRegex =
[
"cloudmusic.exe", //NetEaseCloudMusic
"^17588BrandonWong\\.LyricEase_", //LyricEase
"^48848aaaaaaccd\\.HyPlayer_" //HyPlayer
];
public static bool IsNeteaseFamily(string player)
{
foreach (var regex in _neteaseFamilyRegex)
{
var isMatch = Regex.IsMatch(player, regex);
if (isMatch) return true;
}
return false;
}
}
}

View File

@@ -11,6 +11,7 @@ using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
@@ -33,10 +34,8 @@ namespace BetterLyrics.WinUI3.Helper
public static void HideWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window is Window w)
{
w.Hide();
}
var castedWindow = window as Window;
castedWindow?.Hide();
}
public static void CloseWindow<T>()
@@ -286,22 +285,6 @@ namespace BetterLyrics.WinUI3.Helper
if (_workAreas.Contains(hwnd)) return;
RegisterWorkArea(hwnd);
double y = _liveStatesService.LiveStates.LyricsWindowStatus.DockPlacement == DockPlacement.Top ?
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Top :
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Bottom - _liveStatesService.LiveStates.LyricsWindowStatus.DockHeight;
y -= 1;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Left,
(int)y,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Width,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.DockHeight + 1,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}
private static void RegisterWorkArea(IntPtr hwnd)
@@ -350,55 +333,40 @@ namespace BetterLyrics.WinUI3.Helper
_workAreas.Remove(hwnd);
}
public static void UpdateWorkAreaHeight<T>()
public static void UpdateWorkArea<T>()
{
var window = GetWindowByWindowType<T>() as Window;
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
App.DispatcherQueueTimer?.Debounce(() =>
if (!_workAreas.Contains(hwnd))
return;
var uEdge = _liveStatesService.LiveStates.LyricsWindowStatus.DockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
double top = _liveStatesService.LiveStates.LyricsWindowStatus.DockPlacement == DockPlacement.Top ?
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Top :
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Bottom - _liveStatesService.LiveStates.LyricsWindowStatus.DockHeight;
double bottom = top + _liveStatesService.LiveStates.LyricsWindowStatus.DockHeight;
Shell32.APPBARDATA abd = new()
{
if (!_workAreas.Contains(hwnd))
return;
var uEdge = _liveStatesService.LiveStates.LyricsWindowStatus.DockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
double top = _liveStatesService.LiveStates.LyricsWindowStatus.DockPlacement == DockPlacement.Top ?
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Top :
_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Bottom - _liveStatesService.LiveStates.LyricsWindowStatus.DockHeight;
double bottom = top + _liveStatesService.LiveStates.LyricsWindowStatus.DockHeight;
Shell32.APPBARDATA abd = new()
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = uEdge,
rc = new RECT
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = uEdge,
rc = new RECT
{
Left = (int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Left,
Top = (int)top,
Right = (int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Right,
Bottom = (int)bottom,
},
};
Left = (int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Left,
Top = (int)top,
Right = (int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Right,
Bottom = (int)bottom,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
// 同步窗口实际高度和位置
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Left,
(int)top - 1,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.MonitorBounds.Width,
(int)_liveStatesService.LiveStates.LyricsWindowStatus.DockHeight + 1,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}, TimeSpan.FromMilliseconds(100));
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
}
public static void SetLyricsWindowVisibilityByPlayingStatus()

View File

@@ -199,6 +199,7 @@ namespace BetterLyrics.WinUI3.Models
IsBorderless = true,
IsClickThrough = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
EnvironmentSampleMode = WindowPixelSampleMode.WindowEdge,
LyricsStyleSettings = new()
{
@@ -207,8 +208,7 @@ namespace BetterLyrics.WinUI3.Models
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsPureColorOverlayEnabled = false,
IsCoverOverlayEnabled = false,
IsFluidOverlayEnabled = false,
}
};
}
@@ -223,6 +223,7 @@ namespace BetterLyrics.WinUI3.Models
IsAlwaysOnTopPolling = true,
IsBorderless = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
EnvironmentSampleMode = WindowPixelSampleMode.BelowWindow,
TitleBarArea = TitleBarArea.None,
@@ -233,7 +234,8 @@ namespace BetterLyrics.WinUI3.Models
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsCoverOverlayEnabled = false,
IsFluidOverlayEnabled = false,
IsPureColorOverlayEnabled = true,
}
};
}
@@ -246,18 +248,19 @@ namespace BetterLyrics.WinUI3.Models
WindowBounds = monitorBounds,
IsAlwaysOnTop = true,
IsBorderless = true,
IsShownInSwitchers = false,
TitleBarArea = Enums.TitleBarArea.None,
LyricsLayoutOrientation = Enums.LyricsLayoutOrientation.Vertical,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsFontSize = 96,
LyricsFontSize = 72,
LyricsAlignmentType = Enums.TextAlignmentType.Center,
},
AlbumArtLayoutSettings = new AlbumArtLayoutSettings
{
AutoAlbumArtSize = false,
AlbumArtSize = 148,
SongInfoFontSize = 48,
AlbumArtSize = 128,
SongInfoFontSize = 36,
}
};
}

View File

@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TextAlignmentType SongInfoAlignmentType { get; set; } = TextAlignmentType.Left;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverImageRadius { get; set; } = 12; // 12 % of the cover image size
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverImageShadowAmount { get; set; } = 12;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAutoSongInfoFontSize { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SongInfoFontSize { get; set; } = 18;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowTitle { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowArtists { get; set; } = true;
@@ -28,6 +29,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
SongInfoAlignmentType = this.SongInfoAlignmentType,
CoverImageRadius = this.CoverImageRadius,
CoverImageShadowAmount = this.CoverImageShadowAmount,
IsAutoSongInfoFontSize = this.IsAutoSongInfoFontSize,
SongInfoFontSize = this.SongInfoFontSize,
ShowTitle = this.ShowTitle,
ShowArtists = this.ShowArtists,

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,20 +14,22 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial ElementTheme LyricsBackgroundTheme { get; set; } = ElementTheme.Dark;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayBlurAmount { get; set; } = 100; // 100 % of the cover image size
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlaySpeed { get; set; } = 50; // 50 % of the base rotate speed
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverAcrylicEffectAmount { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSpectrumOverlayEnabled { get; set; } = false;
public LyricsBackgroundSettings() { }
public object Clone()
@@ -39,6 +42,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
PureColorOverlayOpacity = this.PureColorOverlayOpacity,
CoverOverlaySpeed = this.CoverOverlaySpeed,
CoverAcrylicEffectAmount = this.CoverAcrylicEffectAmount,
PaletteGeneratorType = this.PaletteGeneratorType
};
}
}

View File

@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBlurAmount { get; set; } = 5;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsLineFadeEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsLineFadeEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsGlowEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LineRenderingType LyricsGlowEffectScope { get; set; } = LineRenderingType.CurrentChar;

View File

@@ -13,7 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class LyricsStyleSettings : ObservableRecipient, ICloneable
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDynamicLyricsFontSize { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDynamicLyricsFontSize { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsFontSize { get; set; } = 24;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TextAlignmentType LyricsAlignmentType { get; set; } = TextAlignmentType.Left;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBgFontOpacity { get; set; } = 30; // 30% opacity

View File

@@ -11,9 +11,11 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
{
@@ -31,9 +33,9 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
_iTunesHttpClinet = new();
}
public async Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token)
public async Task<IBuffer?> SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token)
{
byte[]? result = null;
IBuffer? result = null;
try
{
@@ -47,15 +49,16 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, title);
result = SearchFile(artist, title)?.AsBuffer();
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
result = bufferFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
foreach (string countryCode in new List<string>() { "us", "cn", "jp", "kr" })
{
result = await SearchiTunesAsync(artist, album, title, countryCode);
var byteArray = await SearchiTunesAsync(artist, album, title, countryCode);
result = byteArray?.AsBuffer();
if (token.IsCancellationRequested) return result;
if (result != null) break;
}

View File

@@ -4,11 +4,12 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
{
public interface IAlbumArtSearchService
{
Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token);
Task<IBuffer?> SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token);
}
}

View File

@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.WinUI.Controls;
using System.Linq;
namespace BetterLyrics.WinUI3.Services.LiveStatesService
@@ -33,11 +34,21 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
{
switch (e.PropertyName)
{
case nameof(LyricsWindowStatus.DockHeight):
case nameof(LyricsWindowStatus.IsWorkArea):
WindowHelper.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
UpdateWindowBoundsWhenWorkArea();
}
break;
case nameof(LyricsWindowStatus.DockHeight):
case nameof(LyricsWindowStatus.DockPlacement):
case nameof(LyricsWindowStatus.MonitorDeviceName):
WindowHelper.UpdateWorkAreaHeight<LyricsWindow>();
WindowHelper.UpdateWorkArea<LyricsWindow>();
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
UpdateWindowBoundsWhenWorkArea();
}
break;
case nameof(LyricsWindowStatus.IsShownInSwitchers):
WindowHelper.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
@@ -85,17 +96,43 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
public void RefreshLyricsWindowStatus()
{
// Order matters!!!
WindowHelper.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
UpdateWindowBoundsWhenWorkArea();
}
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds);
LiveStates.LyricsWindowStatus.UpdateMonitorNameAndBounds();
LiveStates.LyricsWindowStatus.UpdateDemoWindowAndMonitorBounds();
WindowHelper.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
WindowHelper.SetIsAlwaysOnTop<LyricsWindow>(LiveStates.LyricsWindowStatus.IsAlwaysOnTop);
WindowHelper.SetIsClickThrough<LyricsWindow>(LiveStates.LyricsWindowStatus.IsClickThrough);
WindowHelper.SetIsBorderless<LyricsWindow>(LiveStates.LyricsWindowStatus.IsBorderless);
WindowHelper.SetLyricsWindowVisibilityByPlayingStatus();
WindowHelper.SetTitleBarArea<LyricsWindow>(LiveStates.LyricsWindowStatus.TitleBarArea);
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds);
LiveStates.LyricsWindowStatus.UpdateMonitorNameAndBounds();
LiveStates.LyricsWindowStatus.UpdateDemoWindowAndMonitorBounds();
}
private void UpdateWindowBoundsWhenWorkArea()
{
LiveStates.LyricsWindowStatus.WindowBounds = new Windows.Foundation.Rect(
LiveStates.LyricsWindowStatus.MonitorBounds.X,
LiveStates.LyricsWindowStatus.DockPlacement switch
{
Enums.DockPlacement.Top => LiveStates.LyricsWindowStatus.MonitorBounds.Top,
Enums.DockPlacement.Bottom => LiveStates.LyricsWindowStatus.MonitorBounds.Bottom - LiveStates.LyricsWindowStatus.DockHeight - 1,
_ => LiveStates.LyricsWindowStatus.MonitorBounds.Top,
},
LiveStates.LyricsWindowStatus.MonitorBounds.Width,
LiveStates.LyricsWindowStatus.DockPlacement switch
{
Enums.DockPlacement.Top => LiveStates.LyricsWindowStatus.DockHeight,
Enums.DockPlacement.Bottom => LiveStates.LyricsWindowStatus.DockHeight + 1,
_ => LiveStates.LyricsWindowStatus.DockHeight,
}
);
}
}
}

View File

@@ -33,39 +33,38 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
return;
}
byte[]? bytes = await Task.Run(async () => await _albumArtSearchService.SearchAsync(
IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(
SongInfo?.PlayerId ?? "",
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,
_SMTCAlbumArtBytes,
_SMTCAlbumArtBuffer,
token
), token);
if (token.IsCancellationRequested) return;
BitmapDecoder? decoder = null;
if (bytes == null)
if (buffer == null)
{
bytes = await ImageHelper.CreateTextPlaceholderBytesAsync(500, 500);
using var placeHolderStream = await ImageHelper.CreateTextPlaceholderBytesAsync(500, 500);
var tempBuffer = new Windows.Storage.Streams.Buffer((uint)placeHolderStream.Size);
await placeHolderStream.ReadAsync(tempBuffer, (uint)placeHolderStream.Size, InputStreamOptions.None);
buffer = tempBuffer;
token.ThrowIfCancellationRequested();
}
bytes = ImageHelper.MakeSquareWithThemeColor(bytes);
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
token.ThrowIfCancellationRequested();
var decoder = await BitmapDecoder.CreateAsync(stream);
decoder = await ImageHelper.MakeSquareWithThemeColor(buffer, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType);
token.ThrowIfCancellationRequested();
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap);
albumArtSwBitmap.DpiX = 96;
albumArtSwBitmap.DpiY = 96;
token.ThrowIfCancellationRequested();
var albumArtLightAccentColors = ImageHelper.GetAccentColorsFromByte(bytes, 4, false);
var albumArtDarkAccentColors = ImageHelper.GetAccentColorsFromByte(bytes, 4, true);
AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, albumArtLightAccentColors, albumArtDarkAccentColors));
var albumArtLightAccentColors = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false);
var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true);
var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes));
}
}
}

View File

@@ -23,6 +23,7 @@ using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading.Tasks;
using Windows.Media.Control;
@@ -35,6 +36,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<LyricsWindowStatus>>,
IRecipient<PropertyChangedMessage<PaletteGeneratorType>>,
IRecipient<PropertyChangedMessage<ChineseRomanization>>,
IRecipient<PropertyChangedMessage<List<string>>>
{
@@ -58,7 +60,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly MediaManager _mediaManager = new();
private SongInfo? _cachedSongInfo;
private byte[]? _SMTCAlbumArtBytes = null;
private IBuffer? _SMTCAlbumArtBuffer = null;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
@@ -303,7 +305,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
StopSSE();
}
_SMTCAlbumArtBytes = null;
_SMTCAlbumArtBuffer = null;
}
else
{
@@ -322,13 +324,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
fixedArtist = mediaProperties.Artist.Split(" — ").FirstOrDefault() ?? mediaProperties.Artist;
fixedAlbum = mediaProperties.Artist.Split(" — ").LastOrDefault() ?? mediaProperties.AlbumTitle;
}
else if (sessionId == Constants.PlayerID.NetEaseCloudMusic)
else if (PlayerIdMatcher.IsNeteaseFamily(sessionId))
{
songId = mediaProperties.Genres.FirstOrDefault()?.Replace("NCM-", "");
if (songId != null && songId.Length != 10)
{
songId = null;
}
}
_cachedSongInfo = new SongInfo
@@ -356,15 +354,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (sessionId == Constants.PlayerID.LXMusic && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBytes = _lxMusicAlbumArtBytes;
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBytes = await ImageHelper.ToByteArrayAsync(streamReference);
_SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference);
}
else
{
_SMTCAlbumArtBytes = null;
_SMTCAlbumArtBuffer = null;
}
}
@@ -536,7 +534,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_logger.LogInformation("LX Music Album Art URL: {url}", picUrl);
_lxMusicAlbumArtBytes = await ImageHelper.GetImageBytesFromUrlAsync(picUrl);
_SMTCAlbumArtBytes = _lxMusicAlbumArtBytes;
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
UpdateAlbumArt();
}
}
@@ -688,5 +686,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
}
public void Receive(PropertyChangedMessage<PaletteGeneratorType> message)
{
if (message.Sender is LyricsBackgroundSettings)
{
if (message.PropertyName == nameof(LyricsBackgroundSettings.PaletteGeneratorType))
{
UpdateAlbumArt();
}
}
}
}
}

View File

@@ -303,6 +303,12 @@
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Set as default</value>
</data>
<data name="LyricsWindowSwitchButtonToolTip.Content" xml:space="preserve">
<value>Lyrics window switcher</value>
</data>
<data name="LyricsWindowSwitchWindowHelp.Text" xml:space="preserve">
<value>Go to Settings to add more modes</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Album art area only</value>
</data>
@@ -331,9 +337,6 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>Add to playlist</value>
</data>
@@ -445,6 +448,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="PictureInPictureMode" xml:space="preserve">
<value>Picture-in-picture mode</value>
</data>
<data name="SetingsPageContributors.Text" xml:space="preserve">
<value>Contributors</value>
</data>
<data name="SetingsPageDonation.Text" xml:space="preserve">
<value>Donation</value>
</data>
@@ -520,8 +526,8 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Apply</value>
</data>
<data name="SettingsPageAutoSize.Header" xml:space="preserve">
<value>Automatic resizing</value>
<data name="SettingsPageAutoAdjust.Header" xml:space="preserve">
<value>Automatic adjustment</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Automatic startup</value>
@@ -625,9 +631,6 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Draggable area</value>
</data>
<data name="SettingsPageDynamicLyricsFontSize.Header" xml:space="preserve">
<value>Automatic adjustment</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Easing animation type</value>
</data>
@@ -976,6 +979,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>Media library</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>Conservative</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>Monitor this playback source</value>
</data>
@@ -1006,12 +1012,18 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>None</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Aggressive</value>
</data>
<data name="SettingsPageOpacity.Header" xml:space="preserve">
<value>Opacity</value>
</data>
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>Color picker style</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>This folder is already included in the existing folder and does not need to be added again</value>
</data>
@@ -1249,6 +1261,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>Lyrics window switcher</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>Translate server is not set, please configure it in settings first</value>
</data>

View File

@@ -303,6 +303,12 @@
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>既定のブラウザーに設定する</value>
</data>
<data name="LyricsWindowSwitchButtonToolTip.Content" xml:space="preserve">
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="LyricsWindowSwitchWindowHelp.Text" xml:space="preserve">
<value>設定に移動して、さらにモードを追加してください</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートエリアのみ</value>
</data>
@@ -331,9 +337,6 @@
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>BetterLyrics へようこそ</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>没入モード</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>プレイリストに追加します</value>
</data>
@@ -445,6 +448,9 @@
<data name="PictureInPictureMode" xml:space="preserve">
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="SetingsPageContributors.Text" xml:space="preserve">
<value>投稿者</value>
</data>
<data name="SetingsPageDonation.Text" xml:space="preserve">
<value>寄付</value>
</data>
@@ -520,8 +526,8 @@
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>適用する</value>
</data>
<data name="SettingsPageAutoSize.Header" xml:space="preserve">
<value>自動サイズ変更</value>
<data name="SettingsPageAutoAdjust.Header" xml:space="preserve">
<value>自動調整</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
@@ -625,9 +631,6 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>ドラッグ可能</value>
</data>
<data name="SettingsPageDynamicLyricsFontSize.Header" xml:space="preserve">
<value>自動調整</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>アニメーションタイプを緩和します</value>
</data>
@@ -976,6 +979,9 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>保守的</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>この再生ソースを監視します</value>
</data>
@@ -1006,12 +1012,18 @@
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>なし</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>急進的だ</value>
</data>
<data name="SettingsPageOpacity.Header" xml:space="preserve">
<value>不透明度</value>
</data>
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>ファイルエクスプローラーで開きます</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>カラーピックスタイル</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value>
</data>
@@ -1249,6 +1261,9 @@
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>設定を開く</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>翻訳サーバーは設定されていません。最初に設定で構成してください</value>
</data>

View File

@@ -303,6 +303,12 @@
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>기본 값으로 설정</value>
</data>
<data name="LyricsWindowSwitchButtonToolTip.Content" xml:space="preserve">
<value>가사 창 전환기</value>
</data>
<data name="LyricsWindowSwitchWindowHelp.Text" xml:space="preserve">
<value>설정으로 이동하여 모드를 더 추가하세요</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>앨범 아트 영역만</value>
</data>
@@ -331,9 +337,6 @@
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Betterlyrics에 오신 것을 환영합니다</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>몰입 형 모드</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>재생 목록에 추가하십시오</value>
</data>
@@ -445,6 +448,9 @@
<data name="PictureInPictureMode" xml:space="preserve">
<value>사진 인당 모드</value>
</data>
<data name="SetingsPageContributors.Text" xml:space="preserve">
<value>기여자</value>
</data>
<data name="SetingsPageDonation.Text" xml:space="preserve">
<value>기부</value>
</data>
@@ -488,7 +494,7 @@
<value>앨범 표지 소스를 구성합니다</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>앨범아트</value>
<value>앨범 아트 크기</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>앨범 아트 소스</value>
@@ -520,8 +526,8 @@
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>적용하다</value>
</data>
<data name="SettingsPageAutoSize.Header" xml:space="preserve">
<value>자동 크기 조정</value>
<data name="SettingsPageAutoAdjust.Header" xml:space="preserve">
<value>자동 조정</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>자동 시작</value>
@@ -625,9 +631,6 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>드래그 가능</value>
</data>
<data name="SettingsPageDynamicLyricsFontSize.Header" xml:space="preserve">
<value>자동 조정</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>애니메이션 유형 완화</value>
</data>
@@ -976,6 +979,9 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>미디어 라이브러리</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>안전하게</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>이 재생 소스를 모니터링하십시오</value>
</data>
@@ -1006,12 +1012,18 @@
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>없음</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>공격적인</value>
</data>
<data name="SettingsPageOpacity.Header" xml:space="preserve">
<value>투명도</value>
</data>
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>파일 탐색기에서 열립니다</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>색상 선택 스타일</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value>
</data>
@@ -1249,6 +1261,9 @@
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>열기 설정</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>가사 창 전환기</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>번역 서버가 설정되지 않았습니다. 먼저 설정으로 구성하십시오.</value>
</data>

View File

@@ -303,6 +303,12 @@
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>设为默认</value>
</data>
<data name="LyricsWindowSwitchButtonToolTip.Content" xml:space="preserve">
<value>歌词窗口切换器</value>
</data>
<data name="LyricsWindowSwitchWindowHelp.Text" xml:space="preserve">
<value>转到“设置”以添加更多模式</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>仅显示专辑区域</value>
</data>
@@ -331,9 +337,6 @@
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>添加到歌单</value>
</data>
@@ -445,6 +448,9 @@
<data name="PictureInPictureMode" xml:space="preserve">
<value>画中画模式</value>
</data>
<data name="SetingsPageContributors.Text" xml:space="preserve">
<value>贡献者</value>
</data>
<data name="SetingsPageDonation.Text" xml:space="preserve">
<value>捐贈</value>
</data>
@@ -488,7 +494,7 @@
<value>配置专辑封面源</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>专辑封面</value>
<value>专辑封面尺寸</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>专辑封面源</value>
@@ -520,8 +526,8 @@
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>应用</value>
</data>
<data name="SettingsPageAutoSize.Header" xml:space="preserve">
<value>自动调整尺寸</value>
<data name="SettingsPageAutoAdjust.Header" xml:space="preserve">
<value>自动调整</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自动启动</value>
@@ -625,9 +631,6 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>可拖拽区域</value>
</data>
<data name="SettingsPageDynamicLyricsFontSize.Header" xml:space="preserve">
<value>自动调整</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
@@ -976,6 +979,9 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体库</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>保守</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>监听此播放源</value>
</data>
@@ -1006,12 +1012,18 @@
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>无</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>激进</value>
</data>
<data name="SettingsPageOpacity.Header" xml:space="preserve">
<value>不透明度</value>
</data>
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色风格</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data>
@@ -1249,6 +1261,9 @@
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>打开设置</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌词窗口切换器</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未设置Translate服务器请先在设置中进行配置</value>
</data>

View File

@@ -303,6 +303,12 @@
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>設為預設</value>
</data>
<data name="LyricsWindowSwitchButtonToolTip.Content" xml:space="preserve">
<value>歌詞視窗切換器</value>
</data>
<data name="LyricsWindowSwitchWindowHelp.Text" xml:space="preserve">
<value>前往設定以新增更多模式</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>僅限相簿藝術區域</value>
</data>
@@ -331,9 +337,6 @@
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>添加到歌單</value>
</data>
@@ -445,6 +448,9 @@
<data name="PictureInPictureMode" xml:space="preserve">
<value>畫中畫模式</value>
</data>
<data name="SetingsPageContributors.Text" xml:space="preserve">
<value>貢獻者</value>
</data>
<data name="SetingsPageDonation.Text" xml:space="preserve">
<value>捐贈</value>
</data>
@@ -488,7 +494,7 @@
<value>配置專輯封面源</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>專輯封面</value>
<value>相簿藝術尺寸</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>專輯封面來源</value>
@@ -520,8 +526,8 @@
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>應用</value>
</data>
<data name="SettingsPageAutoSize.Header" xml:space="preserve">
<value>自動調整大小</value>
<data name="SettingsPageAutoAdjust.Header" xml:space="preserve">
<value>自動調整</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動啟動</value>
@@ -625,9 +631,6 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>拖動</value>
</data>
<data name="SettingsPageDynamicLyricsFontSize.Header" xml:space="preserve">
<value>自動調整</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>緩動動畫類型</value>
</data>
@@ -976,6 +979,9 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒體庫</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>保守</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>監聽此播放來源</value>
</data>
@@ -1006,12 +1012,18 @@
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>無</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>激進</value>
</data>
<data name="SettingsPageOpacity.Header" xml:space="preserve">
<value>專輯背景層不透明度</value>
</data>
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色風格</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>該資料夾已包含在已有資料夾中,無需再次添加</value>
</data>
@@ -1249,6 +1261,9 @@
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>打開設置</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌詞視窗切換器</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未設定翻譯伺服器,請先在設定中進行配置</value>
</data>

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI;
@@ -84,6 +85,10 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
_isAlbumArtSizeChanged = true;
}
else if (message.PropertyName == nameof(AlbumArtLayoutSettings.IsAutoSongInfoFontSize))
{
UpdateSongInfoFontSize();
}
}
else if (message.Sender is LyricsBackgroundSettings)
{

View File

@@ -160,6 +160,8 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
if (_isCanvasWidthChanged || _isCanvasHeightChanged)
{
UpdateSongInfoFontSize();
_isCoverAcrylicEffectAmountChanged = true;
_effect?.Properties["Width"] = (float)control.ConvertDipsToPixels((float)_canvasWidth, CanvasDpiRounding.Round);
@@ -406,15 +408,19 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
_titleXTransition.Update(_elapsedTime);
_titleYTransition.Update(_elapsedTime);
_lyricsXTransition.Update(_elapsedTime);
_lyricsYTransition.Update(_elapsedTime);
_albumArtXTransition.Update(_elapsedTime);
_albumArtYTransition.Update(_elapsedTime);
_lyricsOpacityTransition.Update(_elapsedTime);
_albumArtOpacityTransition.Update(_elapsedTime);
_immersiveBgOpacityTransition.Update(_elapsedTime);
_immersiveBgColorTransition.Update(_elapsedTime);
_albumArtAccentColor1Transition.Update(_elapsedTime);
_albumArtAccentColor2Transition.Update(_elapsedTime);
_albumArtAccentColor3Transition.Update(_elapsedTime);
@@ -433,7 +439,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
if (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.IsDynamicLyricsFontSize)
{
_lyricsTextFormat.FontSize = (float)Math.Clamp(Math.Min(_canvasHeight, _canvasWidth) / 10, 12, 72);
_lyricsTextFormat.FontSize = (float)Math.Clamp(Math.Min(_canvasHeight, _canvasWidth) / 15, 12, 96);
}
else
{
@@ -779,8 +785,16 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private void UpdateSongInfoFontSize()
{
_titleTextFormat.FontSize = _liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoFontSize;
_artistTextFormat.FontSize = _liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoFontSize - 2;
if (_liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.IsAutoSongInfoFontSize)
{
_titleTextFormat.FontSize = (int)Math.Clamp(Math.Min(_canvasHeight, _canvasWidth) / 20, 8, 72);
}
else
{
_titleTextFormat.FontSize = _liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoFontSize;
}
_artistTextFormat.FontSize = (int)(_titleTextFormat.FontSize * 0.8);
_isSongInfoFontSizeChanged = true;
}

View File

@@ -394,6 +394,9 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private void MediaSessionsService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
_lastAlbumArtCanvasBitmap?.Dispose();
_lastAlbumArtCanvasBitmap = null;
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_albumArtSwBitmap = e.AlbumArtSwBitmap;

View File

@@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsWindowSwitchWindowViewModel : BaseViewModel
{
[ObservableProperty] public partial float RootGridOpacity { get; set; } = 1;
}
}

View File

@@ -154,7 +154,10 @@ namespace BetterLyrics.WinUI3
{
presenter.IsAlwaysOnTop = true;
}
UpdateBackdropAccentColor(hwnd);
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment)
{
UpdateBackdropAccentColor(hwnd);
}
}, Constants.Time.DebounceTimeout);
}
);

View File

@@ -52,5 +52,11 @@ namespace BetterLyrics.WinUI3.ViewModels
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
}
[RelayCommand]
private static void OpenLyricsWindowSwitch()
{
WindowHelper.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
}
}

View File

@@ -18,7 +18,7 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged" RightTapped="RootGrid_RightTapped">
<!-- Lyrics area -->
<renderer:LyricsRenderer />
<!--<Image Source="/Assets/Cover.jpg" />-->

View File

@@ -172,5 +172,13 @@ namespace BetterLyrics.WinUI3.Views
{
ViewModel.TimelineSliderThumbOpacity = 0f;
}
private void RootGrid_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.ShowAt(BottomCommandFlyoutTrigger);
}
}
}
}

View File

@@ -46,7 +46,7 @@
<ScalarTransition />
</StackPanel.OpacityTransition>
<!-- More -->
<!-- Music gallery -->
<Button Click="MusicGalleryButton_Click" Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
@@ -57,6 +57,16 @@
</ToolTipService.ToolTip>
</Button>
<Button Click="LyricsWindowSwitchButton_Click" Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE8AB;" />
<ToolTipService.ToolTip>
<ToolTip x:Uid="LyricsWindowSwitchButtonToolTip" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>

View File

@@ -27,6 +27,8 @@ namespace BetterLyrics.WinUI3.Views
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly WindowMessageMonitor _wmm;
public LyricsWindowViewModel ViewModel { get; private set; } = Ioc.Default.GetRequiredService<LyricsWindowViewModel>();
public LyricsWindow()
{
this.InitializeComponent();
@@ -79,8 +81,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
public LyricsWindowViewModel ViewModel { get; private set; } = Ioc.Default.GetRequiredService<LyricsWindowViewModel>();
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
if (args.DidPositionChange || args.DidSizeChange)
@@ -127,5 +127,10 @@ namespace BetterLyrics.WinUI3.Views
{
ViewModel.ExitOrClose();
}
private void LyricsWindowSwitchButton_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
}
}

View File

@@ -10,7 +10,10 @@
Title="LyricsWindowSwitchWindow"
mc:Ignorable="d">
<Grid>
<Grid x:Name="RootGrid" Opacity="{x:Bind ViewModel.RootGridOpacity, Mode=OneWay}">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid
x:Name="PlaceholderGrid"
Width="1"

View File

@@ -1,4 +1,6 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -25,6 +27,8 @@ namespace BetterLyrics.WinUI3.Views
/// </summary>
public sealed partial class LyricsWindowSwitchWindow : Window
{
public LyricsWindowSwitchWindowViewModel ViewModel { get; private set; } = Ioc.Default.GetRequiredService<LyricsWindowSwitchWindowViewModel>();
public LyricsWindowSwitchWindow()
{
InitializeComponent();
@@ -37,6 +41,19 @@ namespace BetterLyrics.WinUI3.Views
AppWindow.IsShownInSwitchers = false;
this.SetIsAlwaysOnTop(true);
SetTitleBar(PlaceholderGrid);
AppWindow.Changed += AppWindow_Changed;
}
private void AppWindow_Changed(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowChangedEventArgs args)
{
if (args.DidVisibilityChange)
{
if (sender.IsVisible)
{
ViewModel.RootGridOpacity = 1;
}
}
}
}
}

View File

@@ -188,6 +188,18 @@
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageContributors" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="jayfunc" NavigateUri="https://github.com/jayfunc" />
<HyperlinkButton Content="Raspberry-Monster" NavigateUri="https://github.com/Raspberry-Monster" />
<HyperlinkButton Content="ZHider" NavigateUri="https://github.com/ZHider" />
<HyperlinkButton Content="kusutori" NavigateUri="https://github.com/kusutori" />
</StackPanel>
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
<controls:SettingsExpander.ItemsHeader>
<InfoBar

View File

@@ -1,12 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.36105.23 d17.13
VisualStudioVersion = 17.13.36105.23
MinimumVisualStudioVersion = 10.0.40219.1
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "BetterLyrics.WinUI3\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj", "{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist", "Impressionist\Impressionist\Impressionist.csproj", "{A678BCA5-03DE-71E4-73C1-388B7550E4E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorThief.WinUI3", "ColorThief.WinUI3\ColorThief.WinUI3.csproj", "{8F2FE667-2D91-428E-0630-05E6330F9625}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -47,6 +51,30 @@ Global
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.Build.0 = Release|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.ActiveCfg = Release|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.Build.0 = Release|x86
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.Build.0 = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.Build.0 = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

111
ColorThief.WinUI3/CMap.cs Normal file
View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace ColorThiefDotNet
{
/// <summary>
/// Color map
/// </summary>
internal class CMap
{
private readonly List<VBox> vboxes = new List<VBox>();
private List<QuantizedColor> palette;
public void Push(VBox box)
{
palette = null;
vboxes.Add(box);
}
public List<QuantizedColor> GeneratePalette()
{
if(palette == null)
{
palette = (from vBox in vboxes
let rgb = vBox.Avg(false)
let color = FromRgb(rgb[0], rgb[1], rgb[2])
select new QuantizedColor(color, vBox.Count(false))).ToList();
}
return palette;
}
public int Size()
{
return vboxes.Count;
}
public int[] Map(int[] color)
{
foreach(var vbox in vboxes.Where(vbox => vbox.Contains(color)))
{
return vbox.Avg(false);
}
return Nearest(color);
}
public int[] Nearest(int[] color)
{
var d1 = double.MaxValue;
int[] pColor = null;
foreach(var t in vboxes)
{
var vbColor = t.Avg(false);
var d2 = Math.Sqrt(Math.Pow(color[0] - vbColor[0], 2)
+ Math.Pow(color[1] - vbColor[1], 2)
+ Math.Pow(color[2] - vbColor[2], 2));
if(d2 < d1)
{
d1 = d2;
pColor = vbColor;
}
}
return pColor;
}
public VBox FindColor(double targetLuma, double minLuma, double maxLuma, double targetSaturation, double minSaturation, double maxSaturation)
{
VBox max = null;
double maxValue = 0;
var highestPopulation = vboxes.Select(p => p.Count(false)).Max();
foreach(var swatch in vboxes)
{
var avg = swatch.Avg(false);
var hsl = FromRgb(avg[0], avg[1], avg[2]).ToHsl();
var sat = hsl.S;
var luma = hsl.L;
if(sat >= minSaturation && sat <= maxSaturation &&
luma >= minLuma && luma <= maxLuma)
{
var thisValue = Mmcq.CreateComparisonValue(sat, targetSaturation, luma, targetLuma,
swatch.Count(false), highestPopulation);
if(max == null || thisValue > maxValue)
{
max = swatch;
maxValue = thisValue;
}
}
}
return max;
}
public Color FromRgb(int red, int green, int blue)
{
var color = new Color
{
A = 255,
R = (byte)red,
G = (byte)green,
B = (byte)blue
};
return color;
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
namespace ColorThiefDotNet
{
/// <summary>
/// Defines a color in RGB space.
/// </summary>
public struct Color
{
/// <summary>
/// Get or Set the Alpha component value for sRGB.
/// </summary>
public byte A;
/// <summary>
/// Get or Set the Blue component value for sRGB.
/// </summary>
public byte B;
/// <summary>
/// Get or Set the Green component value for sRGB.
/// </summary>
public byte G;
/// <summary>
/// Get or Set the Red component value for sRGB.
/// </summary>
public byte R;
/// <summary>
/// Get HSL color.
/// </summary>
/// <returns></returns>
public HslColor ToHsl()
{
const double toDouble = 1.0 / 255;
var r = toDouble * R;
var g = toDouble * G;
var b = toDouble * B;
var max = Math.Max(Math.Max(r, g), b);
var min = Math.Min(Math.Min(r, g), b);
var chroma = max - min;
double h1;
// ReSharper disable CompareOfFloatsByEqualityOperator
if(chroma == 0)
{
h1 = 0;
}
else if(max == r)
{
h1 = (g - b) / chroma % 6;
}
else if(max == g)
{
h1 = 2 + (b - r) / chroma;
}
else //if (max == b)
{
h1 = 4 + (r - g)/chroma;
}
var lightness = 0.5 * (max - min);
var saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs(2*lightness - 1));
HslColor ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
// ReSharper restore CompareOfFloatsByEqualityOperator
}
public string ToHexString()
{
return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
}
public string ToHexAlphaString()
{
return "#" + A.ToString("X2") + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
}
public override string ToString()
{
if(A == 255)
{
return ToHexString();
}
return ToHexAlphaString();
}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
namespace ColorThiefDotNet
{
public partial class ColorThief
{
/// <summary>
/// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster.
/// </summary>
/// <param name="sourceImage">The source image.</param>
/// <param name="quality">
/// 1 is the highest quality settings. 10 is the default. There is
/// a trade-off between quality and speed. The bigger the number,
/// the faster a color will be returned but the greater the
/// likelihood that it will not be the visually most dominant color.
/// </param>
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
public async Task<QuantizedColor> GetColor(BitmapDecoder sourceImage, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
{
var palette = await GetPalette(sourceImage, 3, quality, ignoreWhite);
var dominantColor = new QuantizedColor(new Color
{
A = Convert.ToByte(palette.Average(a => a.Color.A)),
R = Convert.ToByte(palette.Average(a => a.Color.R)),
G = Convert.ToByte(palette.Average(a => a.Color.G)),
B = Convert.ToByte(palette.Average(a => a.Color.B))
}, Convert.ToInt32(palette.Average(a => a.Population)));
return dominantColor;
}
/// <summary>
/// Use the median cut algorithm to cluster similar colors.
/// </summary>
/// <param name="sourceImage">The source image.</param>
/// <param name="colorCount">The color count.</param>
/// <param name="quality">
/// 1 is the highest quality settings. 10 is the default. There is
/// a trade-off between quality and speed. The bigger the number,
/// the faster a color will be returned but the greater the
/// likelihood that it will not be the visually most dominant color.
/// </param>
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
/// <code>true</code>
public async Task<List<QuantizedColor>> GetPalette(BitmapDecoder sourceImage, int colorCount = DefaultColorCount, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
{
var pixelArray = await GetPixelsFast(sourceImage, quality, ignoreWhite);
var cmap = GetColorMap(pixelArray, colorCount);
if(cmap != null)
{
var colors = cmap.GeneratePalette();
return colors;
}
return new List<QuantizedColor>();
}
private async Task<byte[]> GetIntFromPixel(BitmapDecoder decoder)
{
var pixelsData = await decoder.GetPixelDataAsync();
var pixels = pixelsData.DetachPixelData();
return pixels;
}
private async Task<byte[][]> GetPixelsFast(BitmapDecoder sourceImage, int quality, bool ignoreWhite)
{
if(quality < 1)
{
quality = DefaultQuality;
}
var pixels = await GetIntFromPixel(sourceImage);
var pixelCount = sourceImage.PixelWidth*sourceImage.PixelHeight;
return ConvertPixels(pixels, Convert.ToInt32(pixelCount), quality, ignoreWhite);
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>ColorThief.WinUI3</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,77 @@
using System;
namespace ColorThiefDotNet
{
public partial class ColorThief
{
public const int DefaultColorCount = 5;
public const int DefaultQuality = 10;
public const bool DefaultIgnoreWhite = true;
public const int ColorDepth = 4;
/// <summary>
/// Use the median cut algorithm to cluster similar colors.
/// </summary>
/// <param name="pixelArray">Pixel array.</param>
/// <param name="colorCount">The color count.</param>
/// <returns></returns>
private CMap GetColorMap(byte[][] pixelArray, int colorCount)
{
// Send array to quantize function which clusters values using median
// cut algorithm
if (colorCount > 0)
{
--colorCount;
}
var cmap = Mmcq.Quantize(pixelArray, colorCount);
return cmap;
}
private byte[][] ConvertPixels(byte[] pixels, int pixelCount, int quality, bool ignoreWhite)
{
var expectedDataLength = pixelCount * ColorDepth;
if(expectedDataLength != pixels.Length)
{
throw new ArgumentException("(expectedDataLength = "
+ expectedDataLength + ") != (pixels.length = "
+ pixels.Length + ")");
}
// Store the RGB values in an array format suitable for quantize
// function
// numRegardedPixels must be rounded up to avoid an
// ArrayIndexOutOfBoundsException if all pixels are good.
var numRegardedPixels = (pixelCount + quality - 1) / quality;
var numUsedPixels = 0;
var pixelArray = new byte[numRegardedPixels][];
for(var i = 0; i < pixelCount; i += quality)
{
var offset = i * ColorDepth;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
// If pixel is mostly opaque and not white
if(a >= 125 && !(ignoreWhite && r > 250 && g > 250 && b > 250))
{
pixelArray[numUsedPixels] = new[] {r, g, b};
numUsedPixels++;
}
}
// Remove unused pixels from the array
var copy = new byte[numUsedPixels][];
Array.Copy(pixelArray, copy, numUsedPixels);
return copy;
}
}
}

View File

@@ -0,0 +1,28 @@
namespace ColorThiefDotNet
{
/// <summary>
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
/// </summary>
public struct HslColor
{
/// <summary>
/// The Alpha/opacity in 0..1 range.
/// </summary>
public double A;
/// <summary>
/// The Hue in 0..360 range.
/// </summary>
public double H;
/// <summary>
/// The Lightness in 0..1 range.
/// </summary>
public double L;
/// <summary>
/// The Saturation in 0..1 range.
/// </summary>
public double S;
}
}

387
ColorThief.WinUI3/Mmcq.cs Normal file
View File

@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
namespace ColorThiefDotNet
{
internal static class Mmcq
{
public const int Sigbits = 5;
public const int Rshift = 8 - Sigbits;
public const int Mult = 1 << Rshift;
public const int Histosize = 1 << (3 * Sigbits);
public const int VboxLength = 1 << Sigbits;
public const double FractByPopulation = 0.75;
public const int MaxIterations = 1000;
public const double WeightSaturation = 3d;
public const double WeightLuma = 6d;
public const double WeightPopulation = 1d;
private static readonly VBoxComparer ComparatorProduct = new VBoxComparer();
private static readonly VBoxCountComparer ComparatorCount = new VBoxCountComparer();
public static int GetColorIndex(int r, int g, int b)
{
return (r << (2 * Sigbits)) + (g << Sigbits) + b;
}
/// <summary>
/// Gets the histo.
/// </summary>
/// <param name="pixels">The pixels.</param>
/// <returns>Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error.</returns>
private static int[] GetHisto(IEnumerable<byte[]> pixels)
{
var histo = new int[Histosize];
foreach(var pixel in pixels)
{
var rval = pixel[0] >> Rshift;
var gval = pixel[1] >> Rshift;
var bval = pixel[2] >> Rshift;
var index = GetColorIndex(rval, gval, bval);
histo[index]++;
}
return histo;
}
private static VBox VboxFromPixels(IList<byte[]> pixels, int[] histo)
{
int rmin = 1000000, rmax = 0;
int gmin = 1000000, gmax = 0;
int bmin = 1000000, bmax = 0;
// find min/max
var numPixels = pixels.Count;
for(var i = 0; i < numPixels; i++)
{
var pixel = pixels[i];
var rval = pixel[0] >> Rshift;
var gval = pixel[1] >> Rshift;
var bval = pixel[2] >> Rshift;
if(rval < rmin)
{
rmin = rval;
}
else if(rval > rmax)
{
rmax = rval;
}
if(gval < gmin)
{
gmin = gval;
}
else if(gval > gmax)
{
gmax = gval;
}
if(bval < bmin)
{
bmin = bval;
}
else if(bval > bmax)
{
bmax = bval;
}
}
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
}
private static VBox[] DoCut(char color, VBox vbox, IList<int> partialsum, IList<int> lookaheadsum, int total)
{
int vboxDim1;
int vboxDim2;
switch(color)
{
case 'r':
vboxDim1 = vbox.R1;
vboxDim2 = vbox.R2;
break;
case 'g':
vboxDim1 = vbox.G1;
vboxDim2 = vbox.G2;
break;
default:
vboxDim1 = vbox.B1;
vboxDim2 = vbox.B2;
break;
}
for(var i = vboxDim1; i <= vboxDim2; i++)
{
if(partialsum[i] > total / 2)
{
var vbox1 = vbox.Clone();
var vbox2 = vbox.Clone();
var left = i - vboxDim1;
var right = vboxDim2 - i;
var d2 = left <= right
? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2))
: Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0)));
// avoid 0-count boxes
while(d2 < 0 || partialsum[d2] <= 0)
{
d2++;
}
var count2 = lookaheadsum[d2];
while(count2 == 0 && d2 > 0 && partialsum[d2 - 1] > 0)
{
count2 = lookaheadsum[--d2];
}
// set dimensions
switch(color)
{
case 'r':
vbox1.R2 = d2;
vbox2.R1 = d2 + 1;
break;
case 'g':
vbox1.G2 = d2;
vbox2.G1 = d2 + 1;
break;
default:
vbox1.B2 = d2;
vbox2.B1 = d2 + 1;
break;
}
return new[] {vbox1, vbox2};
}
}
throw new Exception("VBox can't be cut");
}
private static VBox[] MedianCutApply(IList<int> histo, VBox vbox)
{
if(vbox.Count(false) == 0)
{
return null;
}
if(vbox.Count(false) == 1)
{
return new[] {vbox.Clone(), null};
}
// only one pixel, no split
var rw = vbox.R2 - vbox.R1 + 1;
var gw = vbox.G2 - vbox.G1 + 1;
var bw = vbox.B2 - vbox.B1 + 1;
var maxw = Math.Max(Math.Max(rw, gw), bw);
// Find the partial sum arrays along the selected axis.
var total = 0;
var partialsum = new int[VboxLength];
// -1 = not set / 0 = 0
for(var l = 0; l < partialsum.Length; l++)
{
partialsum[l] = -1;
}
// -1 = not set / 0 = 0
var lookaheadsum = new int[VboxLength];
for(var l = 0; l < lookaheadsum.Length; l++)
{
lookaheadsum[l] = -1;
}
int i, j, k, sum, index;
if(maxw == rw)
{
for(i = vbox.R1; i <= vbox.R2; i++)
{
sum = 0;
for(j = vbox.G1; j <= vbox.G2; j++)
{
for(k = vbox.B1; k <= vbox.B2; k++)
{
index = GetColorIndex(i, j, k);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
else if(maxw == gw)
{
for(i = vbox.G1; i <= vbox.G2; i++)
{
sum = 0;
for(j = vbox.R1; j <= vbox.R2; j++)
{
for(k = vbox.B1; k <= vbox.B2; k++)
{
index = GetColorIndex(j, i, k);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
else /* maxw == bw */
{
for(i = vbox.B1; i <= vbox.B2; i++)
{
sum = 0;
for(j = vbox.R1; j <= vbox.R2; j++)
{
for(k = vbox.G1; k <= vbox.G2; k++)
{
index = GetColorIndex(j, k, i);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
for(i = 0; i < VboxLength; i++)
{
if(partialsum[i] != -1)
{
lookaheadsum[i] = total - partialsum[i];
}
}
// determine the cut planes
return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total) : maxw == gw
? DoCut('g', vbox, partialsum, lookaheadsum, total) : DoCut('b', vbox, partialsum, lookaheadsum, total);
}
/// <summary>
/// Inner function to do the iteration.
/// </summary>
/// <param name="lh">The lh.</param>
/// <param name="comparator">The comparator.</param>
/// <param name="target">The target.</param>
/// <param name="histo">The histo.</param>
/// <exception cref="System.Exception">vbox1 not defined; shouldn't happen!</exception>
private static void Iter(List<VBox> lh, IComparer<VBox> comparator, int target, IList<int> histo)
{
var ncolors = 1;
var niters = 0;
while(niters < MaxIterations)
{
var vbox = lh[lh.Count - 1];
if(vbox.Count(false) == 0)
{
lh.Sort(comparator);
niters++;
continue;
}
lh.RemoveAt(lh.Count - 1);
// do the cut
var vboxes = MedianCutApply(histo, vbox);
var vbox1 = vboxes[0];
var vbox2 = vboxes[1];
if(vbox1 == null)
{
throw new Exception(
"vbox1 not defined; shouldn't happen!");
}
lh.Add(vbox1);
if(vbox2 != null)
{
lh.Add(vbox2);
ncolors++;
}
lh.Sort(comparator);
if(ncolors >= target)
{
return;
}
if(niters++ > MaxIterations)
{
return;
}
}
}
public static CMap Quantize(byte[][] pixels, int maxcolors)
{
// short-circuit
if(pixels.Length == 0 || maxcolors < 2 || maxcolors > 256)
{
return null;
}
var histo = GetHisto(pixels);
// get the beginning vbox from the colors
var vbox = VboxFromPixels(pixels, histo);
var pq = new List<VBox> {vbox};
// Round up to have the same behaviour as in JavaScript
var target = (int)Math.Ceiling(FractByPopulation * maxcolors);
// first set of colors, sorted by population
Iter(pq, ComparatorCount, target, histo);
// Re-sort by the product of pixel occupancy times the size in color
// space.
pq.Sort(ComparatorProduct);
// next set - generate the median cuts using the (npix * vol) sorting.
Iter(pq, ComparatorProduct, maxcolors - pq.Count, histo);
// Reverse to put the highest elements first into the color map
pq.Reverse();
// calculate the actual colors
var cmap = new CMap();
foreach(var vb in pq)
{
cmap.Push(vb);
}
return cmap;
}
public static double CreateComparisonValue(double saturation, double targetSaturation, double luma, double targetLuma, int population, int highestPopulation)
{
return WeightedMean(InvertDiff(saturation, targetSaturation), WeightSaturation,
InvertDiff(luma, targetLuma), WeightLuma,
population / (double)highestPopulation, WeightPopulation);
}
private static double WeightedMean(params double[] values)
{
double sum = 0;
double sumWeight = 0;
for(var i = 0; i < values.Length; i += 2)
{
var value = values[i];
var weight = values[i + 1];
sum += value * weight;
sumWeight += weight;
}
return sum / sumWeight;
}
private static double InvertDiff(double value, double targetValue)
{
return 1 - Math.Abs(value - targetValue);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
namespace ColorThiefDotNet
{
public class QuantizedColor
{
public QuantizedColor(Color color, int population)
{
Color = color;
Population = population;
IsDark = CalculateYiqLuma(color) < 128;
}
public Color Color { get; private set; }
public int Population { get; private set; }
public bool IsDark { get; private set; }
public int CalculateYiqLuma(Color color)
{
return Convert.ToInt32(Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f));
}
}
}

163
ColorThief.WinUI3/VBox.cs Normal file
View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
namespace ColorThiefDotNet
{
/// <summary>
/// 3D color space box.
/// </summary>
internal class VBox
{
private readonly int[] histo;
private int[] avg;
public int B1;
public int B2;
private int? count;
public int G1;
public int G2;
public int R1;
public int R2;
private int? volume;
public VBox(int r1, int r2, int g1, int g2, int b1, int b2, int[] histo)
{
R1 = r1;
R2 = r2;
G1 = g1;
G2 = g2;
B1 = b1;
B2 = b2;
this.histo = histo;
}
public int Volume(bool force)
{
if(volume == null || force)
{
volume = (R2 - R1 + 1) * (G2 - G1 + 1) * (B2 - B1 + 1);
}
return volume.Value;
}
public int Count(bool force)
{
if(count == null || force)
{
var npix = 0;
int i;
for(i = R1; i <= R2; i++)
{
int j;
for(j = G1; j <= G2; j++)
{
int k;
for(k = B1; k <= B2; k++)
{
var index = Mmcq.GetColorIndex(i, j, k);
npix += histo[index];
}
}
}
count = npix;
}
return count.Value;
}
public VBox Clone()
{
return new VBox(R1, R2, G1, G2, B1, B2, histo);
}
public int[] Avg(bool force)
{
if(avg == null || force)
{
var ntot = 0;
var rsum = 0;
var gsum = 0;
var bsum = 0;
int i;
for(i = R1; i <= R2; i++)
{
int j;
for(j = G1; j <= G2; j++)
{
int k;
for(k = B1; k <= B2; k++)
{
var histoindex = Mmcq.GetColorIndex(i, j, k);
var hval = histo[histoindex];
ntot += hval;
rsum += Convert.ToInt32((hval * (i + 0.5) * Mmcq.Mult));
gsum += Convert.ToInt32((hval * (j + 0.5) * Mmcq.Mult));
bsum += Convert.ToInt32((hval * (k + 0.5) * Mmcq.Mult));
}
}
}
if(ntot > 0)
{
avg = new[]
{
Math.Abs(rsum / ntot), Math.Abs(gsum / ntot),
Math.Abs(bsum / ntot)
};
}
else
{
avg = new[]
{
Math.Abs(Mmcq.Mult * (R1 + R2 + 1) / 2),
Math.Abs(Mmcq.Mult * (G1 + G2 + 1) / 2),
Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2)
};
}
}
return avg;
}
public bool Contains(int[] pixel)
{
var rval = pixel[0] >> Mmcq.Rshift;
var gval = pixel[1] >> Mmcq.Rshift;
var bval = pixel[2] >> Mmcq.Rshift;
return rval >= R1 && rval <= R2 && gval >= G1 && gval <= G2 && bval >= B1 && bval <= B2;
}
}
internal class VBoxCountComparer : IComparer<VBox>
{
public int Compare(VBox x, VBox y)
{
var a = x.Count(false);
var b = y.Count(false);
return a < b ? -1 : (a > b ? 1 : 0);
}
}
internal class VBoxComparer : IComparer<VBox>
{
public int Compare(VBox x, VBox y)
{
var aCount = x.Count(false);
var bCount = y.Count(false);
var aVolume = x.Volume(false);
var bVolume = y.Volume(false);
// Otherwise sort by products
var a = aCount * aVolume;
var b = bCount * bVolume;
return a < b ? -1 : (a > b ? 1 : 0);
}
}
}

View File

@@ -1,37 +0,0 @@
### ? Where I can find the logs?
`%LocalAppData%\Packages\37412.BetterLyrics_rd1g0rsrrtxw8\LocalCache\logs`
### ? Where I can find the lyrics cache?
`%LocalAppData%\Packages\37412.BetterLyrics_rd1g0rsrrtxw8\LocalCache\lyrics`
### ? How to install ".msixbundle" package? (for test package only)
[See this doc](https://github.com/jayfunc/BetterLyrics/blob/dev/How2Install/How2Install.md)
### ? Lyrics are moving back and forth constantly, how to fix it?
![](Snipaste_2025-08-22_14-59-53.png)
Go to Settings > Playback sources > Disable "Lyrics timeline sync" or increase "Lyrics timeline sync threshold"
### ? Wrong lyrics are shown, how to fix it?
![](Snipaste_2025-08-22_14-47-21.png)
Open search panel to manually search for the correct lyrics.
### ? Playback control panel is not showing in dock mode, how to fix it?
![](Snipaste_2025-08-22_14-50-16.png)
Hover over the bottom of the lyrics window and click on the white line to show the playback control panel.
### ? How to lock/unlock the lyrics window in desktop mode?
![](Snipaste_2025-08-22_14-52-53.png)
![](Snipaste_2025-08-22_14-53-21.png)
Alternatively, you can also use the shortcut `Ctrl+Alt+U` (default) to toggle lock/unlock.
![](Snipaste_2025-08-22_14-53-58.png)
You can change the shortcut in Settings > App appearance and behavior > Unlock and lock shortcut keys
### ? How to enable/disable immersive mode?
![](Snipaste_2025-08-22_14-58-44.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

BIN
FAQ/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

40
FAQ/index.md Normal file
View File

@@ -0,0 +1,40 @@
# Welcome to BetterLyrics
### Where I can find the logs?
`%LocalAppData%\Packages\37412.BetterLyrics_rd1g0rsrrtxw8\LocalCache\logs`
### Where I can find the lyrics cache?
`%LocalAppData%\Packages\37412.BetterLyrics_rd1g0rsrrtxw8\LocalCache\lyrics`
### How to add more modes?
If this is the first time that you use this app, only standard mode was initially added for you. To add more modes, follow the steps below:
Settings -> Lyrics window manager -> Create from templates -> Fullscreen mode
![](PixPin_2025-10-24_18-06-32.gif)
### How to switch modes
You can switch modes by pressing default shortcuts `Ctrl + Alt + S` and then choosing one of the modes displayed on your screen. (Press `Escape` to close the choosing window)
![](PixPin_2025-10-24_18-07-45.gif)
### How to install ".msixbundle" package? (for test package only)
[See this doc](https://github.com/jayfunc/BetterLyrics/blob/dev/How2Install/How2Install.md)
### Lyrics are moving back and forth constantly, how to fix it?
![](Snipaste_2025-08-22_14-59-53.png)
Go to Settings > Playback sources > Disable "Lyrics timeline sync" or increase "Lyrics timeline sync threshold"
### Wrong lyrics are shown, how to fix it?
![](image.png)
Open search panel to manually search for the correct lyrics.
### Playback control panel is not showing in dock mode, how to fix it?
![](Snipaste_2025-08-22_14-50-16.png)
Hover over the bottom of the lyrics window and click on the white line to show the playback control panel.

10
Impressionist/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.idea/
.vscode/
.vs/
bin/
obj/
*.user
.DS_Store

View File

@@ -0,0 +1,92 @@
using BenchmarkDotNet.Attributes;
using Impressionist.Implementations;
using System.Drawing;
using System.Numerics;
#pragma warning disable CA1416 // 验证平台兼容性
namespace Impressionist.Benchmark
{
[MemoryDiagnoser]
public class BenchMark
{
public List<string> fileName = new List<string>()
{
};
public List<Dictionary<Vector3, int>> imageData = new List<Dictionary<Vector3, int>>();
[Benchmark]
public async Task GetPaletteOctTree()
{
foreach (var item in imageData)
{
var result = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(item, 4);
}
}
[Benchmark]
public async Task GetPaletteKMeansPP()
{
foreach (var item in imageData)
{
var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: true);
}
}
[Benchmark]
public async Task GetPaletteKMeans()
{
foreach (var item in imageData)
{
var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: false);
}
}
[Benchmark]
public async Task GetPaletteKMeansPPToLab()
{
foreach (var item in imageData)
{
var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: true, toLab: true);
}
}
[Benchmark]
public async Task GetPaletteKMeansToLab()
{
foreach (var item in imageData)
{
var result = await PaletteGenerators.KMeansPaletteGenerator.CreatePalette(item, 4, useKMeansPP: false, toLab: true);
}
}
[GlobalSetup]
public void Setup()
{
foreach (var item in fileName)
{
using var originalImage = new Bitmap(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "/Pictures/", item));
var result = GetColor(originalImage);
imageData.Add(result);
}
}
Dictionary<Vector3, int> GetColor(Bitmap bmp)
{
var result = new Dictionary<Vector3, int>();
for (var x = 0; x < bmp.Width; x++)
{
for (var y = 0; y < bmp.Height; y++)
{
var clr = bmp.GetPixel(x, y);
if (clr.A == 0)
{
continue;
}
var vec = new Vector3(clr.R, clr.G, clr.B);
if (result.ContainsKey(vec))
{
result[vec]++;
}
else
{
result[vec] = 1;
}
}
}
return result;
}
}
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Pictures\1.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\2.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\3.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\4.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\5.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Pictures\6.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;
namespace Impressionist.Benchmark
{
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<BenchMark>();
}
}
}

View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Impressionist", "Impressionist\Impressionist.csproj", "{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist.Benchmark", "Impressionist.Benchmark\Impressionist.Benchmark.csproj", "{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90BFD9F6-514F-40F1-BE87-7EAFCA55E16D}.Release|Any CPU.Build.0 = Release|Any CPU
{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14FB07AA-3DCD-4AE8-88DF-65EA68C974AF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {085FE9CD-1200-431E-909F-CE0F6ED12289}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,10 @@
namespace Impressionist.Abstractions
{
public struct HSVColor
{
public float H { get; set; }
public float S { get; set; }
public float V { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Impressionist.Abstractions
{
public interface IPaletteGenrator
{
}
}

View File

@@ -0,0 +1,7 @@
namespace Impressionist.Abstractions
{
public interface IThemeColorGenrator
{
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Numerics;
namespace Impressionist.Abstractions
{
public class PaletteResult
{
public List<Vector3> Palette { get; } = new List<Vector3>();
public bool PaletteIsDark { get; }
public ThemeColorResult ThemeColor { get; }
public PaletteResult(List<Vector3> palette, bool paletteIsDark, ThemeColorResult themeColor)
{
Palette = palette;
PaletteIsDark = paletteIsDark;
ThemeColor = themeColor;
}
}
public class ThemeColorResult
{
public Vector3 Color { get; }
public bool ColorIsDark { get; }
public ThemeColorResult(Vector3 color, bool colorIsDark)
{
Color = color;
ColorIsDark = colorIsDark;
}
}
}

View File

@@ -0,0 +1,291 @@
using Impressionist.Abstractions;
using System;
using System.Numerics;
namespace Impressionist.Implementations
{
public static class ColorUtilities
{
public static HSVColor RGBVectorToHSVColor(this Vector3 color)
{
HSVColor hsv = new HSVColor();
float max = Math.Max(Math.Max(color.X, color.Y), color.Z);
float min = Math.Min(Math.Min(color.X, color.Y), color.Z);
hsv.V = max * 100 / 255;
if (max == min)
{
hsv.H = 0;
hsv.S = 0;
}
else
{
hsv.S = (((max - min) / max) * 100);
hsv.H = 0;
if (max == color.X)
{
hsv.H = (60 * (color.Y - color.Z) / (max - min));
if (hsv.H < 0) hsv.H += 360;
}
else if (max == color.Y)
{
hsv.H = (60 * (2 + (color.Z - color.X) / (max - min)));
if (hsv.H < 0) hsv.H += 360;
}
else if (max == color.Z)
{
hsv.H = (60 * (4 + (color.X - color.Y) / (max - min)));
if (hsv.H < 0) hsv.H += 360;
}
}
return hsv;
}
public static Vector3 HSVColorToRGBVector(this HSVColor hsv)
{
if (hsv.H == 360) hsv.H = 0;
int Hi = (int)Math.Floor((float)hsv.H / 60) % 6;
float f = (hsv.H / 60) - Hi;
float p = (hsv.V / 100) * (1 - (hsv.S / 100));
float q = (hsv.V / 100) * (1 - f * (hsv.S / 100));
float t = (hsv.V / 100) * (1 - (1 - f) * (hsv.S / 100));
p *= 255;
q *= 255;
t *= 255;
Vector3 rgb = Vector3.Zero;
switch (Hi)
{
case 0:
rgb = new Vector3(hsv.V * 255 / 100, t, p);
break;
case 1:
rgb = new Vector3(q, hsv.V * 255 / 100, p);
break;
case 2:
rgb = new Vector3(p, hsv.V * 255 / 100, t);
break;
case 3:
rgb = new Vector3(p, q, hsv.V * 255 / 100);
break;
case 4:
rgb = new Vector3(t, p, hsv.V * 255 / 100);
break;
case 5:
rgb = new Vector3(hsv.V * 255 / 100, p, q);
break;
}
return rgb;
}
public static Vector3 RGBVectorToXYZVector(this Vector3 rgb)
{
var red = rgb.X;
var green = rgb.Y;
var blue = rgb.Z;
// normalize red, green, blue values
float rLinear = red / 255.0f;
float gLinear = green / 255.0f;
float bLinear = blue / 255.0f;
// convert to a sRGB form
float r = (rLinear > 0.04045) ? (float)Math.Pow((rLinear + 0.055) / (1 + 0.055), 2.2) : (float)(rLinear / 12.92);
float g = (gLinear > 0.04045) ? (float)Math.Pow((gLinear + 0.055) / (1 + 0.055), 2.2) : (float)(gLinear / 12.92);
float b = (bLinear > 0.04045) ? (float)Math.Pow((bLinear + 0.055) / (1 + 0.055), 2.2) : (float)(bLinear / 12.92);
// converts
return new Vector3(
(r * 0.4124f + g * 0.3576f + b * 0.1805f),
(r * 0.2126f + g * 0.7152f + b * 0.0722f),
(r * 0.0193f + g * 0.1192f + b * 0.9505f)
);
}
public static Vector3 XYZVectorToRGBVector(this Vector3 xyz)
{
var x = xyz.X;
var y = xyz.Y;
var z = xyz.Z;
float[] Clinear = new float[3];
Clinear[0] = x * 3.2410f - y * 1.5374f - z * 0.4986f; // red
Clinear[1] = -x * 0.9692f + y * 1.8760f - z * 0.0416f; // green
Clinear[2] = x * 0.0556f - y * 0.2040f + z * 1.0570f; // blue
for (int i = 0; i < 3; i++)
{
Clinear[i] = (Clinear[i] <= 0.0031308) ? 12.92f * Clinear[i] : (float)((
1 + 0.055) * Math.Pow(Clinear[i], (1.0 / 2.4)) - 0.055);
}
return new Vector3(
Convert.ToInt32(float.Parse(string.Format("{0:0.00}",
Clinear[0] * 255.0))),
Convert.ToInt32(float.Parse(string.Format("{0:0.00}",
Clinear[1] * 255.0))),
Convert.ToInt32(float.Parse(string.Format("{0:0.00}",
Clinear[2] * 255.0)))
);
}
private static float D65X = 0.9505f;
private static float D65Y = 1f;
private static float D65Z = 1.089f;
private static float Fxyz(float t)
{
return ((t > 0.008856) ? (float)Math.Pow(t, (1.0 / 3.0)) : (7.787f * t + 16.0f / 116.0f));
}
public static Vector3 XYZVectorToLABVector(this Vector3 xyz)
{
Vector3 lab = new Vector3();
var x = xyz.X;
var y = xyz.Y;
var z = xyz.Z;
lab.X = 116.0f * Fxyz(y / D65Y) - 16f;
lab.Y = 500.0f * (Fxyz(x / D65X) - Fxyz(y / D65Y));
lab.Z = 200.0f * (Fxyz(y / D65Y) - Fxyz(z / D65Z));
return lab;
}
public static Vector3 LABVectorToXYZVector(this Vector3 lab)
{
float delta = 6.0f / 29.0f;
var l = lab.X;
var a = lab.Y;
var b = lab.Z;
float fy = (l + 16f) / 116.0f;
float fx = fy + (a / 500.0f);
float fz = fy - (b / 200.0f);
return new Vector3(
(fx > delta) ? D65X * (fx * fx * fx) : (fx - 16.0f / 116.0f) * 3 * (
delta * delta) * D65X,
(fy > delta) ? D65Y * (fy * fy * fy) : (fy - 16.0f / 116.0f) * 3 * (
delta * delta) * D65Y,
(fz > delta) ? D65Z * (fz * fz * fz) : (fz - 16.0f / 116.0f) * 3 * (
delta * delta) * D65Z
);
}
public static Vector3 RGBVectorToLABVector(this Vector3 rgb)
{
return rgb.RGBVectorToXYZVector().XYZVectorToLABVector();
}
public static Vector3 LABVectorToRGBVector(this Vector3 lab)
{
return lab.LABVectorToXYZVector().XYZVectorToRGBVector();
}
internal static float A = 0.17883277f;
internal static float B = 0.28466892f;
internal static float C = 0.55991073f;
internal static float HLGGap = 1f / 12f;
internal static float HLGFunction1(float s)
{
return 0.5f * (float)Math.Sqrt(12f * s);
}
internal static float HLGFunction2(float s)
{
return (float)(A * Math.Log(12f * s - B)) + C;
}
public static bool HLGColorIsDark(this HSVColor color)
{
if (color.V < 65) return true;
var s = color.S / 100;
if (s <= HLGGap)
{
var targetV = HLGFunction1(s);
return color.V / 100f < targetV;
}
else
{
var targetV = HLGFunction2(s);
return color.V / 100f < targetV;
}
}
internal static float BT709Gap = 0.018f;
internal static float BT709Function1(float s)
{
return 4.5f * s;
}
internal static float BT709Function2(float s)
{
return (float)(1.099 * Math.Pow(s, 0.45) - 0.099);
}
public static bool BT709ColorIsDark(this HSVColor color)
{
if (color.V < 65) return true;
var s = color.S / 100;
if (s <= BT709Gap)
{
var targetV = BT709Function1(s);
return color.V / 100f < targetV;
}
else
{
var targetV = BT709Function2(s);
return color.V / 100f < targetV;
}
}
internal static float sRGBGap = 0.0031308f;
internal static float sRGBFunction1(float s)
{
return 12.92f * s;
}
internal static float sRGBFunction2(float s)
{
return (float)(1.055 * Math.Pow(s, 1 / 2.4) - 0.055);
}
public static bool sRGBColorIsDark(this HSVColor color)
{
if (color.V < 65) return true;
var s = color.S / 100;
if (s <= sRGBGap)
{
var targetV = sRGBFunction1(s);
return color.V / 100f < targetV;
}
else
{
var targetV = sRGBFunction2(s);
return color.V / 100f < targetV;
}
}
public static bool RGBVectorLStarIsDark(this Vector3 rgb)
{
var limitedColor = rgb / 255f;
var y = 0.2126f * ChannelToLin(limitedColor.X) + 0.7152f * ChannelToLin(limitedColor.Y) + 0.0722f * ChannelToLin(limitedColor.Z);
var lStar = YToLStar(y);
return lStar <= 55;
}
public static float ChannelToLin(float value)
{
if (value <= 0.04045f)
{
return value / 12.92f;
}
else
{
return (float)Math.Pow((value + 0.055) / 1.055, 2.4);
}
}
public static float YToLStar(float y)
{
if (y <= (216f / 24389f))
{ // The CIE standard states 0.008856 but 216/24389 is the intent for 0.008856451679036
return y * (24389f / 27f); // The CIE standard states 903.3, but 24389/27 is the intent, making 903.296296296296296
}
else
{
return (float)Math.Pow(y, (1f / 3f)) * 116f - 16f;
}
}
}
}

View File

@@ -0,0 +1,209 @@
using Impressionist.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
namespace Impressionist.Implementations
{
// I'm really appreciate wieslawsoltes's PaletteGenerator. Which make this project possible.
public class KMeansPaletteGenerator :
IThemeColorGenrator,
IPaletteGenrator
{
public Task<ThemeColorResult> CreateThemeColor(Dictionary<Vector3, int> sourceColor, bool ignoreWhite = false, bool toLab = false)
{
var builder = sourceColor.AsEnumerable();
if (ignoreWhite && sourceColor.Count > 1)
{
builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250);
}
if (toLab)
{
builder = builder.Select(t => new KeyValuePair<Vector3, int>(t.Key.RGBVectorToLABVector(), t.Value));
}
var targetColor = builder.ToDictionary(t => t.Key, t => t.Value);
var clusters = KMeansCluster(targetColor, 1, false);
var colorVector = clusters.First();
if (toLab)
{
colorVector = clusters.First().LABVectorToRGBVector();
}
var isDark = colorVector.RGBVectorLStarIsDark();
return Task.FromResult(new ThemeColorResult(colorVector, isDark));
}
public async Task<PaletteResult> CreatePalette(Dictionary<Vector3, int> sourceColor, int clusterCount, bool ignoreWhite = false, bool toLab = false, bool useKMeansPP = false)
{
if (sourceColor.Count == 1)
{
ignoreWhite = false;
useKMeansPP = false;
}
var colorResult = await CreateThemeColor(sourceColor, ignoreWhite, toLab);
var builder = sourceColor.AsEnumerable();
var colorIsDark = colorResult.ColorIsDark;
if (colorIsDark)
{
builder = builder.Where(t => t.Key.RGBVectorLStarIsDark());
}
else
{
if (!ignoreWhite)
{
builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark());
}
else
{
builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark() && (t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250));
}
}
if (toLab)
{
builder = builder.Select(t => new KeyValuePair<Vector3, int>(t.Key.RGBVectorToLABVector(), t.Value));
}
var targetColors = builder.ToDictionary(t => t.Key, t => t.Value);
var clusters = KMeansCluster(targetColors, clusterCount, useKMeansPP);
var dominantColors = new List<Vector3>();
foreach (var cluster in clusters)
{
var representative = cluster;
if (toLab)
{
representative = representative.LABVectorToRGBVector();
}
dominantColors.Add(representative);
}
var result = new List<Vector3>();
var count = dominantColors.Count;
for (int i = 0; i < clusterCount; i++)
{
// You know, it is always hard to fullfill a palette when you have no enough colors. So please forgive me when placing the same color over and over again.
result.Add(dominantColors[i % count]);
}
return new PaletteResult(result, colorIsDark, colorResult);
}
static Vector3[] KMeansCluster(Dictionary<Vector3, int> colors, int numClusters, bool useKMeansPP)
{
// Initialize the clusters, reduces the total number when total colors is less than clusters
var clusterCount = Math.Min(numClusters, colors.Count);
var clusters = new List<Dictionary<Vector3, int>>();
for (int i = 0; i < clusterCount; i++)
{
clusters.Add(new Dictionary<Vector3, int>());
}
// Select the initial cluster centers randomly
Vector3[] centers = null;
if (!useKMeansPP)
{
centers = colors.Keys.OrderByDescending(t => Guid.NewGuid()).Take(clusterCount).ToArray();
}
else
{
centers = KMeansPlusPlusCluster(colors, clusterCount).ToArray();
}
// Loop until the clusters stabilize
var changed = true;
while (changed)
{
changed = false;
// Assign each color to the nearest cluster center
foreach (var color in colors.Keys)
{
var nearest = FindNearestCenter(color, centers);
var clusterIndex = Array.IndexOf(centers, nearest);
clusters[clusterIndex][color] = colors[color];
}
// Recompute the cluster centers
for (int i = 0; i < Math.Min(numClusters, clusterCount); i++)
{
var sumX = 0f;
var sumY = 0f;
var sumZ = 0f;
var count = 0f;
foreach (var color in clusters[i].Keys)
{
sumX += color.X * colors[color];
sumY += color.Y * colors[color];
sumZ += color.Z * colors[color];
count += colors[color];
}
var x = (sumX / count);
var y = (sumY / count);
var z = (sumZ / count);
var newCenter = new Vector3(x, y, z);
if (!newCenter.Equals(centers[i]))
{
centers[i] = newCenter;
changed = true;
}
}
}
// Return the clusters
return centers;
}
static Vector3 FindNearestCenter(Vector3 color, Vector3[] centers)
{
var nearest = centers[0];
var minDist = float.MaxValue;
foreach (var center in centers)
{
var dist = Vector3.Distance(color, center); // The original version implemented a Distance method by wieslawsoltes himself, I changed that to Vector ones.
if (dist < minDist)
{
nearest = center;
minDist = dist;
}
}
return nearest;
}
static List<Vector3> KMeansPlusPlusCluster(Dictionary<Vector3, int> colors, int numClusters)
{
Random random = new Random();
var clusterCount = Math.Min(numClusters, colors.Count);
var clusters = new List<Vector3>();
var targetColor = colors.Keys.ToList();
var index = random.Next(targetColor.Count);
clusters.Add(targetColor[index]);
for (int i = 1; i < clusterCount; i++)
{
float accumulatedDistances = 0f;
float[] accDistances = new float[targetColor.Count];
for (int vectorId = 0; vectorId < targetColor.Count; vectorId++)
{
var minDistanceItem = clusters[0];
var minDistance = Vector3.Distance(minDistanceItem, targetColor[vectorId]);
for (int clusterIdx = 1; clusterIdx < i; clusterIdx++)
{
float currentDistance = Vector3.Distance(clusters[clusterIdx], targetColor[vectorId]);
if (currentDistance < minDistance)
{
minDistance = currentDistance;
}
accumulatedDistances += minDistance * minDistance;
accDistances[vectorId] = accumulatedDistances;
}
}
float targetPoint = (float)random.NextDouble() * accumulatedDistances;
for (int vectorId = 0; vectorId < targetColor.Count; vectorId++)
{
if (accDistances[vectorId] >= targetPoint)
{
clusters.Add(targetColor[vectorId]);
break;
}
}
}
return clusters;
}
}
}

View File

@@ -0,0 +1,325 @@
using Impressionist.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
namespace Impressionist.Implementations
{
public class OctTreePaletteGenerator :
IThemeColorGenrator,
IPaletteGenrator
{
public Task<ThemeColorResult> CreateThemeColor(Dictionary<Vector3, int> sourceColor, bool ignoreWhite = false)
{
var quantizer = new PaletteQuantizer();
var builder = sourceColor.AsEnumerable();
if (ignoreWhite && sourceColor.Count > 1)
{
builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250);
}
var targetColor = builder.ToDictionary(t => t.Key, t => t.Value);
foreach (var color in targetColor)
{
quantizer.AddColorRange(color.Key, color.Value);
}
quantizer.Quantize(1);
var index = new List<Vector3>() { targetColor.Keys.FirstOrDefault() };
var result = quantizer.GetThemeResult();
var colorIsDark = result.RGBVectorLStarIsDark();
return Task.FromResult(new ThemeColorResult(result, colorIsDark));
}
public async Task<PaletteResult> CreatePalette(Dictionary<Vector3, int> sourceColor, int clusterCount, bool ignoreWhite = false, bool? isDark = null)
{
var quantizer = new PaletteQuantizer();
if (sourceColor.Count == 1)
{
ignoreWhite = false;
}
var builder = sourceColor.AsEnumerable();
var colorResult = await CreateThemeColor(sourceColor, ignoreWhite);
var colorIsDark = false;
if (isDark == null)
{
if (ignoreWhite)
{
builder = builder.Where(t => t.Key.X <= 250 || t.Key.Y <= 250 || t.Key.Z <= 250);
}
colorIsDark = colorResult.ColorIsDark;
if (colorIsDark)
{
builder = builder.Where(t => t.Key.RGBVectorLStarIsDark());
}
else
{
builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark());
}
}
else
{
colorIsDark = isDark.Value;
if (colorIsDark)
{
builder = builder.Where(t => t.Key.RGBVectorLStarIsDark());
}
else
{
builder = builder.Where(t => !t.Key.RGBVectorLStarIsDark());
}
}
var targetColor = builder.ToDictionary(t => t.Key, t => t.Value);
foreach (var color in targetColor)
{
quantizer.AddColorRange(color.Key, color.Value);
}
quantizer.Quantize(clusterCount);
var index = targetColor.Keys.ToList();
List<Vector3> quantizeResult;
if (colorIsDark)
{
quantizeResult = quantizer.GetPaletteResult(clusterCount);
}
else
{
quantizeResult = quantizer.GetPaletteResult(clusterCount);
}
List<Vector3> result;
if (quantizeResult.Count < clusterCount)
{
var count = quantizeResult.Count;
result = new List<Vector3>();
for (int i = 0; i < clusterCount; i++)
{
// You know, it is always hard to fullfill a palette when you have no enough colors. So please forgive me when placing the same color over and over again.
result.Add(quantizeResult[i % count]);
}
}
else
{
result = quantizeResult;
}
return new PaletteResult(result, colorIsDark, colorResult);
}
private class PaletteQuantizer
{
private readonly Node Root;
private IDictionary<int, List<Node>> levelNodes;
public PaletteQuantizer()
{
Root = new Node(this);
levelNodes = new Dictionary<int, List<Node>>();
for (int i = 0; i < 8; i++)
{
levelNodes[i] = new List<Node>();
}
}
public void AddColor(Vector3 color)
{
Root.AddColor(color, 0);
}
public void AddColorRange(Vector3 color, int count)
{
Root.AddColorRange(color, 0, count);
}
public void AddLevelNode(Node node, int level)
{
levelNodes[level].Add(node);
}
public List<Vector3> GetPaletteResult()
{
return Root.GetPaletteResult().Keys.ToList();
}
public List<Vector3> GetPaletteResult(int count)
{
return Root.GetPaletteResult().OrderByDescending(t=>t.Value).Take(count).Select(t=>t.Key).ToList();
}
public Vector3 GetThemeResult()
{
return Root.GetThemeResult();
}
public void Quantize(int colorCount)
{
var nodesToRemove = levelNodes[7].Count - colorCount;
int level = 6;
var toBreak = false;
while (level >= 0 && nodesToRemove > 0)
{
var leaves = levelNodes[level]
.Where(n => n.ChildrenCount - 1 <= nodesToRemove)
.OrderBy(n => n.ChildrenCount);
foreach (var leaf in leaves)
{
if (leaf.ChildrenCount > nodesToRemove)
{
toBreak = true;
continue;
}
nodesToRemove -= (leaf.ChildrenCount - 1);
leaf.Merge();
if (nodesToRemove <= 0)
{
break;
}
}
levelNodes.Remove(level + 1);
level--;
if (toBreak)
{
break;
}
}
}
}
private class Node
{
private readonly PaletteQuantizer parent;
private Node[] Children = new Node[8];
private Vector3 Color { get; set; }
private int Count { get; set; } = 0;
public int ChildrenCount => Children.Count(c => c != null);
public Node(PaletteQuantizer parent)
{
this.parent = parent;
}
public void AddColor(Vector3 color, int level)
{
if (level < 8)
{
var index = GetIndex(color, level);
if (Children[index] == null)
{
var newNode = new Node(parent);
Children[index] = newNode;
parent.AddLevelNode(newNode, level);
}
Children[index].AddColor(color, level + 1);
}
else
{
Color = color;
Count++;
}
}
public void AddColorRange(Vector3 color, int level, int count)
{
if (level < 8)
{
var index = GetIndex(color, level);
if (Children[index] == null)
{
var newNode = new Node(parent);
Children[index] = newNode;
parent.AddLevelNode(newNode, level);
}
Children[index].AddColorRange(color, level + 1, count);
}
else
{
Color = color;
Count += count;
}
}
public Vector3 GetColor(Vector3 color, int level)
{
if (ChildrenCount == 0)
{
return Color;
}
else
{
var index = GetIndex(color, level);
return Children[index].GetColor(color, level + 1);
}
}
public Vector3 GetThemeResult()
{
var paletteResult = GetPaletteResult();
var sum = new Vector3(0, 0, 0);
var count = 0;
foreach (var item in paletteResult)
{
sum += item.Key * item.Value;
count += item.Value;
}
return sum / count;
}
public Dictionary<Vector3, int> GetPaletteResult()
{
var result = new Dictionary<Vector3, int>();
if (!Children.Any(t => t != null)) result[Color] = Count;
else
{
foreach (var child in Children)
{
if (child != null)
{
child.NodeGetResult(result);
}
}
}
return result;
}
private void NodeGetResult(Dictionary<Vector3, int> result)
{
if (!Children.Any(t => t != null)) result[Color] = Count;
else
{
foreach (var child in Children)
{
if (child != null)
{
child.NodeGetResult(result);
}
}
}
}
private byte GetIndex(Vector3 color, int level)
{
byte ret = 0;
var mask = Convert.ToByte(0b10000000 >> level);
if (((byte)color.X & mask) != 0)
{
ret |= 0b100;
}
if (((byte)color.Y & mask) != 0)
{
ret |= 0b010;
}
if (((byte)color.Z & mask) != 0)
{
ret |= 0b001;
}
return ret;
}
public void Merge()
{
Color = Average(Children.Where(c => c != null).Select(c => new Tuple<Vector3, int>(c.Color, c.Count)));
Count = Children.Sum(c => c?.Count ?? 0);
Children = new Node[8];
}
private static Vector3 Average(IEnumerable<Tuple<Vector3, int>> colors)
{
var totals = colors.Sum(c => c.Item2);
return new Vector3(
x: (int)colors.Sum(c => c.Item1.X * c.Item2) / totals,
y: (int)colors.Sum(c => c.Item1.Y * c.Item2) / totals,
z: (int)colors.Sum(c => c.Item1.Z * c.Item2) / totals);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Impressionist.Implementations
{
public static class PaletteGenerators
{
public static readonly KMeansPaletteGenerator KMeansPaletteGenerator = new KMeansPaletteGenerator();
public static readonly OctTreePaletteGenerator OctTreePaletteGenerator = new OctTreePaletteGenerator();
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<IsPublishable>False</IsPublishable>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Numerics.Vectors" Version="4.6.1" />
</ItemGroup>
</Project>

21
Impressionist/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Storyteller Studios and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
Impressionist/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Impressionist
Impressionist is a color clustering project using K-Means or other algorithms.
## Third-Party Notices
[wieslawsoltes/PaletteGenerator](https://github.com/wieslawsoltes/PaletteGenerator/)
[tompazourek/Colourful](https://github.com/tompazourek/Colourful)
[bwaacon/cSharpColourQuantization](https://github.com/bacowan/cSharpColourQuantization)

View File

@@ -1,4 +1,4 @@
[_Click here to view frequently asked questions (FAQ)_](https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md)
[_Click here to view frequently asked questions (FAQ)_](https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/index.md)
![](Promotion/banner.png)
@@ -144,6 +144,10 @@ Check out the article: [BetterLyrics An immersive and smooth lyrics display
### Fullscreen mode
> ⚠️ Due to GIF format and frame rate limitations, the displayed effect is for preview only. Please refer to the actual device for the actual effect.
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![alt text](Screenshots/std-fullscreen.png)
### Docked mode
@@ -154,9 +158,15 @@ Check out the article: [BetterLyrics An immersive and smooth lyrics display
![alt text](Screenshots/desktop.png)
### Song switching animation
> ⚠️ Due to GIF format and frame rate limitations, the displayed effect is for preview only. Please refer to the actual device for the actual effect.
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
## Demonstration
Watch our introduction video (uploaded on 18 Aug 2025) on Bilibili [here](https://www.bilibili.com/video/BV1yLYtzQEME/).
Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
## Try it now
@@ -182,7 +192,7 @@ Watch our introduction video (uploaded on 18 Aug 2025) on Bilibili [here](https:
If you are using a third-party modified Windows, you probably can not launch the app.
To solve this issue, please try to download from [Google Drive (v1.0.82.0)](https://drive.google.com/file/d/1UtGmmDqD5SWGJpp4LmWycHUT9-Bb8fur/view?usp=sharing) (may not be the latest version) and follow the instructions [here](https://github.com/jayfunc/BetterLyrics/blob/dev/How2Install/How2Install.md).
To solve this issue, please try to download from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases/latest) for the download link) and follow the instructions [here](https://github.com/jayfunc/BetterLyrics/blob/dev/Sideloadly/index.md).
## Build

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -5,7 +5,7 @@
Be sure that you have already enable developer mode. To do that, you can follow the steps below:
1. Go to "Settings", select "System", go to "Developer Options".
![alt text](image.png)
![alt text](image-0.png)
2. Turn on "Developer Mode" and enable local PowerShell script allowance.
![alt text](image-1.png)