Compare commits

...

3 Commits

Author SHA1 Message Date
Zhe Fang
db6847b74f Merge pull request #4 from jayfunc/dev
Dev
2025-06-07 19:15:42 -04:00
Zhe Fang
d510892650 add:
1. cross fade animation when switching songs;
2. user can now toggle immersive mode
by button (located at the bottom-right area)
3. user can now toggle full screen mode

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

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

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

View File

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

View File

@@ -57,6 +57,34 @@
<Setter Property="Margin" Value="1,30,0,6" />
</Style.Setters>
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="TitleBarToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- Dimensions -->
</ResourceDictionary>
</Application.Resources>

View File

@@ -32,12 +32,13 @@ namespace BetterLyrics.WinUI3
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
public MainWindow? MainWindow { get; private set; }
public MainWindow? SettingsWindow { get; set; }
public BaseWindow? MainWindow { get; private set; }
public BaseWindow? SettingsWindow { get; set; }
public static ResourceLoader? ResourceLoader { get; private set; }
public static DispatcherQueue DispatcherQueue => DispatcherQueue.GetForCurrentThread();
public static DispatcherQueue? DispatcherQueue { get; private set; }
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
@@ -47,7 +48,9 @@ namespace BetterLyrics.WinUI3
{
this.InitializeComponent();
App.ResourceLoader = new ResourceLoader();
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
Helper.AppInfo.EnsureDirectories();
ConfigureServices();
@@ -67,7 +70,6 @@ namespace BetterLyrics.WinUI3
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton(DispatcherQueue.GetForCurrentThread())
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
@@ -77,6 +79,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
// ViewModels
.AddSingleton<BaseWindowModel>()
.AddSingleton<MainViewModel>()
.AddSingleton<SettingsViewModel>()
.BuildServiceProvider()
@@ -101,7 +104,7 @@ namespace BetterLyrics.WinUI3
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new MainWindow();
MainWindow = new BaseWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
}

View File

@@ -31,7 +31,8 @@
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.2.0" />
<PackageReference Include="DevWinUI" Version="8.3.0" />
<PackageReference Include="DevWinUI.Controls" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public enum TitleBarType
{
Compact,
Extended,
}
}

View File

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

View File

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

View File

@@ -27,6 +27,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const int CoverOverlayOpacity = 100; // 1.0
public const int CoverOverlayBlurAmount = 200;
// Title bar
public const int TitleBarType = 0;
// Album art
public const int CoverImageRadius = 24;
@@ -40,5 +43,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const bool IsLyricsDynamicGlowEffectEnabled = false;
public const int LyricsFontColorType = 0; // Default
public const int LyricsFontSelectedAccentColorIndex = 0;
// Notification
public const bool NeverShowEnterFullScreenMessage = false;
public const bool NeverShowEnterImmersiveModeMessage = false;
}
}

View File

@@ -26,6 +26,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const string CoverOverlayOpacity = "CoverOverlayOpacity";
public const string CoverOverlayBlurAmount = "CoverOverlayBlurAmount";
// Title bar
public const string TitleBarType = "TitleBarType";
// Album art
public const string CoverImageRadius = "CoverImageRadius";
@@ -40,5 +43,10 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const string LyricsFontColorType = "LyricsFontColorType";
public const string LyricsFontSelectedAccentColorIndex =
"LyricsFontSelectedAccentColorIndex";
// Notification
public const string NeverShowEnterFullScreenMessage = "NeverShowEnterFullScreenMessage";
public const string NeverShowEnterImmersiveModeMessage =
"NeverShowEnterImmersiveModeMessage";
}
}

View File

