Split and reorganize Service ViewModel Renderer

This commit is contained in:
Zhe Fang
2025-06-10 20:39:29 -04:00
parent d510892650
commit 81651abfec
49 changed files with 2217 additions and 1631 deletions

View File

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

View File

@@ -40,8 +40,10 @@
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:ThemeTypeToElementThemeConverter x:Key="ThemeTypeToElementThemeConverter" />
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BetterLyrics.WinUI3.Models;
using System.Text;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
@@ -13,11 +11,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.AppLifecycle;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Windows.ApplicationModel.Core;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -32,8 +26,8 @@ namespace BetterLyrics.WinUI3
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
public BaseWindow? MainWindow { get; private set; }
public BaseWindow? SettingsWindow { get; set; }
public HostWindow? MainWindow { get; private set; }
public HostWindow? SettingsWindow { get; set; }
public static ResourceLoader? ResourceLoader { get; private set; }
@@ -76,12 +70,21 @@ namespace BetterLyrics.WinUI3
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IDatabaseService, DatabaseService>()
.AddSingleton<IPlaybackService, PlaybackService>()
// Renderer
.AddSingleton<AlbumArtRenderer>()
.AddSingleton<LyricsRenderer>()
// ViewModels
.AddSingleton<BaseWindowModel>()
.AddSingleton<HostViewModel>()
.AddSingleton<AlbumArtViewModel>()
.AddSingleton<MainViewModel>()
.AddSingleton<BaseViewModel>()
.AddSingleton<GlobalViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsViewModel>()
.AddSingleton<AlbumArtOverlayViewModel>()
.BuildServiceProvider()
);
}
@@ -104,7 +107,7 @@ namespace BetterLyrics.WinUI3
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new BaseWindow();
MainWindow = new HostWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
}

View File

@@ -17,16 +17,10 @@
</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" />
@@ -52,6 +46,9 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
@@ -64,5 +61,6 @@
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
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
{
public class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string path)
{
if (path == App.ResourceLoader!.GetString("MainPageNoLocalFilesMatched"))
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,14 +0,0 @@
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

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
@@ -7,11 +9,15 @@ using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
private static readonly ColorThief _colorThief = new();
public const int AccentColorCount = 3;
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
@@ -21,6 +27,26 @@ namespace BetterLyrics.WinUI3.Helper
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(stream);
return bitmapImage;
}
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
return stream;
@@ -33,5 +59,17 @@ namespace BetterLyrics.WinUI3.Helper
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static async Task<List<Color>> GetAccentColorsFromByte(byte[] bytes) =>
[
.. (
await _colorThief.GetPalette(await GetDecoderFromByte(bytes), AccentColorCount)
).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowHelper
{
[DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int GetDpiForWindow(IntPtr hwnd);
public static int GetDpiForWindow(this Window window)
{
IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
return GetDpiForWindow(hWnd);
}
}
}

View File

@@ -0,0 +1,98 @@
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;
namespace BetterLyrics.WinUI3
{
public partial class HostViewModel : ObservableObject
{
private readonly ISettingsService _settingsService;
[ObservableProperty]
private double _appLogoImageIconHeight = 18;
[ObservableProperty]
private double _titleBarFontSize = 11;
[ObservableProperty]
private double _titleBarHeight = 48;
[ObservableProperty]
private Notification _notification = new();
[ObservableProperty]
private bool _showInfoBar = false;
public HostViewModel(ISettingsService 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;
}
}
);
WeakReferenceMessenger.Default.Register<TitleBarTypeChangedMessage>(
this,
(r, m) =>
{
UpdateTitleBarStyle(m.Value);
}
);
}
public void UpdateTitleBarStyle(TitleBarType titleBarType)
{
switch (titleBarType)
{
case TitleBarType.Compact:
TitleBarHeight = (double)App.Current.Resources["TitleBarCompactHeight"];
AppLogoImageIconHeight = 18;
TitleBarFontSize = 11;
break;
case TitleBarType.Extended:
TitleBarHeight = (double)App.Current.Resources["TitleBarExpandedHeight"];
AppLogoImageIconHeight = 20;
TitleBarFontSize = 14;
break;
default:
break;
}
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
if (Notification.RelatedSettingsKeyName is string key)
_settingsService.Set(key, value);
}
private bool? AlreadyForeverDismissedThisMessage()
{
if (Notification.RelatedSettingsKeyName is string key)
return _settingsService.Get(key, SettingsDefaultValues.NeverShowMessage);
return null;
}
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.Views.BaseWindow"
x:Class="BetterLyrics.WinUI3.HostWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
@@ -11,7 +11,10 @@
xmlns:media="using:CommunityToolkit.WinUI.Media"
mc:Ignorable="d">
<Grid x:Name="RootGrid" KeyDown="RootGrid_KeyDown">
<Grid
x:Name="RootGrid"
KeyDown="RootGrid_KeyDown"
RequestedTheme="{x:Bind GlobalSettingsViewModel.Theme, Mode=OneWay}">
<Frame
x:Name="RootFrame"
@@ -20,41 +23,24 @@
<Grid
x:Name="TopCommandGrid"
Height="{StaticResource TitleBarCompactHeight}"
Height="{x:Bind ViewModel.TitleBarHeight, Mode=OneWay}"
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"
Height="{x:Bind ViewModel.AppLogoImageIconHeight, Mode=OneWay}"
Margin="16,0"
Source="ms-appx:///Assets/Logo.png" />
<TextBlock
x:Name="AppTitleTextBlock"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="SemiBold"
Opacity=".5"
Text="{x:Bind Title, Mode=OneWay}" />
@@ -69,7 +55,7 @@
<Button Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xE712;" />
<Button.Flyout>
@@ -93,7 +79,7 @@
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2D;" />
</Button>
<!-- Window Maximise -->
@@ -103,7 +89,7 @@
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2E;" />
</Button>
<!-- Window Restore -->
@@ -114,7 +100,7 @@
Visibility="Collapsed">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2F;" />
</Button>
<!-- Window Close -->
@@ -124,10 +110,9 @@
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{x:Bind WindowModel.TitleBarFontSize, Mode=OneWay}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2C;" />
</Button>
</StackPanel>
</Grid>
@@ -139,10 +124,10 @@
VerticalAlignment="Bottom"
Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}"
IsClosable="False"
IsOpen="{x:Bind WindowModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind WindowModel.Notification.Message, Mode=OneWay}"
IsOpen="{x:Bind ViewModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind ViewModel.Notification.Message, Mode=OneWay}"
Opacity="0"
Severity="{x:Bind WindowModel.Notification.Severity, Mode=OneWay}">
Severity="{x:Bind ViewModel.Notification.Severity, Mode=OneWay}">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
@@ -150,9 +135,9 @@
<CheckBox
x:Name="HostInfoBarCheckBox"
x:Uid="BaseWindowHostInfoBarCheckBox"
Command="{x:Bind WindowModel.SwitchInfoBarNeverShowItAgainCheckBoxCommand}"
Command="{x:Bind ViewModel.SwitchInfoBarNeverShowItAgainCheckBoxCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsChecked, Mode=OneWay}"
Visibility="{x:Bind WindowModel.Notification.Visibility, Mode=OneWay}" />
Visibility="{x:Bind ViewModel.Notification.Visibility, Mode=OneWay}" />
</InfoBar.ActionButton>
<InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard">

View File

@@ -1,14 +1,12 @@
using System;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
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.Extensions.Logging;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
@@ -17,90 +15,58 @@ using Microsoft.UI.Xaml.Navigation;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Views
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 BaseWindow : Window
public sealed partial class HostWindow : Window
{
public BaseWindowModel WindowModel { get; set; }
public HostViewModel ViewModel { get; set; } = Ioc.Default.GetService<HostViewModel>()!;
public GlobalViewModel GlobalSettingsViewModel { get; set; } =
Ioc.Default.GetService<GlobalViewModel>()!;
public BaseWindow()
private readonly ILogger<HostWindow> _logger = Ioc.Default.GetService<
ILogger<HostWindow>
>()!;
public HostWindow()
{
this.InitializeComponent();
AppWindow.Changed += AppWindow_Changed;
WindowModel = Ioc.Default.GetService<BaseWindowModel>()!;
WindowModel.SettingsService.PropertyChanged += SettingsService_PropertyChanged;
SettingsService_PropertyChanged(
null,
new System.ComponentModel.PropertyChangedEventArgs(nameof(SettingsService.Theme))
);
SettingsService_PropertyChanged(
null,
new System.ComponentModel.PropertyChangedEventArgs(
nameof(SettingsService.BackdropType)
)
);
SettingsService_PropertyChanged(
null,
new System.ComponentModel.PropertyChangedEventArgs(
nameof(SettingsService.TitleBarType)
)
);
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SetTitleBar(TopCommandGrid);
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
GlobalSettingsViewModel.BackdropType
);
ViewModel.UpdateTitleBarStyle(GlobalSettingsViewModel.TitleBarType);
WeakReferenceMessenger.Default.Register<SystemBackdropChangedMessage>(
this,
(r, m) =>
{
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(m.Value);
}
);
WeakReferenceMessenger.Default.Register<IsImmersiveModeChangedMessage>(
this,
(r, m) =>
{
TopCommandGrid.Opacity = m.Value ? 0 : 1;
}
);
}
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
UpdateTitleBarWindowButtonsVisibility();
}
private void SettingsService_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
)
{
switch (e.PropertyName)
{
case nameof(SettingsService.Theme):
RootGrid.RequestedTheme = (ElementTheme)WindowModel.SettingsService.Theme;
break;
case nameof(SettingsService.BackdropType):
SystemBackdrop = null;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
(BackdropType)WindowModel.SettingsService.BackdropType
);
break;
case nameof(SettingsService.TitleBarType):
switch ((TitleBarType)WindowModel.SettingsService.TitleBarType)
{
case TitleBarType.Compact:
TopCommandGrid.Height = (double)
App.Current.Resources["TitleBarCompactHeight"];
AppLogoImageIcon.Height = 18;
WindowModel.TitleBarFontSize = 11;
break;
case TitleBarType.Extended:
TopCommandGrid.Height = (double)
App.Current.Resources["TitleBarExpandedHeight"];
AppLogoImageIcon.Height = 20;
WindowModel.TitleBarFontSize = 14;
break;
default:
break;
}
break;
default:
break;
}
if (args.DidPresenterChange)
UpdateTitleBarWindowButtonsVisibility();
}
public void Navigate(Type type)
@@ -130,7 +96,6 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
UpdateTitleBarWindowButtonsVisibility();
}
}
@@ -139,7 +104,6 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Minimize();
UpdateTitleBarWindowButtonsVisibility();
}
}
@@ -148,37 +112,44 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Restore();
UpdateTitleBarWindowButtonsVisibility();
}
}
private void UpdateTitleBarWindowButtonsVisibility()
{
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
switch (AppWindow.Presenter.Kind)
{
MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
break;
case AppWindowPresenterKind.FullScreen:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
break;
case AppWindowPresenterKind.Overlapped:
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (overlappedPresenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
else if (AppWindow.Presenter is FullScreenPresenter)
{
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (overlappedPresenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
break;
default:
break;
}
}
@@ -194,7 +165,6 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop;
UpdateTitleBarWindowButtonsVisibility();
}
}
@@ -224,8 +194,6 @@ namespace BetterLyrics.WinUI3.Views
default:
break;
}
UpdateTitleBarWindowButtonsVisibility();
}
private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace BetterLyrics.WinUI3.Messages
{
internal class AlbumArtCornerRadiusChangedMessage(int value)
: ValueChangedMessage<int>(value) { }
}

