Compare commits

...

89 Commits

Author SHA1 Message Date
Zhe Fang
a93b535667 Merge pull request #7 from jayfunc/dev
update to v1.0.5.0
2025-06-23 13:41:33 -04:00
Zhe Fang
0eca011054 fix 2025-06-23 13:37:35 -04:00
Zhe Fang
68b7601b0f update readme 2025-06-22 23:14:17 -04:00
Zhe Fang
894fe935a5 fix: listview can not be placed in settingsexapnder 2025-06-22 23:13:10 -04:00
Zhe Fang
0befdf48dd add support for ttml format 2025-06-22 22:38:03 -04:00
Zhe Fang
827602766d add: advanced lrc support 2025-06-22 21:59:26 -04:00
Zhe Fang
11c3002b77 add: x animation for lyrics; update: y scroll animation 2025-06-22 08:46:39 -04:00
Zhe Fang
9d193b7b71 add: support lyrics search provider reorder 2025-06-21 14:21:04 -04:00
Zhe Fang
811cd760d4 add: support char by char animation in real condition (not mock) (only work woth QQ music lyrics source selected) 2025-06-21 12:45:29 -04:00
Zhe Fang
fe5039db78 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-21 08:29:33 -04:00
Zhe Fang
a42a3cdb88 fix: incorrect aot state after switching back to standard mode 2025-06-21 08:29:30 -04:00
Zhe Fang
ee003e1764 Create README.CN.md 2025-06-20 23:00:58 -04:00
Zhe Fang
749ab2ca1a Update README.md 2025-06-20 22:48:27 -04:00
Zhe Fang
4bbde71bfa Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-20 22:38:33 -04:00
Zhe Fang
7f3fbda237 remove unused deps 2025-06-20 22:38:26 -04:00
Zhe Fang
06d2e19ee2 update README.md 2025-06-20 22:29:39 -04:00
Zhe Fang
cbcb140bec update README.md 2025-06-20 22:28:57 -04:00
Zhe Fang
bc8fd4de31 fix: add title and artist as the first line lyrics 2025-06-20 21:03:43 -04:00
Zhe Fang
9e8bc3b7df Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-20 19:17:24 -04:00
Zhe Fang
f9070eed5d add: qq music lyrics provider 2025-06-20 19:17:17 -04:00
Zhe Fang
447533db12 Update README.md 2025-06-19 22:24:38 -04:00
Zhe Fang
1fe8675743 update: credit in ColorThief.cs file 2025-06-19 18:02:36 -04:00
Zhe Fang
6775f9af57 fix: title bar not hide when switching between standard mode and dock mode 2025-06-19 16:39:42 -04:00
Zhe Fang
bf9107754d update: readme 2025-06-19 16:32:25 -04:00
Zhe Fang
906d8d7d49 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-19 16:25:19 -04:00
Zhe Fang
517e026ca9 add: support get lyrics from web (thanks https://lrclib.net/); fix: unstable titlebar show and hide animation 2025-06-19 16:25:14 -04:00
Zhe Fang
9535306a92 Update README.md 2025-06-18 21:46:05 -04:00
Zhe Fang
222ac42357 update: readme 2025-06-18 19:58:34 -04:00
Zhe Fang
2c55b11e70 Merge pull request #6 from jayfunc/dev
fix
2025-06-18 17:16:06 -04:00
Zhe Fang
703fc26ceb Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-18 17:07:58 -04:00
Zhe Fang
52ae59154a fix: add lyrics folder 2025-06-18 17:07:56 -04:00
Zhe Fang
82f22ac5d1 chore: update README.md 2025-06-18 05:51:08 -04:00
Zhe Fang
f0f74ba195 Delete .github/workflows/dotnet-desktop.yml 2025-06-17 21:53:48 -04:00
Zhe Fang
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
Zhe Fang
8e5e35ec23 Merge branch 'stable' into dev 2025-06-17 21:49:58 -04:00
Zhe Fang
0a63b6d8cd fix: language 2025-06-17 21:47:17 -04:00
Zhe Fang
8b01e47c8e fix: bugs; change: glow effect 2025-06-17 21:34:21 -04:00
Zhe Fang
66d8705066 change: remove system tray, combine desktop and in-app lyrics mode into one single window 2025-06-17 14:01:52 -04:00
Zhe Fang
aa1f9f071f fix: bugs about switching between desktop mode and in-app mode; add: system tray icon; todo: add lyrics entry in system tray; limit window opening counts (pre-release now) 2025-06-16 21:57:56 -04:00
Zhe Fang
81c2f45f95 add: system tray entry 2025-06-16 11:57:10 -04:00
Zhe Fang
d5ae75e5a4 chore: update readme 2025-06-15 22:59:05 -04:00
Zhe Fang
3ca2b90eff refactor: restructure project to follow MVVM architecture (changes are still in progress, more bugs may be existed) 2025-06-15 22:33:32 -04:00
Zhe Fang
d1e48e95b7 fix: treat empty lyrics content in metadata as a mark for successfully resolve lyrics 2025-06-14 18:01:29 -04:00
Zhe Fang
12b78b374c fix: dynamic background on desktop lyrics not working after re-open it 2025-06-14 15:56:20 -04:00
Zhe Fang
cd857a2807 add: auto adaptive (immersive) background for desktop lyrics; fix: change get method (cached in memory if not changed) for settings viewmodel; fix: incorrect display position for desktop lyrics 2025-06-14 13:14:38 -04:00
Zhe Fang
def2c9820a fix: sometimes incorrect album art background scale 2025-06-13 19:22:56 -04:00
Zhe Fang
63c0577e73 chore: temporarily stop github actions 2025-06-13 16:34:32 -04:00
Zhe Fang
e028ec2f0f feat: no lyrics placeholder 2025-06-13 16:31:47 -04:00
Zhe Fang
4258ab6957 feat: add desktop lyrics support 2025-06-13 14:33:10 -04:00
Zhe Fang
894081b097 fix: update artifact upload path to specify the correct package directory 2025-06-12 11:58:23 -04:00
Zhe Fang
772f41b236 fix: update artifact upload path to use github.workspace 2025-06-12 11:49:36 -04:00
Zhe Fang
e6db08c593 fix: refactor pfx path handling and improve msbuild command readability 2025-06-12 11:37:25 -04:00
Zhe Fang
ea4a4ad072 fix: update certificate path in workflow and remove temporary key file 2025-06-12 11:27:48 -04:00
Zhe Fang
b4bd479d3c fix: update .NET version and action versions in workflow configuration 2025-06-12 10:57:12 -04:00
Zhe Fang
9745b7e558 fix: update solution name in workflow configuration 2025-06-12 10:55:31 -04:00
Zhe Fang
eb666fd8f2 fix: update branch triggers for workflow to stable and dev 2025-06-12 10:54:42 -04:00
Zhe Fang
2404c54bb6 feat: add temporary key file for package signing 2025-06-12 10:53:51 -04:00
Zhe Fang
221cd67c39 fix: update .gitignore to exclude temporary key file for package signing 2025-06-12 10:53:41 -04:00
Zhe Fang
7c311972b5 chore: update dotnet-desktop.yml for improved workflow configuration 2025-06-12 07:36:55 -04:00
Zhe Fang
9b42aebb56 feat: add temporary key file for package signing 2025-06-12 07:36:43 -04:00
Zhe Fang
e96320c9ad fix: specify PackageCertificateKeyFile in msbuild command for app package creation 2025-06-12 07:30:15 -04:00
Zhe Fang
fdafb96852 fix: update publish profiles and build configuration for ARM64, x64, and x86 platforms 2025-06-12 07:23:29 -04:00
Zhe Fang
cf5e4a7e8c feat: add ARM64 publish profile for better deployment support 2025-06-12 07:23:03 -04:00
Zhe Fang
4b4651bf6e Update dotnet-desktop.yml 2025-06-12 06:39:55 -04:00
Zhe Fang
a3e7503537 Update dotnet-desktop.yml 2025-06-12 06:18:39 -04:00
Zhe Fang
30fab426df add .pubxml 2025-06-11 22:37:25 -04:00
Zhe Fang
7746a04bd9 fix: update MSIX package upload path in GitHub Actions workflow 2025-06-11 22:30:28 -04:00
Zhe Fang
a437f382cb fix: update MSIX package creation settings for store upload 2025-06-11 22:18:29 -04:00
Zhe Fang
852741bbfc add: implement GitHub Actions workflow for building and packaging WinUI 3 MSIX application 2025-06-11 22:08:09 -04:00
Zhe Fang
7d839c655f support desktop lyrics 2025-06-11 22:03:50 -04:00
Zhe Fang
ba8aad9831 Update dotnet-desktop.yml 2025-06-11 10:22:38 -04:00
Zhe Fang
2970b5e246 Update dotnet-desktop.yml 2025-06-11 10:19:05 -04:00
Zhe Fang
80a68b2612 Update dotnet-desktop.yml 2025-06-11 10:16:47 -04:00
Zhe Fang
088d2fa78b Update dotnet-desktop.yml 2025-06-11 10:16:31 -04:00
Zhe Fang
a4bc63d352 Update dotnet-desktop.yml 2025-06-11 10:08:18 -04:00
Zhe Fang
e8c428614a Update dotnet-desktop.yml 2025-06-11 10:01:30 -04:00
Zhe Fang
4f336282bb Update dotnet-desktop.yml 2025-06-11 09:54:03 -04:00
Zhe Fang
74daa48536 Update dotnet-desktop.yml 2025-06-11 09:51:54 -04:00
Zhe Fang
3dc14e52d8 Update dotnet-desktop.yml 2025-06-11 09:42:48 -04:00
Zhe Fang
1a736c13d5 Delete nuget.config 2025-06-11 09:36:58 -04:00
Zhe Fang
377d68d83c Update dotnet-desktop.yml 2025-06-11 09:33:40 -04:00
Zhe Fang
f512e686b0 Create nuget.config 2025-06-11 09:27:09 -04:00
Zhe Fang
004dcbb4f4 Create dotnet-desktop.yml 2025-06-11 09:19:00 -04:00
Zhe Fang
1bfad8740c update README.md 2025-06-11 05:47:11 -04:00
Zhe Fang
5b71f44bf3 fix 2025-06-10 22:37:32 -04:00
Zhe Fang
81651abfec Split and reorganize Service ViewModel Renderer 2025-06-10 20:39:29 -04:00
Zhe Fang
db6847b74f Merge pull request #4 from jayfunc/dev
Dev
2025-06-07 19:15:42 -04:00
Zhe Fang
d510892650 add:
1. cross fade animation when switching songs;
2. user can now toggle immersive mode
by button (located at the bottom-right area)
3. user can now toggle full screen mode

known bugs:
1. window presenter can not be listened properly when entering / exiting full screen mode

bug fixed:
1. unproper image scale (caused by calculate by pixels not dips)

and some other changes...
2025-06-07 18:01:59 -04:00
Zhe Fang
a0e51d976e add: user can now change title bar size; support full screen mode; some other user interface adjustment (still in testing) 2025-06-06 22:03:28 -04:00
118 changed files with 8709 additions and 3569 deletions

3
.gitignore vendored
View File

@@ -404,4 +404,5 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
*.sln.iml
*.sln.iml
/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx

View File

@@ -10,8 +10,8 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.1.0" />
Publisher="CN=Zhe"
Version="1.0.5.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
@@ -30,6 +30,8 @@
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Applications>

View File

@@ -5,7 +5,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:BetterLyrics.WinUI3">
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:media="using:CommunityToolkit.WinUI.Media">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -40,8 +41,11 @@
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:ThemeTypeToElementThemeConverter x:Key="ThemeTypeToElementThemeConverter" />
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
@@ -57,6 +61,34 @@
<Setter Property="Margin" Value="1,30,0,6" />
</Style.Setters>
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="TitleBarToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- Dimensions -->
</ResourceDictionary>
</Application.Resources>

View File

@@ -1,23 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.AppLifecycle;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Windows.ApplicationModel.Core;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -32,12 +27,11 @@ namespace BetterLyrics.WinUI3
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
public MainWindow? MainWindow { get; private set; }
public MainWindow? SettingsWindow { get; set; }
public static ResourceLoader? ResourceLoader { get; private set; }
public static DispatcherQueue DispatcherQueue => DispatcherQueue.GetForCurrentThread();
public static DispatcherQueue? DispatcherQueue { get; private set; }
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
@@ -47,38 +41,72 @@ namespace BetterLyrics.WinUI3
{
this.InitializeComponent();
App.ResourceLoader = new ResourceLoader();
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
Helper.AppInfo.EnsureDirectories();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
AppInfo.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void CurrentDomain_FirstChanceException(
object? sender,
System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e
)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
private void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
private void CurrentDomain_UnhandledException(
object sender,
System.UnhandledExceptionEventArgs e
)
{
_logger.LogError(e.ExceptionObject.ToString(), "CurrentDomain_UnhandledException");
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(Helper.AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.WriteTo.File(AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton(DispatcherQueue.GetForCurrentThread())
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
// ViewModels
.AddSingleton<MainViewModel>()
.AddTransient<HostWindowViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<LyricsSettingsControlViewModel>()
.BuildServiceProvider()
);
}
@@ -96,14 +124,9 @@ namespace BetterLyrics.WinUI3
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new MainWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
WindowHelper.OpenLyricsWindow();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -8,6 +8,7 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="Logo.ico" />
@@ -23,6 +24,8 @@
/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference
Include="CommunityToolkit.WinUI.Controls.SettingsControls"
Version="8.2.250402"
@@ -31,26 +34,44 @@
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="z440.atl.core" Version="6.24.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
@@ -60,8 +81,21 @@
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
</Project>

View File

@@ -8,20 +8,24 @@ using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
internal class ThemeTypeToElementThemeConverter : IValueConverter
internal class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int themeType)
if (value is Enum)
{
return (ElementTheme)themeType;
return System.Convert.ToInt32(value);
}
return ElementTheme.Default;
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return 0;
if (value is int && targetType.IsEnum)
{
return Enum.ToObject(targetType, value);
}
return Enum.ToObject(targetType, 0);
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class IntToCornerRadius : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int intValue && parameter is double controlHeight)
{
return new Microsoft.UI.Xaml.CornerRadius(intValue / 100f * controlHeight / 2);
}
return new Microsoft.UI.Xaml.CornerRadius(0);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class LyricsSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalLrcFile"
),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalMusicFile"
),
LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
"LyricsSearchProviderLrcLib"
),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderEslrcFile"
),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderTtmlFile"
),
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null),
};
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string path)
{
if (path == App.ResourceLoader!.GetString("MainPageNoLocalFilesMatched"))
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

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 AutoStartWindowType
{
StandardMode,
DockMode,
}
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum BackdropType
{
None = 0,
Mica = 1,
MicaAlt = 2,
DesktopAcrylic = 3,
Transparent = 4,
}
}

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
public enum Language
{
@@ -12,5 +12,7 @@ namespace BetterLyrics.WinUI3.Models
English,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
}
}

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 LocalSearchTargetProps
{
LyricsOnly,
LyricsAndAlbumArt,
}
}

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsAlignmentType
{

View File

@@ -0,0 +1,10 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsDisplayType
{
AlbumArtOnly,
LyricsOnly,
SplitView,
PlaceholderOnly,
}
}

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFontColorType
{

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Text;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFontWeight
{
Thin,
ExtraLight,
Light,
SemiLight,
Normal,
Medium,
SemiBold,
Bold,
ExtraBold,
Black,
ExtraBlack,
}
public static class LyricsFontWeightExtensions
{
public static FontWeight ToFontWeight(this LyricsFontWeight weight)
{
return weight switch
{
LyricsFontWeight.Thin => FontWeights.Thin,
LyricsFontWeight.ExtraLight => FontWeights.ExtraLight,
LyricsFontWeight.Light => FontWeights.Light,
LyricsFontWeight.SemiLight => FontWeights.SemiLight,
LyricsFontWeight.Normal => FontWeights.Normal,
LyricsFontWeight.Medium => FontWeights.Medium,
LyricsFontWeight.SemiBold => FontWeights.SemiBold,
LyricsFontWeight.Bold => FontWeights.Bold,
LyricsFontWeight.ExtraBold => FontWeights.ExtraBold,
LyricsFontWeight.Black => FontWeights.Black,
LyricsFontWeight.ExtraBlack => FontWeights.ExtraBlack,
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(
nameof(weight),
weight,
null
),
};
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFormat
{
Lrc,
Eslrc,
Ttml,
}
public static class LyricsFormatExtensions
{
public static string ToFileExtension(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.Eslrc => ".eslrc",
LyricsFormat.Ttml => ".ttml",
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
};
}
public static LyricsFormat? Detect(string content)
{
if (
content.StartsWith("<?xml")
&& System.Text.RegularExpressions.Regex.IsMatch(content, @"<tt(:\w+)?\b")
)
{
return LyricsFormat.Ttml;
}
// 检测标准LRC和增强型LRC
else if (
System.Text.RegularExpressions.Regex.IsMatch(content, @"\[\d{1,2}:\d{2}")
|| System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<\d{1,2}:\d{2}\.\d{2,3}>"
)
)
{
return LyricsFormat.Lrc;
}
else
{
return null;
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsGlowEffectScope
{
WholeLyrics,
CurrentLine,
CurrentChar,
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsHighlightType
{
LineByLine,
CharByChar,
}
}

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsPlayingState
{

View File

@@ -0,0 +1,11 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider
{
LrcLib,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsStatus
{
NotFound,
Found,
Loading,
}
}

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 LyricsType
{
InAppLyrics,
DesktopLyrics,
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum MusicSearchMatchMode
{
TitleAndArtist,
TitleArtistAlbumAndDuration,
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum TitleBarType
{
Compact,
Extended,
}
public static class TitleBarTypeExtensions
{
public static double GetHeight(this TitleBarType titleBarType)
{
return titleBarType switch
{
TitleBarType.Compact => 32.0,
TitleBarType.Extended => 48.0,
_ => throw new ArgumentOutOfRangeException(
nameof(titleBarType),
titleBarType,
null
),
};
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class IsPlayingChangedEventArgs(bool isPlaying) : EventArgs
{
public bool IsPlaying { get; set; } = isPlaying;
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs : EventArgs
{
public string Folder { get; }
public string FilePath { get; }
public WatcherChangeTypes ChangeType { get; }
public LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType)
{
Folder = folder;
FilePath = filePath;
ChangeType = changeType;
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Events
{
public class SongInfoChangedEventArgs(SongInfo? songInfo) : EventArgs
{
public SongInfo? SongInfo { get; set; } = songInfo;
}
}

View File

@@ -1,13 +1,79 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
namespace BetterLyrics.WinUI3.Helper
{
public static class AnimationHelper
public class AnimationHelper
{
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
public const int DebounceDefaultDuration = 200;
}
public class ValueTransition<T>
where T : struct
{
private T _currentValue;
private T _startValue;
private T _targetValue;
private float _progress;
private float _durationSeconds;
private bool _isTransitioning;
private Func<T, T, float, T> _interpolator;
public ValueTransition(
T initialValue,
float durationSeconds,
Func<T, T, float, T> interpolator
)
{
_currentValue = initialValue;
_startValue = initialValue;
_targetValue = initialValue;
_durationSeconds = durationSeconds;
_progress = 1f;
_isTransitioning = false;
_interpolator = interpolator;
}
public T Value => _currentValue;
public bool IsTransitioning => _isTransitioning;
public void StartTransition(T targetValue)
{
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
_targetValue = targetValue;
_progress = 0f;
_isTransitioning = true;
}
}
public void Update(TimeSpan elapsedTime)
{
if (!_isTransitioning)
return;
_progress += (float)elapsedTime.TotalSeconds / _durationSeconds;
if (_progress >= 1f)
{
_progress = 1f;
_currentValue = _targetValue;
_isTransitioning = false;
}
else
{
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
}
public void Reset(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_isTransitioning = false;
}
}
}

View File

@@ -41,19 +41,21 @@ namespace BetterLyrics.WinUI3.Helper
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
// Data Files
private static string DatabaseFileName => "database.db";
public static string DatabasePath => Path.Combine(LocalFolder, DatabaseFileName);
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string OnlineLyricsCacheDirectory =>
Path.Combine(CacheFolder, "online-lyrics");
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
public static string TestMusicPath => Path.Combine(AssetsFolder, TestMusicFileName);
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(OnlineLyricsCacheDirectory);
}
}
}

View File

@@ -1,14 +1,32 @@
namespace BetterLyrics.WinUI3.Helper
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
public class ColorHelper
public static class ColorHelper
{
public static Windows.UI.Color LerpColor(Windows.UI.Color a, Windows.UI.Color b, double t)
public static Windows.UI.Color ToWindowsUIColor(this System.Drawing.Color color)
{
byte A = (byte)(a.A + (b.A - a.A) * t);
byte R = (byte)(a.R + (b.R - a.R) * t);
byte G = (byte)(a.G + (b.G - a.G) * t);
byte B = (byte)(a.B + (b.B - a.B) * t);
return Windows.UI.Color.FromArgb(A, R, G, B);
return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B);
}
public static Color GetInterpolatedColor(
float progress,
Color startColor,
Color targetColor
)
{
byte Lerp(byte a, byte b) => (byte)(a + (progress * (b - a)));
return Color.FromArgb(
Lerp(startColor.A, targetColor.A),
Lerp(startColor.R, targetColor.R),
Lerp(startColor.G, targetColor.G),
Lerp(startColor.B, targetColor.B)
);
}
}
}

View File

@@ -4,6 +4,11 @@ using System.Linq;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
// This file is a fork of https://github.com/KSemenenko/ColorThief
// and is used to extract dominant colors from images in WinUI3 application
// I have modified it to fit the WinUI3 environment and removed unnecessary parts
// Credits to KSemenenko for the original work.
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockHelper
{
private static readonly HashSet<IntPtr> _registered = [];
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static void Disable(Window window)
{
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
SetWindowPos(
hwnd,
IntPtr.Zero,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
_originalPositions.Remove(hwnd);
}
UnregisterAppBar(hwnd);
}
public static void Enable(Window window, int appBarHeight)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_originalWindowStyle.ContainsKey(hwnd))
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
if (GetWindowRect(hwnd, out var rect))
{
_originalPositions[hwnd] = rect;
}
}
RegisterAppBar(hwnd, appBarHeight);
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
screenWidth,
appBarHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
#region AppBar registration
private const uint ABM_NEW = 0x00000000;
private const uint ABM_REMOVE = 0x00000001;
private const uint ABM_SETPOS = 0x00000003;
private const int ABE_TOP = 1;
[StructLayout(LayoutKind.Sequential)]
private struct APPBARDATA
{
public int cbSize;
public IntPtr hWnd;
public uint uCallbackMessage;
public uint uEdge;
public RECT rc;
public int lParam;
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int left,
top,
right,
bottom;
}
[DllImport("shell32.dll", SetLastError = true)]
private static extern uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData);
private static void RegisterAppBar(IntPtr hwnd, int height)
{
if (_registered.Contains(hwnd))
return;
APPBARDATA abd = new()
{
cbSize = Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uEdge = ABE_TOP,
rc = new RECT
{
left = 0,
top = 0,
right = GetSystemMetrics(SM_CXSCREEN),
bottom = height,
},
};
SHAppBarMessage(ABM_NEW, ref abd);
SHAppBarMessage(ABM_SETPOS, ref abd);
_registered.Add(hwnd);
}
private static void UnregisterAppBar(IntPtr hwnd)
{
if (!_registered.Contains(hwnd))
return;
APPBARDATA abd = new() { cbSize = Marshal.SizeOf<APPBARDATA>(), hWnd = hwnd };
SHAppBarMessage(ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
#endregion
#region Win32 Helper and Constants
private const int SWP_NOACTIVATE = 0x0010;
private const int SWP_NOOWNERZORDER = 0x0200;
private const int SWP_SHOWWINDOW = 0x0040;
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 0;
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int X,
int Y,
int cx,
int cy,
uint uFlags
);
/// <summary>
/// 更改已注册 AppBar 的高度。
/// </summary>
/// <param name="window">目标窗口</param>
/// <param name="newHeight">新的高度</param>
public static void UpdateAppBarHeight(IntPtr hwnd, int newHeight)
{
if (!_registered.Contains(hwnd))
return;
APPBARDATA abd = new()
{
cbSize = Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uEdge = ABE_TOP,
rc = new RECT
{
left = 0,
top = 0,
right = GetSystemMetrics(SM_CXSCREEN),
bottom = newHeight,
},
};
SHAppBarMessage(ABM_SETPOS, ref abd);
// 同步窗口实际高度
SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
GetSystemMetrics(SM_CXSCREEN),
newHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
}
#endregion
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
{
public class FileHelper
{
public static Encoding GetEncoding(string filename)
{
var bytes = File.ReadAllBytes(filename);
var cdet = new CharsetDetector();
cdet.Feed(bytes, 0, bytes.Length);
cdet.DataEnd();
var encoding = cdet.Charset;
if (encoding == null)
{
return Encoding.UTF8;
}
return Encoding.GetEncoding(encoding);
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
{
private readonly WinEventDelegate _winEventDelegate;
private readonly List<IntPtr> _hooks = new();
private IntPtr _currentForeground = IntPtr.Zero;
private readonly IntPtr _selfHwnd;
private readonly DispatcherTimer _pollingTimer;
private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 100;
public delegate void WindowChangedHandler(IntPtr hwnd);
private readonly WindowChangedHandler _onWindowChanged;
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new WinEventDelegate(WinEventProc);
_pollingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_pollingTimer.Tick += (_, _) =>
{
if (_currentForeground != IntPtr.Zero && _currentForeground != _selfHwnd)
_onWindowChanged?.Invoke(_currentForeground);
};
}
public void Start()
{
// Hook: foreground changes and minimize end
_hooks.Add(
SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_MINIMIZEEND,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
// Hook: window move/resize (location change)
_hooks.Add(
SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_OBJECT_LOCATIONCHANGE,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
_pollingTimer.Start();
}
public void Stop()
{
foreach (var hook in _hooks)
UnhookWinEvent(hook);
_hooks.Clear();
_pollingTimer.Stop();
}
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
)
{
if (hwnd == IntPtr.Zero || hwnd == _selfHwnd)
return;
var now = DateTime.Now;
if ((now - _lastEventTime).TotalMilliseconds < ThrottleIntervalMs)
return;
_lastEventTime = now;
if (eventType == EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;
_onWindowChanged?.Invoke(hwnd);
}
else if (
(eventType == EVENT_OBJECT_LOCATIONCHANGE || eventType == EVENT_SYSTEM_MINIMIZEEND)
&& hwnd == _currentForeground
)
{
_onWindowChanged?.Invoke(hwnd);
}
}
#region WinAPI
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags
);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
}
}

View File

@@ -1,17 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
private static readonly ColorThief _colorThief = new();
public const int AccentColorCount = 3;
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
@@ -21,6 +32,26 @@ namespace BetterLyrics.WinUI3.Helper
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(stream);
return bitmapImage;
}
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
return stream;
@@ -33,5 +64,93 @@ namespace BetterLyrics.WinUI3.Helper
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static async Task<List<Color>> GetAccentColorsFromByte(byte[] bytes) =>
[
.. (
await _colorThief.GetPalette(await GetDecoderFromByte(bytes), AccentColorCount)
).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(
string text,
int width,
int height
)
{
var device = CanvasDevice.GetSharedDevice();
var renderTarget = new CanvasRenderTarget(device, width, height, 96);
// 居中绘制文字
using (var ds = renderTarget.CreateDrawingSession())
{
// 背景色
ds.Clear(Colors.LightGray);
// 文字格式
var format = new CanvasTextFormat
{
FontSize = Math.Min(width, height) / 6f,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
HorizontalAlignment = CanvasHorizontalAlignment.Center,
VerticalAlignment = CanvasVerticalAlignment.Center,
WordWrapping = CanvasWordWrapping.Wrap,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
Options = CanvasDrawTextOptions.Default,
};
// 设定边距
float margin = Math.Min(width, height) / 12f;
float availableWidth = width - 2 * margin;
float availableHeight = height - 2 * margin;
// 计算合适的字体大小以适应内容区域
float fontSize = format.FontSize;
float minFontSize = 8f;
float maxFontSize = format.FontSize;
CanvasTextLayout layout;
do
{
format.FontSize = fontSize;
layout = new CanvasTextLayout(
ds,
text,
format,
availableWidth,
availableHeight
);
if (
layout.LayoutBounds.Width <= availableWidth
&& layout.LayoutBounds.Height <= availableHeight
)
break;
fontSize -= 1f;
} while (fontSize >= minFontSize);
// 居中绘制文字(在内容区域内居中)
var bounds = layout.LayoutBounds;
var x = margin + (availableWidth - (float)bounds.Width) / 2f - (float)bounds.X;
var y = margin + (availableHeight - (float)bounds.Height) / 2f - (float)bounds.Y;
ds.DrawTextLayout(layout, new Vector2(x, y), Colors.DarkGray);
}
// 保存为 PNG 并转为 byte[]
using (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;
}
}
}
}

View File

@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
{
private List<List<LyricsLine>> _multiLangLyricsLines = [];
public List<List<LyricsLine>> Parse(
string raw,
LyricsFormat? lyricsFormat = null,
string? title = null,
string? artist = null,
int durationMs = 0
)
{
_multiLangLyricsLines = [];
switch (lyricsFormat)
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw, durationMs);
break;
case LyricsFormat.Ttml:
ParseTtml(raw, durationMs);
break;
default:
break;
}
return _multiLangLyricsLines;
}
private void PostProcessLyricsLines(List<LyricsLine> lines)
{
if (lines.Count > 0 && lines[0].StartMs > 0)
{
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = lines[0].StartMs,
Text = "",
CharTimings = [],
}
);
}
}
private void ParseLrc(string raw, int durationMs)
{
var lines = raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
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})(\]|\>)([^\[\]\<\>]*)"
);
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 0)
{
lrcLines.Add(
(
syllables[0].Item1,
string.Concat(syllables.Select(s => s.Item2)),
syllables
)
);
}
else
{
// 普通LRC行
var bracketRegex = new Regex(@"\[(\d{2}):(\d{2})\.(\d{2,3})\]");
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 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'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex.Replace(line, "").Trim();
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
}
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = grouped.Max(g => g.Count());
// 初始化每种语言的歌词列表
_multiLangLyricsLines.Clear();
for (int i = 0; i < languageCount; i++)
_multiLangLyricsLines.Add(new List<LyricsLine>());
// 遍历每个时间分组
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 如果该语言有翻译,取对应行,否则用原文(第一行)
var (start, text, syllables) =
langIdx < linesInGroup.Count ? linesInGroup[langIdx] : linesInGroup[0];
var line = new LyricsLine
{
StartMs = start,
EndMs = 0, // 稍后统一修正
Text = text,
CharTimings = [],
};
if (syllables != null && syllables.Count > 0)
{
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int charEnd = (j + 1 < syllables.Count) ? syllables[j + 1].Item1 : 0;
line.CharTimings.Add(
new CharTiming { StartMs = charStart, EndMs = charEnd }
);
}
}
_multiLangLyricsLines[langIdx].Add(line);
}
}
// 修正 EndMs
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
var linesInSingleLang = _multiLangLyricsLines[langIdx];
for (int i = 0; i < linesInSingleLang.Count; i++)
{
if (i + 1 < linesInSingleLang.Count)
linesInSingleLang[i].EndMs = linesInSingleLang[i + 1].StartMs;
else
linesInSingleLang[i].EndMs = durationMs;
// 修正 CharTimings 的最后一个 EndMs
var timings = linesInSingleLang[i].CharTimings;
if (timings.Count > 0)
{
for (int j = 0; j < timings.Count; j++)
{
if (j + 1 < timings.Count)
timings[j].EndMs = timings[j + 1].StartMs;
else
timings[j].EndMs = linesInSingleLang[i].EndMs;
}
}
}
PostProcessLyricsLines(linesInSingleLang);
}
}
private void ParseTtml(string raw, int durationMs)
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null)
return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 处理分词分时
var spans = p.Elements()
.Where(s =>
s.Name.LocalName == "span"
&& s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))
== null
)
.ToList();
string text = string.Concat(spans.Select(s => s.Value));
var charTimings = new List<CharTiming>();
for (int i = 0; i < spans.Count; i++)
{
var span = spans[i];
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
if (sStartMs == 0 && sEndMs == 0)
continue;
if (sEndMs == 0)
sEndMs =
(i + 1 < spans.Count)
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = sEndMs });
}
if (spans.Count == 0)
text = p.Value.Trim();
singleLangLyricsLine.Add(
new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
Text = text,
CharTimings = charTimings,
}
);
}
PostProcessLyricsLines(singleLangLyricsLine);
_multiLangLyricsLines.Add(singleLangLyricsLine);
}
catch
{
// 解析失败,忽略
}
}
private int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class MathHelper
{
public static List<int> GetAllFactors(int n)
{
var result = new SortedSet<int>();
for (int i = 1; i <= Math.Sqrt(n); i++)
{
if (n % i == 0)
{
result.Add(i);
result.Add(n / i);
}
}
return [.. result];
}
}
}