@@ -33,28 +33,6 @@ namespace BetterLyrics.WinUI3.Services.Settings
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
private void WatchMultipleDirectories(IEnumerable<string> directories)
{
foreach (var dir in directories)
{
if (!Directory.Exists(dir))
continue;
var watcher = new FileSystemWatcher
{
Path = dir,
Filter = "*.*",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
}
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
App.DispatcherQueue.TryEnqueue(() => { });
}
public bool IsFirstRun
{
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
@@ -64,6 +42,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase = false;
[ObservableProperty]
private bool _isImmersiveMode = false;
// Theme
public int Theme
{
@@ -172,6 +153,13 @@ namespace BetterLyrics.WinUI3.Services.Settings
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
{
@@ -250,6 +238,28 @@ namespace BetterLyrics.WinUI3.Services.Settings
}
}
//Notification
public bool NeverShowEnterFullScreenMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterFullScreenMessage,
SettingsDefaultValues.NeverShowEnterFullScreenMessage
);
set => Set(SettingsKeys.NeverShowEnterFullScreenMessage, value);
}
public bool NeverShowEnterImmersiveModeMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterImmersiveModeMessage,
SettingsDefaultValues.NeverShowEnterImmersiveModeMessage
);
set => Set(SettingsKeys.NeverShowEnterImmersiveModeMessage, value);
}
// Utils
private T? Get<T>(string key, T? defaultValue = default)
{
if (_localSettings.Values.TryGetValue(key, out object? value))

View File

@@ -276,6 +276,9 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
</data>
@@ -309,35 +312,11 @@
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>Rebuilding the database, please wait...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's get started now</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>The top area is the title bar</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the top area to show it</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<value>Setup lyrics database now</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom left corner and then click to open settings page</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom area to show it</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>The bottom area is the command area</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>Toggle "show lyrics only" here</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>When lyrics exist, you can switch them in the lower right corner</value>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's setup lyrics database now</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
@@ -369,4 +348,28 @@
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Corner radius</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Title bar size</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compact</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Extended</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>Always on top</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>Full screen</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>Press Esc to exit full screen mode</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Hover back again to show the toggle button</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
</root>

View File

@@ -276,6 +276,9 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅展示歌词</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词效果</value>
</data>
@@ -309,36 +312,12 @@
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重构数据库中,请稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>来看看怎么使用这款应用吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>顶部区域是标题栏</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在顶部区域以显示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>现在就来初始化歌词数据库吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在左下角后单击以进入设置页面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在底部区域以显示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部区域是命令栏</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此处切换“仅展示歌词”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>当歌词存在时可在右下角进行切换</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
</data>
@@ -369,4 +348,28 @@
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圆角半径</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>标题栏大小</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>紧凑</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>扩展</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>将应用置于顶层</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全屏</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>按 Esc 退出全屏模式</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次悬停以显示切换按钮</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value> 不再显示此消息</value>
</data>
</root>

View File

@@ -276,6 +276,9 @@
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅展示歌詞</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞效果</value>
</data>
@@ -309,36 +312,12 @@
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重構資料庫中,請稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>來看看怎麼使用這款應用程式吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>頂部區域是標題欄</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在頂部區域以顯示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>現在就來初始化歌詞資料庫吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在左下角後點擊以進入設定頁面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在底部區域以顯示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部區域是命令列</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此切換“僅展示歌詞”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>當歌詞存在時可在右下角進行切換</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
</data>
@@ -369,4 +348,28 @@
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圓角半徑</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>標題列大小</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>緊湊</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>擴充</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>將應用置於頂層</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全螢幕</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>按 Esc 退出全螢幕模式</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次懸停以顯示切換按鈕</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再顯示此訊息</value>
</data>
</root>

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowModel : ObservableObject
{
public SettingsService SettingsService { get; private set; }
[ObservableProperty]
private int _titleBarFontSize = 11;
[ObservableProperty]
private Notification _notification = new();
[ObservableProperty]
private bool _showInfoBar = false;
public BaseWindowModel(SettingsService settingsService)
{
SettingsService = settingsService;
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (
!Notification.IsForeverDismissable
|| AlreadyForeverDismissedThisMessage() == false
)
{
Notification.Visibility = Notification.IsForeverDismissable
? Visibility.Visible
: Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
switch (Notification.RelatedSettingsKeyName)
{
case SettingsKeys.NeverShowEnterFullScreenMessage:
SettingsService.NeverShowEnterFullScreenMessage = value;
break;
case SettingsKeys.NeverShowEnterImmersiveModeMessage:
SettingsService.NeverShowEnterImmersiveModeMessage = value;
break;
default:
break;
}
}
private bool? AlreadyForeverDismissedThisMessage() =>
Notification.RelatedSettingsKeyName switch
{
SettingsKeys.NeverShowEnterFullScreenMessage =>
SettingsService.NeverShowEnterFullScreenMessage,
SettingsKeys.NeverShowEnterImmersiveModeMessage =>
SettingsService.NeverShowEnterImmersiveModeMessage,
_ => null,
};
}
}

View File

@@ -132,7 +132,7 @@ namespace BetterLyrics.WinUI3.ViewModels
return result;
}
public async Task<(List<LyricsLine>, SoftwareBitmap?, uint, uint)> SetSongInfoAsync(
public async Task<(List<LyricsLine>, SoftwareBitmap?)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
@@ -201,12 +201,7 @@ namespace BetterLyrics.WinUI3.ViewModels
stream.Dispose();
}
return (
GetLyrics(track),
coverSoftwareBitmap,
coverImagePixelWidth,
coverImagePixelHeight
);
return (GetLyrics(track), coverSoftwareBitmap);
}
}
}

View File

@@ -1,12 +1,16 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel.Core;
using Windows.Media;
using Windows.Media.Playback;
@@ -73,9 +77,12 @@ namespace BetterLyrics.WinUI3.ViewModels
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
if (existed)
{
MainWindow.StackedNotificationsBehavior?.Show(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"),
Helper.AnimationHelper.StackedNotificationsShowingDuration
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
)
)
);
}
else