View File

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

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace BetterLyrics.WinUI3.Messages
{
public class LyricsFontColorChangedMessage() : ValueChangedMessage<object?>(null) { }
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace BetterLyrics.WinUI3.Messages
{
public class LyricsRelayoutRequestedMessage() : ValueChangedMessage<object?>(null) { }
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace BetterLyrics.WinUI3.Messages
{
public class ReFindSongInfoRequestedMessage() : ValueChangedMessage<object?>(null) { }
}

View File

@@ -0,0 +1,12 @@
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;
namespace BetterLyrics.WinUI3.Messages
{
public class SongInfoChangedMessage(SongInfo? value) : ValueChangedMessage<SongInfo?>(value) { }
}

View File

@@ -0,0 +1,13 @@
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;
namespace BetterLyrics.WinUI3.Messages
{
public class TitleBarTypeChangedMessage(TitleBarType value)
: ValueChangedMessage<TitleBarType>(value) { }
}

View File

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

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using Windows.UI;
using static ATL.LyricsInfo;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject
{
[ObservableProperty]
private string? _title;
[ObservableProperty]
private string? _artist;
[ObservableProperty]
private ObservableCollection<string>? _filesUsed;
[ObservableProperty]
private bool? _isLyricsExisted;
[ObservableProperty]
private string? _sourceAppUserModelId = null;
public List<LyricsLine>? LyricsLines { get; set; } = null;
public byte[]? AlbumArt { get; set; } = null;
public SongInfo() { }
/// <summary>
/// Try to parse lyrics from the track, optionally override the raw lyrics string.
/// </summary>
/// <param name="track"></param>
/// <param name="overrideRaw"></param>
public void ParseLyrics(Track track, string? overrideRaw = null)
{
List<LyricsLine> result = [];
if (overrideRaw != null)
track.Lyrics.ParseLRC(overrideRaw);
var lyricsPhrases = track.Lyrics.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0)
{
if (lyricsPhrases[0].TimestampMs > 0)
{
var placeholder = new LyricsPhrase(0, " ");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result.Add(lyricsLine);
lyricsLine = null;
}
}
LyricsLines = result;
IsLyricsExisted = result.Count != 0;
}
}
}

View File

@@ -1,29 +1,28 @@
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.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI;
using Windows.Graphics.Imaging;
namespace BetterLyrics.WinUI3.Rendering
{
public class CoverBackgroundRenderer
public class AlbumArtRenderer
{
private readonly SettingsService _settingsService;
public float RotateAngle { get; set; } = 0f;
private float _rotateAngle = 0f;
private SoftwareBitmap? _lastSoftwareBitmap = null;
private SoftwareBitmap? _softwareBitmap = null;
public SoftwareBitmap? SoftwareBitmap
private SoftwareBitmap? SoftwareBitmap
{
get => _softwareBitmap;
set
@@ -45,17 +44,38 @@ namespace BetterLyrics.WinUI3.Rendering
private DateTimeOffset _transitionStartTime;
private bool _isTransitioning = false;
public CoverBackgroundRenderer()
private readonly float _coverRotateSpeed = 0.003f;
private readonly AlbumArtOverlayViewModel _viewModel;
public AlbumArtRenderer(AlbumArtOverlayViewModel albumArtRendererSettingsViewModel)
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
_viewModel = albumArtRendererSettingsViewModel;
WeakReferenceMessenger.Default.Register<AlbumArtRenderer, SongInfoChangedMessage>(
this,
async (r, m) =>
{
if (m.Value?.AlbumArt == null) { }
else
{
SoftwareBitmap = await (
await ImageHelper.GetDecoderFromByte(m.Value.AlbumArt)
).GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied
);
}
}
);
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (!_settingsService.IsCoverOverlayEnabled || SoftwareBitmap == null)
if (!_viewModel.IsCoverOverlayEnabled || SoftwareBitmap == null)
return;
ds.Transform = Matrix3x2.CreateRotation(RotateAngle, control.Size.ToVector2() * 0.5f);
ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
var overlappedCovers = new CanvasCommandList(control);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
@@ -72,10 +92,10 @@ namespace BetterLyrics.WinUI3.Rendering
using var coverOverlayEffect = new OpacityEffect
{
Opacity = _settingsService.CoverOverlayOpacity / 100f,
Opacity = _viewModel.CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = _settingsService.CoverOverlayBlurAmount,
BlurAmount = _viewModel.CoverOverlayBlurAmount,
Source = overlappedCovers,
},
};
@@ -91,8 +111,8 @@ namespace BetterLyrics.WinUI3.Rendering
float opacity
)
{
float imageWidth = (float)(softwareBitmap.PixelWidth * 96f / softwareBitmap.DpiX);
float imageHeight = (float)(softwareBitmap.PixelHeight * 96f / softwareBitmap.DpiY);
float imageWidth = (float)(softwareBitmap.PixelWidth * 96f / control.Dpi);
float imageHeight = (float)(softwareBitmap.PixelHeight * 96f / control.Dpi);
var scaleFactor =
(float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
/ Math.Min(imageWidth, imageHeight);
@@ -131,6 +151,12 @@ namespace BetterLyrics.WinUI3.Rendering
_lastSoftwareBitmap = null;
}
}
if (_viewModel.IsDynamicCoverOverlay)
{
_rotateAngle += _coverRotateSpeed;
_rotateAngle %= MathF.PI * 2;
}
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Rendering
{
public interface IRenderer { }
}

View File

@@ -1,24 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Rendering
{
public class PureLyricsRenderer
public class LyricsRenderer
{
private readonly SettingsService _settingsService;
private readonly LyricsViewModel _viewModel;
public ICanvasAnimatedControl? Control { private get; set; } = null;
private Color _fontColor;
private readonly float _defaultOpacity = 0.3f;
private readonly float _highlightedOpacity = 1.0f;
@@ -38,19 +48,127 @@ namespace BetterLyrics.WinUI3.Rendering
private bool _forceToScroll = false;
private float _lyricsGlowEffectAngle = 0f;
private readonly float _lyricsGlowEffectSpeed = 0.01f;
private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
private readonly float _lyricsGlowEffectMaxBlurAmount = 6f;
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; }
private TimeSpan _currentTime = TimeSpan.Zero;
public List<LyricsLine> LyricsLines { get; set; } = [];
private List<LyricsLine> _lyricsLines = [];
public PureLyricsRenderer()
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public LyricsRenderer(LyricsViewModel settingsViewModel)
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
_viewModel = settingsViewModel;
UpdateFontColor();
WeakReferenceMessenger.Default.Register<LyricsRenderer, PlayingPositionChangedMessage>(
this,
(r, m) =>
{
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
_currentTime = m.Value;
}
);
}
);
WeakReferenceMessenger.Default.Register<LyricsRenderer, SongInfoChangedMessage>(
this,
(r, m) =>
{
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
async () =>
{
_lyricsLines = m.Value?.LyricsLines ?? [];
await ForceToScrollToCurrentPlayingLineAsync();
}
);
}
);
WeakReferenceMessenger.Default.Register<LyricsRenderer, ThemeChangedMessage>(
this,
(r, m) =>
{
UpdateFontColor();
}
);
WeakReferenceMessenger.Default.Register<LyricsRenderer, LyricsFontColorChangedMessage>(
this,
(r, m) =>
{
UpdateFontColor();
}
);
WeakReferenceMessenger.Default.Register<LyricsRenderer, LyricsRelayoutRequestedMessage>(
this,
async (r, m) =>
{
await ReLayoutAsync();
}
);
}
public void AddElapsedTime(TimeSpan elapsedTime)
{
_currentTime += elapsedTime;
}
private void UpdateFontColor()
{
switch ((LyricsFontColorType)_viewModel.LyricsFontColorType)
{
case LyricsFontColorType.Default:
_fontColor = (
App.Current.Resources["ControlFillColorDefaultBrush"] as SolidColorBrush
)!.Color;
break;
case LyricsFontColorType.Dominant:
_fontColor = _viewModel.CoverImageDominantColors[
Math.Max(
0,
Math.Min(
_viewModel.CoverImageDominantColors.Count - 1,
_viewModel.LyricsFontSelectedAccentColorIndex
)
)
];
break;
default:
break;
}
}
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < _lyricsLines.Count; i++)
{
var line = _lyricsLines[i];
if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds)
{
continue;
}
return i;
}
return -1;
}
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries()
@@ -61,35 +179,33 @@ namespace BetterLyrics.WinUI3.Rendering
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (LyricsLines.Count == 0)
if (_lyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, LyricsLines.Count - 1);
return new Tuple<int, int>(0, _lyricsLines.Count - 1);
}
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
byte r,
byte g,
byte b
)
private void DrawLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var (displayStartLineIndex, displayEndLineIndex) =
GetVisibleLyricsLineIndexBoundaries();
var r = _fontColor.R;
var g = _fontColor.G;
var b = _fontColor.B;
for (
int i = displayStartLineIndex;
LyricsLines.Count > 0
_lyricsLines.Count > 0
&& i >= 0
&& i < LyricsLines.Count
&& i < _lyricsLines.Count
&& i <= displayEndLineIndex;
i++
)
{
var line = LyricsLines[i];
var line = _lyricsLines[i];
if (line.TextLayout == null)
{
@@ -103,7 +219,7 @@ namespace BetterLyrics.WinUI3.Rendering
float centerX = position.X;
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType)
switch ((LyricsAlignmentType)_viewModel.LyricsAlignmentType)
{
case LyricsAlignmentType.Left:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
@@ -188,23 +304,152 @@ namespace BetterLyrics.WinUI3.Rendering
}
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
// Lyrics only layer
using var lyrics = new CanvasCommandList(control);
using (var lyricsDs = lyrics.CreateDrawingSession())
{
DrawLyrics(control, lyricsDs);
}
using var glowedLyrics = new CanvasCommandList(control);
using (var glowedLyricsDs = glowedLyrics.CreateDrawingSession())
{
if (_viewModel.IsLyricsGlowEffectEnabled)
{
glowedLyricsDs.DrawImage(
new GaussianBlurEffect
{
Source = lyrics,
BlurAmount =
MathF.Sin(_lyricsGlowEffectAngle)
* (
_lyricsGlowEffectMaxBlurAmount
- _lyricsGlowEffectMinBlurAmount
)
/ 2f
+ (_lyricsGlowEffectMaxBlurAmount + _lyricsGlowEffectMinBlurAmount)
/ 2f,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Quality,
}
);
}
glowedLyricsDs.DrawImage(lyrics);
}
// Mock gradient blurred lyrics layer
using var combinedBlurredLyrics = new CanvasCommandList(control);
using var combinedBlurredLyricsDs = combinedBlurredLyrics.CreateDrawingSession();
if (_viewModel.LyricsBlurAmount == 0)
{
combinedBlurredLyricsDs.DrawImage(glowedLyrics);
}
else
{
double step = 0.05;
double overlapFactor = 0;
for (double i = 0; i <= 0.5 - step; i += step)
{
using var blurredLyrics = new GaussianBlurEffect
{
Source = glowedLyrics,
BlurAmount = (float)(_viewModel.LyricsBlurAmount * (1 - i / (0.5 - step))),
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
};
using var topCropped = new CropEffect
{
Source = blurredLyrics,
SourceRectangle = new Rect(
0,
control.Size.Height * i,
control.Size.Width,
control.Size.Height * step * (1 + overlapFactor)
),
};
using var bottomCropped = new CropEffect
{
Source = blurredLyrics,
SourceRectangle = new Rect(
0,
control.Size.Height * (1 - i - step * (1 + overlapFactor)),
control.Size.Width,
control.Size.Height * step * (1 + overlapFactor)
),
};
combinedBlurredLyricsDs.DrawImage(topCropped);
combinedBlurredLyricsDs.DrawImage(bottomCropped);
}
}
// Masked mock gradient blurred lyrics layer
using var maskedCombinedBlurredLyrics = new CanvasCommandList(control);
using (
var maskedCombinedBlurredLyricsDs =
maskedCombinedBlurredLyrics.CreateDrawingSession()
)
{
if (_viewModel.LyricsVerticalEdgeOpacity == 100)
{
maskedCombinedBlurredLyricsDs.DrawImage(combinedBlurredLyrics);
}
else
{
using var mask = new CanvasCommandList(control);
using (var maskDs = mask.CreateDrawingSession())
{
DrawGradientOpacityMask(control, maskDs);
}
maskedCombinedBlurredLyricsDs.DrawImage(
new AlphaMaskEffect { Source = combinedBlurredLyrics, AlphaMask = mask }
);
}
}
// Draw the final composed layer
ds.DrawImage(maskedCombinedBlurredLyrics);
}
private void DrawGradientOpacityMask(
ICanvasAnimatedControl control,
CanvasDrawingSession ds
)
{
byte verticalEdgeAlpha = (byte)(255 * _viewModel.LyricsVerticalEdgeOpacity / 100f);
using var maskBrush = new CanvasLinearGradientBrush(
control,
[
new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
new() { Position = 0.5f, Color = Color.FromArgb(255, 0, 0, 0) },
new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height),
};
ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
}
public async Task ForceToScrollToCurrentPlayingLineAsync()
{
_forceToScroll = true;
await Task.Delay(1);
await Task.Delay(100);
_forceToScroll = false;
}
public async Task ReLayoutAsync(ICanvasAnimatedControl control)
public async Task ReLayoutAsync()
{
if (control == null)
if (Control == null)
return;
float leftMargin = (float)(CanvasWidth - LimitedLineWidth - _rightMargin);
using CanvasTextFormat textFormat = new()
{
FontSize = _settingsService.LyricsFontSize,
FontSize = _viewModel.LyricsFontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontWeight = FontWeights.Bold,
@@ -213,13 +458,13 @@ namespace BetterLyrics.WinUI3.Rendering
float y = (float)CanvasHeight / 2;
// Init Positions
for (int i = 0; i < LyricsLines.Count; i++)
for (int i = 0; i < _lyricsLines.Count; i++)
{
var line = LyricsLines[i];
var line = _lyricsLines[i];
// Calculate layout bounds
line.TextLayout = new CanvasTextLayout(
control.Device,
Control.Device,
line.Text,
textFormat,
(float)LimitedLineWidth,
@@ -230,19 +475,37 @@ namespace BetterLyrics.WinUI3.Rendering
y +=
(float)line.TextLayout.LayoutBounds.Height
/ line.TextLayout.LineCount
* (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
* (line.TextLayout.LineCount + _viewModel.LyricsLineSpacingFactor);
}
await ForceToScrollToCurrentPlayingLineAsync();
}
public void CalculateScaleAndOpacity(int currentPlayingLineIndex)
public async Task CalculateAsync()
{
if (_lyricsLines.LastOrDefault()?.TextLayout == null)
{
await ReLayoutAsync();
}
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
CalculateScaleAndOpacity(currentPlayingLineIndex);
CalculatePosition(currentPlayingLineIndex);
if (_viewModel.IsLyricsDynamicGlowEffectEnabled)
{
_lyricsGlowEffectAngle += _lyricsGlowEffectSpeed;
_lyricsGlowEffectAngle %= MathF.PI * 2;
}
}
private void CalculateScaleAndOpacity(int currentPlayingLineIndex)
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
for (int i = startLineIndex; LyricsLines.Count > 0 && i <= endLineIndex; i++)
for (int i = startLineIndex; _lyricsLines.Count > 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
var line = _lyricsLines[i];
bool linePlaying = i == currentPlayingLineIndex;
@@ -251,7 +514,7 @@ namespace BetterLyrics.WinUI3.Rendering
if (i + 1 <= endLineIndex)
{
lineExitingDurationMs = Math.Min(
LyricsLines[i + 1].DurationMs,
_lyricsLines[i + 1].DurationMs,
lineExitingDurationMs
);
}
@@ -275,11 +538,11 @@ namespace BetterLyrics.WinUI3.Rendering
opacity = _highlightedOpacity;
playProgress =
((float)CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
((float)_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
/ line.DurationMs;
var durationFromStartMs =
CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering)
{
@@ -300,7 +563,7 @@ namespace BetterLyrics.WinUI3.Rendering
playProgress = 1;
var durationToEndMs =
CurrentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
_currentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting)
{
@@ -330,7 +593,7 @@ namespace BetterLyrics.WinUI3.Rendering
}
}
public void CalculatePosition(ICanvasAnimatedControl control, int currentPlayingLineIndex)
private void CalculatePosition(int currentPlayingLineIndex)
{
if (currentPlayingLineIndex < 0)
{
@@ -345,7 +608,7 @@ namespace BetterLyrics.WinUI3.Rendering
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = LyricsLines?[currentPlayingLineIndex];
LyricsLine? currentPlayingLine = _lyricsLines?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
@@ -358,12 +621,12 @@ namespace BetterLyrics.WinUI3.Rendering
}
var lineScrollingProgress =
(CurrentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
(_currentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
/ Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
var targetYScrollOffset = (float)(
-currentPlayingLine.Position.Y
+ LyricsLines![0].Position.Y
+ _lyricsLines![0].Position.Y
- currentPlayingLine.TextLayout.LayoutBounds.Height / 2
- _lastTotalYScroll
);
@@ -392,7 +655,7 @@ namespace BetterLyrics.WinUI3.Rendering
// Update Positions
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
var line = _lyricsLines[i];
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
{
@@ -403,7 +666,7 @@ namespace BetterLyrics.WinUI3.Rendering
}
if (
_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
>= control.Size.Height
>= Control.Size.Height
)
{
if (_endVisibleLineIndex == -1)

View File

@@ -4,18 +4,19 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using SQLite;
using Ude;
using Windows.Media.Control;
using Windows.Storage;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.Database
{
public class DatabaseService
public class DatabaseService : IDatabaseService
{
private readonly SQLiteConnection _connection;
@@ -30,7 +31,7 @@ namespace BetterLyrics.WinUI3.Services.Database
}
}
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<string> paths)
public async Task RebuildDatabaseAsync(IList<string> paths)
{
await Task.Run(() =>
{
@@ -57,13 +58,27 @@ namespace BetterLyrics.WinUI3.Services.Database
});
}
public Track? GetMusicMetadata(
public async Task<SongInfo> FindSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
if (mediaProps == null || mediaProps.Title == null || mediaProps.Artist == null)
return null;
return new();
var songInfo = new SongInfo { Title = mediaProps?.Title, Artist = mediaProps?.Artist };
// App.ResourceLoader!.GetString("MainPageNoLocalFilesMatched");
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
songInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
}
return FindSongInfo(songInfo, mediaProps!.Title, mediaProps!.Artist);
}
public SongInfo FindSongInfo(SongInfo initSongInfo, string searchTitle, string searchArtist)
{
var founds = _connection
.Table<MetadataIndex>()
// Look up by Title and Artist (these two props were fetched by reading metadata in music file befoe) first
@@ -73,66 +88,61 @@ namespace BetterLyrics.WinUI3.Services.Database
(
m.Title != null
&& m.Artist != null
&& m.Title.Contains(mediaProps.Title)
&& m.Artist.Contains(mediaProps.Artist)
&& m.Title.Contains(searchTitle)
&& m.Artist.Contains(searchArtist)
)
|| (
m.Path != null
&& m.Path.Contains(mediaProps.Title)
&& m.Path.Contains(mediaProps.Artist)
&& m.Path.Contains(searchTitle)
&& m.Path.Contains(searchArtist)
)
)
.ToList();
if (founds == null || founds.Count == 0)
foreach (var found in founds)
{
return null;
}
else
{
var first = new Track(founds[0].Path);
if (founds.Count == 1)
if (initSongInfo.LyricsLines == null || initSongInfo.AlbumArt == null)
{
return first;
Track track = new(found.Path);
initSongInfo.ParseLyrics(track);
// Find lyrics
if (initSongInfo.LyricsLines == null && found?.Path?.EndsWith(".lrc") == true)
{
using (FileStream fs = File.OpenRead(found.Path))
{
_charsetDetector.Feed(fs);
_charsetDetector.DataEnd();
}
string content;
if (_charsetDetector.Charset != null)
{
Encoding encoding = Encoding.GetEncoding(_charsetDetector.Charset);
content = File.ReadAllText(found.Path, encoding);
}
else
{
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
initSongInfo.ParseLyrics(track, content);
initSongInfo.FilesUsed ??= [];
initSongInfo.FilesUsed.Add(found.Path);
}
// Finf album art
if (initSongInfo.AlbumArt == null)
{
if (track.EmbeddedPictures.Count > 0)
{
initSongInfo.AlbumArt = track.EmbeddedPictures[0].PictureData;
}
}
}
else
{
if (first.Lyrics.Exists())
{
return first;
}
else
{
foreach (var found in founds)
{
if (found.Path.EndsWith(".lrc"))
{
using (FileStream fs = File.OpenRead(found.Path))
{
_charsetDetector.Feed(fs);
_charsetDetector.DataEnd();
}
string content;
if (_charsetDetector.Charset != null)
{
Encoding encoding = Encoding.GetEncoding(
_charsetDetector.Charset
);
content = File.ReadAllText(found.Path, encoding);
}
else
{
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
first.Lyrics.ParseLRC(content);
return first;
}
}
return first;
}
}
break;
}
return initSongInfo;
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Database
{
public interface IDatabaseService
{
Task RebuildDatabaseAsync(IList<string> paths);
Task<SongInfo> FindSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
);
SongInfo FindSongInfo(SongInfo initSongInfo, string searchTitle, string searchArtist);
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services.Playback
{
public interface IPlaybackService { }
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
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.Rendering;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using DevWinUI;
using Microsoft.UI.Dispatching;
using Windows.Media.Control;
using Windows.System;
namespace BetterLyrics.WinUI3.Services.Playback
{
public partial class PlaybackService : IPlaybackService
{
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
private readonly IDatabaseService _databaseService;
public PlaybackService(IDatabaseService databaseService)
{
_databaseService = databaseService;
InitMediaManager();
}
private async void InitMediaManager()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(
GlobalSystemMediaTransportControlsSession? sender,
PlaybackInfoChangedEventArgs? args
)
{
if (sender == null)
{
WeakReferenceMessenger.Default.Send(new PlayingStatusChangedMessage(false));
return;
}
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
WeakReferenceMessenger.Default.Send(new PlayingStatusChangedMessage(false));
return;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
WeakReferenceMessenger.Default.Send(new PlayingStatusChangedMessage(true));
return;
default:
return;
}
}
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
{
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -=
CurrentSession_TimelinePropertiesChanged;
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
App.DispatcherQueueTimer!.Debounce(
async () =>
{
// _logger.LogDebug("CurrentSession_MediaPropertiesChanged");
if (sender == null)
WeakReferenceMessenger.Default.Send(new SongInfoChangedMessage(null));
else
{
try
{
var songInfo = await _databaseService.FindSongInfoAsync(
await sender.TryGetMediaPropertiesAsync()
);
songInfo.SourceAppUserModelId = sender.SourceAppUserModelId;
WeakReferenceMessenger.Default.Send(
new SongInfoChangedMessage(songInfo)
);
}
catch (Exception) { }
}
},
TimeSpan.FromMilliseconds(AnimationHelper.DebounceDefaultDuration)
);
}
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
{
if (sender == null)
{
WeakReferenceMessenger.Default.Send(
new PlayingPositionChangedMessage(TimeSpan.Zero)
);
}
else
WeakReferenceMessenger.Default.Send(
new PlayingPositionChangedMessage(sender.GetTimelineProperties().Position)
);
// _logger.LogDebug(_currentTime);
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings
{
public interface ISettingsService
{
T? Get<T>(string key, T? defaultValue = default);
void Set<T>(string key, T value);
}
}

View File

@@ -45,7 +45,6 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const int LyricsFontSelectedAccentColorIndex = 0;
// Notification
public const bool NeverShowEnterFullScreenMessage = false;
public const bool NeverShowEnterImmersiveModeMessage = false;
public const bool NeverShowMessage = false;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -15,252 +16,18 @@ using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.Settings
{
public partial class SettingsService : ObservableObject
public partial class SettingsService : ISettingsService
{
private readonly ApplicationDataContainer _localSettings;
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 = false;
[ObservableProperty]
private bool _isImmersiveMode = false;
// Theme
public int Theme
{
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set
{
Set(SettingsKeys.ThemeType, value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
}
}
// Music
private ObservableCollection<string> _musicLibraries;
public ObservableCollection<string> MusicLibraries
{
get { return _musicLibraries; }
set
{
if (_musicLibraries != null)
{
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
_musicLibraries = value;
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
SaveMusicLibraries();
OnPropertyChanged();
}
}
private void SaveMusicLibraries()
{
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language
{
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set
{
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language)
{
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Models.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Models.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Models.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
}
}
// Backdrop
public int BackdropType
{
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set
{
Set(SettingsKeys.BackdropType, value);
WeakReferenceMessenger.Default.Send(
new SystemBackdropChangedMessage((BackdropType)value)
);
}
}
public bool IsCoverOverlayEnabled
{
get =>
Get(
SettingsKeys.IsCoverOverlayEnabled,
SettingsDefaultValues.IsCoverOverlayEnabled
);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay
{
get =>
Get(
SettingsKeys.IsDynamicCoverOverlay,
SettingsDefaultValues.IsDynamicCoverOverlay
);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity
{
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount
{
get =>
Get(
SettingsKeys.CoverOverlayBlurAmount,
SettingsDefaultValues.CoverOverlayBlurAmount
);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
// 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
{
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount
{
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity
{
get =>
Get(
SettingsKeys.LyricsVerticalEdgeOpacity,
SettingsDefaultValues.LyricsVerticalEdgeOpacity
);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor
{
get =>
Get(
SettingsKeys.LyricsLineSpacingFactor,
SettingsDefaultValues.LyricsLineSpacingFactor
);
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
}
public int LyricsFontSize
{
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set => Set(SettingsKeys.LyricsFontSize, value);
}
public bool IsLyricsGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsGlowEffectEnabled,
SettingsDefaultValues.IsLyricsGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsDynamicGlowEffectEnabled,
SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
public int LyricsFontColorType
{
get => Get(SettingsKeys.LyricsFontColorType, SettingsDefaultValues.LyricsFontColorType);
set => Set(SettingsKeys.LyricsFontColorType, value);
}
public int LyricsFontSelectedAccentColorIndex
{
get =>
Get(
SettingsKeys.LyricsFontSelectedAccentColorIndex,
SettingsDefaultValues.LyricsFontSelectedAccentColorIndex
);
set
{
if (value >= 0)
Set(SettingsKeys.LyricsFontSelectedAccentColorIndex, value);
}
}
//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)
public T? Get<T>(string key, T? defaultValue = default)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
@@ -270,10 +37,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
return defaultValue;
}
private void Set<T>(string key, T value, [CallerMemberName] string? propertyName = null)
public void Set<T>(string key, T value)
{
_localSettings.Values[key] = value;
OnPropertyChanged(propertyName);
}
}
}

View File

@@ -273,8 +273,8 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Lyrics only</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
@@ -372,4 +372,13 @@
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
<data name="MainPageNoLocalFilesMatched" xml:space="preserve">
<value>No local files matched</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Album art only</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Split view</value>
</data>
</root>

View File

@@ -273,8 +273,8 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字体大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅示歌词</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>仅示歌词</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
@@ -370,6 +370,15 @@
<value>再次悬停以显示切换按钮</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value> 不再显示此消息</value>
<value>不再显示此消息</value>
</data>
<data name="MainPageNoLocalFilesMatched" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>仅显示专辑封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分屏视图</value>
</data>
</root>

View File

@@ -273,8 +273,8 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字體大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅示歌詞</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>僅示歌詞</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
@@ -372,4 +372,13 @@
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再顯示此訊息</value>
</data>
<data name="MainPageNoLocalFilesMatched" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>僅顯示專輯封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分割畫面視圖</value>
</data>
</root>

View File

@@ -0,0 +1,43 @@
using BetterLyrics.WinUI3.Services.Settings;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class AlbumArtOverlayViewModel : BaseViewModel
{
public AlbumArtOverlayViewModel(ISettingsService settingsService)
: base(settingsService) { }
public bool IsCoverOverlayEnabled
{
get =>
Get(
SettingsKeys.IsCoverOverlayEnabled,
SettingsDefaultValues.IsCoverOverlayEnabled
);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay
{
get =>
Get(
SettingsKeys.IsDynamicCoverOverlay,
SettingsDefaultValues.IsDynamicCoverOverlay
);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity
{
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount
{
get =>
Get(
SettingsKeys.CoverOverlayBlurAmount,
SettingsDefaultValues.CoverOverlayBlurAmount
);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
namespace BetterLyrics.WinUI3.ViewModels
{
public class AlbumArtViewModel : BaseViewModel
{
public int CoverImageRadius
{
get => Get(SettingsKeys.CoverImageRadius, SettingsDefaultValues.CoverImageRadius);
set
{
Set(SettingsKeys.CoverImageRadius, value);
WeakReferenceMessenger.Default.Send(new AlbumArtCornerRadiusChangedMessage(value));
}
}
public AlbumArtViewModel(ISettingsService settingsService)
: base(settingsService) { }
}
}

View File

@@ -0,0 +1,27 @@
using System.Runtime.CompilerServices;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.ViewModels
{
public class BaseViewModel : ObservableObject
{
private readonly ISettingsService _settingsService;
public BaseViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
}
protected void Set(string key, object value, [CallerMemberName] string? propertyName = null)
{
_settingsService.Set(key, value);
OnPropertyChanged(propertyName);
}
protected T? Get<T>(string key, T? defaultValue = default)
{
return _settingsService.Get(key, defaultValue);
}
}
}

View File

@@ -1,82 +0,0 @@
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

@@ -0,0 +1,51 @@
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.ViewModels
{
public class GlobalViewModel : BaseViewModel
{
public bool IsFirstRun
{
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
set => Set(SettingsKeys.IsFirstRun, value);
}
public ElementTheme Theme
{
get => (ElementTheme)Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set
{
Set(SettingsKeys.ThemeType, (int)value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(value));
}
}
public BackdropType BackdropType
{
get => (BackdropType)Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set
{
Set(SettingsKeys.BackdropType, (int)value);
WeakReferenceMessenger.Default.Send(new SystemBackdropChangedMessage(value));
}
}
public TitleBarType TitleBarType
{
get => (TitleBarType)Get(SettingsKeys.TitleBarType, SettingsDefaultValues.TitleBarType);
set
{
Set(SettingsKeys.TitleBarType, (int)value);
WeakReferenceMessenger.Default.Send(new TitleBarTypeChangedMessage(value));
}
}
public GlobalViewModel(ISettingsService settingsService)
: base(settingsService) { }
}
}

View File

@@ -0,0 +1,129 @@
using System.Collections.ObjectModel;
using System.Linq;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsViewModel : BaseViewModel
{
[ObservableProperty]
private ObservableCollection<Color> _coverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, ImageHelper.AccentColorCount),
];
public int LyricsAlignmentType
{
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount
{
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity
{
get =>
Get(
SettingsKeys.LyricsVerticalEdgeOpacity,
SettingsDefaultValues.LyricsVerticalEdgeOpacity
);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor
{
get =>
Get(
SettingsKeys.LyricsLineSpacingFactor,
SettingsDefaultValues.LyricsLineSpacingFactor
);
set
{
Set(SettingsKeys.LyricsLineSpacingFactor, value);
WeakReferenceMessenger.Default.Send(new LyricsRelayoutRequestedMessage());
}
}
public int LyricsFontSize
{
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set
{
Set(SettingsKeys.LyricsFontSize, value);
WeakReferenceMessenger.Default.Send(new LyricsRelayoutRequestedMessage());
}
}
public bool IsLyricsGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsGlowEffectEnabled,
SettingsDefaultValues.IsLyricsGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsDynamicGlowEffectEnabled,
SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
public int LyricsFontColorType
{
get => Get(SettingsKeys.LyricsFontColorType, SettingsDefaultValues.LyricsFontColorType);
set
{
Set(SettingsKeys.LyricsFontColorType, value);
WeakReferenceMessenger.Default.Send(new LyricsFontColorChangedMessage());
}
}
public int LyricsFontSelectedAccentColorIndex
{
get =>
Get(
SettingsKeys.LyricsFontSelectedAccentColorIndex,
SettingsDefaultValues.LyricsFontSelectedAccentColorIndex
);
set
{
if (value >= 0)
{
Set(SettingsKeys.LyricsFontSelectedAccentColorIndex, value);
WeakReferenceMessenger.Default.Send(new LyricsFontColorChangedMessage());
}
}
}
public LyricsViewModel(ISettingsService settingsService)
: base(settingsService)
{
WeakReferenceMessenger.Default.Register<LyricsViewModel, SongInfoChangedMessage>(
this,
async (r, m) =>
{
if (m.Value?.AlbumArt == null)
CoverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, ImageHelper.AccentColorCount),
];
else
{
CoverImageDominantColors =
[
.. await ImageHelper.GetAccentColorsFromByte(m.Value.AlbumArt),
];
}
}
);
}
}
}

View File

@@ -1,207 +1,214 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common;
using System.IO;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using DevWinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using Ude;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using Windows.UI;
using static System.Net.Mime.MediaTypeNames;
using static ATL.LyricsInfo;
using static CommunityToolkit.WinUI.Animations.Expressions.ExpressionValues;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private bool _isAnyMusicSessionExisted = false;
[ObservableProperty]
private string? _title;
[ObservableProperty]
private string? _artist;
[ObservableProperty]
private ObservableCollection<Color> _coverImageDominantColors;
private ObservableCollection<bool> _isDisplayTypeEnabled =
[
.. Enumerable.Repeat(false, Enum.GetValues<DisplayType>().Length),
];
[ObservableProperty]
private BitmapImage? _coverImage;
[ObservableProperty]
private SongInfo? _songInfo = null;
[ObservableProperty]
private int _displayType = (int)Models.DisplayType.PlaceholderOnly;
private int? _preferredDisplayType = 2;
[ObservableProperty]
private bool _aboutToUpdateUI;
[ObservableProperty]
private bool _isSmallScreenMode;
private bool _isPlaying = false;
[ObservableProperty]
private bool _showLyricsOnly = false;
private ObservableCollection<string> _matchedLocalFilePath = [];
[ObservableProperty]
private bool _lyricsExisted = false;
private readonly ColorThief _colorThief = new();
private readonly SettingsService _settingsService;
private readonly DatabaseService _databaseService;
private readonly int _accentColorCount = 3;
public MainViewModel(SettingsService settingsService, DatabaseService databaseService)
private bool _isImmersiveMode = false;
public bool IsImmersiveMode
{
get => _isImmersiveMode;
set
{
_isImmersiveMode = value;
OnPropertyChanged();
WeakReferenceMessenger.Default.Send(new IsImmersiveModeChangedMessage(value));
if (value)
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
isForeverDismissable: true,
relatedSettingsKeyName: SettingsKeys.NeverShowEnterImmersiveModeMessage
)
)
);
}
}
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly IDatabaseService _databaseService;
public MainViewModel(IPlaybackService playbackService, IDatabaseService databaseService)
{
_settingsService = settingsService;
_databaseService = databaseService;
CoverImageDominantColors =
WeakReferenceMessenger.Default.Register<MainViewModel, PlayingStatusChangedMessage>(
this,
(r, m) =>
{
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
IsPlaying = m.Value;
}
);
}
);
WeakReferenceMessenger.Default.Register<MainViewModel, SongInfoChangedMessage>(
this,
(r, m) =>
{
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
async () =>
{
await UpdateSongInfoUI(m.Value);
}
);
}
);
WeakReferenceMessenger.Default.Register<MainViewModel, ReFindSongInfoRequestedMessage>(
this,
async (r, m) =>
{
if (SongInfo == null || SongInfo.Title == null || SongInfo.Artist == null)
return;
await UpdateSongInfoUI(
_databaseService.FindSongInfo(SongInfo, SongInfo.Title, SongInfo.Artist)
);
}
);
}
private async Task UpdateSongInfoUI(SongInfo? songInfo)
{
AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
SongInfo = songInfo;
await Task.Delay(1);
CoverImage =
(songInfo?.AlbumArt == null)
? null
: await ImageHelper.GetBitmapImageFromBytesAsync(songInfo.AlbumArt);
IsDisplayTypeEnabled =
[
.. Enumerable.Repeat(Colors.Transparent, _accentColorCount),
.. Enumerable.Repeat(false, Enum.GetValues<DisplayType>().Length),
];
}
public List<LyricsLine> GetLyrics(Track? track)
{
List<LyricsLine> result = [];
var lyricsPhrases = track?.Lyrics?.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0)
if (songInfo == null)
{
if (lyricsPhrases[0].TimestampMs > 0)
{
var placeholder = new LyricsPhrase(0, " ");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result.Add(lyricsLine);
lyricsLine = null;
}
}
LyricsExisted = result.Count != 0;
if (!LyricsExisted)
{
ShowLyricsOnly = false;
}
return result;
}
public async Task<(List<LyricsLine>, SoftwareBitmap?)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
SoftwareBitmap? coverSoftwareBitmap = null;
uint coverImagePixelWidth = 0;
uint coverImagePixelHeight = 0;
Title = mediaProps?.Title;
Artist = mediaProps?.Artist;
IRandomAccessStream? stream = null;
var track = _databaseService.GetMusicMetadata(mediaProps);
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference)
{
stream = await reference.OpenReadAsync();
IsDisplayTypeEnabled[(int)Models.DisplayType.PlaceholderOnly] = true;
DisplayType = (int)Models.DisplayType.PlaceholderOnly;
}
else
{
if (track?.EmbeddedPictures.Count > 0)
if (songInfo.LyricsLines?.Count > 0)
{
var bytes = track.EmbeddedPictures[0].PictureData;
if (bytes != null)
IsDisplayTypeEnabled[(int)Models.DisplayType.LyricsOnly] = true;
}
if (songInfo.AlbumArt != null)
{
IsDisplayTypeEnabled[(int)Models.DisplayType.AlbumArtOnly] = true;
}
IsDisplayTypeEnabled[(int)Models.DisplayType.SplitView] =
IsDisplayTypeEnabled[(int)Models.DisplayType.LyricsOnly]
&& IsDisplayTypeEnabled[(int)Models.DisplayType.AlbumArtOnly];
// Set checked
if (
IsDisplayTypeEnabled[(int)Models.DisplayType.SplitView]
&& _preferredDisplayType == (int)Models.DisplayType.SplitView
)
{
DisplayType = (int)Models.DisplayType.SplitView;
}
else
{
if (
IsDisplayTypeEnabled[(int)Models.DisplayType.LyricsOnly]
&& _preferredDisplayType == (int)Models.DisplayType.LyricsOnly
)
{
stream = await Helper.ImageHelper.GetStreamFromBytesAsync(bytes);
DisplayType = (int)Models.DisplayType.LyricsOnly;
}
else if (
IsDisplayTypeEnabled[(int)Models.DisplayType.AlbumArtOnly]
&& _preferredDisplayType == (int)Models.DisplayType.AlbumArtOnly
)
{
DisplayType = (int)Models.DisplayType.AlbumArtOnly;
}
}
}
// Set cover image and dominant colors
if (stream == null)
{
CoverImage = null;
CoverImageDominantColors =
[
.. Enumerable.Repeat(Colors.Transparent, _accentColorCount),
];
_settingsService.LyricsFontSelectedAccentColorIndex =
_settingsService.LyricsFontSelectedAccentColorIndex;
}
else
{
CoverImage = new BitmapImage();
await CoverImage.SetSourceAsync(stream);
stream.Seek(0);
AboutToUpdateUI = false;
}
var decoder = await BitmapDecoder.CreateAsync(stream);
coverImagePixelHeight = decoder.PixelHeight;
coverImagePixelWidth = decoder.PixelWidth;
public void OpenMatchedFileFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
);
}
coverSoftwareBitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied
);
CoverImageDominantColors =
[
.. (await _colorThief.GetPalette(decoder, _accentColorCount)).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
_settingsService.LyricsFontSelectedAccentColorIndex =
_settingsService.LyricsFontSelectedAccentColorIndex;
stream.Dispose();
}
return (GetLyrics(track), coverSoftwareBitmap);
[RelayCommand]
private void OnDisplayTypeChanged(object value)
{
int index = Convert.ToInt32(value);
_preferredDisplayType = index;
DisplayType = index;
}
}
}

View File

@@ -1,61 +1,127 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Xaml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Windows.ApplicationModel.Core;
using Windows.Media;
using Windows.Globalization;
using Windows.Media.Playback;
using Windows.Storage.Pickers;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsViewModel(
DatabaseService databaseService,
SettingsService settingsService,
MainViewModel mainViewModel
) : ObservableObject
public partial class SettingsViewModel : BaseViewModel
{
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase = false;
// Music
private ObservableCollection<string> _musicLibraries;
public ObservableCollection<string> MusicLibraries
{
get { return _musicLibraries; }
set
{
if (_musicLibraries != null)
{
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
_musicLibraries = value;
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
SaveMusicLibraries();
OnPropertyChanged();
}
}
private void SaveMusicLibraries()
{
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language
{
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set
{
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language)
{
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Models.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Models.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Models.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
}
}
private readonly MediaPlayer _mediaPlayer = new();
private readonly DatabaseService _databaseService = databaseService;
private readonly IDatabaseService _databaseService;
public SettingsService SettingsService => settingsService;
public string Version => AppInfo.AppVersion;
public MainViewModel MainViewModel => mainViewModel;
public SettingsViewModel(IDatabaseService databaseService, ISettingsService settingsService)
: base(settingsService)
{
_databaseService = databaseService;
public string Version => Helper.AppInfo.AppVersion;
_musicLibraries =
[
.. JsonConvert.DeserializeObject<List<string>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)!
)!,
];
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
[RelayCommand]
private async Task RebuildLyricsIndexDatabaseAsync()
{
SettingsService.IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildMusicMetadataIndexDatabaseAsync(
SettingsService.MusicLibraries
);
SettingsService.IsRebuildingLyricsIndexDatabase = false;
IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildDatabaseAsync(MusicLibraries);
IsRebuildingLyricsIndexDatabase = false;
WeakReferenceMessenger.Default.Send(new ReFindSongInfoRequestedMessage());
}
public async Task RemoveFolderAsync(string path)
{
SettingsService.MusicLibraries.Remove(path);
MusicLibraries.Remove(path);
await RebuildLyricsIndexDatabaseAsync();
}
[RelayCommand]
private async Task SelectAndAddFolderAsync()
{
var picker = new FolderPicker();
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
@@ -74,7 +140,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private async Task AddFolderAsync(string path)
{
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
bool existed = MusicLibraries.Any((x) => x == path);
if (existed)
{
WeakReferenceMessenger.Default.Send(
@@ -87,7 +153,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
SettingsService.MusicLibraries.Add(path);
MusicLibraries.Add(path);
await RebuildLyricsIndexDatabaseAsync();
}
}

View File

@@ -6,6 +6,7 @@
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
@@ -14,318 +15,231 @@
xmlns:ui="using:CommunityToolkit.WinUI"
NavigationCacheMode="Required"
mc:Ignorable="d">
<Page.Resources>
<converters:DoubleToVisibilityConverter
x:Key="LyricsOnlyToVisibilityConverter"
GreaterThan="0.9"
LessThan="1.1" />
</Page.Resources>
<Grid x:Name="RootGrid">
<Grid.Resources>
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
</Grid.Resources>
<Grid
x:Name="TopPlaceholder"
Height="36"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded"
Paused="{x:Bind ViewModel.IsPlaying, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
SizeChanged="LyricsCanvas_SizeChanged"
Update="LyricsCanvas_Update" />
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid x:Name="MainGrid">
</Grid>
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<!-- Song info area -->
<Grid x:Name="SongInfoGrid" Margin="36">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded"
SizeChanged="LyricsCanvas_SizeChanged"
Update="LyricsCanvas_Update">
<canvas:CanvasAnimatedControl.OpacityTransition>
<ScalarTransition />
</canvas:CanvasAnimatedControl.OpacityTransition>
</canvas:CanvasAnimatedControl>
<Grid x:Name="LyricsPlaceholderGrid" SizeChanged="LyricsPlaceholderGrid_SizeChanged" />
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoGrid" Margin="36">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SongInfoColumnDefinition" Width="*" />
<ColumnDefinition x:Name="SpacerColumnDefinition" Width="36" />
<ColumnDefinition x:Name="LyricsAreaColumnDefinition" Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="SongInfoInnerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<!-- Cover area -->
<RowDefinition Height="20*" />
<!-- Spacer -->
<RowDefinition Height="2*" />
<!-- Title and artist area -->
<RowDefinition Height="5*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid.Resources>
<Storyboard x:Key="HideSongInfoGrid">
<!-- Animation for song info -->
<Storyboard x:Name="SongInfoStackPanelFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="ShowSongInfoGrid">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="SongInfoStackPanelFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<!-- Cover area -->
<Grid
x:Name="LyricsPlaceholderGrid"
Grid.Column="2"
SizeChanged="LyricsPlaceholderGrid_SizeChanged" />
<Grid
x:Name="SongInfoInnerGrid"
Grid.Column="0"
Grid.ColumnSpan="3">
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<!-- Cover area -->
<RowDefinition Height="20*" />
<!-- Spacer -->
<RowDefinition Height="2*" />
<!-- Title and artist area -->
<RowDefinition Height="5*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid.Resources>
<!-- Animation for song info -->
<Storyboard x:Name="SongInfoStackPanelFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="SongInfoStackPanelFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="SongInfoInnerGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<!-- Cover area -->
<Grid
x:Name="CoverArea"
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid x:Name="CoverImageGrid" CornerRadius="24">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
x:Name="CoverArea"
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid x:Name="CoverImageGrid" Loaded="CoverImageGrid_Loaded">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
<!-- Title and artist -->
<StackPanel Grid.Row="3" Orientation="Vertical">
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
</Grid>
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.Resources>
<Storyboard x:Key="ShowMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="HideMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</TextBlock.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</TextBlock>
<!-- Title and artist -->
<StackPanel Grid.Row="3" Orientation="Vertical">
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
</Grid>
</Grid>
<!-- No music playing placeholder -->
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.OpacityTransition>
<ScalarTransition />
</TextBlock.OpacityTransition>
</TextBlock>
<!-- Bottom-right command area -->
<Grid
x:Name="BottomCommandGrid"
Margin="0,0,4,4"
@@ -342,13 +256,13 @@
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
Binding="{x:Bind ViewModel.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0.5" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
Binding="{x:Bind ViewModel.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
@@ -358,29 +272,107 @@
<StackPanel HorizontalAlignment="Right" Spacing="4">
<Button
x:Name="LyricsOnlyButton"
x:Uid="MainWindowLyricsOnly"
Content="{ui:FontIcon Glyph=&#xE97C;}"
Style="{StaticResource GhostButtonStyle}">
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay}">
<RadioButton
x:Uid="MainPageAlbumArtOnly"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
IsEnabled="{x:Bind ViewModel.IsDisplayTypeEnabled[0], Mode=OneWay}"
Tag="0" />
<RadioButton
x:Uid="MainPageLyriscOnly"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
IsEnabled="{x:Bind ViewModel.IsDisplayTypeEnabled[1], Mode=OneWay}"
Tag="1" />
<RadioButton
x:Uid="MainPageSplitView"
Command="{x:Bind ViewModel.DisplayTypeChangedCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Tag}"
IsEnabled="{x:Bind ViewModel.IsDisplayTypeEnabled[2], Mode=OneWay}"
Tag="2" />
</RadioButtons>
</Flyout>
</Button.Flyout>
</Button>
<ToggleButton
x:Name="ImmersiveModeButton"
x:Uid="MainWindowImmersiveMode"
IsChecked="{x:Bind SettingsService.IsImmersiveMode, Mode=TwoWay}"
Style="{StaticResource GhostToggleButtonStyle}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF131;" />
</ToggleButton>
Content="{ui:FontIcon Glyph=&#xF131;}"
IsChecked="{x:Bind ViewModel.IsImmersiveMode, Mode=TwoWay}"
Style="{StaticResource GhostToggleButtonStyle}" />
<ToggleButton
x:Name="LyricsOnlyButton"
x:Uid="MainWindowLyricsOnly"
IsChecked="{x:Bind ViewModel.ShowLyricsOnly, Mode=TwoWay}"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ViewModel.LyricsExisted, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE66C;" />
</ToggleButton>
<Button Content="{ui:FontIcon Glyph=&#xF167;}" Style="{StaticResource GhostButtonStyle}">
<Button.Flyout>
<Flyout>
<StackPanel Spacing="16">
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xED35;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.SourceAppUserModelId, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEC4F;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE77B;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF0E3;" />
<ListView ItemsSource="{x:Bind ViewModel.MatchedLocalFilePath, Mode=OneWay}" SelectionMode="None">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock VerticalAlignment="Center" Text="{Binding Mode=OneWay}" />
<Button
Click="OpenMatchedFileButton_Click"
Content="{ui:FontIcon Glyph=&#xE838;}"
Style="{StaticResource GhostButtonStyle}"
Tag="{Binding Mode=OneWay}"
Visibility="{Binding Converter={StaticResource MatchedLocalFilesPathToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button
x:Name="SettingsButton"
Click="SettingsButton_Click"
Style="{StaticResource GhostButtonStyle}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF8B0;" />
</Button>
Content="{ui:FontIcon Glyph=&#xF8B0;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
@@ -395,25 +387,75 @@
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<!-- Narrow -->
<VisualState x:Name="NarrowState">
<!-- Album art only -->
<VisualState x:Name="AlbumArtOnly">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="0" />
<Setter Target="SongInfoGrid.Opacity" Value="1" />
<Setter Target="LyricsGrid.Opacity" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.Column)" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Wide -->
<VisualState x:Name="WideState">
<!-- Lyrics only -->
<VisualState x:Name="LyricsOnly">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="*" />
<Setter Target="SongInfoGrid.Opacity" Value="0" />
<Setter Target="LyricsGrid.Opacity" Value="1" />
<Setter Target="LyricsPlaceholderGrid.(Grid.Column)" Value="0" />
<Setter Target="LyricsPlaceholderGrid.(Grid.ColumnSpan)" Value="3" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Split view -->
<VisualState x:Name="SplitView">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoGrid.Opacity" Value="1" />
<Setter Target="SongInfoInnerGrid.(Grid.Column)" Value="0" />
<Setter Target="SongInfoInnerGrid.(Grid.ColumnSpan)" Value="1" />
<Setter Target="LyricsGrid.Opacity" Value="1" />
<Setter Target="LyricsPlaceholderGrid.(Grid.Column)" Value="2" />
<Setter Target="LyricsPlaceholderGrid.(Grid.ColumnSpan)" Value="1" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Placeholder only -->
<VisualState x:Name="PlaceholderOnly">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="3" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoGrid.Opacity" Value="0" />
<Setter Target="LyricsGrid.Opacity" Value="0" />
<Setter Target="MainPageNoMusicPlayingTextBlock.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>

View File

@@ -1,30 +1,14 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.Media.Control;
using Color = Windows.UI.Color;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -37,305 +21,49 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel => (MainViewModel)DataContext;
private SettingsService SettingsService { get; set; }
private readonly CoverBackgroundRenderer _coverImageAsBackgroundRenderer = new();
private readonly PureLyricsRenderer _pureLyricsRenderer = new();
private GlobalViewModel GlobalSettingsViewModel { get; set; } =
Ioc.Default.GetService<GlobalViewModel>()!;
private readonly float _coverRotateSpeed = 0.003f;
private readonly LyricsRenderer _lyricsRenderer = Ioc.Default.GetService<LyricsRenderer>()!;
private float _lyricsGlowEffectAngle = 0f;
private readonly float _lyricsGlowEffectSpeed = 0.01f;
private readonly AlbumArtRenderer _albumArtRenderer =
Ioc.Default.GetService<AlbumArtRenderer>()!;
private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
private readonly float _lyricsGlowEffectMaxBlurAmount = 6f;
private readonly ILogger<MainPage> _logger = Ioc.Default.GetService<ILogger<MainPage>>()!;
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
private Color _lyricsColor;
private readonly ILogger<MainPage> _logger;
public AlbumArtViewModel AlbumArtViewModel { get; set; } =
Ioc.Default.GetService<AlbumArtViewModel>()!;
public MainPage()
{
this.InitializeComponent();
_logger = Ioc.Default.GetService<ILogger<MainPage>>()!;
SettingsService = Ioc.Default.GetService<SettingsService>()!;
DataContext = Ioc.Default.GetService<MainViewModel>();
SettingsService.PropertyChanged += SettingsService_PropertyChanged;
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
// set lyrics font color
SetLyricsColor();
if (SettingsService.IsFirstRun)
if (GlobalSettingsViewModel.IsFirstRun)
{
WelcomeTeachingTip.IsOpen = true;
}
}
private async void SettingsService_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
)
{
switch (e.PropertyName)
{
case nameof(SettingsService.LyricsFontSize):
case nameof(SettingsService.LyricsLineSpacingFactor):
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
break;
case nameof(SettingsService.IsRebuildingLyricsIndexDatabase):
if (!SettingsService.IsRebuildingLyricsIndexDatabase)
{
CurrentSession_MediaPropertiesChanged(_currentSession, null);
}
break;
case nameof(SettingsService.Theme):
case nameof(SettingsService.LyricsFontColorType):
case nameof(SettingsService.LyricsFontSelectedAccentColorIndex):
await Task.Delay(1);
SetLyricsColor();
break;
case nameof(SettingsService.CoverImageRadius):
CoverImageGrid.CornerRadius = new CornerRadius(
SettingsService.CoverImageRadius / 100f * (CoverImageGrid.ActualHeight / 2)
);
break;
case nameof(SettingsService.IsImmersiveMode):
if (SettingsService.IsImmersiveMode)
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
isForeverDismissable: true,
relatedSettingsKeyName: SettingsKeys.NeverShowEnterImmersiveModeMessage
)
)
);
break;
default:
break;
}
}
private async void ViewModel_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
)
{
switch (e.PropertyName)
{
case nameof(ViewModel.ShowLyricsOnly):
if (ViewModel.ShowLyricsOnly)
{
Grid.SetColumn(LyricsPlaceholderGrid, 0);
Grid.SetColumnSpan(LyricsPlaceholderGrid, 3);
}
else
{
Grid.SetColumn(LyricsPlaceholderGrid, 2);
Grid.SetColumnSpan(LyricsPlaceholderGrid, 1);
}
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
break;
default:
break;
}
}
private void SetLyricsColor()
{
switch ((LyricsFontColorType)SettingsService.LyricsFontColorType)
{
case LyricsFontColorType.Default:
_lyricsColor = ((SolidColorBrush)LyricsCanvas.Foreground).Color;
break;
case LyricsFontColorType.Dominant:
_lyricsColor = ViewModel.CoverImageDominantColors[
Math.Max(
0,
Math.Min(
ViewModel.CoverImageDominantColors.Count - 1,
SettingsService.LyricsFontSelectedAccentColorIndex
)
)
];
break;
default:
break;
}
}
private async void InitMediaManager()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
_sessionManager.SessionsChanged += SessionManager_SessionsChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
{
if (sender == null)
{
_pureLyricsRenderer.CurrentTime = TimeSpan.Zero;
return;
}
_pureLyricsRenderer.CurrentTime = sender.GetTimelineProperties().Position;
// _logger.LogDebug(_currentTime);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(
GlobalSystemMediaTransportControlsSession? sender,
PlaybackInfoChangedEventArgs? args
)
{
App.DispatcherQueue!.TryEnqueue(
DispatcherQueuePriority.Normal,
() =>
WeakReferenceMessenger.Default.Register<MainPage, AlbumArtCornerRadiusChangedMessage>(
this,
(r, m) =>
{
if (sender == null)
{
LyricsCanvas.Paused = true;
return;
}
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
LyricsCanvas.Paused = false;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
LyricsCanvas.Paused = true;
break;
default:
break;
}
UpdateAlbumArtCornerRadius(m.Value);
}
);
}
private void SessionManager_SessionsChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
SessionsChangedEventArgs? args
)
private void UpdateAlbumArtCornerRadius(int cornerRadius)
{
// _logger.LogDebug("SessionManager_SessionsChanged");
}
if (CoverImageGrid.ActualHeight == double.NaN)
return;
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
{
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -=
CurrentSession_TimelinePropertiesChanged;
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
}
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
App.DispatcherQueueTimer!.Debounce(
() =>
{
// _logger.LogDebug("CurrentSession_MediaPropertiesChanged");
App.DispatcherQueue!.TryEnqueue(
DispatcherQueuePriority.High,
async () =>
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps =
null;
if (_currentSession != null)
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
ViewModel.AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
(
_pureLyricsRenderer.LyricsLines,
_coverImageAsBackgroundRenderer.SoftwareBitmap
) = await ViewModel.SetSongInfoAsync(mediaProps);
// Force to show lyrics and scroll to current line even if the music is not playing
LyricsCanvas.Paused = false;
await _pureLyricsRenderer.ForceToScrollToCurrentPlayingLineAsync();
await Task.Delay(1);
// Detect and recover the music state
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
ViewModel.AboutToUpdateUI = false;
if (_pureLyricsRenderer.LyricsLines.Count == 0)
{
Grid.SetColumnSpan(SongInfoInnerGrid, 3);
}
else
{
Grid.SetColumnSpan(SongInfoInnerGrid, 1);
}
}
);
},
TimeSpan.FromMilliseconds(AnimationHelper.DebounceDefaultDuration)
CoverImageGrid.CornerRadius = new CornerRadius(
cornerRadius / 100f * CoverImageGrid.ActualHeight / 2
);
}
@@ -347,143 +75,8 @@ namespace BetterLyrics.WinUI3.Views
{
using var ds = args.DrawingSession;
var r = _lyricsColor.R;
var g = _lyricsColor.G;
var b = _lyricsColor.B;
// Draw (dynamic) cover image as the very first layer
_coverImageAsBackgroundRenderer.Draw(sender, ds);
// Lyrics only layer
using var lyrics = new CanvasCommandList(sender);
using (var lyricsDs = lyrics.CreateDrawingSession())
{
_pureLyricsRenderer.Draw(sender, lyricsDs, r, g, b);
}
using var glowedLyrics = new CanvasCommandList(sender);
using (var glowedLyricsDs = glowedLyrics.CreateDrawingSession())
{
if (SettingsService.IsLyricsGlowEffectEnabled)
{
glowedLyricsDs.DrawImage(
new GaussianBlurEffect
{
Source = lyrics,
BlurAmount =
MathF.Sin(_lyricsGlowEffectAngle)
* (
_lyricsGlowEffectMaxBlurAmount
- _lyricsGlowEffectMinBlurAmount
)
/ 2f
+ (_lyricsGlowEffectMaxBlurAmount + _lyricsGlowEffectMinBlurAmount)
/ 2f,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Quality,
}
);
}
glowedLyricsDs.DrawImage(lyrics);
}
// Mock gradient blurred lyrics layer
using var combinedBlurredLyrics = new CanvasCommandList(sender);
using var combinedBlurredLyricsDs = combinedBlurredLyrics.CreateDrawingSession();
if (SettingsService.LyricsBlurAmount == 0)
{
combinedBlurredLyricsDs.DrawImage(glowedLyrics);
}
else
{
double step = 0.05;
double overlapFactor = 0;
for (double i = 0; i <= 0.5 - step; i += step)
{
using var blurredLyrics = new GaussianBlurEffect
{
Source = glowedLyrics,
BlurAmount = (float)(
SettingsService.LyricsBlurAmount * (1 - i / (0.5 - step))
),
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
};
using var topCropped = new CropEffect
{
Source = blurredLyrics,
SourceRectangle = new Rect(
0,
sender.Size.Height * i,
sender.Size.Width,
sender.Size.Height * step * (1 + overlapFactor)
),
};
using var bottomCropped = new CropEffect
{
Source = blurredLyrics,
SourceRectangle = new Rect(
0,
sender.Size.Height * (1 - i - step * (1 + overlapFactor)),
sender.Size.Width,
sender.Size.Height * step * (1 + overlapFactor)
),
};
combinedBlurredLyricsDs.DrawImage(topCropped);
combinedBlurredLyricsDs.DrawImage(bottomCropped);
}
}
// Masked mock gradient blurred lyrics layer
using var maskedCombinedBlurredLyrics = new CanvasCommandList(sender);
using (
var maskedCombinedBlurredLyricsDs =
maskedCombinedBlurredLyrics.CreateDrawingSession()
)
{
if (SettingsService.LyricsVerticalEdgeOpacity == 100)
{
maskedCombinedBlurredLyricsDs.DrawImage(combinedBlurredLyrics);
}
else
{
using var mask = new CanvasCommandList(sender);
using (var maskDs = mask.CreateDrawingSession())
{
DrawGradientOpacityMask(sender, maskDs, r, g, b);
}
maskedCombinedBlurredLyricsDs.DrawImage(
new AlphaMaskEffect { Source = combinedBlurredLyrics, AlphaMask = mask }
);
}
}
// Draw the final composed layer
ds.DrawImage(maskedCombinedBlurredLyrics);
}
private void DrawGradientOpacityMask(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
byte r,
byte g,
byte b
)
{
byte verticalEdgeAlpha = (byte)(255 * SettingsService.LyricsVerticalEdgeOpacity / 100f);
using var maskBrush = new CanvasLinearGradientBrush(
control,
[
new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b) },
new() { Position = 0.5f, Color = Color.FromArgb(255, r, g, b) },
new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b) },
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height),
};
ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
_albumArtRenderer.Draw(sender, ds);
_lyricsRenderer.Draw(sender, ds);
}
// Comsumes CPU related resources
@@ -492,56 +85,17 @@ namespace BetterLyrics.WinUI3.Views
CanvasAnimatedUpdateEventArgs args
)
{
_pureLyricsRenderer.CurrentTime += args.Timing.ElapsedTime;
_lyricsRenderer.AddElapsedTime(args.Timing.ElapsedTime);
if (SettingsService.IsDynamicCoverOverlay)
{
_coverImageAsBackgroundRenderer.RotateAngle += _coverRotateSpeed;
_coverImageAsBackgroundRenderer.RotateAngle %= MathF.PI * 2;
}
if (SettingsService.IsLyricsDynamicGlowEffectEnabled)
{
_lyricsGlowEffectAngle += _lyricsGlowEffectSpeed;
_lyricsGlowEffectAngle %= MathF.PI * 2;
}
_coverImageAsBackgroundRenderer.Calculate(sender);
if (_pureLyricsRenderer.LyricsLines.LastOrDefault()?.TextLayout == null)
{
_pureLyricsRenderer.ReLayoutAsync(sender);
}
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
_pureLyricsRenderer.CalculateScaleAndOpacity(currentPlayingLineIndex);
_pureLyricsRenderer.CalculatePosition(sender, currentPlayingLineIndex);
}
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < _pureLyricsRenderer.LyricsLines.Count; i++)
{
var line = _pureLyricsRenderer.LyricsLines[i];
if (line.EndPlayingTimestampMs < _pureLyricsRenderer.CurrentTime.TotalMilliseconds)
{
continue;
}
return i;
}
return -1;
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
InitMediaManager();
_albumArtRenderer.Calculate(sender);
_lyricsRenderer.CalculateAsync();
}
private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
if (App.Current.SettingsWindow is null)
{
var settingsWindow = new BaseWindow();
var settingsWindow = new HostWindow();
settingsWindow.Navigate(typeof(SettingsPage));
App.Current.SettingsWindow = settingsWindow;
}
@@ -559,7 +113,7 @@ namespace BetterLyrics.WinUI3.Views
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
SettingsService.IsFirstRun = false;
GlobalSettingsViewModel.IsFirstRun = false;
}
private void CoverArea_SizeChanged(object sender, SizeChangedEventArgs e)
@@ -575,7 +129,7 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
BottomCommandGrid.Opacity = .5;
}
@@ -584,21 +138,36 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == .5)
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Opacity == .5)
BottomCommandGrid.Opacity = 0;
}
private async void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
_pureLyricsRenderer.LimitedLineWidth = e.NewSize.Width;
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
_lyricsRenderer.LimitedLineWidth = e.NewSize.Width;
await _lyricsRenderer.ReLayoutAsync();
}
private async void LyricsCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
_pureLyricsRenderer.CanvasWidth = e.NewSize.Width;
_pureLyricsRenderer.CanvasHeight = e.NewSize.Height;
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
_lyricsRenderer.CanvasWidth = e.NewSize.Width;
_lyricsRenderer.CanvasHeight = e.NewSize.Height;
await _lyricsRenderer.ReLayoutAsync();
}
private void OpenMatchedFileButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.OpenMatchedFileFolderInFileExplorer((string)(sender as Button)!.Tag);
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
_lyricsRenderer.Control = (ICanvasAnimatedControl)sender;
}
private void CoverImageGrid_Loaded(object sender, RoutedEventArgs e)
{
UpdateAlbumArtCornerRadius(AlbumArtViewModel.CoverImageRadius);
}
}
}

View File

@@ -30,9 +30,9 @@
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.SettingsService.MusicLibraries, Mode=OneWay}">
ItemsSource="{x:Bind ViewModel.MusicLibraries, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard Header="{Binding}">
@@ -61,13 +61,13 @@
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -87,9 +87,9 @@
<controls:SettingsCard
x:Uid="SettingsPageRebuildDatabase"
HeaderIcon="{ui:FontIcon Glyph=&#xE621;}"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
IsEnabled="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<controls:SettingsCard.Description>
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</controls:SettingsCard.Description>
<StackPanel>
<Button x:Uid="SettingsPageRebuildDatabaseButton" Command="{x:Bind ViewModel.RebuildLyricsIndexDatabaseCommand}" />
@@ -97,7 +97,7 @@
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
Visibility="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -106,7 +106,7 @@
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.Theme, Mode=TwoWay}">
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind GlobalSettingsViewModel.Theme, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageFollowSystem" />
<ComboBoxItem x:Uid="SettingsPageLight" />
<ComboBoxItem x:Uid="SettingsPageDark" />
@@ -114,7 +114,7 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageBackdrop" HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}">
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.BackdropType, Mode=TwoWay}">
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind GlobalSettingsViewModel.BackdropType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageNoBackdrop" />
<ComboBoxItem x:Uid="SettingsPageMica" />
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
@@ -126,7 +126,7 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTitleBarType" HeaderIcon="{ui:FontIcon Glyph=&#xE66A;}">
<ComboBox x:Name="TitleBarTypeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.TitleBarType, Mode=TwoWay}">
<ComboBox x:Name="TitleBarTypeComboBox" SelectedIndex="{x:Bind GlobalSettingsViewModel.TitleBarType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageCompactTitleBar" />
<ComboBoxItem x:Uid="SettingsPageExtendedTitleBar" />
</ComboBox>
@@ -136,7 +136,7 @@
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.Language, Mode=TwoWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.Language, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
@@ -162,17 +162,17 @@
<TextBlock x:Uid="SettingsPageCoverOverlayGPUUsage" />
</StackPanel>
</controls:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind AlbumArtRendererSettingsViewModel.IsCoverOverlayEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsDynamicCoverOverlay, Mode=TwoWay}" />
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind AlbumArtRendererSettingsViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind AlbumArtRendererSettingsViewModel.IsDynamicCoverOverlay, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind AlbumArtRendererSettingsViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind AlbumArtRendererSettingsViewModel.CoverOverlayOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
@@ -184,17 +184,17 @@
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=TwoWay}" />
Value="{x:Bind AlbumArtRendererSettingsViewModel.CoverOverlayOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind AlbumArtRendererSettingsViewModel.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=OneWay}" />
Text="{x:Bind AlbumArtRendererSettingsViewModel.CoverOverlayBlurAmount, Mode=OneWay}" />
<Slider
Maximum="200"
Minimum="50"
@@ -202,7 +202,7 @@
StepFrequency="10"
TickFrequency="10"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=TwoWay}" />
Value="{x:Bind AlbumArtRendererSettingsViewModel.CoverOverlayBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -214,7 +214,7 @@
<controls:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon Glyph=&#xE71A;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverImageRadius, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind AlbumArtViewModel.CoverImageRadius, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
@@ -226,14 +226,14 @@
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverImageRadius, Mode=TwoWay}" />
Value="{x:Bind AlbumArtViewModel.CoverImageRadius, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageLyricsStyle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsAlignmentType, Mode=TwoWay}">
<ComboBox SelectedIndex="{x:Bind LyricsRendererSettingsViewModel.LyricsAlignmentType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
@@ -243,19 +243,19 @@
<controls:SettingsExpander x:Uid="SettingsPageLyricsFontColor" HeaderIcon="{ui:FontIcon Glyph=&#xE8D3;}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
Binding="{x:Bind LyricsRendererSettingsViewModel.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsExpanded" Value="False" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
Binding="{x:Bind LyricsRendererSettingsViewModel.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="IsExpanded" Value="True" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=TwoWay}">
<ComboBox SelectedIndex="{x:Bind LyricsRendererSettingsViewModel.LyricsFontColorType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDefault" />
<ComboBoxItem x:Uid="SettingsPageLyricsFontColorDominant" />
</ComboBox>
@@ -263,19 +263,19 @@
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Vertical">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
Binding="{x:Bind LyricsRendererSettingsViewModel.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.LyricsFontColorType, Mode=OneWay}"
Binding="{x:Bind LyricsRendererSettingsViewModel.LyricsFontColorType, Mode=OneWay}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<GridView ItemsSource="{x:Bind ViewModel.MainViewModel.CoverImageDominantColors, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsFontSelectedAccentColorIndex, Mode=TwoWay}">
<GridView ItemsSource="{x:Bind LyricsRendererSettingsViewModel.CoverImageDominantColors, Mode=OneWay}" SelectedIndex="{x:Bind LyricsRendererSettingsViewModel.LyricsFontSelectedAccentColorIndex, Mode=TwoWay}">
<GridView.ItemTemplate>
<DataTemplate>
<GridViewItem>
@@ -312,22 +312,22 @@
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=OneWay}" />
Text="{x:Bind LyricsRendererSettingsViewModel.LyricsFontSize, Mode=OneWay}" />
<Slider
Maximum="48"
Maximum="96"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=TwoWay}" />
Value="{x:Bind LyricsRendererSettingsViewModel.LyricsFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon Glyph=&#xF579;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind LyricsRendererSettingsViewModel.LyricsLineSpacingFactor, Mode=OneWay}" />
<TextBlock
x:Uid="SettingsPageLyricsLineSpacingFactorUnit"
Margin="0,0,14,0"
@@ -339,7 +339,7 @@
StepFrequency="0.1"
TickFrequency="0.1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=TwoWay}" />
Value="{x:Bind LyricsRendererSettingsViewModel.LyricsLineSpacingFactor, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -348,7 +348,7 @@
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon Glyph=&#xF573;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind LyricsRendererSettingsViewModel.LyricsVerticalEdgeOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
@@ -360,7 +360,7 @@
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
Value="{x:Bind LyricsRendererSettingsViewModel.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -376,7 +376,7 @@
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=OneWay}" />
Text="{x:Bind LyricsRendererSettingsViewModel.LyricsBlurAmount, Mode=OneWay}" />
<Slider
Maximum="10"
Minimum="0"
@@ -384,7 +384,7 @@
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=TwoWay}" />
Value="{x:Bind LyricsRendererSettingsViewModel.LyricsBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -392,11 +392,11 @@
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon Glyph=&#xE9A9;}"
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind LyricsRendererSettingsViewModel.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsDynamicGlowEffect" IsEnabled="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsDynamicGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsDynamicGlowEffect" IsEnabled="{x:Bind LyricsRendererSettingsViewModel.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsRendererSettingsViewModel.IsLyricsDynamicGlowEffectEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>

View File

@@ -1,3 +1,4 @@
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
@@ -14,6 +15,13 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class SettingsPage : Page
{
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
public AlbumArtOverlayViewModel AlbumArtRendererSettingsViewModel =>
Ioc.Default.GetService<AlbumArtOverlayViewModel>()!;
public LyricsViewModel LyricsRendererSettingsViewModel =>
Ioc.Default.GetService<LyricsViewModel>()!;
public GlobalViewModel GlobalSettingsViewModel =>
Ioc.Default.GetService<GlobalViewModel>()!;
public AlbumArtViewModel AlbumArtViewModel => Ioc.Default.GetService<AlbumArtViewModel>()!;
public SettingsPage()
{