View File

@@ -1,4 +1,4 @@
using DevWinUI;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;
@@ -11,12 +11,10 @@ namespace BetterLyrics.WinUI3.Helper
return backdropType switch
{
BackdropType.None => null,
BackdropType.Mica => new MicaSystemBackdrop(MicaKind.Base),
BackdropType.MicaAlt => new MicaSystemBackdrop(MicaKind.BaseAlt),
BackdropType.Mica => new MicaBackdrop { Kind = MicaKind.Base },
BackdropType.MicaAlt => new MicaBackdrop { Kind = MicaKind.BaseAlt },
BackdropType.DesktopAcrylic => new DesktopAcrylicBackdrop(),
BackdropType.AcrylicThin => new AcrylicSystemBackdrop(DesktopAcrylicKind.Thin),
BackdropType.AcrylicBase => new AcrylicSystemBackdrop(DesktopAcrylicKind.Base),
BackdropType.Transparent => new TransparentBackdrop(),
BackdropType.Transparent => new WinUIEx.TransparentTintBackdrop(),
_ => null,
};
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace BetterLyrics.WinUI3.Helper
{
public class VisualHelper
{
/// <summary>
/// Source: https://stackoverflow.com/a/61626933/11048731
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="depObj"></param>
/// <returns></returns>
public static List<T> FindVisualChildren<T>(DependencyObject depObj)
where T : DependencyObject
{
List<T> list = [];
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T t)
{
list.Add(t);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}
}
}
return list;
}
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColorBelow(IntPtr myHwnd)
{
if (!GetWindowRect(myHwnd, out RECT myRect))
return Color.Transparent;
int screenWidth = GetSystemMetrics(SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
gDest.ReleaseHdc(hdcDest);
ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0,
g = 0,
b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0)
return Color.Transparent;
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
#region Win32 Imports & Structs
private const int SRCCOPY = 0x00CC0020;
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
int dwRop
);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);
private enum SystemMetric
{
SM_CXSCREEN = 0,
SM_CYSCREEN = 1,
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
}

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowHelper
{
private static readonly Dictionary<Type, Window> _windowCache = new();
public static void HideSystemTitleBar(this Window window)
{
window.ExtendsContentIntoTitleBar = true;
window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
}
public static void HideSystemTitleBarAndSetCustomTitleBar(
this Window window,
UIElement titleBar
)
{
window.HideSystemTitleBar();
window.SetTitleBar(titleBar);
}
public static void OpenSettingsWindow()
{
OpenOrShowWindow(typeof(SettingsPage));
}
public static void OpenLyricsWindow()
{
OpenOrShowWindow(typeof(LyricsPage));
}
private static void OpenOrShowWindow(Type pageType)
{
if (_windowCache.TryGetValue(pageType, out var window))
{
window.TryShow();
}
else
{
var newWindow = new HostWindow();
TrackWindow(newWindow, pageType);
newWindow.ViewModel.FramePageType = pageType;
newWindow.Navigate(pageType);
newWindow.Activate();
}
}
public static void TrackWindow(Window window, Type pageType = null)
{
if (pageType != null)
{
_windowCache[pageType] = window;
}
if (!_activeWindows.Contains(window))
_activeWindows.Add(window);
}
public static Window GetWindowForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return window;
}
}
}
return null;
}
// get dpi for an element
static public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return element.XamlRoot.RasterizationScale;
}
}
}
return 0.0;
}
public static List<Window> ActiveWindows
{
get { return _activeWindows; }
}
private static List<Window> _activeWindows = new List<Window>();
public static void TryShow(this Window window)
{
if (window is not null)
{
window.Activate();
}
}
public static void TryHide(this Window window)
{
if (window is not null)
{
window.Hide();
}
}
public static Window GetWindowByFramePageType(Type type)
{
foreach (var cachedWindow in _windowCache)
{
if (cachedWindow.Key == type)
{
return cachedWindow.Value;
}
}
return null;
}
}
}