View File

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

View File

@@ -0,0 +1,240 @@
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 CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Behaviors;
using DevWinUI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
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
{
/// <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 BaseWindowModel WindowModel { get; set; }
public BaseWindow()
{
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);
}
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;
}
}
public void Navigate(Type type)
{
RootFrame.Navigate(type);
}
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
if (RootFrame.CurrentSourcePageType == typeof(MainPage))
{
App.Current.Exit();
}
else if (RootFrame.CurrentSourcePageType == typeof(SettingsPage))
{
App.Current.SettingsWindow!.AppWindow.Hide();
}
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
UpdateTitleBarWindowButtonsVisibility();
}
}
private void MinimiseButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Minimize();
UpdateTitleBarWindowButtonsVisibility();
}
}
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Restore();
UpdateTitleBarWindowButtonsVisibility();
}
}
private void UpdateTitleBarWindowButtonsVisibility()
{
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{
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;
}
}
private void RootFrame_Navigated(object sender, NavigationEventArgs e)
{
AppWindow.Title = Title = App.ResourceLoader!.GetString(
$"{e.SourcePageType.Name}Title"
);
}
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop;
UpdateTitleBarWindowButtonsVisibility();
}
}
private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e)
{
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
break;
case AppWindowPresenterKind.FullScreen:
AppWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
break;
case AppWindowPresenterKind.Overlapped:
AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Models.Notification(
App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
isForeverDismissable: true,
relatedSettingsKeyName: SettingsKeys.NeverShowEnterFullScreenMessage
)
)
);
break;
default:
break;
}
UpdateTitleBarWindowButtonsVisibility();
}
private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (
AppWindow.Presenter is FullScreenPresenter
&& e.Key == Windows.System.VirtualKey.Escape
)
AppWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
}
}
}

View File

@@ -3,6 +3,7 @@
x:Class="BetterLyrics.WinUI3.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -14,7 +15,7 @@
NavigationCacheMode="Required"
mc:Ignorable="d">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<Grid x:Name="RootGrid">
<Grid.Resources>
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
</Grid.Resources>
@@ -34,38 +35,11 @@
Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded"
SizeChanged="LyricsCanvas_SizeChanged"
Update="LyricsCanvas_Update">
<canvas:CanvasAnimatedControl.Resources>
<Storyboard x:Key="LyricsCanvasFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="LyricsCanvasFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</canvas:CanvasAnimatedControl.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<canvas:CanvasAnimatedControl.OpacityTransition>
<ScalarTransition />
</canvas:CanvasAnimatedControl.OpacityTransition>
</canvas:CanvasAnimatedControl>
</Grid>
@@ -112,6 +86,11 @@
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Grid
x:Name="LyricsPlaceholderGrid"
Grid.Column="2"
SizeChanged="LyricsPlaceholderGrid_SizeChanged" />
<Grid
x:Name="SongInfoInnerGrid"
Grid.Column="0"
@@ -349,60 +328,60 @@
<Grid
x:Name="BottomCommandGrid"
Padding="2,0"
Margin="0,0,4,4"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="0">
<Grid.Resources>
<Storyboard x:Name="BottomCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="BottomCommandGridFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
Opacity=".5"
PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.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}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<AppBarButton
x:Name="SettingsButton"
Click="SettingsButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE713;" />
</AppBarButton>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Spacing="4">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<AppBarToggleButton
<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>
<ToggleButton
x:Name="LyricsOnlyButton"
x:Uid="MainWindowLyricsOnly"
IsChecked="{x:Bind ViewModel.ShowLyricsOnly, Mode=TwoWay}"
LabelPosition="Collapsed"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ViewModel.LyricsExisted, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE66C;" />
</ToggleButton>
<Button
x:Name="SettingsButton"
Click="SettingsButton_Click"
Style="{StaticResource GhostButtonStyle}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF8B0;" />
</Button>
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF15F;" />
</AppBarToggleButton>
</StackPanel>
</Grid>
@@ -412,55 +391,7 @@
x:Uid="MainPageWelcomeTeachingTip"
Closed="WelcomeTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind RootGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="1/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="TopCommandTeachingTip"
x:Uid="MainPageTitleBarTeachingTip"
Closed="TopCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind TopPlaceholder}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="2/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="BottomCommandTeachingTip"
x:Uid="MainPageBottomCommandTeachingTip"
Closed="BottomCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind BottomCommandGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="3/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="LyricsOnlyTeachingTip"
x:Uid="MainPageLyricsOnlyTeachingTip"
Closed="LyricsOnlyTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind LyricsOnlyButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="4/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="InitDatabaseTeachingTip"
x:Uid="MainPageInitDatabaseTeachingTip"
Closed="InitDatabaseTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind SettingsButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="5/5" />
</TeachingTip.Content>
</TeachingTip>
Target="{x:Bind SettingsButton}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>

