Compare commits

...

12 Commits

Author SHA1 Message Date
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
Zhe Fang
80ef86478b Merge pull request #2 from jayfunc/dev
Dev
2025-06-05 22:02:40 -04:00
Zhe Fang
9736a4c9cc add: user can set lyrics color now (choose one from album art accent colors) 2025-06-05 21:59:30 -04:00
Zhe Fang
d01be4b883 add support for editing album art corner radius 2025-06-05 17:44:04 -04:00
Zhe Fang
b470b91d0e format c# code 2025-06-05 15:36:31 -04:00
Zhe Fang
61f4e5706b update md 2025-06-05 10:40:38 -04:00
Zhe Fang
6f2d3a3505 update download methods 2025-06-05 10:34:33 -04:00
Zhe Fang
58b7cd2520 fix bug: when toggle to lyrics only mode and switch to song with no lyrics the album art is hidden 2025-06-05 10:04:13 -04:00
Zhe Fang
b2319e7983 fix lrc file supporting bug 2025-06-04 23:04:12 -04:00
Zhe Fang
a3b250dd46 update docs, fix a lot bugs 2025-06-04 20:12:45 -04:00
125 changed files with 3261 additions and 1862 deletions

View File

@@ -40,15 +40,16 @@
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateKeyFile>BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx</PackageCertificateKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
@@ -80,6 +81,7 @@
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx" />
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 B

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -5,12 +5,13 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18"
IgnorableNamespaces="uap rescap uap18">
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.1.0" />
Version="1.0.2.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
@@ -26,7 +27,9 @@
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
</Resources>
<Applications>

View File

@@ -16,12 +16,8 @@
<!-- Theme -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="SemiTransparentSystemBaseHighColor">#80000000</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="SemiTransparentSystemBaseHighColor">#80FFFFFF</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Light" />
<ResourceDictionary x:Key="Dark" />
</ResourceDictionary.ThemeDictionaries>
<!-- Brush -->
@@ -48,6 +44,7 @@
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
@@ -60,6 +57,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,63 +1,112 @@
using BetterLyrics.WinUI3.Services.Database;
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 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 System.Text;
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.
namespace BetterLyrics.WinUI3 {
namespace BetterLyrics.WinUI3
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application {
public static App Current => (App)Application.Current;
public MainWindow? MainWindow { get; private set; }
public MainWindow? SettingsWindow { get; set; }
public partial class App : Application
{
private readonly ILogger<App> _logger;
public static ResourceLoader ResourceLoader = new();
public static new App Current => (App)Application.Current;
public BaseWindow? MainWindow { get; private set; }
public BaseWindow? SettingsWindow { get; set; }
public static DispatcherQueue DispatcherQueue => DispatcherQueue.GetForCurrentThread();
public static ResourceLoader? ResourceLoader { get; private set; }
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
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App() {
public App()
{
this.InitializeComponent();
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
Helper.AppInfo.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
UnhandledException += App_UnhandledException;
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(Helper.AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
// ViewModels
.AddSingleton<BaseWindowModel>()
.AddSingleton<MainViewModel>()
.AddSingleton<SettingsViewModel>()
.BuildServiceProvider()
);
}
private void App_UnhandledException(
object sender,
Microsoft.UI.Xaml.UnhandledExceptionEventArgs e
)
{
_logger.LogError(e.Exception, "App_UnhandledException");
e.Handled = true;
}
/// <summary>
/// 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) {
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton(DispatcherQueue.GetForCurrentThread())
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
// ViewModels
.AddSingleton<MainViewModel>()
.AddSingleton<SettingsViewModel>()
.BuildServiceProvider());
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new MainWindow();
MainWindow = new BaseWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -2,42 +2,57 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<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.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.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="DevWinUI" Version="8.2.0" />
<PackageReference Include="DevWinUI" Version="8.3.0" />
<PackageReference Include="DevWinUI.Controls" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" 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="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="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="z440.atl.core" Version="6.24.0" />
</ItemGroup>
<!-- Publish Properties -->
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
@@ -45,4 +60,9 @@
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
</Project>
<PropertyGroup>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
</PropertyGroup>
</Project>

View File

@@ -1,22 +1,27 @@
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace BetterLyrics.WinUI3.Converter {
public class ColorToBrushConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
if (value is Color color) {
namespace BetterLyrics.WinUI3.Converter
{
public class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Color color)
{
return new SolidColorBrush(color);
}
return new SolidColorBrush();
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -1,21 +1,26 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter {
internal class ThemeTypeToElementThemeConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
if (value is int themeType) {
namespace BetterLyrics.WinUI3.Converter
{
internal class ThemeTypeToElementThemeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int themeType)
{
return (ElementTheme)themeType;
}
return ElementTheme.Default;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return 0;
}
}

View File

@@ -1,44 +1,13 @@
using Microsoft.UI.Xaml;
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
using System;
namespace BetterLyrics.WinUI3.Helper {
/// <summary>
/// Edited based on: https://stackoverflow.com/a/25236507/11048731
/// </summary>
public class AnimationHelper : DependencyObject {
public static int GetAnimationDuration(DependencyObject obj) {
return (int)obj.GetValue(AnimationDurationProperty);
}
public static void SetAnimationDuration(DependencyObject obj, int value) {
obj.SetValue(AnimationDurationProperty, value);
}
// Using a DependencyProperty as the backing store for AnimationDuration.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.RegisterAttached("AnimationDuration", typeof(int),
typeof(AnimationHelper), new PropertyMetadata(0,
OnAnimationDurationChanged));
private static void OnAnimationDurationChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
FrameworkElement element = d as FrameworkElement;
var ms = (int)e.NewValue;
if (ms < 0) return;
var key = "LyricsLineCharGradientInTextBlock";
foreach (var timeline in (element.Resources[key] as Storyboard).Children) {
foreach (var keyFrame in (timeline as DoubleAnimationUsingKeyFrames).KeyFrames) {
(keyFrame as LinearDoubleKeyFrame).KeyTime =
KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(ms));
}
}
}
namespace BetterLyrics.WinUI3.Helper
{
public static class AnimationHelper
{
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
public const int DebounceDefaultDuration = 200;
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
using System;
using System.IO;
using Windows.ApplicationModel;
using Windows.Storage;
public static class AppInfo
{
// App Metadata
public const string AppName = "BetterLyrics";
public const string AppDisplayName = "Better Lyrics";
public const string AppAuthor = "Zhe Fang";
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
// Environment Info
public static bool IsDebug =>
#if DEBUG
true;
#else
false;
#endif
// Base Folders
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
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");
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
public static string TestMusicPath => Path.Combine(AssetsFolder, TestMusicFileName);
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LocalFolder);
}
}
}

View File

@@ -4,9 +4,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public static class CollectionHelper {
public static T? SafeGet<T>(this IList<T> list, int index) {
namespace BetterLyrics.WinUI3.Helper
{
public static class CollectionHelper
{
public static T? SafeGet<T>(this IList<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
return default;
return list[index];

View File

@@ -1,12 +1,14 @@
namespace BetterLyrics.WinUI3.Helper {
public class ColorHelper {
public static Windows.UI.Color LerpColor(Windows.UI.Color a, Windows.UI.Color b, double t) {
namespace BetterLyrics.WinUI3.Helper
{
public class ColorHelper
{
public static Windows.UI.Color LerpColor(Windows.UI.Color a, Windows.UI.Color b, double t)
{
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);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Helper
internal class CMap
{
private readonly List<VBox> vboxes = new List<VBox>();
private List<QuantizedColor> palette;
private List<QuantizedColor>? palette;
public void Push(VBox box)
{
@@ -24,10 +24,12 @@ namespace BetterLyrics.WinUI3.Helper
{
if (palette == null)
{
palette = (from vBox in vboxes
let rgb = vBox.Avg(false)
let color = FromRgb(rgb[0], rgb[1], rgb[2])
select new QuantizedColor(color, vBox.Count(false))).ToList();
palette = (
from vBox in vboxes
let rgb = vBox.Avg(false)
let color = FromRgb(rgb[0], rgb[1], rgb[2])
select new QuantizedColor(color, vBox.Count(false))
).ToList();
}
return palette;
@@ -38,7 +40,7 @@ namespace BetterLyrics.WinUI3.Helper
return vboxes.Count;
}
public int[] Map(int[] color)
public int[]? Map(int[] color)
{
foreach (var vbox in vboxes.Where(vbox => vbox.Contains(color)))
{
@@ -47,17 +49,19 @@ namespace BetterLyrics.WinUI3.Helper
return Nearest(color);
}
public int[] Nearest(int[] color)
public int[]? Nearest(int[] color)
{
var d1 = double.MaxValue;
int[] pColor = null;
int[]? pColor = null;
foreach (var t in vboxes)
{
var vbColor = t.Avg(false);
var d2 = Math.Sqrt(Math.Pow(color[0] - vbColor[0], 2)
+ Math.Pow(color[1] - vbColor[1], 2)
+ Math.Pow(color[2] - vbColor[2], 2));
var d2 = Math.Sqrt(
Math.Pow(color[0] - vbColor[0], 2)
+ Math.Pow(color[1] - vbColor[1], 2)
+ Math.Pow(color[2] - vbColor[2], 2)
);
if (d2 < d1)
{
d1 = d2;
@@ -67,9 +71,16 @@ namespace BetterLyrics.WinUI3.Helper
return pColor;
}
public VBox FindColor(double targetLuma, double minLuma, double maxLuma, double targetSaturation, double minSaturation, double maxSaturation)
public VBox FindColor(
double targetLuma,
double minLuma,
double maxLuma,
double targetSaturation,
double minSaturation,
double maxSaturation
)
{
VBox max = null;
VBox? max = null;
double maxValue = 0;
var highestPopulation = vboxes.Select(p => p.Count(false)).Max();
@@ -80,11 +91,21 @@ namespace BetterLyrics.WinUI3.Helper
var sat = hsl.S;
var luma = hsl.L;
if (sat >= minSaturation && sat <= maxSaturation &&
luma >= minLuma && luma <= maxLuma)
if (
sat >= minSaturation
&& sat <= maxSaturation
&& luma >= minLuma
&& luma <= maxLuma
)
{
var thisValue = Mmcq.CreateComparisonValue(sat, targetSaturation, luma, targetLuma,
swatch.Count(false), highestPopulation);
var thisValue = Mmcq.CreateComparisonValue(
sat,
targetSaturation,
luma,
targetLuma,
swatch.Count(false),
highestPopulation
);
if (max == null || thisValue > maxValue)
{
@@ -97,14 +118,14 @@ namespace BetterLyrics.WinUI3.Helper
return max;
}
public Color FromRgb(int red, int green, int blue)
public RGB FromRgb(int red, int green, int blue)
{
var color = new Color
var color = new RGB
{
A = 255,
R = (byte)red,
G = (byte)green,
B = (byte)blue
B = (byte)blue,
};
return color;
@@ -114,7 +135,7 @@ namespace BetterLyrics.WinUI3.Helper
/// <summary>
/// Defines a color in RGB space.
/// </summary>
public struct Color
public struct RGB
{
/// <summary>
/// Get or Set the Alpha component value for sRGB.
@@ -269,9 +290,12 @@ namespace BetterLyrics.WinUI3.Helper
private static VBox VboxFromPixels(IList<byte[]> pixels, int[] histo)
{
int rmin = 1000000, rmax = 0;
int gmin = 1000000, gmax = 0;
int bmin = 1000000, bmax = 0;
int rmin = 1000000,
rmax = 0;
int gmin = 1000000,
gmax = 0;
int bmin = 1000000,
bmax = 0;
// find min/max
var numPixels = pixels.Count;
@@ -313,7 +337,13 @@ namespace BetterLyrics.WinUI3.Helper
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
}
private static VBox[] DoCut(char color, VBox vbox, IList<int> partialsum, IList<int> lookaheadsum, int total)
private static VBox[] DoCut(
char color,
VBox vbox,
IList<int> partialsum,
IList<int> lookaheadsum,
int total
)
{
int vboxDim1;
int vboxDim2;
@@ -344,9 +374,10 @@ namespace BetterLyrics.WinUI3.Helper
var left = i - vboxDim1;
var right = vboxDim2 - i;
var d2 = left <= right
? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2))
: Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0)));
var d2 =
left <= right
? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2))
: Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0)));
// avoid 0-count boxes
while (d2 < 0 || partialsum[d2] <= 0)
@@ -383,7 +414,7 @@ namespace BetterLyrics.WinUI3.Helper
throw new Exception("VBox can't be cut");
}
private static VBox[] MedianCutApply(IList<int> histo, VBox vbox)
private static VBox?[]? MedianCutApply(IList<int> histo, VBox vbox)
{
if (vbox.Count(false) == 0)
{
@@ -391,7 +422,7 @@ namespace BetterLyrics.WinUI3.Helper
}
if (vbox.Count(false) == 1)
{
return new[] { vbox.Clone(), null };
return [vbox.Clone(), null];
}
// only one pixel, no split
@@ -417,7 +448,11 @@ namespace BetterLyrics.WinUI3.Helper
lookaheadsum[l] = -1;
}
int i, j, k, sum, index;
int i,
j,
k,
sum,
index;
if (maxw == rw)
{
@@ -480,8 +515,9 @@ namespace BetterLyrics.WinUI3.Helper
}
// determine the cut planes
return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total) : maxw == gw
? DoCut('g', vbox, partialsum, lookaheadsum, total) : DoCut('b', vbox, partialsum, lookaheadsum, total);
return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total)
: maxw == gw ? DoCut('g', vbox, partialsum, lookaheadsum, total)
: DoCut('b', vbox, partialsum, lookaheadsum, total);
}
/// <summary>
@@ -492,7 +528,12 @@ namespace BetterLyrics.WinUI3.Helper
/// <param name="target">The target.</param>
/// <param name="histo">The histo.</param>
/// <exception cref="System.Exception">vbox1 not defined; shouldn't happen!</exception>
private static void Iter(List<VBox> lh, IComparer<VBox> comparator, int target, IList<int> histo)
private static void Iter(
List<VBox> lh,
IComparer<VBox> comparator,
int target,
IList<int> histo
)
{
var ncolors = 1;
var niters = 0;
@@ -511,13 +552,12 @@ namespace BetterLyrics.WinUI3.Helper
// do the cut
var vboxes = MedianCutApply(histo, vbox);
var vbox1 = vboxes[0];
var vbox2 = vboxes[1];
var vbox1 = vboxes?[0];
var vbox2 = vboxes?[1];
if (vbox1 == null)
{
throw new Exception(
"vbox1 not defined; shouldn't happen!");
throw new Exception("vbox1 not defined; shouldn't happen!");
}
lh.Add(vbox1);
@@ -579,11 +619,23 @@ namespace BetterLyrics.WinUI3.Helper
return cmap;
}
public static double CreateComparisonValue(double saturation, double targetSaturation, double luma, double targetLuma, int population, int highestPopulation)
public static double CreateComparisonValue(
double saturation,
double targetSaturation,
double luma,
double targetLuma,
int population,
int highestPopulation
)
{
return WeightedMean(InvertDiff(saturation, targetSaturation), WeightSaturation,
InvertDiff(luma, targetLuma), WeightLuma,
population / (double)highestPopulation, WeightPopulation);
return WeightedMean(
InvertDiff(saturation, targetSaturation),
WeightSaturation,
InvertDiff(luma, targetLuma),
WeightLuma,
population / (double)highestPopulation,
WeightPopulation
);
}
private static double WeightedMean(params double[] values)
@@ -611,20 +663,22 @@ namespace BetterLyrics.WinUI3.Helper
public class QuantizedColor
{
public QuantizedColor(Color color, int population)
public QuantizedColor(RGB color, int population)
{
Color = color;
Population = population;
IsDark = CalculateYiqLuma(color) < 128;
}
public Color Color { get; private set; }
public RGB Color { get; private set; }
public int Population { get; private set; }
public bool IsDark { get; private set; }
public int CalculateYiqLuma(Color color)
public int CalculateYiqLuma(RGB color)
{
return Convert.ToInt32(Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f));
return Convert.ToInt32(
Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f)
);
}
}
@@ -732,8 +786,9 @@ namespace BetterLyrics.WinUI3.Helper
{
avg = new[]
{
Math.Abs(rsum / ntot), Math.Abs(gsum / ntot),
Math.Abs(bsum / ntot)
Math.Abs(rsum / ntot),
Math.Abs(gsum / ntot),
Math.Abs(bsum / ntot),
};
}
else
@@ -742,7 +797,7 @@ namespace BetterLyrics.WinUI3.Helper
{
Math.Abs(Mmcq.Mult * (R1 + R2 + 1) / 2),
Math.Abs(Mmcq.Mult * (G1 + G2 + 1) / 2),
Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2)
Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2),
};
}
}
@@ -809,14 +864,16 @@ namespace BetterLyrics.WinUI3.Helper
private byte[][] ConvertPixels(byte[] pixels, int pixelCount, int quality, bool ignoreWhite)
{
var expectedDataLength = pixelCount * ColorDepth;
if (expectedDataLength != pixels.Length)
{
throw new ArgumentException("(expectedDataLength = "
+ expectedDataLength + ") != (pixels.length = "
+ pixels.Length + ")");
throw new ArgumentException(
"(expectedDataLength = "
+ expectedDataLength
+ ") != (pixels.length = "
+ pixels.Length
+ ")"
);
}
// Store the RGB values in an array format suitable for quantize
@@ -864,17 +921,24 @@ namespace BetterLyrics.WinUI3.Helper
/// </param>
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
public async Task<QuantizedColor> GetColor(BitmapDecoder sourceImage, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
public async Task<QuantizedColor> GetColor(
BitmapDecoder sourceImage,
int quality = DefaultQuality,
bool ignoreWhite = DefaultIgnoreWhite
)
{
var palette = await GetPalette(sourceImage, 3, quality, ignoreWhite);
var dominantColor = new QuantizedColor(new Color
{
A = Convert.ToByte(palette.Average(a => a.Color.A)),
R = Convert.ToByte(palette.Average(a => a.Color.R)),
G = Convert.ToByte(palette.Average(a => a.Color.G)),
B = Convert.ToByte(palette.Average(a => a.Color.B))
}, Convert.ToInt32(palette.Average(a => a.Population)));
var dominantColor = new QuantizedColor(
new RGB
{
A = Convert.ToByte(palette.Average(a => a.Color.A)),
R = Convert.ToByte(palette.Average(a => a.Color.R)),
G = Convert.ToByte(palette.Average(a => a.Color.G)),
B = Convert.ToByte(palette.Average(a => a.Color.B)),
},
Convert.ToInt32(palette.Average(a => a.Population))
);
return dominantColor;
}
@@ -893,7 +957,12 @@ namespace BetterLyrics.WinUI3.Helper
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
/// <code>true</code>
public async Task<List<QuantizedColor>> GetPalette(BitmapDecoder sourceImage, int colorCount = DefaultColorCount, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
public async Task<List<QuantizedColor>> GetPalette(
BitmapDecoder sourceImage,
int colorCount = DefaultColorCount,
int quality = DefaultQuality,
bool ignoreWhite = DefaultIgnoreWhite
)
{
var pixelArray = await GetPixelsFast(sourceImage, quality, ignoreWhite);
var cmap = GetColorMap(pixelArray, colorCount);
@@ -912,7 +981,11 @@ namespace BetterLyrics.WinUI3.Helper
return pixels;
}
private async Task<byte[][]> GetPixelsFast(BitmapDecoder sourceImage, int quality, bool ignoreWhite)
private async Task<byte[][]> GetPixelsFast(
BitmapDecoder sourceImage,
int quality,
bool ignoreWhite
)
{
if (quality < 1)
{

View File

@@ -1,21 +0,0 @@
using BetterLyrics.WinUI3.Models;
using SQLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class DatabaseHelper {
private static SQLiteConnection _database;
public static SQLiteConnection InitializeDatabase() {
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MusicMetadataIndex.db");
_database = new SQLiteConnection(dbPath);
_database.CreateTable<MetadataIndex>(); // Create table if it doesn't exist
return _database;
}
}
}

View File

@@ -4,9 +4,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class EasingHelper {
namespace BetterLyrics.WinUI3.Helper
{
public class EasingHelper
{
/// <summary>
/// No easing
/// </summary>
@@ -25,23 +26,24 @@ namespace BetterLyrics.WinUI3.Helper {
/// <summary>
/// Acceleration until halfway then deceleration
/// </summary>
public static float EaseInOutQuad(float t) {
return t < 0.5f
? 2 * t * t
: -1 + (4 - 2 * t) * t;
public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
/// <summary>
/// Smoother transition than linear
/// </summary>
public static float SmoothStep(float t) {
public static float SmoothStep(float t)
{
return t * t * (3 - 2 * t);
}
/// <summary>
/// Even smoother transition with continuous first and second derivatives
/// </summary>
public static float SmootherStep(float t) {
public static float SmootherStep(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
}
}

View File

@@ -1,8 +1,9 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
@@ -11,7 +12,9 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
@@ -22,5 +25,13 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
}
}

View File

@@ -4,13 +4,18 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class MathHelper {
public static List<int> GetAllFactors(int n) {
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) {
for (int i = 1; i <= Math.Sqrt(n); i++)
{
if (n % i == 0)
{
result.Add(i);
result.Add(n / i);
}
@@ -18,6 +23,5 @@ namespace BetterLyrics.WinUI3.Helper {
return [.. result];
}
}
}
}

View File

@@ -6,8 +6,10 @@ namespace BetterLyrics.WinUI3.Helper
{
public class SystemBackdropHelper
{
public static SystemBackdrop? CreateSystemBackdrop(BackdropType backdropType) {
return backdropType switch {
public static SystemBackdrop? CreateSystemBackdrop(BackdropType backdropType)
{
return backdropType switch
{
BackdropType.None => null,
BackdropType.Mica => new MicaSystemBackdrop(MicaKind.Base),
BackdropType.MicaAlt => new MicaSystemBackdrop(MicaKind.BaseAlt),
@@ -18,6 +20,5 @@ namespace BetterLyrics.WinUI3.Helper
_ => null,
};
}
}
}

View File

@@ -1,32 +1,40 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml;
using System;
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 {
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 = new List<T>();
if (depObj != null) {
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) {
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) {
list.Add((T)child);
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) {
if (childItems != null && childItems.Count > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -1,191 +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">
<!-- 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>
<TextBlock
x:Name="AppTitleTextBlock"
Margin="0,-4,0,0"
VerticalAlignment="Center"
Text="{x:Bind Title}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<!-- 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,152 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Behaviors;
using DevWinUI;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
using WinRT;
using WinRT.Interop;
// 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 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 BackButton_Click(object sender, RoutedEventArgs e) {
if (RootFrame.CanGoBack) {
RootFrame.GoBack();
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e) {
if (RootFrame.CurrentSourcePageType == typeof(MainPage)) {
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

@@ -0,0 +1,15 @@
using System;
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 ShowNotificatonMessage(Notification value)
: ValueChangedMessage<Notification>(value) { }
}

View File

@@ -1,14 +1,13 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using DevWinUI;
using System;
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 : ValueChangedMessage<BackdropType> {
public SystemBackdropChangedMessage(BackdropType value) : base(value) {
}
}
namespace BetterLyrics.WinUI3.Messages
{
public class SystemBackdropChangedMessage(BackdropType value)
: ValueChangedMessage<BackdropType>(value) { }
}

View File

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

View File

@@ -4,8 +4,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public enum Language {
namespace BetterLyrics.WinUI3.Models
{
public enum Language
{
FollowSystem,
English,
SimplifiedChinese,

View File

@@ -4,8 +4,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public enum LyricsAlignmentType {
namespace BetterLyrics.WinUI3.Models
{
public enum LyricsAlignmentType
{
Left,
Center,
Right,

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 enum LyricsFontColorType
{
Default,
Dominant,
}
}

View File

@@ -1,14 +1,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Graphics.Canvas.Text;
using System.Collections.Generic;
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;
namespace BetterLyrics.WinUI3.Models {
public class LyricsLine {
namespace BetterLyrics.WinUI3.Models
{
public class LyricsLine
{
public List<string> Texts { get; set; } = [];
public int LanguageIndex { get; set; } = 0;
@@ -37,6 +38,5 @@ namespace BetterLyrics.WinUI3.Models {
public float Opacity { get; set; }
public CanvasTextLayout TextLayout { get; set; }
}
}

View File

@@ -4,19 +4,23 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public enum LyricsPlayingState {
namespace BetterLyrics.WinUI3.Models
{
public enum LyricsPlayingState
{
/// <summary>
/// Not played yet, will be playing in the future
/// </summary>
NotPlayed,
/// <summary>
/// Playing
/// </summary>
Playing,
/// <summary>
/// Has already played
/// </summary>
Played
Played,
}
}

View File

@@ -1,15 +1,12 @@
using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public class MetadataIndex {
namespace BetterLyrics.WinUI3.Models
{
public class MetadataIndex
{
[PrimaryKey]
public string Path { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public string? Path { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public partial class MusicFolder : ObservableObject {
[ObservableProperty]
private string _path;
public bool IsValid => Directory.Exists(Path);
public MusicFolder(string path) {
Path = path;
}
}
}

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]
private InfoBarSeverity _severity;
[ObservableProperty]
private string? _message;
[ObservableProperty]
private bool _isForeverDismissable;
[ObservableProperty]
private Visibility _visibility;
[ObservableProperty]
private string? _relatedSettingsKeyName;
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,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public enum TitleBarType
{
Compact,
Extended,
}
}

View File

@@ -0,0 +1,116 @@
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,136 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Graphics.Imaging;
namespace BetterLyrics.WinUI3.Rendering
{
public class CoverBackgroundRenderer
{
private readonly SettingsService _settingsService;
public float RotateAngle { get; set; } = 0f;
private SoftwareBitmap? _lastSoftwareBitmap = null;
private SoftwareBitmap? _softwareBitmap = null;
public SoftwareBitmap? SoftwareBitmap
{
get => _softwareBitmap;
set
{
if (_softwareBitmap != null)
{
_lastSoftwareBitmap = _softwareBitmap;
_transitionStartTime = DateTimeOffset.Now;
_isTransitioning = true;
_transitionAlpha = 0f;
}
_softwareBitmap = value;
}
}
private float _transitionAlpha = 1f;
private TimeSpan _transitionDuration = TimeSpan.FromMilliseconds(1000);
private DateTimeOffset _transitionStartTime;
private bool _isTransitioning = false;
public CoverBackgroundRenderer()
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (!_settingsService.IsCoverOverlayEnabled || SoftwareBitmap == null)
return;
ds.Transform = Matrix3x2.CreateRotation(RotateAngle, control.Size.ToVector2() * 0.5f);
var overlappedCovers = new CanvasCommandList(control);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
if (_isTransitioning && _lastSoftwareBitmap != null)
{
DrawImgae(control, overlappedCoversDs, _lastSoftwareBitmap, 1 - _transitionAlpha);
DrawImgae(control, overlappedCoversDs, SoftwareBitmap, _transitionAlpha);
}
else
{
DrawImgae(control, overlappedCoversDs, SoftwareBitmap, 1);
}
using var coverOverlayEffect = new OpacityEffect
{
Opacity = _settingsService.CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = _settingsService.CoverOverlayBlurAmount,
Source = overlappedCovers,
},
};
ds.DrawImage(coverOverlayEffect);
ds.Transform = Matrix3x2.Identity;
}
private void DrawImgae(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
SoftwareBitmap softwareBitmap,
float opacity
)
{
float imageWidth = (float)(softwareBitmap.PixelWidth * 96f / softwareBitmap.DpiX);
float imageHeight = (float)(softwareBitmap.PixelHeight * 96f / softwareBitmap.DpiY);
var scaleFactor =
(float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
/ Math.Min(imageWidth, imageHeight);
ds.DrawImage(
new OpacityEffect
{
Source = new ScaleEffect
{
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(scaleFactor),
Source = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap),
},
Opacity = opacity,
},
(float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
(float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
);
}
public void Calculate(ICanvasAnimatedControl control)
{
if (_isTransitioning)
{
var elapsed = DateTimeOffset.Now - _transitionStartTime;
float progress = (float)(
elapsed.TotalMilliseconds / _transitionDuration.TotalMilliseconds
);
_transitionAlpha = Math.Clamp(progress, 0f, 1f);
if (_transitionAlpha >= 1f)
{
_isTransitioning = false;
_lastSoftwareBitmap?.Dispose();
_lastSoftwareBitmap = null;
}
}
}
}
}

View File

@@ -0,0 +1,422 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Text;
using Windows.UI;
namespace BetterLyrics.WinUI3.Rendering
{
public class PureLyricsRenderer
{
private readonly SettingsService _settingsService;
private readonly float _defaultOpacity = 0.3f;
private readonly float _highlightedOpacity = 1.0f;
private readonly float _defaultScale = 0.95f;
private readonly float _highlightedScale = 1.0f;
private readonly int _lineEnteringDurationMs = 800;
private readonly int _lineExitingDurationMs = 800;
private readonly int _lineScrollDurationMs = 800;
private float _lastTotalYScroll = 0.0f;
private float _totalYScroll = 0.0f;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private bool _forceToScroll = false;
private readonly double _rightMargin = 36;
public double LimitedLineWidth { get; set; } = 0;
public double CanvasWidth { get; set; } = 0;
public double CanvasHeight { get; set; } = 0;
public TimeSpan CurrentTime { get; set; }
public List<LyricsLine> LyricsLines { get; set; } = [];
public PureLyricsRenderer()
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
}
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries()
{
// _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
return new Tuple<int, int>(_startVisibleLineIndex, _endVisibleLineIndex);
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (LyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, LyricsLines.Count - 1);
}
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
byte r,
byte g,
byte b
)
{
var (displayStartLineIndex, displayEndLineIndex) =
GetVisibleLyricsLineIndexBoundaries();
for (
int i = displayStartLineIndex;
LyricsLines.Count > 0
&& i >= 0
&& i < LyricsLines.Count
&& i <= displayEndLineIndex;
i++
)
{
var line = LyricsLines[i];
if (line.TextLayout == null)
{
return;
}
float progressPerChar = 1f / line.Text.Length;
var position = line.Position;
float centerX = position.X;
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType)
{
case LyricsAlignmentType.Left:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)LimitedLineWidth / 2;
break;
case LyricsAlignmentType.Right:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)LimitedLineWidth;
break;
default:
break;
}
int startIndex = 0;
// Set brush
for (int j = 0; j < line.TextLayout.LineCount; j++)
{
int count = line.TextLayout.LineMetrics[j].CharacterCount;
var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
float subLinePlayingProgress = Math.Clamp(
(line.PlayingProgress * line.Text.Length - startIndex) / count,
0,
1
);
using var horizontalFillBrush = new CanvasLinearGradientBrush(
control,
[
new()
{
Position = 0,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position =
subLinePlayingProgress * (1 + progressPerChar)
- progressPerChar,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position = subLinePlayingProgress * (1 + progressPerChar),
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
new()
{
Position = 1.5f,
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
]
)
{
StartPoint = new Vector2(
(float)(regions[0].LayoutBounds.Left + position.X),
0
),
EndPoint = new Vector2(
(float)(regions[^1].LayoutBounds.Right + position.X),
0
),
};
line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
startIndex += count;
}
// Scale
ds.Transform =
Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
* Matrix3x2.CreateTranslation(0, _totalYScroll);
// _logger.LogDebug(_totalYScroll);
ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
public async Task ForceToScrollToCurrentPlayingLineAsync()
{
_forceToScroll = true;
await Task.Delay(1);
_forceToScroll = false;
}
public async Task ReLayoutAsync(ICanvasAnimatedControl control)
{
if (control == null)
return;
float leftMargin = (float)(CanvasWidth - LimitedLineWidth - _rightMargin);
using CanvasTextFormat textFormat = new()
{
FontSize = _settingsService.LyricsFontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontWeight = FontWeights.Bold,
//FontFamily = "Segoe UI Mono",
};
float y = (float)CanvasHeight / 2;
// Init Positions
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
// Calculate layout bounds
line.TextLayout = new CanvasTextLayout(
control.Device,
line.Text,
textFormat,
(float)LimitedLineWidth,
(float)CanvasHeight
);
line.Position = new Vector2(leftMargin, y);
y +=
(float)line.TextLayout.LayoutBounds.Height
/ line.TextLayout.LineCount
* (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
}
await ForceToScrollToCurrentPlayingLineAsync();
}
public void CalculateScaleAndOpacity(int currentPlayingLineIndex)
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
for (int i = startLineIndex; LyricsLines.Count > 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
bool linePlaying = i == currentPlayingLineIndex;
var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
var lineExitingDurationMs = _lineExitingDurationMs;
if (i + 1 <= endLineIndex)
{
lineExitingDurationMs = Math.Min(
LyricsLines[i + 1].DurationMs,
lineExitingDurationMs
);
}
float lineEnteringProgress = 0.0f;
float lineExitingProgress = 0.0f;
bool lineEntering = false;
bool lineExiting = false;
float scale = _defaultScale;
float opacity = _defaultOpacity;
float playProgress = 0;
if (linePlaying)
{
line.PlayingState = LyricsPlayingState.Playing;
scale = _highlightedScale;
opacity = _highlightedOpacity;
playProgress =
((float)CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
/ line.DurationMs;
var durationFromStartMs =
CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering)
{
lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
scale =
_defaultScale
+ (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
opacity =
_defaultOpacity
+ (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
}
}
else
{
if (i < currentPlayingLineIndex)
{
line.PlayingState = LyricsPlayingState.Played;
playProgress = 1;
var durationToEndMs =
CurrentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting)
{
lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
scale =
_highlightedScale
- (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
opacity =
_highlightedOpacity
- (_highlightedOpacity - _defaultOpacity)
* (float)lineExitingProgress;
}
}
else
{
line.PlayingState = LyricsPlayingState.NotPlayed;
}
}
line.EnteringProgress = lineEnteringProgress;
line.ExitingProgress = lineExitingProgress;
line.Scale = scale;
line.Opacity = opacity;
line.PlayingProgress = playProgress;
}
}
public void CalculatePosition(ICanvasAnimatedControl control, int currentPlayingLineIndex)
{
if (currentPlayingLineIndex < 0)
{
return;
}
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0)
{
return;
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = LyricsLines?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
return;
}
if (currentPlayingLine.TextLayout == null)
{
return;
}
var lineScrollingProgress =
(CurrentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
/ Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
var targetYScrollOffset = (float)(
-currentPlayingLine.Position.Y
+ LyricsLines![0].Position.Y
- currentPlayingLine.TextLayout.LayoutBounds.Height / 2
- _lastTotalYScroll
);
var yScrollOffset =
targetYScrollOffset
* EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
bool isScrollingNow = lineScrollingProgress <= 1;
if (isScrollingNow)
{
_totalYScroll = _lastTotalYScroll + yScrollOffset;
}
else
{
if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1)
{
_totalYScroll = _lastTotalYScroll + targetYScrollOffset;
}
_lastTotalYScroll = _totalYScroll;
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update Positions
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
{
if (_startVisibleLineIndex == -1)
{
_startVisibleLineIndex = i;
}
}
if (
_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (_endVisibleLineIndex == -1)
{
_endVisibleLineIndex = i;
}
}
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
{
_endVisibleLineIndex = endLineIndex;
}
}
}
}

View File

@@ -1,76 +1,127 @@
using ATL;
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 System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Ude;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Database {
public class DatabaseService {
namespace BetterLyrics.WinUI3.Services.Database
{
public class DatabaseService
{
private readonly SQLiteConnection _connection;
private readonly CharsetDetector _charsetDetector = new();
public DatabaseService() {
string dbPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MusicMetadataIndex.db"
);
_connection = new SQLiteConnection(dbPath);
_connection.CreateTable<MetadataIndex>();
public DatabaseService()
{
_connection = new SQLiteConnection(Helper.AppInfo.DatabasePath);
if (_connection.GetTableInfo("MetadataIndex").Count == 0)
{
_connection.CreateTable<MetadataIndex>();
}
}
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<MusicFolder> musicFolders) {
await Task.Run(() => {
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<string> paths)
{
await Task.Run(() =>
{
_connection.DeleteAll<MetadataIndex>();
foreach (var localMusicFolder in musicFolders) {
if (localMusicFolder.IsValid) {
foreach (var file in Directory.GetFiles(localMusicFolder.Path)) {
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,
});
_connection.Insert(
new MetadataIndex
{
Path = file,
Title = track.Title,
Artist = track.Artist,
}
);
}
}
}
});
}
public Track? GetMusicMetadata(string? title, string? artist) {
var founds = _connection.Table<MetadataIndex>()
.Where(m => m.Title == title && m.Artist == artist).ToList();
if (founds == null || founds.Count == 0) {
public Track? GetMusicMetadata(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
if (mediaProps == null || mediaProps.Title == null || mediaProps.Artist == null)
return null;
} else {
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) {
if (founds.Count == 1)
{
return first;
} else {
if (first.Lyrics.Exists()) {
}
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)) {
}
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);
if (_charsetDetector.Charset != null)
{
Encoding encoding = Encoding.GetEncoding(
_charsetDetector.Charset
);
content = File.ReadAllText(found.Path, encoding);
} else {
}
else
{
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
first.Lyrics.ParseLRC(content);
@@ -83,7 +134,5 @@ namespace BetterLyrics.WinUI3.Services.Database {
}
}
}
}
}

View File

@@ -1,26 +1,38 @@
using BetterLyrics.WinUI3.Models;
using System;
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 {
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;
// Title bar
public const int TitleBarType = 0;
// Album art
public const int CoverImageRadius = 24;
// Lyrics
public const int LyricsAlignmentType = 1; // Center
public const int LyricsBlurAmount = 0;
@@ -29,5 +41,11 @@ namespace BetterLyrics.WinUI3.Services.Settings {
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;
// Notification
public const bool NeverShowEnterFullScreenMessage = false;
public const bool NeverShowEnterImmersiveModeMessage = false;
}
}

View File

@@ -4,22 +4,34 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings {
public static class SettingsKeys {
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";
// Title bar
public const string TitleBarType = "TitleBarType";
// Album art
public const string CoverImageRadius = "CoverImageRadius";
// Lyrics
public const string LyricsAlignmentType = "LyricsAlignmentType";
public const string LyricsBlurAmount = "LyricsBlurAmount";
@@ -28,6 +40,13 @@ namespace BetterLyrics.WinUI3.Services.Settings {
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";
// Notification
public const string NeverShowEnterFullScreenMessage = "NeverShowEnterFullScreenMessage";
public const string NeverShowEnterImmersiveModeMessage =
"NeverShowEnterImmersiveModeMessage";
}
}

View File

@@ -1,68 +1,71 @@
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
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;
using Windows.System;
namespace BetterLyrics.WinUI3.Services.Settings {
public partial class SettingsService : ObservableObject {
namespace BetterLyrics.WinUI3.Services.Settings
{
public partial class SettingsService : ObservableObject
{
private readonly ApplicationDataContainer _localSettings;
public bool IsFirstRun {
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
_musicLibraries =
[
.. JsonConvert.DeserializeObject<List<string>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)!
)!,
];
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
public bool IsFirstRun
{
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
set => Set(SettingsKeys.IsFirstRun, value);
}
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase;
private bool _isRebuildingLyricsIndexDatabase = false;
[ObservableProperty]
private bool _isImmersiveMode = false;
// Theme
public int Theme {
public int Theme
{
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set {
set
{
Set(SettingsKeys.ThemeType, value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
}
}
// Music
private ObservableCollection<MusicFolder> _musicLibraries;
private ObservableCollection<string> _musicLibraries;
public ObservableCollection<MusicFolder> MusicLibraries {
get {
if (_musicLibraries == null) {
var list = JsonConvert.DeserializeObject<List<MusicFolder>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)
);
_musicLibraries = new ObservableCollection<MusicFolder>(list);
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
return _musicLibraries;
}
set {
if (_musicLibraries != null) {
public ObservableCollection<string> MusicLibraries
{
get { return _musicLibraries; }
set
{
if (_musicLibraries != null)
{
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
@@ -73,17 +76,20 @@ namespace BetterLyrics.WinUI3.Services.Settings {
}
}
private void SaveMusicLibraries() {
private void SaveMusicLibraries()
{
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language {
public int Language
{
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set {
set
{
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language) {
switch ((Models.Language)Language)
{
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
@@ -103,81 +109,171 @@ namespace BetterLyrics.WinUI3.Services.Settings {
}
// Backdrop
public int BackdropType {
public int BackdropType
{
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set {
set
{
Set(SettingsKeys.BackdropType, value);
WeakReferenceMessenger.Default.Send(new SystemBackdropChangedMessage((BackdropType)value));
WeakReferenceMessenger.Default.Send(
new SystemBackdropChangedMessage((BackdropType)value)
);
}
}
public bool IsCoverOverlayEnabled {
get => Get(SettingsKeys.IsCoverOverlayEnabled, SettingsDefaultValues.IsCoverOverlayEnabled);
public bool IsCoverOverlayEnabled
{
get =>
Get(
SettingsKeys.IsCoverOverlayEnabled,
SettingsDefaultValues.IsCoverOverlayEnabled
);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay {
get => Get(SettingsKeys.IsDynamicCoverOverlay, SettingsDefaultValues.IsDynamicCoverOverlay);
public bool IsDynamicCoverOverlay
{
get =>
Get(
SettingsKeys.IsDynamicCoverOverlay,
SettingsDefaultValues.IsDynamicCoverOverlay
);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity {
public int CoverOverlayOpacity
{
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount {
get => Get(SettingsKeys.CoverOverlayBlurAmount, SettingsDefaultValues.CoverOverlayBlurAmount);
public int CoverOverlayBlurAmount
{
get =>
Get(
SettingsKeys.CoverOverlayBlurAmount,
SettingsDefaultValues.CoverOverlayBlurAmount
);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
// Title bar
public int TitleBarType
{
get => Get(SettingsKeys.TitleBarType, SettingsDefaultValues.TitleBarType);
set => Set(SettingsKeys.TitleBarType, value);
}
// Album art
public int CoverImageRadius
{
get => Get(SettingsKeys.CoverImageRadius, SettingsDefaultValues.CoverImageRadius);
set => Set(SettingsKeys.CoverImageRadius, value);
}
// Lyrics
public int LyricsAlignmentType {
public int LyricsAlignmentType
{
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount {
public int LyricsBlurAmount
{
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity {
get => Get(SettingsKeys.LyricsVerticalEdgeOpacity, SettingsDefaultValues.LyricsVerticalEdgeOpacity);
public int LyricsVerticalEdgeOpacity
{
get =>
Get(
SettingsKeys.LyricsVerticalEdgeOpacity,
SettingsDefaultValues.LyricsVerticalEdgeOpacity
);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor {
get => Get(SettingsKeys.LyricsLineSpacingFactor, SettingsDefaultValues.LyricsLineSpacingFactor);
public float LyricsLineSpacingFactor
{
get =>
Get(
SettingsKeys.LyricsLineSpacingFactor,
SettingsDefaultValues.LyricsLineSpacingFactor
);
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
}
public int LyricsFontSize {
public int LyricsFontSize
{
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set => Set(SettingsKeys.LyricsFontSize, value);
}
public bool IsLyricsGlowEffectEnabled {
get => Get(SettingsKeys.IsLyricsGlowEffectEnabled, SettingsDefaultValues.IsLyricsGlowEffectEnabled);
public bool IsLyricsGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsGlowEffectEnabled,
SettingsDefaultValues.IsLyricsGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled {
get => Get(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled);
public bool IsLyricsDynamicGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsDynamicGlowEffectEnabled,
SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
private readonly ApplicationDataContainer _localSettings;
private readonly DatabaseService _databaseService;
public SettingsService(DatabaseService databaseService) {
_localSettings = ApplicationData.Current.LocalSettings;
_databaseService = databaseService;
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)) {
//Notification
public bool NeverShowEnterFullScreenMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterFullScreenMessage,
SettingsDefaultValues.NeverShowEnterFullScreenMessage
);
set => Set(SettingsKeys.NeverShowEnterFullScreenMessage, value);
}
public bool NeverShowEnterImmersiveModeMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterImmersiveModeMessage,
SettingsDefaultValues.NeverShowEnterImmersiveModeMessage
);
set => Set(SettingsKeys.NeverShowEnterImmersiveModeMessage, value);
}
// Utils
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) {
private void Set<T>(string key, T value, [CallerMemberName] string? propertyName = null)
{
_localSettings.Values[key] = value;
OnPropertyChanged(propertyName);
}
}
}

View File

@@ -126,6 +126,9 @@
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>Open in file explorer</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>Remove from app</value>
</data>
@@ -273,10 +276,13 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<value>Album background</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
@@ -306,37 +312,64 @@
<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">
<value>Developer options</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>Play test music</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>Play using system player</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Log</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>Font color</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>Default</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>Album art accent color</value>
</data>
<data name="SettingsPageAlbumStyle.Text" 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>
</root>

View File

@@ -126,6 +126,9 @@
<data name="SettingsPageOpenPath.Content" 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>
@@ -273,10 +276,13 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅展示歌词</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词效果</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<value>专辑背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
@@ -306,37 +312,64 @@
<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">
<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.Text" 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>
</root>

View File

@@ -126,6 +126,9 @@
<data name="SettingsPageOpenPath.Content" 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>
@@ -273,10 +276,13 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅展示歌詞</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞效果</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<value>專輯背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
@@ -306,37 +312,64 @@
<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">
<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.Text" 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>
</root>

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowModel : ObservableObject
{
public SettingsService SettingsService { get; private set; }
[ObservableProperty]
private int _titleBarFontSize = 11;
[ObservableProperty]
private Notification _notification = new();
[ObservableProperty]
private bool _showInfoBar = false;
public BaseWindowModel(SettingsService settingsService)
{
SettingsService = settingsService;
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;
}
}
);
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
switch (Notification.RelatedSettingsKeyName)
{
case SettingsKeys.NeverShowEnterFullScreenMessage:
SettingsService.NeverShowEnterFullScreenMessage = value;
break;
case SettingsKeys.NeverShowEnterImmersiveModeMessage:
SettingsService.NeverShowEnterImmersiveModeMessage = value;
break;
default:
break;
}
}
private bool? AlreadyForeverDismissedThisMessage() =>
Notification.RelatedSettingsKeyName switch
{
SettingsKeys.NeverShowEnterFullScreenMessage =>
SettingsService.NeverShowEnterFullScreenMessage,
SettingsKeys.NeverShowEnterImmersiveModeMessage =>
SettingsService.NeverShowEnterImmersiveModeMessage,
_ => null,
};
}
}

View File

@@ -1,24 +1,35 @@
using ATL;
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 System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
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 {
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private bool _isAnyMusicSessionExisted = false;
@@ -28,8 +39,8 @@ namespace BetterLyrics.WinUI3.ViewModels {
[ObservableProperty]
private string? _artist;
public List<Color> CoverImageDominantColors { get; set; } =
[Colors.Transparent, Colors.Transparent, Colors.Transparent];
[ObservableProperty]
private ObservableCollection<Color> _coverImageDominantColors;
[ObservableProperty]
private BitmapImage? _coverImage;
@@ -46,20 +57,33 @@ namespace BetterLyrics.WinUI3.ViewModels {
[ObservableProperty]
private bool _lyricsExisted = false;
private Helper.ColorThief _colorThief = new();
private readonly ColorThief _colorThief = new();
private readonly SettingsService _settingsService;
private readonly DatabaseService _databaseService;
public MainViewModel(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) {
public List<LyricsLine> GetLyrics(Track? track)
{
List<LyricsLine> result = [];
var lyricsPhrases = track?.Lyrics.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0) {
if (lyricsPhrases[0].TimestampMs > 0) {
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);
@@ -68,40 +92,50 @@ namespace BetterLyrics.WinUI3.ViewModels {
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++) {
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count) {
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
} else {
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine {
StartPlayingTimestampMs = startTimestampMs,
};
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs) {
if (endTimestampMs == startTimestampMs)
{
continue;
} else {
}
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, ICanvasAnimatedControl control) {
public async Task<(List<LyricsLine>, SoftwareBitmap?)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
SoftwareBitmap? coverSoftwareBitmap = null;
uint coverImagePixelWidth = 0;
uint coverImagePixelHeight = 0;
@@ -111,26 +145,37 @@ namespace BetterLyrics.WinUI3.ViewModels {
IRandomAccessStream? stream = null;
var track = _databaseService.GetMusicMetadata(Title, Artist);
var track = _databaseService.GetMusicMetadata(mediaProps);
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference) {
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference)
{
stream = await reference.OpenReadAsync();
} else {
if (track?.EmbeddedPictures.Count > 0) {
}
else
{
if (track?.EmbeddedPictures.Count > 0)
{
var bytes = track.EmbeddedPictures[0].PictureData;
if (bytes != null) {
if (bytes != null)
{
stream = await Helper.ImageHelper.GetStreamFromBytesAsync(bytes);
}
}
}
// Set cover image and dominant colors
if (stream == null) {
if (stream == null)
{
CoverImage = null;
for (int i = 0; i < 3; i++) {
CoverImageDominantColors[i] = Colors.Transparent;
}
} else {
CoverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, _accentColorCount),
];
_settingsService.LyricsFontSelectedAccentColorIndex =
_settingsService.LyricsFontSelectedAccentColorIndex;
}
else
{
CoverImage = new BitmapImage();
await CoverImage.SetSourceAsync(stream);
stream.Seek(0);
@@ -144,19 +189,19 @@ namespace BetterLyrics.WinUI3.ViewModels {
BitmapAlphaMode.Premultiplied
);
var quantizedColors = await _colorThief.GetPalette(decoder, 3);
for (int i = 0; i < 3; i++) {
Helper.QuantizedColor quantizedColor = quantizedColors[i];
CoverImageDominantColors[i] = Color.FromArgb(
quantizedColor.Color.A, quantizedColor.Color.R, quantizedColor.Color.G, quantizedColor.Color.B);
}
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);
return (GetLyrics(track), coverSoftwareBitmap);
}
}
}

View File

@@ -1,54 +1,60 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using System;
using System;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel.Core;
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 : ObservableObject {
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsViewModel(
DatabaseService databaseService,
SettingsService settingsService,
MainViewModel mainViewModel
) : ObservableObject
{
private readonly MediaPlayer _mediaPlayer = new();
private readonly DatabaseService _databaseService;
private readonly DatabaseService _databaseService = databaseService;
[ObservableProperty]
private SettingsService _settingsService;
public SettingsService SettingsService => settingsService;
[ObservableProperty]
private string _version;
public MainViewModel MainViewModel => mainViewModel;
public SettingsViewModel(DatabaseService databaseService, SettingsService settingsService) {
_databaseService = databaseService;
_settingsService = settingsService;
var version = Package.Current.Id.Version;
Version = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
public string Version => Helper.AppInfo.AppVersion;
[RelayCommand]
private async Task RebuildLyricsIndexDatabaseAsync() {
private async Task RebuildLyricsIndexDatabaseAsync()
{
SettingsService.IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildMusicMetadataIndexDatabaseAsync(SettingsService.MusicLibraries);
await _databaseService.RebuildMusicMetadataIndexDatabaseAsync(
SettingsService.MusicLibraries
);
SettingsService.IsRebuildingLyricsIndexDatabase = false;
}
[RelayCommand]
private async Task RemoveFolderAsync(MusicFolder musicFolder) {
SettingsService.MusicLibraries.Remove(musicFolder);
public async Task RemoveFolderAsync(string path)
{
SettingsService.MusicLibraries.Remove(path);
await RebuildLyricsIndexDatabaseAsync();
}
[RelayCommand]
private async Task AddFolderAsync() {
private async Task SelectAndAddFolderAsync()
{
var picker = new FolderPicker();
picker.FileTypeFilter.Add("*");
@@ -58,41 +64,67 @@ namespace BetterLyrics.WinUI3.ViewModels {
var folder = await picker.PickSingleFolderAsync();
if (folder != null) {
string path = folder.Path;
bool existed = SettingsService.MusicLibraries.Count((x) => x.Path == path) > 0;
if (existed) {
MainWindow.StackedNotificationsBehavior?.Show(App.ResourceLoader.GetString("SettingsPagePathExistedInfo"), 3900);
} else {
SettingsService.MusicLibraries.Add(new MusicFolder(path));
await RebuildLyricsIndexDatabaseAsync();
}
App.Current.SettingsWindow!.AppWindow.MoveInZOrderAtTop();
if (folder != null)
{
await AddFolderAsync(folder.Path);
}
}
private async Task AddFolderAsync(string path)
{
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
if (existed)
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
)
)
);
}
else
{
SettingsService.MusicLibraries.Add(path);
await RebuildLyricsIndexDatabaseAsync();
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync() {
await Launcher.LaunchUriAsync(new Uri("https://github.com/jayfunc/BetterLyrics"));
private static async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
}
private static void OpenFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = path,
UseShellExecute = true,
}
);
}
public static void OpenMusicFolder(string path)
{
OpenFolderInFileExplorer(path);
}
[RelayCommand]
private void OpenFolderInFileExplorer(MusicFolder musicFolder) {
Process.Start(new ProcessStartInfo {
FileName = "explorer.exe",
Arguments = musicFolder.Path,
UseShellExecute = true
});
}
[RelayCommand]
private void RestartApp() {
private static void RestartApp()
{
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
Microsoft.Windows.AppLifecycle.AppInstance.Restart("");
// If the restart fails, handle it here.
switch (failureReason) {
switch (failureReason)
{
case AppRestartFailureReason.RestartPending:
break;
case AppRestartFailureReason.NotInForeground:
@@ -104,5 +136,18 @@ namespace BetterLyrics.WinUI3.ViewModels {
}
}
[RelayCommand]
private async Task PlayTestingMusicTask()
{
await AddFolderAsync(Helper.AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(Helper.AppInfo.TestMusicPath));
_mediaPlayer.Play();
}
[RelayCommand]
private static void OpenLogFolder()
{
OpenFolderInFileExplorer(Helper.AppInfo.LogDirectory);
}
}
}

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.Views.BaseWindow"
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"
mc:Ignorable="d">
<Grid x:Name="RootGrid" KeyDown="RootGrid_KeyDown">
<Frame
x:Name="RootFrame"
Navigated="RootFrame_Navigated"
NavigationFailed="RootFrame_NavigationFailed" />
<Grid
x:Name="TopCommandGrid"
Height="{StaticResource TitleBarCompactHeight}"
VerticalAlignment="Top"
Background="Transparent">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<ImageIcon
x:Name="AppLogoImageIcon"
Height="18"
Margin="16,0"
Source="ms-appx:///Assets/Logo.png" />
<TextBlock
x:Name="AppTitleTextBlock"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="SemiBold"
Opacity=".5"
Text="{x:Bind Title, Mode=OneWay}" />
</StackPanel>
<StackPanel
HorizontalAlignment="Right"
Opacity=".5"
Orientation="Horizontal">
<Button Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xE712;" />
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
x:Name="AOTFlyoutItem"
x:Uid="BaseWindowAOTFlyoutItem"
Click="AOTFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="FullScreenFlyoutItem"
x:Uid="BaseWindowFullScreenFlyoutItem"
Click="FullScreenFlyoutItem_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 WindowModel.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 WindowModel.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 WindowModel.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 WindowModel.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 WindowModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind WindowModel.Notification.Message, Mode=OneWay}"
Opacity="0"
Severity="{x:Bind WindowModel.Notification.Severity, Mode=OneWay}">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
<InfoBar.ActionButton>
<CheckBox
x:Name="HostInfoBarCheckBox"
x:Uid="BaseWindowHostInfoBarCheckBox"
Command="{x:Bind WindowModel.SwitchInfoBarNeverShowItAgainCheckBoxCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsChecked, Mode=OneWay}"
Visibility="{x:Bind WindowModel.Notification.Visibility, Mode=OneWay}" />
</InfoBar.ActionButton>
<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>

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