View File

@@ -1,197 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
SizeChanged="Window_SizeChanged"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Frame
x:Name="RootFrame"
Navigated="RootFrame_Navigated"
NavigationFailed="RootFrame_NavigationFailed" />
<Grid
x:Name="TopCommandGrid"
Padding="2,0"
VerticalAlignment="Top"
Background="Transparent"
Opacity="0">
<Grid.Resources>
<Storyboard x:Name="TopCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="TopCommandGridFadeOutStoryboard">
<DoubleAnimation
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeInStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeOutStoryboard}" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<ImageIcon
Height="24"
Margin="16,0"
Source="ms-appx:///Assets/Logo.png" />
<TextBlock
x:Name="AppTitleTextBlock"
Margin="0,-4,0,0"
VerticalAlignment="Center"
Text="{x:Bind Title}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<!-- Always On Top -->
<AppBarButton
x:Name="AOTButton"
Click="AOTButton_Click"
LabelPosition="Collapsed">
<Grid>
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE840;" />
<FontIcon
x:Name="PinnedFontIcon"
FontFamily="Segoe Fluent Icons"
Glyph="&#xE841;"
Opacity="0">
<FontIcon.Resources>
<Storyboard x:Key="ShowPinnedFontIconStoryboard">
<DoubleAnimation
Storyboard.TargetName="PinnedFontIcon"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.3" />
</Storyboard>
<Storyboard x:Key="HidePinnedFontIconStoryboard">
<DoubleAnimation
Storyboard.TargetName="PinnedFontIcon"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.3" />
</Storyboard>
</FontIcon.Resources>
</FontIcon>
</Grid>
</AppBarButton>
<!-- Window Mini -->
<AppBarButton
x:Name="MiniButton"
Click="MiniButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEE49;" />
</AppBarButton>
<!-- Window Unmini -->
<AppBarButton
x:Name="UnminiButton"
Click="UnminiButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEE47;" />
</AppBarButton>
<!-- Window Minimise -->
<AppBarButton
x:Name="MinimiseButton"
Click="MinimiseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE921;" />
</AppBarButton>
<!-- Window Maximise -->
<AppBarButton
x:Name="MaximiseButton"
Click="MaximiseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE922;" />
</AppBarButton>
<!-- Window Restore -->
<AppBarButton
x:Name="RestoreButton"
Click="RestoreButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE923;" />
</AppBarButton>
<!-- Window Close -->
<AppBarButton
x:Name="CloseButton"
Click="CloseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE8BB;" />
</AppBarButton>
</StackPanel>
</Grid>
<InfoBar
x:Name="HostInfoBar"
Margin="18"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
IsClosable="False"
Opacity="0">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
<InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard">
<!-- Opacity -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBar" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="0" />
</DoubleAnimationUsingKeyFrames>
<!-- Y -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBarTransform" Storyboard.TargetProperty="Y">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="20" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="20" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</InfoBar.Resources>
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource InfoBarShowAndHideStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid>
</Window>

View File

@@ -1,178 +0,0 @@
using System;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Behaviors;
using DevWinUI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window
{
private readonly OverlappedPresenter _presenter;
private readonly SettingsService _settingsService;
public static StackedNotificationsBehavior? StackedNotificationsBehavior
{
get;
private set;
}
public MainWindow()
{
this.InitializeComponent();
_settingsService = Ioc.Default.GetService<SettingsService>()!;
RootGrid.RequestedTheme = (ElementTheme)_settingsService.Theme;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
(BackdropType)_settingsService.BackdropType
);
WeakReferenceMessenger.Default.Register<ThemeChangedMessage>(
this,
(r, m) =>
{
RootGrid.RequestedTheme = m.Value;
}
);
WeakReferenceMessenger.Default.Register<SystemBackdropChangedMessage>(
this,
(r, m) =>
{
SystemBackdrop = null;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(m.Value);
}
);
// AppWindow.SetIcon("white_round.ico");
StackedNotificationsBehavior = NotificationQueue;
_presenter = (OverlappedPresenter)AppWindow.Presenter;
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SetTitleBar(TopCommandGrid);
}
public void Navigate(Type type)
{
RootFrame.Navigate(type);
}
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
if (RootFrame.CurrentSourcePageType == typeof(MainPage))
{
(
(RootFrame.Content as MainPage)!.FindChild("LyricsCanvas")
as CanvasAnimatedControl
)!.Paused = true;
App.Current.Exit();
}
else if (RootFrame.CurrentSourcePageType == typeof(SettingsPage))
{
App.Current.SettingsWindow!.AppWindow.Hide();
}
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
{
_presenter.Maximize();
//MaximiseButton.Visibility = Visibility.Collapsed;
//RestoreButton.Visibility = Visibility.Visible;
}
private void MinimiseButton_Click(object sender, RoutedEventArgs e)
{
_presenter.Minimize();
}
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
_presenter.Restore();
//MaximiseButton.Visibility = Visibility.Visible;
//RestoreButton.Visibility = Visibility.Collapsed;
}
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
if (_presenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (_presenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
private void MiniButton_Click(object sender, RoutedEventArgs e)
{
AppWindow.Resize(new Windows.Graphics.SizeInt32(144, 48));
MiniButton.Visibility = Visibility.Collapsed;
UnminiButton.Visibility = Visibility.Visible;
MinimiseButton.Visibility = Visibility.Collapsed;
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Collapsed;
}
private void UnminiButton_Click(object sender, RoutedEventArgs e)
{
AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 600));
MiniButton.Visibility = Visibility.Visible;
UnminiButton.Visibility = Visibility.Collapsed;
MinimiseButton.Visibility = Visibility.Visible;
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
}
private void RootFrame_Navigated(object sender, NavigationEventArgs e)
{
AppWindow.Title = Title = App.ResourceLoader!.GetString(
$"{e.SourcePageType.Name}Title"
);
}
private void AOTButton_Click(object sender, RoutedEventArgs e)
{
_presenter.IsAlwaysOnTop = !_presenter.IsAlwaysOnTop;
string prefix;
if (_presenter.IsAlwaysOnTop)
{
prefix = "Show";
}
else
{
prefix = "Hide";
}
(PinnedFontIcon.Resources[$"{prefix}PinnedFontIconStoryboard"] as Storyboard)!.Begin();
}
}
}

View File

@@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Messages
{
public class ThemeChangedMessage(ElementTheme value)
: ValueChangedMessage<ElementTheme>(value) { }
public class ShowNotificatonMessage(Notification value)
: ValueChangedMessage<Notification>(value)
{ }
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
using DevWinUI;
namespace BetterLyrics.WinUI3.Messages
{
public class SystemBackdropChangedMessage(BackdropType value)
: ValueChangedMessage<BackdropType>(value) { }
}

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.Models
{
public class CharTiming
{
public int StartMs { get; set; }
public int EndMs { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LocalLyricsFolder : ObservableObject
{
[ObservableProperty]
public partial string Path { get; set; }
[ObservableProperty]
public partial bool IsEnabled { get; set; }
public LocalLyricsFolder() { }
public LocalLyricsFolder(string path, bool isEnabled)
{
Path = path;
IsEnabled = isEnabled;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsData
{
public int LanguageIndex { get; set; } = 0;
public List<LyricsLine> LyricsLines => MultiLangLyricsLines[LanguageIndex];
public List<List<LyricsLine>> MultiLangLyricsLines { get; set; } = [];
}
}

View File

@@ -1,27 +1,22 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Graphics.Canvas.Text;
using Windows.Foundation;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsLine
{
public List<string> Texts { get; set; } = [];
public string Text { get; set; } = "";
public int LanguageIndex { get; set; } = 0;
public List<CharTiming> CharTimings { get; set; } = [];
public string Text => Texts[LanguageIndex];
public int StartPlayingTimestampMs { get; set; }
public int EndPlayingTimestampMs { get; set; }
public int StartMs { get; set; }
public int EndMs { get; set; }
public LyricsPlayingState PlayingState { get; set; }
public int DurationMs => EndPlayingTimestampMs - StartPlayingTimestampMs;
public int DurationMs => EndMs - StartMs;
public float EnteringProgress { get; set; }
@@ -37,6 +32,23 @@ namespace BetterLyrics.WinUI3.Models
public float Opacity { get; set; }
public CanvasTextLayout TextLayout { get; set; }
public LyricsLine Clone()
{
return new LyricsLine
{
Text = this.Text,
CharTimings = this.CharTimings,
StartMs = this.StartMs,
EndMs = this.EndMs,
PlayingState = this.PlayingState,
EnteringProgress = this.EnteringProgress,
ExitingProgress = this.ExitingProgress,
PlayingProgress = this.PlayingProgress,
Position = this.Position,
CenterPosition = this.CenterPosition,
Scale = this.Scale,
Opacity = this.Opacity,
};
}
}
}

View File

@@ -0,0 +1,22 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LyricsSearchProviderInfo : ObservableObject
{
[ObservableProperty]
public partial LyricsSearchProvider Provider { get; set; }
[ObservableProperty]
public partial bool IsEnabled { get; set; }
public LyricsSearchProviderInfo() { }
public LyricsSearchProviderInfo(LyricsSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,12 +0,0 @@
using SQLite;
namespace BetterLyrics.WinUI3.Models
{
public class MetadataIndex
{
[PrimaryKey]
public string? Path { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Models
{
public partial class Notification : ObservableObject
{
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
[ObservableProperty]
public partial string? Message { get; set; }
[ObservableProperty]
public partial bool IsForeverDismissable { get; set; }
[ObservableProperty]
public partial Visibility Visibility { get; set; }
[ObservableProperty]
public partial string? RelatedSettingsKeyName { get; set; }
public Notification(
string? message = null,
InfoBarSeverity severity = InfoBarSeverity.Informational,
bool isForeverDismissable = false,
string? relatedSettingsKeyName = null
)
{
Message = message;
Severity = severity;
IsForeverDismissable = isForeverDismissable;
Visibility = IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
RelatedSettingsKeyName = relatedSettingsKeyName;
}
}
}

View File

@@ -0,0 +1,29 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject
{
[ObservableProperty]
public partial string Title { get; set; }
[ObservableProperty]
public partial string Artist { get; set; }
[ObservableProperty]
public partial string? Album { get; set; }
/// <summary>
/// In milliseconds
/// </summary>
[ObservableProperty]
public partial double? DurationMs { get; set; }
[ObservableProperty]
public partial string? SourceAppUserModelId { get; set; } = null;
public byte[]? AlbumArt { get; set; } = null;
public SongInfo() { }
}
}

View File

@@ -1,116 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
namespace BetterLyrics.WinUI3
{
public class Program
{
[STAThread]
static int Main(string[] args)
{
WinRT.ComWrappersSupport.InitializeComWrappers();
bool isRedirect = DecideRedirection();
if (!isRedirect)
{
Application.Start(
(p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread()
);
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
}
);
}
return 0;
}
private static bool DecideRedirection()
{
bool isRedirect = false;
AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
ExtendedActivationKind kind = args.Kind;
AppInstance keyInstance = AppInstance.FindOrRegisterForKey(Helper.AppInfo.AppName);
if (keyInstance.IsCurrent)
{
keyInstance.Activated += OnActivated;
}
else
{
isRedirect = true;
RedirectActivationTo(args, keyInstance);
}
return isRedirect;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateEvent(
IntPtr lpEventAttributes,
bool bManualReset,
bool bInitialState,
string lpName
);
[DllImport("kernel32.dll")]
private static extern bool SetEvent(IntPtr hEvent);
[DllImport("ole32.dll")]
private static extern uint CoWaitForMultipleObjects(
uint dwFlags,
uint dwMilliseconds,
ulong nHandles,
IntPtr[] pHandles,
out uint dwIndex
);
[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);
private static IntPtr redirectEventHandle = IntPtr.Zero;
// Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete.
public static void RedirectActivationTo(
AppActivationArguments args,
AppInstance keyInstance
)
{
redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
uint CWMO_DEFAULT = 0;
uint INFINITE = 0xFFFFFFFF;
_ = CoWaitForMultipleObjects(
CWMO_DEFAULT,
INFINITE,
1,
[redirectEventHandle],
out uint handleIndex
);
// Bring the window to the foreground
Process process = Process.GetProcessById((int)keyInstance.ProcessId);
SetForegroundWindow(process.MainWindowHandle);
}
private static void OnActivated(object sender, AppActivationArguments args)
{
ExtendedActivationKind kind = args.Kind;
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>ARM64</Platform>
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
<PublishDir>bin\\\win-arm64\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x64</Platform>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishDir>bin\\\win-x64\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x86</Platform>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<PublishDir>bin\\\win-x86\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Renderer.LyricsRenderer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Loaded="LyricsCanvas_Loaded"
Update="LyricsCanvas_Update" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,43 @@
using System.Diagnostics;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Renderer
{
public sealed partial class LyricsRenderer : UserControl
{
public LyricsRendererViewModel ViewModel { get; set; }
public LyricsRenderer()
{
InitializeComponent();
ViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
}
private void LyricsCanvas_Draw(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args
)
{
ViewModel.Draw(sender, args.DrawingSession);
}
private void LyricsCanvas_Update(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args
)
{
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RequestRelayout();
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Serialization
{
[JsonSerializable(typeof(List<LyricsSearchProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext { }
}

View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using SQLite;
using Ude;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Database
{
public class DatabaseService
{
private readonly SQLiteConnection _connection;
private readonly CharsetDetector _charsetDetector = new();
public DatabaseService()
{
_connection = new SQLiteConnection(Helper.AppInfo.DatabasePath);
if (_connection.GetTableInfo("MetadataIndex").Count == 0)
{
_connection.CreateTable<MetadataIndex>();
}
}
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<string> paths)
{
await Task.Run(() =>
{
_connection.DeleteAll<MetadataIndex>();
foreach (var path in paths)
{
if (Directory.Exists(path))
{
foreach (var file in Directory.GetFiles(path))
{
var fileExtension = Path.GetExtension(file);
var track = new Track(file);
_connection.Insert(
new MetadataIndex
{
Path = file,
Title = track.Title,
Artist = track.Artist,
}
);
}
}
}
});
}
public Track? GetMusicMetadata(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
if (mediaProps == null || mediaProps.Title == null || mediaProps.Artist == null)
return null;
var founds = _connection
.Table<MetadataIndex>()
// Look up by Title and Artist (these two props were fetched by reading metadata in music file befoe) first
// then by Path (music file name usually contains song name and artist so this can be a second way to look up for)
// Please note for .lrc file, only the second way works for it
.Where(m =>
(
m.Title != null
&& m.Artist != null
&& m.Title.Contains(mediaProps.Title)
&& m.Artist.Contains(mediaProps.Artist)
)
|| (
m.Path != null
&& m.Path.Contains(mediaProps.Title)
&& m.Path.Contains(mediaProps.Artist)
)
)
.ToList();
if (founds == null || founds.Count == 0)
{
return null;
}
else
{
var first = new Track(founds[0].Path);
if (founds.Count == 1)
{
return first;
}
else
{
if (first.Lyrics.Exists())
{
return first;
}
else
{
foreach (var found in founds)
{
if (found.Path.EndsWith(".lrc"))
{
using (FileStream fs = File.OpenRead(found.Path))
{
_charsetDetector.Feed(fs);
_charsetDetector.DataEnd();
}
string content;
if (_charsetDetector.Charset != null)
{
Encoding encoding = Encoding.GetEncoding(
_charsetDetector.Charset
);
content = File.ReadAllText(found.Path, encoding);
}
else
{
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
first.Lyrics.ParseLRC(content);
return first;
}
}
return first;
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services
{
public interface ILibWatcherService
{
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public void UpdateWatchers(List<LocalLyricsFolder> folders);
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface IMusicSearchService
{
Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
);
byte[]? SearchAlbumArtAsync(string title, string artist);
}
}

View File

@@ -0,0 +1,17 @@
using System;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services
{
public interface IPlaybackService
{
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
SongInfo? SongInfo { get; }
bool IsPlaying { get; }
TimeSpan Position { get; }
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Services
{
public interface ISettingsService
{
bool IsFirstRun { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
// App appearance
ElementTheme ThemeType { get; set; }
BackdropType BackdropType { get; set; }
TitleBarType TitleBarType { get; set; }
Language Language { get; set; }
// App behavior
AutoStartWindowType AutoStartWindowType { get; set; }
// Album art background
bool IsCoverOverlayEnabled { get; set; }
bool IsDynamicCoverOverlayEnabled { get; set; }
int CoverOverlayOpacity { get; set; }
int CoverOverlayBlurAmount { get; set; }
// Album art cover style
int CoverImageRadius { get; set; }
// Lyrics style and effetc
LyricsAlignmentType LyricsAlignmentType { get; set; }
LyricsFontWeight LyricsFontWeight { get; set; }
int LyricsBlurAmount { get; set; }
int LyricsVerticalEdgeOpacity { get; set; }
float LyricsLineSpacingFactor { get; set; }
int LyricsFontSize { get; set; }
bool IsLyricsGlowEffectEnabled { get; set; }
LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
LyricsFontColorType LyricsFontColorType { get; set; }
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using global::BetterLyrics.WinUI3.Events;
using global::BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services
{
public class LibWatcherService : IDisposable, ILibWatcherService
{
private readonly ISettingsService _settingsService;
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public LibWatcherService(ISettingsService settingsService)
{
_settingsService = settingsService;
UpdateWatchers(_settingsService.LocalLyricsFolders);
}
public void UpdateWatchers(List<LocalLyricsFolder> folders)
{
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
{
if (!folders.Any(x => x.Path == key && x.IsEnabled))
{
_watchers[key].Dispose();
_watchers.Remove(key);
}
}
// 添加新的监听
foreach (var folder in folders)
{
if (
!_watchers.ContainsKey(folder.Path)
&& Directory.Exists(folder.Path)
&& folder.IsEnabled
)
{
var watcher = new FileSystemWatcher(folder.Path)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
watcher.Created += (s, e) => OnChanged(folder.Path, e);
watcher.Changed += (s, e) => OnChanged(folder.Path, e);
watcher.Deleted += (s, e) => OnChanged(folder.Path, e);
watcher.Renamed += (s, e) => OnChanged(folder.Path, e);
_watchers[folder.Path] = watcher;
}
}
}
private void OnChanged(string folder, FileSystemEventArgs e)
{
App.DispatcherQueue!.TryEnqueue(
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
}
);
}
public void Dispose()
{
foreach (var watcher in _watchers.Values)
{
watcher.Dispose();
}
_watchers.Clear();
}
}
}
}

View File

@@ -0,0 +1,354 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace BetterLyrics.WinUI3.Services
{
public class MusicSearchService : IMusicSearchService
{
private readonly HttpClient _httpClient;
private readonly ISettingsService _settingsService;
public MusicSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
}
public byte[]? SearchAlbumArtAsync(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*.*",
SearchOption.AllDirectories
)
)
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}
}
return null;
}
public async Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
)
{
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
// Check cache first
var cachedLyrics = ReadCache(title, artist, LyricsFormat.Lrc);
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, LyricsFormat.Lrc);
}
break;
default:
break;
}
string? searchedLyrics = null;
switch (provider.Provider)
{
case LyricsSearchProvider.LocalMusicFile:
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
break;
case LyricsSearchProvider.LocalLrcFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Lrc
);
break;
case LyricsSearchProvider.LocalEslrcFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Eslrc
);
break;
case LyricsSearchProvider.LocalTtmlFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Ttml
);
break;
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLib(
title,
artist,
album,
(int)(durationMs / 1000),
matchMode
);
break;
default:
break;
}
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
WriteCache(title, artist, searchedLyrics, LyricsFormat.Lrc);
return (searchedLyrics, LyricsFormat.Lrc);
case LyricsSearchProvider.LocalMusicFile:
return (searchedLyrics, LyricsFormatExtensions.Detect(searchedLyrics));
case LyricsSearchProvider.LocalLrcFile:
return (searchedLyrics, LyricsFormat.Lrc);
case LyricsSearchProvider.LocalEslrcFile:
return (searchedLyrics, LyricsFormat.Eslrc);
case LyricsSearchProvider.LocalTtmlFile:
return (searchedLyrics, LyricsFormat.Ttml);
default:
break;
}
}
}
return (null, null);
}
private static int LevenshteinDistance(string a, string b)
{
if (string.IsNullOrEmpty(a))
return b.Length;
if (string.IsNullOrEmpty(b))
return a.Length;
int[,] d = new int[a.Length + 1, b.Length + 1];
for (int i = 0; i <= a.Length; i++)
d[i, 0] = i;
for (int j = 0; j <= b.Length; j++)
d[0, j] = j;
for (int i = 1; i <= a.Length; i++)
for (int j = 1; j <= b.Length; j++)
d[i, j] = Math.Min(
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
d[i - 1, j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)
);
return d[a.Length, b.Length];
}
// 判断相似度
private static bool FuzzyMatch(string fileName, string title, string artist)
{
var normFile = Normalize(fileName);
var normTarget1 = Normalize(title + artist);
var normTarget2 = Normalize(artist + title);
int dist1 = LevenshteinDistance(normFile, normTarget1);
int dist2 = LevenshteinDistance(normFile, normTarget2);
return dist1 <= 3 || dist2 <= 3; // 阈值可调整
}
private static string Normalize(string s)
{
if (string.IsNullOrWhiteSpace(s))
return "";
var sb = new StringBuilder();
foreach (var c in s.ToLowerInvariant())
{
if (char.IsLetterOrDigit(c))
sb.Append(c);
}
return sb.ToString();
}
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*.*",
SearchOption.AllDirectories
)
)
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
//Track track = new(file);
//var plain = track.Lyrics.UnsynchronizedLyrics;
try
{
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (plain != null && plain != string.Empty)
{
return plain;
}
}
catch (Exception) { }
}
}
}
}
return null;
}
private async Task<string?> LocalLyricsSearchInLyricsFiles(
string title,
string artist,
LyricsFormat format
)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*{format.ToFileExtension()}",
SearchOption.AllDirectories
)
)
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(
file,
FileHelper.GetEncoding(file)
);
if (raw != null)
{
return raw;
}
}
}
}
}
return null;
}
private async Task<string?> SearchLrcLib(
string title,
string artist,
string album,
int duration,
MusicSearchMatchMode matchMode
)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
if (matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
var json = await response.Content.ReadAsStringAsync();
var jArr = JsonSerializer.Deserialize(
json,
Serialization.SourceGenerationContext.Default.JsonElement
);
if (jArr.ValueKind == JsonValueKind.Array && jArr.GetArrayLength() > 0)
{
var first = jArr[0];
var syncedLyrics = first.GetProperty("syncedLyrics").GetString();
var result = string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
}
return null;
}
private void WriteCache(string title, string artist, string lyrics, LyricsFormat format)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
private string? ReadCache(string title, string artist, LyricsFormat format)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
private static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Windows.ApplicationModel;
using Windows.Media.Control;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services
{
public partial class PlaybackService : IPlaybackService
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
public SongInfo? SongInfo { get; private set; }
public bool IsPlaying { get; private set; }
public TimeSpan Position { get; private set; }
private readonly IMusicSearchService _musicSearchService;
public PlaybackService(
ISettingsService settingsService,
IMusicSearchService musicSearchService
)
{
_musicSearchService = musicSearchService;
InitMediaManager().ConfigureAwait(true);
}
private async Task InitMediaManager()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(
GlobalSystemMediaTransportControlsSession? sender,
PlaybackInfoChangedEventArgs? args
)
{
if (sender == null)
{
IsPlaying = false;
}
else
{
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
IsPlaying = false;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
IsPlaying = true;
break;
default:
break;
}
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(IsPlaying));
}
);
}
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
{
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -=
CurrentSession_TimelinePropertiesChanged;
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private async void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (sender == null)
{
SongInfo = null;
}
else
{
try
{
mediaProps = await sender.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
if (mediaProps == null)
{
SongInfo = null;
}
else
{
SongInfo = new SongInfo
{
Title = mediaProps.Title,
Artist = mediaProps.Artist,
Album = mediaProps?.AlbumTitle ?? string.Empty,
DurationMs = _currentSession
?.GetTimelineProperties()
.EndTime.TotalMilliseconds,
SourceAppUserModelId = _currentSession?.SourceAppUserModelId,
};
if (
SongInfo.SourceAppUserModelId?.Contains(Package.Current.Id.FamilyName)
?? false
)
{
SongInfo.Title = "甜度爆表";
SongInfo.Artist = "AI";
}
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
}
else
{
SongInfo.AlbumArt = _musicSearchService.SearchAlbumArtAsync(
SongInfo.Title,
SongInfo.Artist
);
if (SongInfo.AlbumArt == null)
{
SongInfo.AlbumArt = await ImageHelper.CreateTextPlaceholderBytesAsync(
$"{SongInfo.Artist} - {SongInfo.Title}",
400,
400
);
}
}
}
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
);
}
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
{
if (sender == null)
{
Position = TimeSpan.Zero;
}
else
{
Position = sender.GetTimelineProperties().Position;
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(Position));
}
);
// _logger.LogDebug(_currentTime);
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services.Settings
{
public static class SettingsDefaultValues
{
public const bool IsFirstRun = true;
// Theme
public const int ThemeType = 0; // Follow system
// Language
public const int Language = 0; // Default
// Music
public const string MusicLibraries = "[]";
// Backdrop
public const int BackdropType = 5; // Acrylic Base
public const bool IsCoverOverlayEnabled = true;
public const bool IsDynamicCoverOverlay = true;
public const int CoverOverlayOpacity = 100; // 1.0
public const int CoverOverlayBlurAmount = 200;
// Album art
public const int CoverImageRadius = 24;
// Lyrics
public const int LyricsAlignmentType = 1; // Center
public const int LyricsBlurAmount = 0;
public const int LyricsVerticalEdgeOpacity = 0; // 0.0
public const float LyricsLineSpacingFactor = 0.5f;
public const int LyricsFontSize = 28;
public const bool IsLyricsGlowEffectEnabled = false;
public const bool IsLyricsDynamicGlowEffectEnabled = false;
public const int LyricsFontColorType = 0; // Default
public const int LyricsFontSelectedAccentColorIndex = 0;
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings
{
public static class SettingsKeys
{
public const string IsFirstRun = "IsFirstRun";
// Theme
public const string ThemeType = "ThemeType";
// Language
public const string Language = "Language";
// Music
public const string MusicLibraries = "MusicLibraries";
// Backdrop
public const string BackdropType = "BackdropType";
public const string IsCoverOverlayEnabled = "IsCoverOverlayEnabled";
public const string IsDynamicCoverOverlay = "IsDynamicCoverOverlay";
public const string CoverOverlayOpacity = "CoverOverlayOpacity";
public const string CoverOverlayBlurAmount = "CoverOverlayBlurAmount";
// Album art
public const string CoverImageRadius = "CoverImageRadius";
// Lyrics
public const string LyricsAlignmentType = "LyricsAlignmentType";
public const string LyricsBlurAmount = "LyricsBlurAmount";
public const string LyricsVerticalEdgeOpacity = "LyricsVerticalEdgeOpacity";
public const string LyricsLineSpacingFactor = "LyricsLineSpacingFactor";
public const string LyricsFontSize = "LyricsFontSize";
public const string IsLyricsGlowEffectEnabled = "IsLyricsGlowEffectEnabled";
public const string IsLyricsDynamicGlowEffectEnabled = "IsLyricsDynamicGlowEffectEnabled";
public const string LyricsFontColorType = "LyricsFontColorType";
public const string LyricsFontSelectedAccentColorIndex =
"LyricsFontSelectedAccentColorIndex";
}
}

View File

@@ -1,269 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using BetterLyrics.WinUI3.Messages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Xaml;
using Newtonsoft.Json;
using Windows.Globalization;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.Settings
{
public partial class SettingsService : ObservableObject
{
private readonly ApplicationDataContainer _localSettings;
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
_musicLibraries =
[
.. JsonConvert.DeserializeObject<List<string>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)!
)!,
];
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
private void WatchMultipleDirectories(IEnumerable<string> directories)
{
foreach (var dir in directories)
{
if (!Directory.Exists(dir))
continue;
var watcher = new FileSystemWatcher
{
Path = dir,
Filter = "*.*",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
}
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
App.DispatcherQueue.TryEnqueue(() => { });
}
public bool IsFirstRun
{
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
set => Set(SettingsKeys.IsFirstRun, value);
}
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase = false;
// Theme
public int Theme
{
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set
{
Set(SettingsKeys.ThemeType, value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
}
}
// Music
private ObservableCollection<string> _musicLibraries;
public ObservableCollection<string> MusicLibraries
{
get { return _musicLibraries; }
set
{
if (_musicLibraries != null)
{
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
_musicLibraries = value;
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
SaveMusicLibraries();
OnPropertyChanged();
}
}
private void SaveMusicLibraries()
{
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language
{
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set
{
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language)
{
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Models.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Models.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Models.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
}
}
// Backdrop
public int BackdropType
{
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set
{
Set(SettingsKeys.BackdropType, value);
WeakReferenceMessenger.Default.Send(
new SystemBackdropChangedMessage((BackdropType)value)
);
}
}
public bool IsCoverOverlayEnabled
{
get =>
Get(
SettingsKeys.IsCoverOverlayEnabled,
SettingsDefaultValues.IsCoverOverlayEnabled
);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay
{
get =>
Get(
SettingsKeys.IsDynamicCoverOverlay,
SettingsDefaultValues.IsDynamicCoverOverlay
);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity
{
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount
{
get =>
Get(
SettingsKeys.CoverOverlayBlurAmount,
SettingsDefaultValues.CoverOverlayBlurAmount
);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
// Album art
public int CoverImageRadius
{
get => Get(SettingsKeys.CoverImageRadius, SettingsDefaultValues.CoverImageRadius);
set => Set(SettingsKeys.CoverImageRadius, value);
}
// Lyrics
public int LyricsAlignmentType
{
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount
{
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity
{
get =>
Get(
SettingsKeys.LyricsVerticalEdgeOpacity,
SettingsDefaultValues.LyricsVerticalEdgeOpacity
);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor
{
get =>
Get(
SettingsKeys.LyricsLineSpacingFactor,
SettingsDefaultValues.LyricsLineSpacingFactor
);
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
}
public int LyricsFontSize
{
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set => Set(SettingsKeys.LyricsFontSize, value);
}
public bool IsLyricsGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsGlowEffectEnabled,
SettingsDefaultValues.IsLyricsGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsDynamicGlowEffectEnabled,
SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
public int LyricsFontColorType
{
get => Get(SettingsKeys.LyricsFontColorType, SettingsDefaultValues.LyricsFontColorType);
set => Set(SettingsKeys.LyricsFontColorType, value);
}
public int LyricsFontSelectedAccentColorIndex
{
get =>
Get(
SettingsKeys.LyricsFontSelectedAccentColorIndex,
SettingsDefaultValues.LyricsFontSelectedAccentColorIndex
);
set
{
if (value >= 0)
Set(SettingsKeys.LyricsFontSelectedAccentColorIndex, value);
}
}
private T? Get<T>(string key, T? defaultValue = default)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
return (T)Convert.ChangeType(value, typeof(T));
}
return defaultValue;
}
private void Set<T>(string key, T value, [CallerMemberName] string? propertyName = null)
{
_localSettings.Values[key] = value;
OnPropertyChanged(propertyName);
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using Microsoft.UI.Xaml;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services
{
public class SettingsService : ISettingsService
{
private readonly ApplicationDataContainer _localSettings;
private const string IsFirstRunKey = "IsFirstRun";
// Lyrics lib
private const string LocalLyricsFoldersKey = "LocalLyricsFolders";
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
// App appearance
private const string ThemeTypeKey = "ThemeType";
private const string LanguageKey = "Language";
private const string BackdropTypeKey = "BackdropType";
// App behavior
private const string AutoStartWindowTypeKey = "AutoStartWindowType";
// Album art
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
private const string IsDynamicCoverOverlayEnabledKey = "IsDynamicCoverOverlayEnabled";
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
private const string CoverOverlayBlurAmountKey = "CoverOverlayBlurAmount";
private const string TitleBarTypeKey = "TitleBarType";
private const string CoverImageRadiusKey = "CoverImageRadius";
private const string LyricsAlignmentTypeKey = "LyricsAlignmentType";
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
private const string LyricsBlurAmountKey = "LyricsBlurAmount";
private const string LyricsVerticalEdgeOpacityKey = "LyricsVerticalEdgeOpacity";
private const string LyricsLineSpacingFactorKey = "LyricsLineSpacingFactor";
private const string LyricsFontSizeKey = "LyricsFontSize";
private const string IsLyricsGlowEffectEnabledKey = "IsLyricsGlowEffectEnabled";
private const string LyricsFontColorTypeKey = "LyricsFontColorType";
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
public bool IsFirstRun
{
get => GetValue<bool>(IsFirstRunKey);
set => SetValue(IsFirstRunKey, value);
}
public List<LocalLyricsFolder> LocalLyricsFolders
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LocalLyricsFoldersKey) ?? "[]",
SourceGenerationContext.Default.ListLocalLyricsFolder
)!;
set =>
SetValue(
LocalLyricsFoldersKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLocalLyricsFolder
)
);
}
public List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LyricsSearchProvidersInfoKey) ?? "[]",
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)!;
set =>
SetValue(
LyricsSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)
);
}
public ElementTheme ThemeType
{
get => (ElementTheme)GetValue<int>(ThemeTypeKey);
set => SetValue(ThemeTypeKey, (int)value);
}
public Language Language
{
get => (Language)GetValue<int>(LanguageKey);
set => SetValue(LanguageKey, (int)value);
}
public BackdropType BackdropType
{
get => (BackdropType)GetValue<int>(BackdropTypeKey);
set => SetValue(BackdropTypeKey, (int)value);
}
public AutoStartWindowType AutoStartWindowType
{
get => (AutoStartWindowType)GetValue<int>(AutoStartWindowTypeKey);
set => SetValue(AutoStartWindowTypeKey, (int)value);
}
public bool IsCoverOverlayEnabled
{
get => GetValue<bool>(IsCoverOverlayEnabledKey);
set => SetValue(IsCoverOverlayEnabledKey, value);
}
public bool IsDynamicCoverOverlayEnabled
{
get => GetValue<bool>(IsDynamicCoverOverlayEnabledKey);
set => SetValue(IsDynamicCoverOverlayEnabledKey, value);
}
public int CoverOverlayOpacity
{
get => GetValue<int>(CoverOverlayOpacityKey);
set => SetValue(CoverOverlayOpacityKey, value);
}
public int CoverOverlayBlurAmount
{
get => GetValue<int>(CoverOverlayBlurAmountKey);
set => SetValue(CoverOverlayBlurAmountKey, value);
}
public TitleBarType TitleBarType
{
get => (TitleBarType)GetValue<int>(TitleBarTypeKey);
set => SetValue(TitleBarTypeKey, (int)value);
}
public int CoverImageRadius
{
get => GetValue<int>(CoverImageRadiusKey);
set => SetValue(CoverImageRadiusKey, value);
}
public LyricsAlignmentType LyricsAlignmentType
{
get => (LyricsAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
set => SetValue(LyricsAlignmentTypeKey, (int)value);
}
public LyricsFontWeight LyricsFontWeight
{
get => (LyricsFontWeight)GetValue<int>(LyricsFontWeightKey);
set => SetValue(LyricsFontWeightKey, (int)value);
}
public int LyricsBlurAmount
{
get => GetValue<int>(LyricsBlurAmountKey);
set => SetValue(LyricsBlurAmountKey, value);
}
public int LyricsVerticalEdgeOpacity
{
get => GetValue<int>(LyricsVerticalEdgeOpacityKey);
set => SetValue(LyricsVerticalEdgeOpacityKey, value);
}
public float LyricsLineSpacingFactor
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
set => SetValue(LyricsLineSpacingFactorKey, value);
}
public int LyricsFontSize
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
}
public bool IsLyricsGlowEffectEnabled
{
get => GetValue<bool>(IsLyricsGlowEffectEnabledKey);
set => SetValue(IsLyricsGlowEffectEnabledKey, value);
}
public LyricsGlowEffectScope LyricsGlowEffectScope
{
get => (LyricsGlowEffectScope)GetValue<int>(LyricsGlowEffectScopeKey);
set => SetValue(LyricsGlowEffectScopeKey, (int)value);
}
public LyricsFontColorType LyricsFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsFontColorTypeKey);
set => SetValue(LyricsFontColorTypeKey, (int)value);
}
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
SetDefault(IsFirstRunKey, true);
// Lyrics lib
SetDefault(LocalLyricsFoldersKey, "[]");
SetDefault(
LyricsSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
Enum.GetValues<LyricsSearchProvider>()
.Select(p => new LyricsSearchProviderInfo(p, true))
.ToList(),
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)
);
if (LyricsSearchProvidersInfo.Count != Enum.GetValues<LyricsSearchProvider>().Length)
{
LyricsSearchProvidersInfo = Enum.GetValues<LyricsSearchProvider>()
.Select(p => new LyricsSearchProviderInfo(
p,
LyricsSearchProvidersInfo
.Where(x => x.Provider == p)
.FirstOrDefault()
?.IsEnabled ?? true
))
.ToList();
}
// App appearance
SetDefault(ThemeTypeKey, (int)ElementTheme.Default);
SetDefault(LanguageKey, (int)Language.FollowSystem);
SetDefault(BackdropTypeKey, (int)BackdropType.DesktopAcrylic);
// App behavior
SetDefault(AutoStartWindowTypeKey, (int)AutoStartWindowType.StandardMode);
// Album art
SetDefault(IsCoverOverlayEnabledKey, true);
SetDefault(IsDynamicCoverOverlayEnabledKey, true);
SetDefault(CoverOverlayOpacityKey, 75); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 200);
SetDefault(TitleBarTypeKey, (int)TitleBarType.Compact);
SetDefault(CoverImageRadiusKey, 24); // 24 %
// Lyrics
SetDefault(LyricsAlignmentTypeKey, (int)LyricsAlignmentType.Center);
SetDefault(LyricsFontWeightKey, (int)LyricsFontWeight.Bold);
SetDefault(LyricsBlurAmountKey, 0);
SetDefault(LyricsFontColorTypeKey, (int)LyricsFontColorType.Default);
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LyricsGlowEffectScope.CurrentChar);
}
private T? GetValue<T>(string key)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
return (T)value;
}
return default;
}
private void SetValue<T>(string key, T value)
{
_localSettings.Values[key] = value;
}
private void SetDefault<T>(string key, T value)
{
if (_localSettings.Values.ContainsKey(key) && _localSettings.Values[key] is T)
return;
_localSettings.Values[key] = value;
}
}
}

View File

@@ -121,10 +121,7 @@
<value>Local music libraries</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>Add folders storing music or lyrics to build lyrics index database</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>Open in file explorer</value>
<value>Add folders storing music or lyrics</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value>
@@ -223,14 +220,11 @@
<value>Album art background opacity</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>Settings</value>
<value>Settings - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics style</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>Alignment</value>
</data>
@@ -273,76 +267,46 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Lyrics only</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>Album background</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>About</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>Lyrics library</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>App appearance</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Glow effect</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>Dynamic glow effect</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>Glow effect scope</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>Rebuild lyrics index database</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>Rebuild</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>Configure lyrics search providers</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>Rebuilding the database, please wait...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's get started now</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>The top area is the title bar</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the top area to show it</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<value>Setup lyrics database now</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom left corner and then click to open settings page</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom area to show it</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>The bottom area is the command area</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>Toggle "show lyrics only" here</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>When lyrics exist, you can switch them in the lower right corner</value>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's setup lyrics database now</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>Developer options</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -363,10 +327,163 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>Album art accent color</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>Album art style</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Corner radius</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Title bar size</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compact</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Extended</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>Always on top</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>Full screen</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>Press Esc to exit full screen mode</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Hover back again to show the toggle button</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>No local files matched</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Album art only</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Split view</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>Change display type</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>Switch to desktop lyrics mode</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-picture mode</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Exit picture-in-picture mode</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>Lyrics 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>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>App behavior</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>When starting the app</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>Activate standard mode</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>Activate dock mode</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System tray - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>Dock mode</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>Font weight</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>Thin</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>Extra Light</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semi Light</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>Normal</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Medium</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>Semi Bold</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>Bold</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>Extra Bold</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>Black</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>Extra Black</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>Whole lyrics</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>Current line</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>Current char</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>Loading lyrics...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>Local .LRC files</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>Local music files</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML files</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
</data>
</root>

View File

@@ -0,0 +1,489 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>地元の音楽図書館</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>音楽や歌詞を保存するフォルダーを追加します</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>ファイルエクスプローラーで開きます</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>アプリから削除します</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>次のアイテムを削除しても安全です</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>このパスの元のファイルとフォルダーは、このアプリから削除するときに削除されません</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>フォルダーを追加します</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>テーマ</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>言語</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムをフォローします</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>ライト</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>暗い</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>これはオープンソースアプリです</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>新しいウィンドウで開きます</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>GitHubのソースコードを参照してください</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>バージョン</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>なし</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>雲母</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>マイカル</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>デスクトップアクリル</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>アクリルベース</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>アクリル薄い</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>デフォルト</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>変更を適用するためのアプリを再起動します</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>パスはコンピューターでは見つかりません</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>フォルダーが追加されました。二度と追加しないでください。</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>オーバーレイアルバムアートの背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>ダイナミックアルバムアートの背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>アルバムアートの背景不透明</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定 - BetterLyrics</value>
</data>
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>アライメント</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>中心</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>左</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>アルバムアートバックグラウンドブラー量</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>ぼやけの量</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>この値を調整すると、アルバム画像のバックグラウンドブラー強度も増加します。</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>現在の値:</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>ぼかしが有効になっている場合のGPU使用量が大幅に高くなります&gt; 0</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>この機能を有効にすると、GPUの使用率がわずかに増加します</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上端と下端の不透明度</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>ライン間隔</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x線の高さ</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>フォントサイズ</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>没入モード</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>アルバムの背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>について</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞ライブラリ</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>アプリの外観</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>グロー効果</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>グローエフェクトスコープ</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>歌詞検索プロバイダーを構成します</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>追加</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>BetterLyrics へようこそ</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>今すぐ歌詞データベースをセットアップしましょう</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>今は音楽が再生されていません</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開発者オプション</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>テスト音楽を再生します</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>システムプレーヤーを使用して再生します</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>ログ</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>フォントカラー</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>デフォルト</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>アルバムアートアクセントカラー</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>アルバムアートスタイル</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>コーナー半径</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>タイトルバーサイズ</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>コンパクト</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>拡張</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>常にトップ</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全画面表示</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>ESCを押して、フルスクリーンモードを終了します</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再びホバリングして、トグルボタンを表示します</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>このメッセージを二度と見せないでください</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>一致するローカルファイルはありません</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートのみ</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分割ビュー</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>表示タイプを変更します</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>デスクトップ歌詞モードに切り替えます</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモードを終了します</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌詞効果</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌詞スタイル</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>アプリの動作</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>アプリを起動するとき</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>標準モードをアクティブにします</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>ドックモードをアクティブにします</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>システムトレイ - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>ドックモード</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>フォント重量</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>薄い</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>余分な光</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>ライト</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>半光</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>普通</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中くらい</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>セミボールド</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>大胆な</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>余分な太字</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黒</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>余分な黒</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>歌詞全体</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>現在の行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>現在の文字</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>歌詞の読み込み...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>ローカル.LRCファイル</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>ローカル音楽ファイル</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>ローカル.ESLRCファイル</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>ローカル.TTMLファイル</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください</value>
</data>
</root>

View File

@@ -0,0 +1,489 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>로컬 음악 도서관</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>음악이나 가사를 저장하는 폴더 추가</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>파일 탐색기에서 열립니다</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>앱에서 제거하십시오</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>다음 항목을 제거하는 것이 안전합니다</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>이 경로의 원본 파일과 폴더는이 앱에서 제거 할 때 삭제되지 않습니다.</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>폴더를 추가하십시오</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>주제</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>언어</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>시스템을 따르십시오</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>빛</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>어두운</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>이것은 오픈 소스 앱입니다</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>새 창에서 열립니다</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>GitHub의 소스 코드를 참조하십시오</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>버전</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>없음</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>운모</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>운모 대체</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>데스크탑 아크릴</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>아크릴베이스</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>아크릴 얇은</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>투명한</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>배경</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>기본</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>변경 사항을 적용하려면 앱을 다시 시작하십시오</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>경로는 컴퓨터에서 찾을 수 없습니다</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>폴더가 추가되었습니다. 다시 추가하지 마십시오.</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>오버레이 앨범 아트 배경</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>동적 앨범 아트 배경</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>앨범 아트 배경 불투명도</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>설정 - BetterLyrics</value>
</data>
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>조정</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>센터</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>왼쪽</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>오른쪽</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>앨범 아트 배경 흐림 금액</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>흐림 금액</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>이 값을 조정하면 앨범 이미지의 배경 흐림 강도가 증가합니다.</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>현재 가치 :</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Blur가 활성화 될 때 상당히 높은 GPU 사용량 (&gt; 0)</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>이 기능을 활성화하면 GPU 사용률이 약간 증가합니다</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>상단 및 하단 가장자리 불투명도</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>라인 간격</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>X 라인 높이</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>글꼴 크기</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>가사 만</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>몰입 형 모드</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>앨범 배경</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>에 대한</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>가사 도서관</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>앱 모양</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>글로우 효과</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>글로우 효과 범위</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>가사 검색 제공 업체를 구성하십시오</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>추가하다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Betterlyrics에 오신 것을 환영합니다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>지금 가사 데이터베이스를 설정합시다</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>지금 음악이 재생되지 않습니다</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>개발자 옵션</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>테스트 음악을 재생하십시오</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>시스템 플레이어를 사용하여 재생하십시오</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>통나무</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>글꼴 색상</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>기본</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>앨범 아트 악센트 색상</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>앨범 아트 스타일</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>코너 반경</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>제목 바 크기</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>콤팩트</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>펼친</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>항상 위에</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>전체 화면</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>ESC를 눌러 전체 화면 모드를 종료하십시오</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>토글 버튼을 표시하려면 다시 다시 가져옵니다</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>이 메시지를 다시 표시하지 마십시오</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>로컬 파일이 일치하지 않습니다</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>앨범 아트 만</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>분할보기</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>디스플레이 유형을 변경하십시오</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>데스크탑 가사 모드로 전환하십시오</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>사진 인당 모드</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-Picture 모드 종료</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>가사 효과</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>가사 스타일</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>앱 동작</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>앱을 시작할 때</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>표준 모드를 ​​활성화합니다</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>도크 모드를 활성화하십시오</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>시스템 트레이 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>도크 모드</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>글꼴 무게</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>얇은</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>여분의 빛</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>빛</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>반 빛</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>정상</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>중간</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>반 대담한</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>용감한</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>추가 대담한</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>검은색</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>여분의 검은 색</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>전체 가사</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>현재 라인</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>현재 숯</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>설정</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>가사로드 ...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>로컬 .LRC 파일</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>로컬 음악 파일</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>로컬 .ESLRC 파일</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>로컬 .TTML 파일</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.</value>
</data>
</root>

View File

@@ -121,10 +121,7 @@
<value>本地音乐媒体库</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>添加存放音乐或歌词的文件夹以构建歌词索引数据库</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
<value>添加存放音乐或歌词的文件夹</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
@@ -223,14 +220,11 @@
<value>专辑图片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>设置</value>
<value>设置 - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
@@ -273,76 +267,46 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字体大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅示歌词</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>仅示歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词效果</value>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>专辑背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>关于</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌词库</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>应用外观</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>动态辉光效果</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>辉光效果作用范围</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重构歌词索引数据库</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重构</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌词搜索服务</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重构数据库中,请稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>来看看怎么使用这款应用吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>顶部区域是标题栏</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在顶部区域以显示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>现在就来初始化歌词数据库吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在左下角后单击以进入设置页面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在底部区域以显示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部区域是命令栏</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此处切换“仅展示歌词”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>当歌词存在时可在右下角进行切换</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>开发者选项</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -363,10 +327,163 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>专辑强调色</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>专辑封面样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圆角半径</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>标题栏大小</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>紧凑</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>扩展</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>将应用置于顶层</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全屏</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>按 Esc 退出全屏模式</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次悬停以显示切换按钮</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再显示此消息</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>仅显示专辑封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分屏视图</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>切换显示模式</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>切换到桌面歌词模式</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>画中画模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出画中画模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌词动效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>应用行为</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>启动应用时</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>启动标准模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>启动停靠模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系统托盘 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字体粗细</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>极细</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>超细</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>细体</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>次细</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>常规</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中等</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>次粗</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>粗体</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>特粗</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黑体</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌词</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>当前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>当前字符</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>设置</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>加载歌词中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .LRC 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
</root>

View File

@@ -121,10 +121,7 @@
<value>本地音樂媒體庫</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>新增存放音樂或歌詞的資料夾以建立歌詞索引資料庫</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
<value>新增存放音樂或歌詞的資料夾</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
@@ -223,14 +220,11 @@
<value>專輯圖片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定</value>
<value>設定 - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>對齊方式</value>
</data>
@@ -273,76 +267,46 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字體大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅示歌詞</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>僅示歌詞</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞效果</value>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>專輯背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>關於</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞庫</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>應用外觀</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>輝光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>動態輝光效果</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>輝光效果作用範圍</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重構歌詞索引資料庫</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重構</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌詞搜尋服務</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重構資料庫中,請稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>來看看怎麼使用這款應用程式吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>頂部區域是標題欄</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在頂部區域以顯示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>現在就來初始化歌詞資料庫吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在左下角後點擊以進入設定頁面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在底部區域以顯示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部區域是命令列</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此切換“僅展示歌詞”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>當歌詞存在時可在右下角進行切換</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開發者選項</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -363,10 +327,163 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>專輯強調色</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>專輯封面樣式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圓角半徑</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>標題列大小</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>緊湊</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>擴充</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>將應用置於頂層</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全螢幕</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>按 Esc 退出全螢幕模式</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次懸停以顯示切換按鈕</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再顯示此訊息</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>僅顯示專輯封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分割畫面視圖</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>切換顯示模式</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>切換到桌面歌詞模式</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>畫中畫模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出畫中畫模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌詞動效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>該資料夾已包含在已有資料夾中,無需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>應用行為</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>啟動應用程式時</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>啟動標準模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>啟動停靠模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系統托盤 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字體粗細</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>超細</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>細體</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>次細</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>常規</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中等</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>次粗</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>粗體</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>特粗</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黑體</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌詞</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>目前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>目前字元</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>載入歌詞中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .lRC 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音樂文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾</value>
</data>
</root>

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.CompilerServices;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseViewModel : ObservableRecipient, IDisposable
{
private protected readonly ISettingsService _settingsService;
private protected readonly DispatcherQueue _dispatcherQueue =
DispatcherQueue.GetForCurrentThread();
public BaseViewModel(ISettingsService settingsService)
{
IsActive = true;
_settingsService = settingsService;
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,223 @@
using System;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3
{
public partial class HostWindowViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<TitleBarType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<BackdropType>>,
IRecipient<PropertyChangedMessage<int>>
{
private ForegroundWindowWatcherHelper? _watcherHelper = null;
[ObservableProperty]
public partial Type FramePageType { get; set; }
[ObservableProperty]
public partial ElementTheme ThemeType { get; set; }
[ObservableProperty]
public partial double AppLogoImageIconHeight { get; set; }
[ObservableProperty]
public partial double TitleBarFontSize { get; set; }
[ObservableProperty]
public partial double TitleBarHeight { get; set; }
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
[ObservableProperty]
public partial TitleBarType TitleBarType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDockMode { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color ActivatedWindowAccentColor { get; set; }
public HostWindowViewModel(ISettingsService settingsService)
: base(settingsService)
{
TitleBarType = _settingsService.TitleBarType;
ThemeType = _settingsService.ThemeType;
OnTitleBarTypeChanged(TitleBarType);
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (
!Notification.IsForeverDismissable
|| AlreadyForeverDismissedThisMessage() == false
)
{
Notification.Visibility = Notification.IsForeverDismissable
? Visibility.Visible
: Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
}
private void StartWatchWindowColorChange()
{
var hwnd = WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
);
_watcherHelper = new ForegroundWindowWatcherHelper(
hwnd,
onWindowChanged =>
{
UpdateAccentColor(hwnd);
}
);
_watcherHelper.Start();
UpdateAccentColor(hwnd);
}
private void StopWatchWindowColorChange()
{
_watcherHelper?.Stop();
_watcherHelper = null;
}
partial void OnFramePageTypeChanged(Type value)
{
if (value != null)
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
_settingsService.BackdropType
);
}
}
public void UpdateAccentColor(nint hwnd)
{
ActivatedWindowAccentColor = WindowColorHelper
.GetDominantColorBelow(hwnd)
.ToWindowsUIColor();
}
partial void OnTitleBarTypeChanged(TitleBarType value)
{
switch (value)
{
case TitleBarType.Compact:
AppLogoImageIconHeight = 18;
TitleBarFontSize = 11;
break;
case TitleBarType.Extended:
AppLogoImageIconHeight = 20;
TitleBarFontSize = 14;
break;
default:
break;
}
TitleBarHeight = value.GetHeight();
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
//if (Notification.RelatedSettingsKeyName is string key)
// _settingsService.SetValue(key, value);
}
private bool? AlreadyForeverDismissedThisMessage()
{
//if (Notification.RelatedSettingsKeyName is string key)
// return _settingsService.Get(key, SettingsDefaultValues.NeverShowMessage);
//return null;
return null;
}
[RelayCommand]
private void ToggleDockMode()
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
IsDockMode = !IsDockMode;
if (IsDockMode)
{
DockHelper.Enable(window, _settingsService.LyricsFontSize * 3);
StartWatchWindowColorChange();
}
else
{
DockHelper.Disable(window);
StopWatchWindowColorChange();
}
}
public void Receive(PropertyChangedMessage<TitleBarType> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.TitleBarType))
{
TitleBarType = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
ThemeType = message.NewValue;
}
public void Receive(PropertyChangedMessage<BackdropType> message)
{
WindowHelper.GetWindowByFramePageType(FramePageType).SystemBackdrop =
SystemBackdropHelper.CreateSystemBackdrop(message.NewValue);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
if (IsDockMode)
{
DockHelper.UpdateAppBarHeight(
WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
),
message.NewValue * 3
);
}
}
}
}
}
}

View File

@@ -0,0 +1,232 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<LyricsStatus>>
{
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToDockMode;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial double LimitedLineWidth { get; set; } = 0.0;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsDisplayType DisplayType { get; set; } =
LyricsDisplayType.PlaceholderOnly;
[ObservableProperty]
public partial BitmapImage? CoverImage { get; set; }
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
[ObservableProperty]
public partial LyricsStatus LyricsStatus { get; set; } = LyricsStatus.Loading;
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } =
LyricsDisplayType.SplitView;
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial bool AboutToUpdateUI { get; set; }
[ObservableProperty]
public partial double CoverImageGridActualHeight { get; set; }
[ObservableProperty]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
public partial CornerRadius CoverImageGridCornerRadius { get; set; }
[ObservableProperty]
public partial bool IsWelcomeTeachingTipOpen { get; set; }
[ObservableProperty]
public partial bool IsFirstRun { get; set; }
[ObservableProperty]
public partial bool IsNotMockMode { get; set; } = true;
private readonly IPlaybackService _playbackService;
public LyricsPageViewModel(
ISettingsService settingsService,
IPlaybackService playbackService
)
: base(settingsService)
{
LyricsFontSize = _settingsService.LyricsFontSize;
CoverImageRadius = _settingsService.CoverImageRadius;
_playbackService = playbackService;
_playbackService.SongInfoChanged += async (_, args) =>
await UpdateSongInfoUI(args.SongInfo).ConfigureAwait(true);
IsFirstRun = _settingsService.IsFirstRun;
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
}
partial void OnCoverImageRadiusChanged(int value)
{
if (double.IsNaN(CoverImageGridActualHeight))
return;
CoverImageGridCornerRadius = new CornerRadius(
value / 100f * CoverImageGridActualHeight / 2
);
}
partial void OnCoverImageGridActualHeightChanged(double value)
{
if (double.IsNaN(value))
return;
CoverImageGridCornerRadius = new CornerRadius(CoverImageRadius / 100f * value / 2);
}
partial void OnIsFirstRunChanged(bool value)
{
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
[RelayCommand]
private void OnDisplayTypeChanged(object value)
{
int index = Convert.ToInt32(value);
PreferredDisplayType = (LyricsDisplayType)index;
DisplayType = (LyricsDisplayType)index;
}
[RelayCommand]
private void OpenSettingsWindow()
{
WindowHelper.OpenSettingsWindow();
}
public async Task UpdateSongInfoUI(SongInfo? songInfo)
{
AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
SongInfo = songInfo;
CoverImage =
(songInfo?.AlbumArt == null)
? null
: await ImageHelper.GetBitmapImageFromBytesAsync(songInfo.AlbumArt);
TrySwitchToPreferredDisplayType(songInfo);
AboutToUpdateUI = false;
}
private void TrySwitchToPreferredDisplayType(SongInfo? songInfo)
{
LyricsDisplayType displayType;
if (songInfo == null)
{
displayType = LyricsDisplayType.PlaceholderOnly;
}
else if (PreferredDisplayType is LyricsDisplayType preferredDisplayType)
{
displayType = preferredDisplayType;
}
else
{
displayType = LyricsDisplayType.SplitView;
}
DisplayType = displayType;
}
public void OpenMatchedFileFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
}
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
{
IsNotMockMode = !message.NewValue;
if (message.NewValue)
{
_preferredDisplayTypeBeforeSwitchToDockMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToDockMode;
}
TrySwitchToPreferredDisplayType(SongInfo);
}
}
}
public void Receive(PropertyChangedMessage<LyricsStatus> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsStatus))
{
LyricsStatus = message.NewValue;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterInAppLyrics.WinUI3.ViewModels
{
public partial class LyricsSettingsControlViewModel : BaseViewModel
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
public LyricsSettingsControlViewModel(ISettingsService settingsService)
: base(settingsService)
{
IsActive = true;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
LyricsFontColorType = _settingsService.LyricsFontColorType;
}
partial void OnLyricsAlignmentTypeChanged(LyricsAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFontColorType = value;
}
partial void OnLyricsGlowEffectScopeChanged(LyricsGlowEffectScope value)
{
_settingsService?.LyricsGlowEffectScope = value;
}
}
}

View File

@@ -1,212 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using DevWinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using Ude;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using Windows.UI;
using static System.Net.Mime.MediaTypeNames;
using static ATL.LyricsInfo;
using static CommunityToolkit.WinUI.Animations.Expressions.ExpressionValues;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private bool _isAnyMusicSessionExisted = false;
[ObservableProperty]
private string? _title;
[ObservableProperty]
private string? _artist;
[ObservableProperty]
private ObservableCollection<Color> _coverImageDominantColors;
[ObservableProperty]
private BitmapImage? _coverImage;
[ObservableProperty]
private bool _aboutToUpdateUI;
[ObservableProperty]
private bool _isSmallScreenMode;
[ObservableProperty]
private bool _showLyricsOnly = false;
[ObservableProperty]
private bool _lyricsExisted = false;
private readonly ColorThief _colorThief = new();
private readonly SettingsService _settingsService;
private readonly DatabaseService _databaseService;
private readonly int _accentColorCount = 3;
public MainViewModel(SettingsService settingsService, DatabaseService databaseService)
{
_settingsService = settingsService;
_databaseService = databaseService;
CoverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, _accentColorCount),
];
}
public List<LyricsLine> GetLyrics(Track? track)
{
List<LyricsLine> result = [];
var lyricsPhrases = track?.Lyrics?.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0)
{
if (lyricsPhrases[0].TimestampMs > 0)
{
var placeholder = new LyricsPhrase(0, " ");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result.Add(lyricsLine);
lyricsLine = null;
}
}
LyricsExisted = result.Count != 0;
if (!LyricsExisted)
{
ShowLyricsOnly = false;
}
return result;
}
public async Task<(List<LyricsLine>, SoftwareBitmap?, uint, uint)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
SoftwareBitmap? coverSoftwareBitmap = null;
uint coverImagePixelWidth = 0;
uint coverImagePixelHeight = 0;
Title = mediaProps?.Title;
Artist = mediaProps?.Artist;
IRandomAccessStream? stream = null;
var track = _databaseService.GetMusicMetadata(mediaProps);
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference)
{
stream = await reference.OpenReadAsync();
}
else
{
if (track?.EmbeddedPictures.Count > 0)
{
var bytes = track.EmbeddedPictures[0].PictureData;
if (bytes != null)
{
stream = await Helper.ImageHelper.GetStreamFromBytesAsync(bytes);
}
}
}
// Set cover image and dominant colors
if (stream == null)
{
CoverImage = null;
CoverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, _accentColorCount),
];
_settingsService.LyricsFontSelectedAccentColorIndex =
_settingsService.LyricsFontSelectedAccentColorIndex;
}
else
{
CoverImage = new BitmapImage();
await CoverImage.SetSourceAsync(stream);
stream.Seek(0);
var decoder = await BitmapDecoder.CreateAsync(stream);
coverImagePixelHeight = decoder.PixelHeight;
coverImagePixelWidth = decoder.PixelWidth;
coverSoftwareBitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied
);
CoverImageDominantColors =
[
.. (await _colorThief.GetPalette(decoder, _accentColorCount)).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
_settingsService.LyricsFontSelectedAccentColorIndex =
_settingsService.LyricsFontSelectedAccentColorIndex;
stream.Dispose();
}
return (
GetLyrics(track),
coverSoftwareBitmap,
coverImagePixelWidth,
coverImagePixelHeight
);
}
}
}

View File

@@ -1,97 +1,305 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.Core;
using Windows.Globalization;
using Windows.Media;
using Windows.Media.Playback;
using Windows.Storage.Pickers;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsViewModel(
DatabaseService databaseService,
SettingsService settingsService,
MainViewModel mainViewModel
) : ObservableObject
public partial class SettingsViewModel : ObservableRecipient
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial BackdropType BackdropType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TitleBarType TitleBarType { get; set; }
[ObservableProperty]
public partial AutoStartWindowType AutoStartWindowType { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsCoverOverlayEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDynamicCoverOverlayEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
[ObservableProperty]
public partial Enums.Language Language { get; set; }
public string Version { get; set; } = AppInfo.AppVersion;
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
private readonly MediaPlayer _mediaPlayer = new();
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
private readonly DatabaseService _databaseService = databaseService;
public SettingsService SettingsService => settingsService;
public MainViewModel MainViewModel => mainViewModel;
public string Version => Helper.AppInfo.AppVersion;
[RelayCommand]
private async Task RebuildLyricsIndexDatabaseAsync()
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
IPlaybackService playbackService
)
{
SettingsService.IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildMusicMetadataIndexDatabaseAsync(
SettingsService.MusicLibraries
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
ThemeType = _settingsService.ThemeType;
BackdropType = _settingsService.BackdropType;
TitleBarType = _settingsService.TitleBarType;
AutoStartWindowType = _settingsService.AutoStartWindowType;
IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
}
partial void OnLanguageChanged(Enums.Language value)
{
switch (value)
{
case Enums.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Enums.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Enums.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Enums.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
case Enums.Language.Japanese:
ApplicationLanguages.PrimaryLanguageOverride = "ja-JP";
break;
case Enums.Language.Korean:
ApplicationLanguages.PrimaryLanguageOverride = "ko-KR";
break;
default:
break;
}
_settingsService.Language = Language;
}
partial void OnThemeTypeChanged(ElementTheme value)
{
_settingsService.ThemeType = value;
}
partial void OnBackdropTypeChanged(BackdropType value)
{
_settingsService.BackdropType = value;
}
partial void OnTitleBarTypeChanged(TitleBarType value)
{
_settingsService.TitleBarType = value;
RootGridMargin = new Thickness(0, value.GetHeight(), 0, 0);
}
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
{
_settingsService.AutoStartWindowType = value;
}
partial void OnCoverImageRadiusChanged(int value)
{
_settingsService.CoverImageRadius = value;
}
partial void OnIsCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsCoverOverlayEnabled = value;
}
partial void OnIsDynamicCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsDynamicCoverOverlayEnabled = value;
}
partial void OnCoverOverlayOpacityChanged(int value)
{
_settingsService.CoverOverlayOpacity = value;
}
partial void OnCoverOverlayBlurAmountChanged(int value)
{
_settingsService.CoverOverlayBlurAmount = value;
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
public void OnLyricsSearchProvidersReordered()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
SettingsService.IsRebuildingLyricsIndexDatabase = false;
}
public async Task RemoveFolderAsync(string path)
{
SettingsService.MusicLibraries.Remove(path);
await RebuildLyricsIndexDatabaseAsync();
}
[RelayCommand]
private async Task SelectAndAddFolderAsync()
private async Task SelectAndAddFolderAsync(UIElement sender)
{
var picker = new FolderPicker();
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(App.Current.MainWindow);
var hwnd = WindowNative.GetWindowHandle(WindowHelper.GetWindowForElement(sender));
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
App.Current.SettingsWindow!.AppWindow.MoveInZOrderAtTop();
if (folder != null)
{
await AddFolderAsync(folder.Path);
AddFolderAsync(folder.Path);
}
}
private async Task AddFolderAsync(string path)
private void AddFolderAsync(string path)
{
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
if (existed)
var normalizedPath =
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
if (
LocalLyricsFolders.Any(x =>
Path.GetFullPath(x.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.Equals(
normalizedPath.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase
)
)
)
{
MainWindow.StackedNotificationsBehavior?.Show(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"),
Helper.AnimationHelper.StackedNotificationsShowingDuration
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
)
)
);
}
else if (
LocalLyricsFolders.Any(item =>
normalizedPath.StartsWith(
Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase
)
)
)
{
// 添加的文件夹是现有文件夹的子文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo")
)
)
);
}
else if (
LocalLyricsFolders.Any(item =>
Path.GetFullPath(item.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase)
)
)
{
// 添加的文件夹是现有文件夹的父文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo")
)
)
);
}
else
{
SettingsService.MusicLibraries.Add(path);
await RebuildLyricsIndexDatabaseAsync();
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
[RelayCommand]
private static async Task LaunchProjectGitHubPageAsync()
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
await Launcher.LaunchUriAsync(new Uri(AppInfo.GithubUrl));
}
private static void OpenFolderInFileExplorer(string path)
private void OpenFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
@@ -103,13 +311,13 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public static void OpenMusicFolder(string path)
public void OpenMusicFolder(LocalLyricsFolder folder)
{
OpenFolderInFileExplorer(path);
OpenFolderInFileExplorer(folder.Path);
}
[RelayCommand]
private static void RestartApp()
private void RestartApp()
{
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
@@ -130,17 +338,33 @@ namespace BetterLyrics.WinUI3.ViewModels
}
[RelayCommand]
private async Task PlayTestingMusicTask()
private void PlayTestingMusicTask()
{
await AddFolderAsync(Helper.AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(Helper.AppInfo.TestMusicPath));
AddFolderAsync(AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(AppInfo.TestMusicPath));
_mediaPlayer.Play();
}
[RelayCommand]
private static void OpenLogFolder()
private void OpenLogFolder()
{
OpenFolderInFileExplorer(Helper.AppInfo.LogDirectory);
OpenFolderInFileExplorer(AppInfo.LogDirectory);
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
}

View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.Views.HostWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid
x:Name="RootGrid"
PointerMoved="RootGrid_PointerMoved"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Frame
x:Name="RootFrame"
Navigated="RootFrame_Navigated"
NavigationFailed="RootFrame_NavigationFailed" />
<Grid
x:Name="TopCommandGrid"
Height="{x:Bind ViewModel.TitleBarHeight, Mode=OneWay}"
VerticalAlignment="Top"
Background="Transparent"
Opacity="0"
PointerMoved="TopCommandGrid_PointerMoved">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<ImageIcon
x:Name="AppLogoImageIcon"
Height="{x:Bind ViewModel.AppLogoImageIconHeight, Mode=OneWay}"
Margin="16,0"
Source="ms-appx:///Assets/Logo.png" />
<TextBlock
x:Name="AppTitleTextBlock"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="SemiBold"
Text="{x:Bind Title, Mode=OneWay}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button x:Name="MoreButton" Style="{StaticResource TitleBarButtonStyle}">
<Grid>
<FontIcon
Margin="0,0,0,8"
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
<FontIcon
Margin="0,8,0,0"
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
</Grid>
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
x:Name="AOTFlyoutItem"
x:Uid="BaseWindowAOTFlyoutItem"
Click="AOTFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="FullScreenFlyoutItem"
x:Uid="BaseWindowFullScreenFlyoutItem"
Click="FullScreenFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="DockFlyoutItem"
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"
x:Uid="BaseWindowMiniFlyoutItem"
Click="MiniFlyoutItem_Click" />
<MenuFlyoutItem
x:Name="SettingsFlyoutItem"
x:Uid="HostWindowSettingsFlyoutItem"
Click="SettingsMenuFlyoutItem_Click" />
</MenuFlyout>
</Button.Flyout>
</Button>
<!-- Window Minimise -->
<Button
x:Name="MinimiseButton"
Click="MinimiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2D;" />
</Button>
<!-- Window Maximise -->
<Button
x:Name="MaximiseButton"
Click="MaximiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2E;" />
</Button>
<!-- Window Restore -->
<Button
x:Name="RestoreButton"
Click="RestoreButton_Click"
Style="{StaticResource TitleBarButtonStyle}"
Visibility="Collapsed">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2F;" />
</Button>
<!-- Window Close -->
<Button
x:Name="CloseButton"
Click="CloseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2C;" />
</Button>
</StackPanel>
</Grid>
<InfoBar
x:Name="HostInfoBar"
Margin="36"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind ViewModel.Notification.Message, Mode=OneWay}"
Opacity="0"
Severity="{x:Bind ViewModel.Notification.Severity, Mode=OneWay}">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
<InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard">
<!-- Opacity -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBar" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="0" />
</DoubleAnimationUsingKeyFrames>
<!-- Y -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBarTransform" Storyboard.TargetProperty="Y">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="20" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="20" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</InfoBar.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource InfoBarShowAndHideStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid>
</Window>

View File

@@ -0,0 +1,252 @@
using System;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using WinRT.Interop;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class HostWindow : Window
{
public HostWindowViewModel ViewModel { get; private set; } =
Ioc.Default.GetRequiredService<HostWindowViewModel>();
private readonly ISettingsService _settingsService =
Ioc.Default.GetRequiredService<ISettingsService>();
public HostWindow(bool alwaysOnTop = false, bool clickThrough = false)
{
this.InitializeComponent();
AppWindow.Changed += AppWindow_Changed;
this.HideSystemTitleBarAndSetCustomTitleBar(TopCommandGrid);
if (clickThrough)
this.SetExtendedWindowStyle(
ExtendedWindowStyle.Transparent | ExtendedWindowStyle.Layered
);
if (alwaysOnTop)
((OverlappedPresenter)AppWindow.Presenter).IsAlwaysOnTop = true;
}
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
if (args.DidPresenterChange)
UpdateTitleBarWindowButtonsVisibility();
}
public void Navigate(Type type)
{
RootFrame.Navigate(type);
}
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
if (RootFrame.SourcePageType == typeof(LyricsPage))
{
Application.Current.Exit();
}
else
{
AppWindow.Hide();
}
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
}
}
private void MinimiseButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Minimize();
}
}
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Restore();
}
}
private void UpdateTitleBarWindowButtonsVisibility()
{
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
break;
case AppWindowPresenterKind.FullScreen:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
break;
case AppWindowPresenterKind.Overlapped:
DockFlyoutItem.Visibility = Visibility.Visible;
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
if (DockFlyoutItem.IsChecked)
{
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
Visibility.Collapsed;
}
else
{
MinimiseButton.Visibility =
AOTFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (overlappedPresenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
TopCommandGrid.Opacity = 0;
break;
default:
break;
}
}
private void RootFrame_Navigated(object sender, NavigationEventArgs e)
{
AppWindow.Title = Title = App.ResourceLoader!.GetString(
$"{e.SourcePageType.Name}Title"
);
if (e.SourcePageType == typeof(LyricsPage))
{
if (_settingsService.AutoStartWindowType == AutoStartWindowType.DockMode)
{
DockFlyoutItem.IsChecked = true;
ViewModel.ToggleDockModeCommand.Execute(null);
}
}
}
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
{
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
overlappedPresenter.IsAlwaysOnTop = !overlappedPresenter.IsAlwaysOnTop;
}
private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e)
{
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
break;
case AppWindowPresenterKind.FullScreen:
AppWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
break;
case AppWindowPresenterKind.Overlapped:
AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
break;
default:
break;
}
}
private void MiniFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (MiniFlyoutItem.IsChecked)
{
AppWindow.SetPresenter(AppWindowPresenterKind.CompactOverlay);
}
else
{
AppWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
}
}
private void SettingsMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenSettingsWindow();
}
private void TopCommandGrid_PointerMoved(object sender, PointerRoutedEventArgs e) { }
private void RootGrid_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(RootGrid);
double y = point.Position.Y;
if (y >= 0 && y <= TopCommandGrid.ActualHeight + 5)
{
if (TopCommandGrid.Opacity == 0)
{
TopCommandGrid.Opacity = .5;
}
}
else
{
if (TopCommandGrid.Opacity == .5)
{
TopCommandGrid.Opacity = 0;
}
}
}
}
}

View File

@@ -0,0 +1,481 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.LyricsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:renderer="using:BetterLyrics.WinUI3.Renderer"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<renderer:LyricsRenderer />
</Grid>
<Grid Margin="36,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<!-- Lyrics placeholder -->
<Grid
x:Name="LyricsPlaceholderGrid"
Opacity=".5"
SizeChanged="LyricsPlaceholderGrid_SizeChanged">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel
x:Name="LyricsNotFoundPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<TextBlock x:Uid="MainPageLyricsNotFound" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
<StackPanel
x:Name="LyricsLoadingPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<TextBlock x:Uid="MainPageLyricsLoading" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoInnerGrid" Margin="0,36">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<!-- Cover area -->
<RowDefinition Height="9*" />
<!-- Spacer -->
<RowDefinition Height="*" />
<!-- Title and artist area -->
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<!-- Cover area -->
<Grid
x:Name="CoverArea"
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid
x:Name="CoverImageGrid"
CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay}"
SizeChanged="CoverImageGrid_SizeChanged">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
</Grid>
<!-- Title and artist -->
<StackPanel
Grid.Row="3"
VerticalAlignment="Top"
Orientation="Vertical">
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
</Grid>
</Grid>
<!-- No music playing placeholder -->
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.OpacityTransition>
<ScalarTransition />
</TextBlock.OpacityTransition>
</TextBlock>
<!-- Bottom-right command area -->
<Grid
x:Name="BottomCommandGrid"
Margin="0,0,4,4"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="0"
PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited"
Visibility="{x:Bind ViewModel.IsNotMockMode, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel HorizontalAlignment="Right" Spacing="4">
<Button
x:Name="DisplayTypeSwitchButton"
x:Uid="MainPageDisplayTypeSwitcher"
Content="{ui:FontIcon Glyph=&#xF246;}"
Style="{StaticResource GhostButtonStyle}">
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}">
<RadioButton
x:Uid="MainPageAlbumArtOnly"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
Tag="0" />
<RadioButton
x:Uid="MainPageLyriscOnly"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
Tag="1" />
<RadioButton
x:Uid="MainPageSplitView"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
Tag="2" />
</RadioButtons>
</Flyout>
</Button.Flyout>
</Button>
<Button
x:Name="MusicInfoButton"
Content="{ui:FontIcon Glyph=&#xF167;}"
Style="{StaticResource GhostButtonStyle}">
<Button.Flyout>
<Flyout>
<StackPanel Spacing="16">
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xED35;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.SourceAppUserModelId, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEC4F;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE77B;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button
x:Name="SettingsButton"
Command="{x:Bind ViewModel.OpenSettingsWindowCommand}"
Content="{ui:FontIcon Glyph=&#xF8B0;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</Grid>
<TeachingTip
x:Name="WelcomeTeachingTip"
x:Uid="MainPageWelcomeTeachingTip"
Closed="WelcomeTeachingTip_Closed"
IsOpen="{x:Bind ViewModel.IsWelcomeTeachingTipOpen, Mode=OneWay}"
Target="{x:Bind SettingsButton}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LayoutStates">
<!-- Album art only -->
<VisualState x:Name="AlbumArtOnly">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsGrid.Opacity" Value="1" />
<Setter Target="LyricsPlaceholderGrid.Opacity" Value="0" />
<Setter Target="LyricsPlaceholderGrid.(Grid.Column)" Value="0" />
<Setter Target="LyricsPlaceholderGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="SongInfoInnerGrid.(Grid.Column)" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="SongInfoInnerGrid.Opacity" Value="1" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Lyrics only -->
<VisualState x:Name="LyricsOnly">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoInnerGrid.Opacity" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.Column)" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="LyricsGrid.Opacity" Value="1" />
<Setter Target="LyricsPlaceholderGrid.(Grid.Column)" Value="0" />
<Setter Target="LyricsPlaceholderGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="LyricsPlaceholderGrid.Opacity" Value="1" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Split view -->
<VisualState x:Name="SplitView">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoInnerGrid.(Grid.Column)" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.ColumnSpan)" Value="1" />
<Setter Target="SongInfoInnerGrid.Opacity" Value="1" />
<Setter Target="LyricsGrid.Opacity" Value="1" />
<Setter Target="LyricsPlaceholderGrid.(Grid.Column)" Value="2" />
<Setter Target="LyricsPlaceholderGrid.(Grid.ColumnSpan)" Value="1" />
<Setter Target="LyricsPlaceholderGrid.Opacity" Value="1" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Placeholder only -->
<VisualState x:Name="PlaceholderOnly">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="3" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoInnerGrid.Opacity" Value="0" />
<Setter Target="LyricsGrid.Opacity" Value="0" />
<Setter Target="LyricsPlaceholderGrid.Opacity" Value="0" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="LyricsStatus">
<VisualState x:Name="Loading">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value=".5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Found">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NotFound">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value=".5" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="MusicPlayingStates">
<VisualState x:Name="MusicPlaying">
<VisualState.StateTriggers>
<ui:IsNotEqualStateTrigger Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}" To="3" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DisplayTypeSwitchButton.Visibility" Value="Visible" />
<Setter Target="MusicInfoButton.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NoMusicPlaying">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="3" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DisplayTypeSwitchButton.Visibility" Value="Collapsed" />
<Setter Target="MusicInfoButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -0,0 +1,72 @@
using System;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class LyricsPage : Page
{
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
public LyricsPage()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetService<LyricsPageViewModel>();
}
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
ViewModel.IsFirstRun = false;
}
private void CoverArea_SizeChanged(object sender, SizeChangedEventArgs e)
{
CoverImageGrid.Width = CoverImageGrid.Height = Math.Min(
CoverArea.ActualWidth,
CoverArea.ActualHeight
);
}
private void BottomCommandGrid_PointerEntered(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (BottomCommandGrid.Opacity == 0)
BottomCommandGrid.Opacity = .5;
}
private void BottomCommandGrid_PointerExited(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (BottomCommandGrid.Opacity == .5)
BottomCommandGrid.Opacity = 0;
}
private void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.LimitedLineWidth = e.NewSize.Width;
}
private void CoverImageGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.CoverImageGridActualHeight = e.NewSize.Height;
}
}
}

View File

@@ -1,489 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
NavigationCacheMode="Required"
mc:Ignorable="d">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<Grid.Resources>
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
</Grid.Resources>
<Grid
x:Name="TopPlaceholder"
Height="36"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<Grid x:Name="MainGrid">
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded"
Update="LyricsCanvas_Update">
<canvas:CanvasAnimatedControl.Resources>
<Storyboard x:Key="LyricsCanvasFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="LyricsCanvasFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</canvas:CanvasAnimatedControl.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</canvas:CanvasAnimatedControl>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoGrid" Margin="36">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SongInfoColumnDefinition" Width="*" />
<ColumnDefinition x:Name="SpacerColumnDefinition" Width="36" />
<ColumnDefinition x:Name="LyricsAreaColumnDefinition" Width="*" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Storyboard x:Key="HideSongInfoGrid">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="ShowSongInfoGrid">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Grid
x:Name="SongInfoInnerGrid"
Grid.Column="0"
Grid.ColumnSpan="3">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<!-- Cover area -->
<RowDefinition Height="20*" />
<!-- Spacer -->
<RowDefinition Height="2*" />
<!-- Title and artist area -->
<RowDefinition Height="5*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid.Resources>
<!-- Animation for song info -->
<Storyboard x:Name="SongInfoStackPanelFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="SongInfoStackPanelFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<!-- Cover area -->
<Grid
x:Name="CoverArea"
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid x:Name="CoverImageGrid" CornerRadius="24">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
</Grid>
<!-- Title and artist -->
<StackPanel Grid.Row="3" Orientation="Vertical">
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
</Grid>
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.Resources>
<Storyboard x:Key="ShowMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="HideMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</TextBlock.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</TextBlock>
</Grid>
</Grid>
<Grid
x:Name="BottomCommandGrid"
Padding="2,0"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="0">
<Grid.Resources>
<Storyboard x:Name="BottomCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="BottomCommandGridFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<AppBarButton
x:Name="SettingsButton"
Click="SettingsButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE713;" />
</AppBarButton>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<AppBarToggleButton
x:Name="LyricsOnlyButton"
x:Uid="MainWindowLyricsOnly"
IsChecked="{x:Bind ViewModel.ShowLyricsOnly, Mode=TwoWay}"
LabelPosition="Collapsed"
Visibility="{x:Bind ViewModel.LyricsExisted, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF15F;" />
</AppBarToggleButton>
</StackPanel>
</Grid>
<TeachingTip
x:Name="WelcomeTeachingTip"
x:Uid="MainPageWelcomeTeachingTip"
Closed="WelcomeTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind RootGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="1/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="TopCommandTeachingTip"
x:Uid="MainPageTitleBarTeachingTip"
Closed="TopCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind TopPlaceholder}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="2/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="BottomCommandTeachingTip"
x:Uid="MainPageBottomCommandTeachingTip"
Closed="BottomCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind BottomCommandGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="3/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="LyricsOnlyTeachingTip"
x:Uid="MainPageLyricsOnlyTeachingTip"
Closed="LyricsOnlyTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind LyricsOnlyButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="4/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="InitDatabaseTeachingTip"
x:Uid="MainPageInitDatabaseTeachingTip"
Closed="InitDatabaseTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind SettingsButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="5/5" />
</TeachingTip.Content>
</TeachingTip>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<!-- Narrow -->
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Wide -->
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="*" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

File diff suppressed because it is too large Load Diff

View File

@@ -8,418 +8,496 @@
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:BetterLyrics.WinUI3.ViewModels"
NavigationCacheMode="Required"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid x:Name="RootGrid" Margin="{x:Bind ViewModel.RootGridMargin, Mode=OneWay}">
<controls:OpacityMaskView HorizontalContentAlignment="Stretch">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeVerticalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<ScrollViewer>
<Grid Margin="36,72,36,72">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<NavigationView
x:Name="NavView"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
PaneDisplayMode="Auto"
SelectionChanged="NavView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem
x:Uid="SettingsPageLyricsLib"
Icon="{ui:FontIcon Glyph=&#xE838;}"
IsSelected="True"
Tag="LyricsLib" />
<NavigationViewItem
x:Uid="SettingsPageAppAppearance"
Icon="{ui:FontIcon Glyph=&#xE771;}"
Tag="AppAppearance" />
<NavigationViewItem
x:Uid="SettingsPageAppBehavior"
Icon="{ui:FontIcon Glyph=&#xE805;}"
Tag="AppBehavior" />
<NavigationViewItem
x:Uid="SettingsPageAlbumOverlay"
Icon="{ui:FontIcon Glyph=&#xE81E;}"
Tag="AlbumArtOverlay" />
<NavigationViewItem
x:Uid="SettingsPageAlbumStyle"
Icon="{ui:FontIcon Glyph=&#xE80A;}"
Tag="AlbumArtStyle" />
<NavigationViewItem
x:Uid="SettingsPageLyricsStyle"
Icon="{ui:FontIcon Glyph=&#xEF60;}"
Tag="LyricsStyle" />
<NavigationViewItem
x:Uid="SettingsPageLyricsEffect"
Icon="{ui:FontIcon Glyph=&#xF4A5;}"
Tag="LyricsEffect" />
<NavigationViewItem
x:Uid="SettingsPageAbout"
Icon="{ui:FontIcon Glyph=&#xE946;}"
Tag="About" />
<NavigationViewItem
x:Uid="SettingsPageDev"
Icon="{ui:FontIcon Glyph=&#xEC7A;}"
Tag="Dev" />
</NavigationView.MenuItems>
<ScrollViewer Padding="36,0">
<Grid Margin="0,36">
<controls:SwitchPresenter Value="{x:Bind ViewModel.NavViewSelectedItemTag, Mode=OneWay}">
<controls:SwitchPresenter.ContentTransitions>
<TransitionCollection>
<PopupThemeTransition />
</TransitionCollection>
</controls:SwitchPresenter.ContentTransitions>
<controls:Case Value="LyricsLib">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.LocalLyricsFolders, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard>
<controls:SettingsCard.Header>
<HyperlinkButton
Click="SettingsPageOpenPathButton_Click"
Content="{Binding Path, Mode=OneWay}"
Tag="{Binding}" />
</controls:SettingsCard.Header>
<StackPanel Orientation="Horizontal">
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
<ToggleSwitch
DataContext="{Binding}"
IsOn="{Binding IsEnabled, Mode=TwoWay}"
Toggled="LocalLyricsFolderToggleSwitch_Toggled" />
</StackPanel>
</controls:SettingsCard>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
<controls:SettingsExpander.ItemsHeader>
<InfoBar
x:Uid="SettingsPageRemoveInfo"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Success">
<TextBlock x:Uid="SettingsPageLyricsLib" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<interactivity:Interaction.Behaviors>
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.SettingsService.MusicLibraries, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard Header="{Binding}">
<StackPanel Orientation="Horizontal">
<HyperlinkButton
x:Uid="SettingsPageOpenPath"
Click="SettingsPageOpenPathButton_Click"
Tag="{Binding}" />
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
</StackPanel>
</controls:SettingsCard>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
<controls:SettingsExpander.ItemsHeader>
<InfoBar
x:Uid="SettingsPageRemoveInfo"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Success">
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:Interaction.Behaviors>
</interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</InfoBar>
</controls:SettingsExpander.ItemsHeader>
<controls:SettingsExpander.ItemsFooter>
<controls:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button
x:Uid="SettingsPageAddFolderButton"
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
CommandParameter="{Binding ElementName=RootGrid}" />
</controls:SettingsCard>
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
</interactivity:Interaction.Behaviors>
<controls:SettingsCard
x:Name="LyricsSearchProvidersSettingsExpander"
x:Uid="SettingsPageLyricsSearchProvidersConfig"
HeaderIcon="{ui:FontIcon Glyph=&#xF6FA;}" />
<ListView
x:Name="LyricsSearchProvidersListView"
Margin="0,-4,0,0"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="LyricsSearchProvidersListView_DragItemsCompleted"
ItemsSource="{x:Bind ViewModel.LyricsSearchProvidersInfo, Mode=OneWay}"
SelectionMode="None">
<ListView.OpacityTransition>
<ScalarTransition />
</ListView.OpacityTransition>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo">
<controls:SettingsCard Padding="60,0,48,0" Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" Toggled="LyricsSearchProviderToggleSwitch_Toggled" />
</controls:SettingsCard>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</InfoBar>
</controls:SettingsExpander.ItemsHeader>
<controls:SettingsExpander.ItemsFooter>
<controls:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button x:Uid="SettingsPageAddFolderButton" Command="{x:Bind ViewModel.SelectAndAddFolderCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
<controls:SettingsCard
x:Uid="SettingsPageRebuildDatabase"
HeaderIcon="{ui:FontIcon Glyph=&#xE621;}"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<controls:SettingsCard.Description>
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</controls:SettingsCard.Description>
<StackPanel>
<Button x:Uid="SettingsPageRebuildDatabaseButton" Command="{x:Bind ViewModel.RebuildLyricsIndexDatabaseCommand}" />
<ProgressBar
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:Case>
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.Theme, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageFollowSystem" />
<ComboBoxItem x:Uid="SettingsPageLight" />
<ComboBoxItem x:Uid="SettingsPageDark" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageBackdrop" HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}">
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.BackdropType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageNoBackdrop" />
<ComboBoxItem x:Uid="SettingsPageMica" />
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
<ComboBoxItem x:Uid="SettingsPageDesktopAcrylic" />
<ComboBoxItem x:Uid="SettingsPageAcrylicThin" />
<ComboBoxItem x:Uid="SettingsPageAcrylicBase" />
<ComboBoxItem x:Uid="SettingsPageTransparent" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.Language, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock x:Uid="SettingsPageAlbumOverlay" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageCoverOverlay"
HeaderIcon="{ui:FontIcon Glyph=&#xE93C;}"
IsExpanded="True">
<controls:SettingsExpander.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageCoverOverlayGPUUsage" />
</StackPanel>
</controls:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsDynamicCoverOverlay, Mode=TwoWay}" />
<controls:Case Value="AppAppearance">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind ViewModel.ThemeType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageFollowSystem" />
<ComboBoxItem x:Uid="SettingsPageLight" />
<ComboBoxItem x:Uid="SettingsPageDark" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<controls:SettingsCard x:Uid="SettingsPageBackdrop" HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}">
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind ViewModel.BackdropType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageNoBackdrop" />
<ComboBoxItem x:Uid="SettingsPageMica" />
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
<ComboBoxItem x:Uid="SettingsPageDesktopAcrylic" />
<ComboBoxItem x:Uid="SettingsPageTransparent" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTitleBarType" HeaderIcon="{ui:FontIcon Glyph=&#xE66A;}">
<ComboBox x:Name="TitleBarTypeComboBox" SelectedIndex="{x:Bind ViewModel.TitleBarType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageCompactTitleBar" />
<ComboBoxItem x:Uid="SettingsPageExtendedTitleBar" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.Language, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
<ComboBoxItem x:Uid="SettingsPageJA" />
<ComboBoxItem x:Uid="SettingsPageKO" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</StackPanel>
</controls:Case>
<controls:Case Value="AppBehavior">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageAutoStartWindow">
<ComboBox SelectedIndex="{x:Bind ViewModel.AutoStartWindowType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageAutoStartInAppLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDesktopLyrics" />
</ComboBox>
</controls:SettingsCard>
</StackPanel>
</controls:Case>
<controls:Case Value="AlbumArtOverlay">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsExpander
x:Uid="SettingsPageCoverOverlay"
HeaderIcon="{ui:FontIcon Glyph=&#xE93C;}"
IsExpanded="True">
<controls:SettingsExpander.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageCoverOverlayGPUUsage" />
</StackPanel>
</controls:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind ViewModel.IsCoverOverlayEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind ViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsDynamicCoverOverlayEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind ViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.CoverOverlayOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="1"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.CoverOverlayOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind ViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.CoverOverlayBlurAmount, Mode=OneWay}" />
<Slider
Maximum="200"
Minimum="50"
SnapsTo="Ticks"
StepFrequency="10"
TickFrequency="10"
TickPlacement="Outside"
Value="{x:Bind ViewModel.CoverOverlayBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</StackPanel>
</controls:Case>
<controls:Case Value="AlbumArtStyle">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon Glyph=&#xE71A;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.CoverImageRadius, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="1"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=TwoWay}" />
Value="{x:Bind ViewModel.CoverImageRadius, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
</StackPanel>
</controls:Case>
<controls:Case Value="LyricsStyle">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind LyricsSettingsControlViewModel.LyricsAlignmentType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontWeight" HeaderIcon="{ui:FontIcon Glyph=&#xE8DD;}">
<ComboBox SelectedIndex="{x:Bind LyricsSettingsControlViewModel.LyricsFontWeight, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsThin" />
<ComboBoxItem x:Uid="SettingsPageLyricsExtraLight" />
<ComboBoxItem x:Uid="SettingsPageLyricsLight" />
<ComboBoxItem x:Uid="SettingsPageLyricsSemiLight" />
<ComboBoxItem x:Uid="SettingsPageLyricsNormal" />
<ComboBoxItem x:Uid="SettingsPageLyricsMedium" />
<ComboBoxItem x:Uid="SettingsPageLyricsSemiBold" />
<ComboBoxItem x:Uid="SettingsPageLyricsBold" />
<ComboBoxItem x:Uid="SettingsPageLyricsExtraBold" />
<ComboBoxItem x:Uid="SettingsPageLyricsBlack" />
<ComboBoxItem x:Uid="SettingsPageLyricsExtraBlack" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontColor" HeaderIcon="{ui:FontIcon Glyph=&#xE8D3;}">
<ComboBox SelectedIndex="{x:Bind LyricsSettingsControlViewModel.LyricsFontColorType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDefault" />
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDominant" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=OneWay}" />
Text="{Binding ElementName=LyricsFontSizeSlider, Path=Value, Mode=OneWay}" />
<Slider
Maximum="200"
Minimum="50"
x:Name="LyricsFontSizeSlider"
Maximum="96"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="10"
TickFrequency="10"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=TwoWay}" />
Value="{x:Bind LyricsSettingsControlViewModel.LyricsFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock x:Uid="SettingsPageAlbumStyle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon Glyph=&#xE71A;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverImageRadius, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverImageRadius, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageLyricsStyle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsAlignmentType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander x:Uid="SettingsPageLyricsFontColor" HeaderIcon="{ui:FontIcon Glyph=&#xE8D3;}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsExpanded" Value="False" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="IsExpanded" Value="True" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDefault" />
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDominant" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Vertical">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<GridView ItemsSource="{x:Bind ViewModel.MainViewModel.CoverImageDominantColors, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsFontSelectedAccentColorIndex, Mode=TwoWay}">
<GridView.ItemTemplate>
<DataTemplate>
<GridViewItem>
<StackPanel>
<Border
Width="64"
Height="64"
CornerRadius="4">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
<TextBlock
Margin="4,0,4,4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</GridViewItem>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=OneWay}" />
<Slider
Maximum="48"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon Glyph=&#xF579;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=OneWay}" />
<TextBlock
x:Uid="SettingsPageLyricsLineSpacingFactorUnit"
Margin="0,0,14,0"
VerticalAlignment="Center" />
<Slider
Maximum="2"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="0.1"
TickFrequency="0.1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageLyricsEffect" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon Glyph=&#xF573;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon Glyph=&#xE727;}">
<controls:SettingsCard.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageLyricsBlurHighGPUUsage" Foreground="{ThemeResource SystemFillColorCautionBrush}" />
<TextBlock x:Uid="SettingsPageLyricsBlurAmountSideEffect" />
</StackPanel>
</controls:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=OneWay}" />
<Slider
Maximum="10"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon Glyph=&#xE9A9;}"
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsDynamicGlowEffect" IsEnabled="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsDynamicGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon Glyph=&#xF579;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{Binding ElementName=LyricsLineSpacingFactorSlider, Path=Value, Mode=OneWay}" />
<TextBlock
x:Uid="SettingsPageLyricsLineSpacingFactorUnit"
Margin="0,0,14,0"
VerticalAlignment="Center" />
<Slider
x:Name="LyricsLineSpacingFactorSlider"
Maximum="2"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="0.1"
TickFrequency="0.1"
TickPlacement="Outside"
Value="{x:Bind LyricsSettingsControlViewModel.LyricsLineSpacingFactor, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</StackPanel>
</controls:Case>
<TextBlock x:Uid="SettingsPageAbout" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:Case Value="LyricsEffect">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard Header="BetterLyrics" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Logo.png}">
<controls:SettingsCard.Description>
<RichTextBlock>
<Paragraph>
<Run x:Uid="SettingsPageVersion" />
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon Glyph=&#xF573;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{Binding ElementName=LyricsVerticalEdgeOpacitySlider, Path=Value, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
x:Name="LyricsVerticalEdgeOpacitySlider"
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard
x:Uid="SettingsPageGitHub"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchProjectGitHubPageCommand}"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}"
IsClickEnabled="True" />
<controls:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon Glyph=&#xE727;}">
<controls:SettingsCard.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageLyricsBlurHighGPUUsage" Foreground="{ThemeResource SystemFillColorCautionBrush}" />
<TextBlock x:Uid="SettingsPageLyricsBlurAmountSideEffect" />
</StackPanel>
</controls:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{Binding ElementName=LyricsBlurAmountSlider, Path=Value, Mode=OneWay}" />
<Slider
x:Name="LyricsBlurAmountSlider"
Maximum="10"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind LyricsSettingsControlViewModel.LyricsBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageDev" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon Glyph=&#xE9A9;}"
IsExpanded="{x:Bind LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsGlowEffectScope" IsEnabled="{x:Bind LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind LyricsSettingsControlViewModel.LyricsGlowEffectScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeWholeLyrics" />
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeCurrentLine" />
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeCurrentChar" />
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<Button x:Uid="SettingsPagePlayingMockMusicButton" Command="{x:Bind ViewModel.PlayTestingMusicTaskCommand}" />
</controls:SettingsCard>
</StackPanel>
</controls:Case>
<controls:SettingsCard x:Uid="SettingsPageLog">
<Button x:Uid="SettingsPageOpenLogFolderButton" Command="{x:Bind ViewModel.OpenLogFolderCommand}" />
</controls:SettingsCard>
<controls:Case Value="About">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard Header="BetterLyrics" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Logo.png}">
<controls:SettingsCard.Description>
<RichTextBlock>
<Paragraph>
<Run x:Uid="SettingsPageVersion" />
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</controls:SettingsCard.Description>
</controls:SettingsCard>
</StackPanel>
<controls:SettingsCard
x:Uid="SettingsPageGitHub"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchProjectGitHubPageCommand}"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}"
IsClickEnabled="True" />
</StackPanel>
</controls:Case>
<controls:Case Value="Dev">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<Button x:Uid="SettingsPagePlayingMockMusicButton" Command="{x:Bind ViewModel.PlayTestingMusicTaskCommand}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLog">
<Button x:Uid="SettingsPageOpenLogFolderButton" Command="{x:Bind ViewModel.OpenLogFolderCommand}" />
</controls:SettingsCard>
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>
</Grid>
</ScrollViewer>
</controls:OpacityMaskView>
</NavigationView>
</Grid>
</Page>

View File

@@ -1,3 +1,8 @@
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
@@ -14,11 +19,13 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class SettingsPage : Page
{
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
public LyricsSettingsControlViewModel LyricsSettingsControlViewModel =>
Ioc.Default.GetRequiredService<LyricsSettingsControlViewModel>();
public SettingsPage()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetService<SettingsViewModel>();
DataContext = Ioc.Default.GetRequiredService<SettingsViewModel>();
}
private void SettingsPageOpenPathButton_Click(
@@ -26,15 +33,53 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
SettingsViewModel.OpenMusicFolder((string)(sender as HyperlinkButton)!.Tag);
ViewModel.OpenMusicFolder((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
}
private async void SettingsPageRemovePathButton_Click(
private void SettingsPageRemovePathButton_Click(
object sender,
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
await ViewModel.RemoveFolderAsync((string)(sender as HyperlinkButton)!.Tag);
ViewModel.RemoveFolderAsync((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
}
private void NavView_SelectionChanged(
NavigationView sender,
NavigationViewSelectionChangedEventArgs args
)
{
ViewModel.NavViewSelectedItemTag = (args.SelectedItem as NavigationViewItem)!.Tag;
}
private void LyricsSearchProviderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LyricsSearchProviderInfo providerInfo)
{
ViewModel.ToggleLyricsSearchProvider(providerInfo);
}
}
}
private void LyricsSearchProvidersListView_DragItemsCompleted(
ListViewBase sender,
DragItemsCompletedEventArgs args
)
{
ViewModel.OnLyricsSearchProvidersReordered();
}
private void LocalLyricsFolderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LocalLyricsFolder localLyricsFolder)
{
ViewModel.ToggleLocalLyricsFolder(localLyricsFolder);
}
}
}
}
}

188
README.CN.md Normal file
View File

@@ -0,0 +1,188 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**Click here to see the English version**_</a>
<div align="center">
  <img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
</div>
<h2 align="center">
BetterLyrics
</h2>
<h3 align="center">
基于 WinUI 3 打造的流畅动态本地歌词显示工具
</h3>
---
## 亮点
- 支持将模糊专辑封面设为背景
- 歌词淡入淡出、缩放等动画流畅自然
- 切换歌曲时界面无缝过渡
- 支持每个字符的渐变卡拉OK发光效果
- 沉浸式桌面歌词Dock 模式)
> 项目仍在开发中,`dev` 分支可能存在 bug。
---
## 支持的歌词源
- 本地歌词:
- 音乐文件内嵌歌词(通过 [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) 读取和解析)
- `.lrc` 文件
- 在线歌词源:
- [LRCLIB](https://lrclib.net/)
- QQ 音乐(通过 [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) 获取和解码)
---
## 多种个性化设置选项
提供了丰富的自定义项:
- 主题模式(浅色、深色、跟随系统)
- 背景样式无、Mica 云母、Acrylic 亚克力、透明)
- 专辑封面背景(动态显示、模糊程度、透明度)
- 歌词样式(对齐方式、字体大小、颜色 **(从专辑封面中提取主题色)**、行间距、透明度、模糊强度、动态**发光**特效)
- 语言(英文、简体中文、繁体中文)
---
## 软件截图
![模式](Screenshots/mode.png)
![发光效果](Screenshots/glow.png)
![发光动画](Screenshots/glow.gif)
![Dock 模式](Screenshots/dock.png)
![沉浸式 Dock](Screenshots/immersive-dock.gif)
![歌词 Dock 动画](Screenshots/dock.gif)
![画中画](Screenshots/pip.png)
![设置界面](Screenshots/settings.png)
![全屏歌词](Screenshots/fs.png)
---
## 演示视频
观看我们的介绍视频「BetterLyrics 阶段性开发成果展示」(上传于 2025 年 5 月 31 日):
[点此观看 B 站视频](https://b23.tv/QjKkYmL)
---
## 立即体验
### 稳定版本
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
 <img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
> **推荐方式****永久免费试用或购买**(免费与付费功能上无差别,若喜欢可购买支持作者)
也可从 Google Drive 下载(详见 [release 页面](https://github.com/jayfunc/BetterLyrics/releases/latest)
> 注意:这是一个 `.zip` 压缩包,请参考[安装指南](How2Install/How2Install.md)进行安装。
### 最新开发版本
可通过 `git clone` 克隆本仓库后自行构建运行。
---
## 播放器适配说明
本项目通过监听 [SMTC](https://learn.microsoft.com/en-ca/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols) 获取当前播放歌曲信息。
理论上,**只要你的播放器支持 SMTC 控件**,加载本地音乐或歌词后即可使用。
兼容性良好的播放器包括但不限于:
- Spotify
- Groove 音乐
- Apple Music
- Windows 媒体播放器
- VLC
- QQ 音乐
- 酷狗音乐
- 酷我音乐
>(注:未测试全部播放器,如有异常欢迎反馈 issue
---
## 后续工作
敬请期待。
---
## 特别感谢
- [LRCLIB](https://lrclib.net/)
- 在线歌词 API 提供源
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- 本地音频元信息读取
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- 简化 Win32 窗口操作
- [TagLib#](https://github.com/mono/taglib-sharp)
- 曾用作元信息解析库
- [Stackoverflow - WPF 动画 Margin 属性](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Bilibili -【WinUI3】SystemBackdropController 教程](https://www.bilibili.com/video/BV1PY4FevEkS)
- [博客园 - .NET App 与 SMTC 交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 游戏循环教程](https://www.cnblogs.com/walterlv/p/10236395.html)
- [Win2D Iris Blur 示例](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 教程合集](https://mvvm.coldwind.top/)
---
## 灵感来源
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
---
## 使用的第三方库
```xml
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Lyricify.Lyrics.Helper" Version="0.1.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.25.0" />
```
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)
## 欢迎提出反馈或建议
感谢。

175
README.md
View File

@@ -1,3 +1,5 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md">_**点此处查看中文说明**_</a>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
</div>
@@ -17,56 +19,92 @@ Your smooth dynamic local lyrics display built with WinUI 3
- Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song
- Gradient Karaoke effect on every single character
- Gradient Karaoke (with glow) effect on every single character
- Immersive desktop lyrics (dock mode)
Coding in progress...
> This project is still under development now, bugs and unexpected behaviors may be existed in the latest dev branch.
## Supported lyrics source
- From your local storage
- Music files (with embedded lyrics)
- [.lrc](https://en.wikipedia.org/wiki/LRC_(file_format)) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
- From online lyrics providers
- [LRCLIB](https://lrclib.net/)
## Customize in your way
We provide more than one setting item to better align with your preference
- Theme (light, dark, follow system)
- Theme
- Light
- Dark
- Follow system
- Backdrop (none, mica, acrylic, transparent)
- Backdrop
- None
- Mica
- Acrylic
- Transparent
- Album art as background (dynamic, blur amount, opacity)
- Album art as background
- Dynamic
- Blur amount
- Opacity
- Lyrics (alignment, font size, line spacing, opacity, blur amount, dynamic glow effect)
- Album art as cover
- Corner radius
- Language (English, Simplified Chinese, Traditional Chinese)
- Lyrics
- Alignment
- Font size
- Font color **(from album art accent color)**
- Line spacing
- Opacity
- Blur amount
- Dynamic **glow** effect
- Whole lyrics
- Line by line
- Word by word
## Live demonstration
- Language
- English
- Simplified Chinese
- Traditional Chinese
- Japanese
- Korean
![alt text](Screenshots/Beelink-SER-8-Moonlight2025-06-0318-35-35-ezgif.com-video-to-gif-converter.gif)
![alt text](Screenshots/BetterLyrics.WinUI3_igDdnc4rzW.gif)
Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(uploaded on 31 May 2025) on Bilibili below (click the cover image to watch):
[![Bilibili](https://i1.hdslb.com/bfs/archive/75a7a1a3803b617574090a91c59785b6c40d0fe5.jpg@672w_378h_1c.avif)](https://b23.tv/QjKkYmL)
## Screenshots
### Split view
![alt text](Screenshots/mode.png)
![alt text](Screenshots/Snipaste_2025-06-03_16-46-55.png)
![alt text](Screenshots/Snipaste_2025-06-03_16-47-43.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-51-22.png)
![alt text](Screenshots/glow.png)
### Fullscreen
![alt text](Screenshots/glow.gif)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-51.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-53-07.png)
![alt text](Screenshots/Snipaste_2025-06-03_18-36-05.png)
![alt text](Screenshots/dock.png)
### Settings
![alt text](Screenshots/immersive-dock.gif)
![alt text](Screenshots/Snipaste_2025-06-03_17-51-52.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-00.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-05.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-11.png)
![alt text](Screenshots/dock.gif)
## Download it now
![alt text](Screenshots/pip.png)
![alt text](Screenshots/settings.png)
![alt text](Screenshots/fs.png)
## Demonstration
Watch our introduction video (uploaded on 31 May 2025) on Bilibili [here](https://b23.tv/QjKkYmL).
## Try it now
### Stable version
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
@@ -74,53 +112,78 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
> **Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version, if you like you can purchase to support me)
Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
Or alternatively get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases/latest) page for the link)
<a href="https://drive.google.com/file/d/1Hh8ijbODIksPmmRYujys7fXngw93Of7I/view?usp=drive_link">
<img src="https://pngimg.com/uploads/google_drive/google_drive_PNG9.png" width="100"/>
</a>
> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
> .zip file, please follow [this doc](How2Install/How2Install.md) to properly install it
### Latest dev version
You can `git clone` this project and build it yourself.
## Setup your app
This project relies on listening messages from [SMTC](https://learn.microsoft.com/en-ca/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols), so most of the music players will work.
### About lyrics
For a better experience, you can use [LDDC](https://github.com/chenmozhijin/LDDC) to download lyrics.
## Future work
To be added later.
## Many thanks to
- [LRCLIB](https://lrclib.net/)
- Online lyrics API provider
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- Used for extracting pictures in music files
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- Provide easy ways to access Win32 API regarding windowing
- [TagLib#](https://github.com/mono/taglib-sharp)
- Used for reading original lyrics content
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## Inspired by
- [BetterNCM](https://github.com/std-microblock/BetterNCM)
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## Third-party libraries that this project uses
```
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="z440.atl.core" Version="6.24.0" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />S
```
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)
## Any issues and PRs are welcomed
If you find a bug please file it in issues or if you have any ideas feel free to share it here.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Some files were not shown because too many files have changed in this diff Show More