View File

@@ -1,31 +1,28 @@
using System;
using System.Collections.Generic;
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.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Text;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Media;
using Windows.Media.Control;
using Color = Windows.UI.Color;
@@ -40,16 +37,10 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel => (MainViewModel)DataContext;
public SettingsService SettingsService { get; set; }
private SettingsService SettingsService { get; set; }
private List<LyricsLine> _lyricsLines = [];
private SoftwareBitmap? _coverSoftwareBitmap = null;
private uint _coverImagePixelWidth = 0;
private uint _coverImagePixelHeight = 0;
private float _coverBitmapRotateAngle = 0f;
private float _coverScaleFactor = 1;
private readonly CoverBackgroundRenderer _coverImageAsBackgroundRenderer = new();
private readonly PureLyricsRenderer _pureLyricsRenderer = new();
private readonly float _coverRotateSpeed = 0.003f;
@@ -59,37 +50,6 @@ namespace BetterLyrics.WinUI3.Views
private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
private readonly float _lyricsGlowEffectMaxBlurAmount = 6f;
private readonly DispatcherQueueTimer _queueTimer;
private TimeSpan _currentTime = TimeSpan.Zero;
private readonly float _defaultOpacity = 0.3f;
private readonly float _highlightedOpacity = 1.0f;
private readonly float _defaultScale = 0.95f;
private readonly float _highlightedScale = 1.0f;
private readonly int _lineEnteringDurationMs = 800;
private readonly int _lineExitingDurationMs = 800;
private readonly int _lineScrollDurationMs = 800;
private float _lastTotalYScroll = 0.0f;
private float _totalYScroll = 0.0f;
private double _lyricsAreaWidth = 0.0f;
private double _lyricsAreaHeight = 0.0f;
private readonly double _lyricsCanvasRightMargin = 36;
private double _lyricsCanvasLeftMargin = 0;
private double _lyricsCanvasMaxTextWidth = 0;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private bool _forceToScroll = false;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
@@ -101,8 +61,6 @@ namespace BetterLyrics.WinUI3.Views
{
this.InitializeComponent();
_queueTimer = _dispatcherQueue.CreateTimer();
_logger = Ioc.Default.GetService<ILogger<MainPage>>()!;
SettingsService = Ioc.Default.GetService<SettingsService>()!;
DataContext = Ioc.Default.GetService<MainViewModel>();
@@ -118,13 +76,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
private async Task ForceToScrollToCurrentPlayingLineAsync()
{
_forceToScroll = true;
await Task.Delay(1);
_forceToScroll = false;
}
private async void SettingsService_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
@@ -134,8 +85,7 @@ namespace BetterLyrics.WinUI3.Views
{
case nameof(SettingsService.LyricsFontSize):
case nameof(SettingsService.LyricsLineSpacingFactor):
LayoutLyrics();
await ForceToScrollToCurrentPlayingLineAsync();
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
break;
case nameof(SettingsService.IsRebuildingLyricsIndexDatabase):
if (!SettingsService.IsRebuildingLyricsIndexDatabase)
@@ -154,12 +104,24 @@ namespace BetterLyrics.WinUI3.Views
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 void ViewModel_PropertyChanged(
private async void ViewModel_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
)
@@ -167,7 +129,17 @@ namespace BetterLyrics.WinUI3.Views
switch (e.PropertyName)
{
case nameof(ViewModel.ShowLyricsOnly):
RootGrid_SizeChanged(null, null);
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;
@@ -213,11 +185,11 @@ namespace BetterLyrics.WinUI3.Views
{
if (sender == null)
{
_currentTime = TimeSpan.Zero;
_pureLyricsRenderer.CurrentTime = TimeSpan.Zero;
return;
}
_currentTime = sender.GetTimelineProperties().Position;
_pureLyricsRenderer.CurrentTime = sender.GetTimelineProperties().Position;
// _logger.LogDebug(_currentTime);
}
@@ -231,7 +203,7 @@ namespace BetterLyrics.WinUI3.Views
PlaybackInfoChangedEventArgs? args
)
{
_dispatcherQueue.TryEnqueue(
App.DispatcherQueue!.TryEnqueue(
DispatcherQueuePriority.Normal,
() =>
{
@@ -242,7 +214,7 @@ namespace BetterLyrics.WinUI3.Views
}
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
_logger.LogDebug(playbackState.ToString());
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
@@ -276,7 +248,7 @@ namespace BetterLyrics.WinUI3.Views
SessionsChangedEventArgs? args
)
{
_logger.LogDebug("SessionManager_SessionsChanged");
// _logger.LogDebug("SessionManager_SessionsChanged");
}
private void SessionManager_CurrentSessionChanged(
@@ -284,7 +256,7 @@ namespace BetterLyrics.WinUI3.Views
CurrentSessionChangedEventArgs? args
)
{
_logger.LogDebug("SessionManager_CurrentSessionChanged");
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
{
@@ -318,11 +290,11 @@ namespace BetterLyrics.WinUI3.Views
MediaPropertiesChangedEventArgs? args
)
{
_queueTimer.Debounce(
App.DispatcherQueueTimer!.Debounce(
() =>
{
_logger.LogDebug("CurrentSession_MediaPropertiesChanged");
_dispatcherQueue.TryEnqueue(
// _logger.LogDebug("CurrentSession_MediaPropertiesChanged");
App.DispatcherQueue!.TryEnqueue(
DispatcherQueuePriority.High,
async () =>
{
@@ -330,13 +302,7 @@ namespace BetterLyrics.WinUI3.Views
null;
if (_currentSession != null)
{
try
{
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
}
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
@@ -344,15 +310,13 @@ namespace BetterLyrics.WinUI3.Views
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
(
_lyricsLines,
_coverSoftwareBitmap,
_coverImagePixelWidth,
_coverImagePixelHeight
_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 ForceToScrollToCurrentPlayingLineAsync();
await _pureLyricsRenderer.ForceToScrollToCurrentPlayingLineAsync();
await Task.Delay(1);
// Detect and recover the music state
CurrentSession_PlaybackInfoChanged(_currentSession, null);
@@ -360,7 +324,7 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.AboutToUpdateUI = false;
if (_lyricsLines.Count == 0)
if (_pureLyricsRenderer.LyricsLines.Count == 0)
{
Grid.SetColumnSpan(SongInfoInnerGrid, 3);
}
@@ -375,31 +339,6 @@ namespace BetterLyrics.WinUI3.Views
);
}
private async void RootGrid_SizeChanged(object? sender, SizeChangedEventArgs? e)
{
//_queueTimer.Debounce(async () => {
_lyricsAreaHeight = LyricsGrid.ActualHeight;
_lyricsAreaWidth = LyricsGrid.ActualWidth;
if (SongInfoColumnDefinition.ActualWidth == 0 || ViewModel.ShowLyricsOnly)
{
_lyricsCanvasLeftMargin = 36;
}
else
{
_lyricsCanvasLeftMargin = 36 + SongInfoColumnDefinition.ActualWidth + 36;
}
_lyricsCanvasMaxTextWidth =
_lyricsAreaWidth - _lyricsCanvasLeftMargin - _lyricsCanvasRightMargin;
LayoutLyrics();
await ForceToScrollToCurrentPlayingLineAsync();
//}, TimeSpan.FromMilliseconds(50));
}
// Comsumes GPU related resources
private void LyricsCanvas_Draw(
ICanvasAnimatedControl sender,
@@ -413,16 +352,13 @@ namespace BetterLyrics.WinUI3.Views
var b = _lyricsColor.B;
// Draw (dynamic) cover image as the very first layer
if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null)
{
DrawCoverImage(sender, ds);
}
_coverImageAsBackgroundRenderer.Draw(sender, ds);
// Lyrics only layer
using var lyrics = new CanvasCommandList(sender);
using (var lyricsDs = lyrics.CreateDrawingSession())
{
DrawLyrics(sender, lyricsDs, r, g, b);
_pureLyricsRenderer.Draw(sender, lyricsDs, r, g, b);
}
using var glowedLyrics = new CanvasCommandList(sender);
@@ -526,158 +462,6 @@ namespace BetterLyrics.WinUI3.Views
ds.DrawImage(maskedCombinedBlurredLyrics);
}
private void DrawLyrics(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
byte r,
byte g,
byte b
)
{
var (displayStartLineIndex, displayEndLineIndex) =
GetVisibleLyricsLineIndexBoundaries();
for (
int i = displayStartLineIndex;
_lyricsLines.Count > 0
&& i >= 0
&& i < _lyricsLines.Count
&& i <= displayEndLineIndex;
i++
)
{
var line = _lyricsLines[i];
if (line.TextLayout == null)
{
return;
}
float progressPerChar = 1f / line.Text.Length;
var position = line.Position;
float centerX = position.X;
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
switch ((LyricsAlignmentType)SettingsService.LyricsAlignmentType)
{
case LyricsAlignmentType.Left:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)_lyricsCanvasMaxTextWidth / 2;
break;
case LyricsAlignmentType.Right:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)_lyricsCanvasMaxTextWidth;
break;
default:
break;
}
int startIndex = 0;
// Set brush
for (int j = 0; j < line.TextLayout.LineCount; j++)
{
int count = line.TextLayout.LineMetrics[j].CharacterCount;
var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
float subLinePlayingProgress = Math.Clamp(
(line.PlayingProgress * line.Text.Length - startIndex) / count,
0,
1
);
using var horizontalFillBrush = new CanvasLinearGradientBrush(
control,
[
new()
{
Position = 0,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position =
subLinePlayingProgress * (1 + progressPerChar)
- progressPerChar,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position = subLinePlayingProgress * (1 + progressPerChar),
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
new()
{
Position = 1.5f,
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
]
)
{
StartPoint = new Vector2(
(float)(regions[0].LayoutBounds.Left + position.X),
0
),
EndPoint = new Vector2(
(float)(regions[^1].LayoutBounds.Right + position.X),
0
),
};
line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
startIndex += count;
}
// Scale
ds.Transform =
Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
* Matrix3x2.CreateTranslation(0, _totalYScroll);
// _logger.LogDebug(_totalYScroll);
ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawCoverImage(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
ds.Transform = Matrix3x2.CreateRotation(
_coverBitmapRotateAngle,
control.Size.ToVector2() * 0.5f
);
using var coverOverlayEffect = new OpacityEffect
{
Opacity = SettingsService.CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = SettingsService.CoverOverlayBlurAmount,
Source = new ScaleEffect
{
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(_coverScaleFactor),
Source = CanvasBitmap.CreateFromSoftwareBitmap(
control,
_coverSoftwareBitmap
),
},
},
};
ds.DrawImage(
coverOverlayEffect,
(float)control.Size.Width / 2 - _coverImagePixelWidth * _coverScaleFactor / 2,
(float)control.Size.Height / 2 - _coverImagePixelHeight * _coverScaleFactor / 2
);
ds.Transform = Matrix3x2.Identity;
}
private void DrawGradientOpacityMask(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
@@ -708,12 +492,12 @@ namespace BetterLyrics.WinUI3.Views
CanvasAnimatedUpdateEventArgs args
)
{
_currentTime += args.Timing.ElapsedTime;
_pureLyricsRenderer.CurrentTime += args.Timing.ElapsedTime;
if (SettingsService.IsDynamicCoverOverlay)
{
_coverBitmapRotateAngle += _coverRotateSpeed;
_coverBitmapRotateAngle %= MathF.PI * 2;
_coverImageAsBackgroundRenderer.RotateAngle += _coverRotateSpeed;
_coverImageAsBackgroundRenderer.RotateAngle %= MathF.PI * 2;
}
if (SettingsService.IsLyricsDynamicGlowEffectEnabled)
{
@@ -721,32 +505,24 @@ namespace BetterLyrics.WinUI3.Views
_lyricsGlowEffectAngle %= MathF.PI * 2;
}
if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null)
{
var diagonal = Math.Sqrt(
Math.Pow(_lyricsAreaWidth, 2) + Math.Pow(_lyricsAreaHeight, 2)
);
_coverImageAsBackgroundRenderer.Calculate(sender);
_coverScaleFactor =
(float)diagonal / Math.Min(_coverImagePixelWidth, _coverImagePixelHeight);
}
if (_lyricsLines.LastOrDefault()?.TextLayout == null)
if (_pureLyricsRenderer.LyricsLines.LastOrDefault()?.TextLayout == null)
{
LayoutLyrics();
_pureLyricsRenderer.ReLayoutAsync(sender);
}
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
UpdateScaleAndOpacity(currentPlayingLineIndex);
UpdatePosition(currentPlayingLineIndex);
_pureLyricsRenderer.CalculateScaleAndOpacity(currentPlayingLineIndex);
_pureLyricsRenderer.CalculatePosition(sender, currentPlayingLineIndex);
}
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < _lyricsLines.Count; i++)
for (int i = 0; i < _pureLyricsRenderer.LyricsLines.Count; i++)
{
var line = _lyricsLines[i];
if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds)
var line = _pureLyricsRenderer.LyricsLines[i];
if (line.EndPlayingTimestampMs < _pureLyricsRenderer.CurrentTime.TotalMilliseconds)
{
continue;
}
@@ -756,239 +532,6 @@ namespace BetterLyrics.WinUI3.Views
return -1;
}
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries()
{
// _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
return new Tuple<int, int>(_startVisibleLineIndex, _endVisibleLineIndex);
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (_lyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _lyricsLines.Count - 1);
}
private void UpdateScaleAndOpacity(int currentPlayingLineIndex)
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
for (int i = startLineIndex; _lyricsLines.Count > 0 && i <= endLineIndex; i++)
{
var line = _lyricsLines[i];
bool linePlaying = i == currentPlayingLineIndex;
var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
var lineExitingDurationMs = _lineExitingDurationMs;
if (i + 1 <= endLineIndex)
{
lineExitingDurationMs = Math.Min(
_lyricsLines[i + 1].DurationMs,
lineExitingDurationMs
);
}
float lineEnteringProgress = 0.0f;
float lineExitingProgress = 0.0f;
bool lineEntering = false;
bool lineExiting = false;
float scale = _defaultScale;
float opacity = _defaultOpacity;
float playProgress = 0;
if (linePlaying)
{
line.PlayingState = LyricsPlayingState.Playing;
scale = _highlightedScale;
opacity = _highlightedOpacity;
playProgress =
((float)_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
/ line.DurationMs;
var durationFromStartMs =
_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering)
{
lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
scale =
_defaultScale
+ (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
opacity =
_defaultOpacity
+ (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
}
}
else
{
if (i < currentPlayingLineIndex)
{
line.PlayingState = LyricsPlayingState.Played;
playProgress = 1;
var durationToEndMs =
_currentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting)
{
lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
scale =
_highlightedScale
- (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
opacity =
_highlightedOpacity
- (_highlightedOpacity - _defaultOpacity)
* (float)lineExitingProgress;
}
}
else
{
line.PlayingState = LyricsPlayingState.NotPlayed;
}
}
line.EnteringProgress = lineEnteringProgress;
line.ExitingProgress = lineExitingProgress;
line.Scale = scale;
line.Opacity = opacity;
line.PlayingProgress = playProgress;
}
}
private void LayoutLyrics()
{
using CanvasTextFormat textFormat = new()
{
FontSize = SettingsService.LyricsFontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontWeight = FontWeights.Bold,
//FontFamily = "Segoe UI Mono",
};
float y = (float)_lyricsAreaHeight / 2;
// Init Positions
for (int i = 0; i < _lyricsLines.Count; i++)
{
var line = _lyricsLines[i];
// Calculate layout bounds
line.TextLayout = new CanvasTextLayout(
LyricsCanvas.Device,
line.Text,
textFormat,
(float)_lyricsCanvasMaxTextWidth,
(float)_lyricsAreaHeight
);
line.Position = new Vector2((float)_lyricsCanvasLeftMargin, y);
y +=
(float)line.TextLayout.LayoutBounds.Height
/ line.TextLayout.LineCount
* (line.TextLayout.LineCount + SettingsService.LyricsLineSpacingFactor);
}
}
private void UpdatePosition(int currentPlayingLineIndex)
{
if (currentPlayingLineIndex < 0)
{
return;
}
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0)
{
return;
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _lyricsLines?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
return;
}
if (currentPlayingLine.TextLayout == null)
{
return;
}
var lineScrollingProgress =
(_currentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
/ Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
var targetYScrollOffset = (float)(
-currentPlayingLine.Position.Y
+ _lyricsLines![0].Position.Y
- currentPlayingLine.TextLayout.LayoutBounds.Height / 2
- _lastTotalYScroll
);
var yScrollOffset =
targetYScrollOffset
* EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
bool isScrollingNow = lineScrollingProgress <= 1;
if (isScrollingNow)
{
_totalYScroll = _lastTotalYScroll + yScrollOffset;
}
else
{
if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1)
{
_totalYScroll = _lastTotalYScroll + targetYScrollOffset;
}
_lastTotalYScroll = _totalYScroll;
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update Positions
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
{
var line = _lyricsLines[i];
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
{
if (_startVisibleLineIndex == -1)
{
_startVisibleLineIndex = i;
}
}
if (
_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
>= _lyricsAreaHeight
)
{
if (_endVisibleLineIndex == -1)
{
_endVisibleLineIndex = i;
}
}
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
{
_endVisibleLineIndex = endLineIndex;
}
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
InitMediaManager();
@@ -998,7 +541,7 @@ namespace BetterLyrics.WinUI3.Views
{
if (App.Current.SettingsWindow is null)
{
var settingsWindow = new MainWindow();
var settingsWindow = new BaseWindow();
settingsWindow.Navigate(typeof(SettingsPage));
App.Current.SettingsWindow = settingsWindow;
}
@@ -1015,38 +558,6 @@ namespace BetterLyrics.WinUI3.Views
}
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
TopCommandTeachingTip.IsOpen = true;
}
private void TopCommandTeachingTip_Closed(
TeachingTip sender,
TeachingTipClosedEventArgs args
)
{
BottomCommandTeachingTip.IsOpen = true;
}
private void BottomCommandTeachingTip_Closed(
TeachingTip sender,
TeachingTipClosedEventArgs args
)
{
LyricsOnlyTeachingTip.IsOpen = true;
}
private void LyricsOnlyTeachingTip_Closed(
TeachingTip sender,
TeachingTipClosedEventArgs args
)
{
InitDatabaseTeachingTip.IsOpen = true;
}
private void InitDatabaseTeachingTip_Closed(
TeachingTip sender,
TeachingTipClosedEventArgs args
)
{
SettingsService.IsFirstRun = false;
}
@@ -1058,5 +569,36 @@ namespace BetterLyrics.WinUI3.Views
CoverArea.ActualHeight
);
}
private void BottomCommandGrid_PointerEntered(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
BottomCommandGrid.Opacity = .5;
}
private void BottomCommandGrid_PointerExited(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (SettingsService.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);
}
private async void LyricsCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
_pureLyricsRenderer.CanvasWidth = e.NewSize.Width;
_pureLyricsRenderer.CanvasHeight = e.NewSize.Height;
await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
}
}
}

View File

@@ -23,6 +23,8 @@
<Grid Margin="36,72,36,72">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Music lib -->
<TextBlock x:Uid="SettingsPageLyricsLib" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
@@ -99,6 +101,8 @@
</StackPanel>
</controls:SettingsCard>
<!-- App appearance -->
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
@@ -121,6 +125,13 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTitleBarType" HeaderIcon="{ui:FontIcon Glyph=&#xE66A;}">
<ComboBox x:Name="TitleBarTypeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.TitleBarType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageCompactTitleBar" />
<ComboBoxItem x:Uid="SettingsPageExtendedTitleBar" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon Glyph=&#xF2B7;}"
@@ -138,6 +149,8 @@
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Album art overlay -->
<TextBlock x:Uid="SettingsPageAlbumOverlay" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
@@ -274,6 +287,9 @@
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
<Border.BackgroundTransition>
<BrushTransition />
</Border.BackgroundTransition>
</Border>
<TextBlock
Margin="4,0,4,4"

View File

@@ -17,7 +17,7 @@ Your smooth dynamic local lyrics display built with WinUI 3
- Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song
- Gradient Karaoke effect on every single character
- **Gradient** Karaoke effect on every single character
Coding in progress...
@@ -31,7 +31,7 @@ We provide more than one setting item to better align with your preference
- Album art as background (dynamic, blur amount, opacity)
- Lyrics (alignment, font size, line spacing, opacity, blur amount, dynamic glow effect)
- Lyrics (alignment, font size, font color **(picked from album art accent color)** line spacing, opacity, blur amount, dynamic **glow** effect)
- Language (English, Simplified Chinese, Traditional Chinese)
@@ -49,22 +49,26 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
### Split view
![alt text](Screenshots/Snipaste_2025-06-03_16-46-55.png)
Non-immersive mode
![alt text](Screenshots/Snipaste_2025-06-07_17-36-26.png)
Immersive mode
![alt text](Screenshots/Snipaste_2025-06-03_16-47-43.png)
### Lyrics only
![alt text](Screenshots/Snipaste_2025-06-03_17-51-22.png)
### Fullscreen
![alt text](Screenshots/Snipaste_2025-06-03_17-52-51.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-53-07.png)
![alt text](Screenshots/Snipaste_2025-06-03_18-36-05.png)
### Settings
![alt text](Screenshots/Snipaste_2025-06-03_17-51-52.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-00.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-05.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-11.png)
![alt text](Screenshots/Snipaste_2025-06-07_17-32-02.png)
![alt text](Screenshots/Snipaste_2025-06-07_17-32-17.png)
![alt text](Screenshots/Snipaste_2025-06-07_17-32-23.png)
## Download it now
@@ -74,13 +78,33 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
> **Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version, if you like you can purchase to support me)
Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
Or alternatively get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases/latest) page for the link)
<a href="https://drive.google.com/file/d/1Hh8ijbODIksPmmRYujys7fXngw93Of7I/view?usp=drive_link">
<img src="https://pngimg.com/uploads/google_drive/google_drive_PNG9.png" width="100"/>
</a>
> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
> .zip file, please follow [this doc](How2Install/How2Install.md) to properly install it
## Setup your app
This project relies on listening messages from [SMTC](https://learn.microsoft.com/en-ca/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols).
So technically, as long as you are using the music apps (like
- Spotify
- Groove Music
- Apple Music
- Windows Media Player
- VLC Media Player
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- 酷我音乐
) which support SMTC, then possibly (I didn't test all of themif you find one fail to listen to, you can open an issue) all you need to do is just load your local music/lyrics lib and you are good to go.
## Future work
- Watching file changes
When you downloading lyrics (using some other tools or your own scripts) while listening to new musics (non-existed on your local disks), this app can automatically load those new files.
> Please note: we are not planning support directly load lyrics files via some music software APIs due to copyright issues.
## Many thanks to
@@ -102,21 +126,21 @@ Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
## Third-party libraries that this project uses
```
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.2.0" />
<PackageReference Include="DevWinUI" Version="8.3.0" />
<PackageReference Include="DevWinUI.Controls" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB