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...
This commit is contained in:
Zhe Fang
2025-06-07 18:01:59 -04:00
parent a0e51d976e
commit d510892650
29 changed files with 903 additions and 658 deletions

View File

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

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,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

@@ -43,5 +43,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const bool IsLyricsDynamicGlowEffectEnabled = false; public const bool IsLyricsDynamicGlowEffectEnabled = false;
public const int LyricsFontColorType = 0; // Default public const int LyricsFontColorType = 0; // Default
public const int LyricsFontSelectedAccentColorIndex = 0; public const int LyricsFontSelectedAccentColorIndex = 0;
// Notification
public const bool NeverShowEnterFullScreenMessage = false;
public const bool NeverShowEnterImmersiveModeMessage = false;
} }
} }

View File

@@ -43,5 +43,10 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const string LyricsFontColorType = "LyricsFontColorType"; public const string LyricsFontColorType = "LyricsFontColorType";
public const string LyricsFontSelectedAccentColorIndex = public const string LyricsFontSelectedAccentColorIndex =
"LyricsFontSelectedAccentColorIndex"; "LyricsFontSelectedAccentColorIndex";
// Notification
public const string NeverShowEnterFullScreenMessage = "NeverShowEnterFullScreenMessage";
public const string NeverShowEnterImmersiveModeMessage =
"NeverShowEnterImmersiveModeMessage";
} }
} }

View File

@@ -238,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) private T? Get<T>(string key, T? defaultValue = default)
{ {
if (_localSettings.Values.TryGetValue(key, out object? value)) if (_localSettings.Values.TryGetValue(key, out object? value))

View File

@@ -369,4 +369,7 @@
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve"> <data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Hover back again to show the toggle button</value> <value>Hover back again to show the toggle button</value>
</data> </data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
</root> </root>

View File

@@ -369,4 +369,7 @@
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve"> <data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次悬停以显示切换按钮</value> <value>再次悬停以显示切换按钮</value>
</data> </data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value> 不再显示此消息</value>
</data>
</root> </root>

View File

@@ -369,4 +369,7 @@
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve"> <data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再次懸停以顯示切換按鈕</value> <value>再次懸停以顯示切換按鈕</value>
</data> </data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再顯示此訊息</value>
</data>
</root> </root>

View File

@@ -3,13 +3,80 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; 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.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.ViewModels namespace BetterLyrics.WinUI3.ViewModels
{ {
public partial class BaseWindowModel : ObservableObject public partial class BaseWindowModel : ObservableObject
{ {
public SettingsService SettingsService { get; private set; }
[ObservableProperty] [ObservableProperty]
private int _titleBarFontSize = 11; 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; return result;
} }
public async Task<(List<LyricsLine>, SoftwareBitmap?, uint, uint)> SetSongInfoAsync( public async Task<(List<LyricsLine>, SoftwareBitmap?)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
) )
{ {
@@ -201,12 +201,7 @@ namespace BetterLyrics.WinUI3.ViewModels
stream.Dispose(); stream.Dispose();
} }
return ( return (GetLyrics(track), coverSoftwareBitmap);
GetLyrics(track),
coverSoftwareBitmap,
coverImagePixelWidth,
coverImagePixelHeight
);
} }
} }
} }

View File

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

View File

@@ -23,41 +23,23 @@
Height="{StaticResource TitleBarCompactHeight}" Height="{StaticResource TitleBarCompactHeight}"
VerticalAlignment="Top" VerticalAlignment="Top"
Background="Transparent"> Background="Transparent">
<Grid.OpacityTransition>
<Grid.Resources> <ScalarTransition />
</Grid.OpacityTransition>
<Storyboard x:Name="TopCommandGridFadeInStoryboard">
<DoubleAnimation
EasingFunction="{StaticResource EaseIn}"
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="TopCommandGridFadeOutStoryboard">
<DoubleAnimation
EasingFunction="{StaticResource EaseOut}"
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}" Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="False"> Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeInStoryboard}" /> <interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}" Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="True"> Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeOutStoryboard}" /> <interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
@@ -157,11 +139,21 @@
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}" Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}"
IsClosable="False" IsClosable="False"
IsOpen="{x:Bind WindowModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind WindowModel.Notification.Message, Mode=OneWay}"
Opacity="0" Opacity="0"
Severity="Informational"> Severity="{x:Bind WindowModel.Notification.Severity, Mode=OneWay}">
<InfoBar.RenderTransform> <InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" /> <TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform> </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> <InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard"> <Storyboard x:Key="InfoBarShowAndHideStoryboard">
<!-- Opacity --> <!-- Opacity -->
@@ -182,7 +174,6 @@
</Storyboard> </Storyboard>
</InfoBar.Resources> </InfoBar.Resources>
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}" Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"

View File

@@ -1,26 +1,18 @@
using System; using System;
using System.ComponentModel.Design; using System.Threading.Tasks;
using System.Diagnostics;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages; using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings; using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using DevWinUI; using DevWinUI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Windows.UI.Input;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -34,24 +26,15 @@ namespace BetterLyrics.WinUI3.Views
{ {
public BaseWindowModel WindowModel { get; set; } public BaseWindowModel WindowModel { get; set; }
private SettingsService SettingsService { get; set; }
public static StackedNotificationsBehavior? StackedNotificationsBehavior
{
get;
private set;
}
public BaseWindow() public BaseWindow()
{ {
this.InitializeComponent(); this.InitializeComponent();
StackedNotificationsBehavior = NotificationQueue; AppWindow.Changed += AppWindow_Changed;
WindowModel = Ioc.Default.GetService<BaseWindowModel>()!; WindowModel = Ioc.Default.GetService<BaseWindowModel>()!;
SettingsService = Ioc.Default.GetService<SettingsService>()!; WindowModel.SettingsService.PropertyChanged += SettingsService_PropertyChanged;
SettingsService.PropertyChanged += SettingsService_PropertyChanged;
SettingsService_PropertyChanged( SettingsService_PropertyChanged(
null, null,
@@ -75,6 +58,11 @@ namespace BetterLyrics.WinUI3.Views
SetTitleBar(TopCommandGrid); SetTitleBar(TopCommandGrid);
} }
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
UpdateTitleBarWindowButtonsVisibility();
}
private void SettingsService_PropertyChanged( private void SettingsService_PropertyChanged(
object? sender, object? sender,
System.ComponentModel.PropertyChangedEventArgs e System.ComponentModel.PropertyChangedEventArgs e
@@ -82,17 +70,17 @@ namespace BetterLyrics.WinUI3.Views
{ {
switch (e.PropertyName) switch (e.PropertyName)
{ {
case nameof(Services.Settings.SettingsService.Theme): case nameof(SettingsService.Theme):
RootGrid.RequestedTheme = (ElementTheme)SettingsService.Theme; RootGrid.RequestedTheme = (ElementTheme)WindowModel.SettingsService.Theme;
break; break;
case nameof(Services.Settings.SettingsService.BackdropType): case nameof(SettingsService.BackdropType):
SystemBackdrop = null; SystemBackdrop = null;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop( SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
(BackdropType)SettingsService.BackdropType (BackdropType)WindowModel.SettingsService.BackdropType
); );
break; break;
case nameof(Services.Settings.SettingsService.TitleBarType): case nameof(SettingsService.TitleBarType):
switch ((TitleBarType)SettingsService.TitleBarType) switch ((TitleBarType)WindowModel.SettingsService.TitleBarType)
{ {
case TitleBarType.Compact: case TitleBarType.Compact:
TopCommandGrid.Height = (double) TopCommandGrid.Height = (double)
@@ -169,6 +157,8 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter) if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{ {
MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible; MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized) if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{ {
@@ -188,6 +178,7 @@ namespace BetterLyrics.WinUI3.Views
RestoreButton.Visibility = RestoreButton.Visibility =
AOTFlyoutItem.Visibility = AOTFlyoutItem.Visibility =
Visibility.Collapsed; Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
} }
} }
@@ -201,7 +192,10 @@ namespace BetterLyrics.WinUI3.Views
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e) private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
{ {
if (AppWindow.Presenter is OverlappedPresenter presenter) if (AppWindow.Presenter is OverlappedPresenter presenter)
AOTFlyoutItem.IsChecked = presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop; {
presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop;
UpdateTitleBarWindowButtonsVisibility();
}
} }
private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e) private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e)
@@ -217,9 +211,14 @@ namespace BetterLyrics.WinUI3.Views
break; break;
case AppWindowPresenterKind.Overlapped: case AppWindowPresenterKind.Overlapped:
AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
StackedNotificationsBehavior?.Show( WeakReferenceMessenger.Default.Send(
App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"), new ShowNotificatonMessage(
AnimationHelper.StackedNotificationsShowingDuration new Models.Notification(
App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
isForeverDismissable: true,
relatedSettingsKeyName: SettingsKeys.NeverShowEnterFullScreenMessage
)
)
); );
break; break;
default: default:
@@ -227,9 +226,6 @@ namespace BetterLyrics.WinUI3.Views
} }
UpdateTitleBarWindowButtonsVisibility(); UpdateTitleBarWindowButtonsVisibility();
FullScreenFlyoutItem.IsChecked =
AppWindow.Presenter.Kind == AppWindowPresenterKind.FullScreen;
} }
private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e) private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)

View File

@@ -15,7 +15,7 @@
NavigationCacheMode="Required" NavigationCacheMode="Required"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged"> <Grid x:Name="RootGrid">
<Grid.Resources> <Grid.Resources>
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness> <Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
</Grid.Resources> </Grid.Resources>
@@ -35,38 +35,11 @@
Draw="LyricsCanvas_Draw" Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded" Loaded="LyricsCanvas_Loaded"
SizeChanged="LyricsCanvas_SizeChanged"
Update="LyricsCanvas_Update"> Update="LyricsCanvas_Update">
<canvas:CanvasAnimatedControl.OpacityTransition>
<canvas:CanvasAnimatedControl.Resources> <ScalarTransition />
<Storyboard x:Key="LyricsCanvasFadeInStoryboard"> </canvas:CanvasAnimatedControl.OpacityTransition>
<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> </canvas:CanvasAnimatedControl>
</Grid> </Grid>
@@ -113,6 +86,11 @@
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<Grid
x:Name="LyricsPlaceholderGrid"
Grid.Column="2"
SizeChanged="LyricsPlaceholderGrid_SizeChanged" />
<Grid <Grid
x:Name="SongInfoInnerGrid" x:Name="SongInfoInnerGrid"
Grid.Column="0" Grid.Column="0"
@@ -357,25 +335,9 @@
Opacity=".5" Opacity=".5"
PointerEntered="BottomCommandGrid_PointerEntered" PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited"> PointerExited="BottomCommandGrid_PointerExited">
<Grid.OpacityTransition>
<Grid.Resources> <ScalarTransition />
</Grid.OpacityTransition>
<Storyboard x:Name="BottomCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To=".5"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="BottomCommandGridFadeOutStoryboard">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
@@ -383,13 +345,13 @@
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}" Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="False"> Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" /> <interactivity:ChangePropertyAction PropertyName="Opacity" Value="0.5" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}" Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="True"> Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" /> <interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>

View File

@@ -1,33 +1,28 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Settings; using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using DevWinUI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes; using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects; using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Text;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation; using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Media;
using Windows.Media.Control; using Windows.Media.Control;
using Color = Windows.UI.Color; using Color = Windows.UI.Color;
@@ -44,14 +39,8 @@ namespace BetterLyrics.WinUI3.Views
public MainViewModel ViewModel => (MainViewModel)DataContext; public MainViewModel ViewModel => (MainViewModel)DataContext;
private SettingsService SettingsService { get; set; } private SettingsService SettingsService { get; set; }
private List<LyricsLine> _lyricsLines = []; private readonly CoverBackgroundRenderer _coverImageAsBackgroundRenderer = new();
private readonly PureLyricsRenderer _pureLyricsRenderer = new();
private SoftwareBitmap? _coverSoftwareBitmap = null;
private uint _coverImagePixelWidth = 0;
private uint _coverImagePixelHeight = 0;
private float _coverBitmapRotateAngle = 0f;
private float _coverScaleFactor = 1;
private readonly float _coverRotateSpeed = 0.003f; private readonly float _coverRotateSpeed = 0.003f;
@@ -61,33 +50,6 @@ namespace BetterLyrics.WinUI3.Views
private readonly float _lyricsGlowEffectMinBlurAmount = 0f; private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
private readonly float _lyricsGlowEffectMaxBlurAmount = 6f; private readonly float _lyricsGlowEffectMaxBlurAmount = 6f;
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 GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null; private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null; private GlobalSystemMediaTransportControlsSession? _currentSession = null;
@@ -114,13 +76,6 @@ namespace BetterLyrics.WinUI3.Views
} }
} }
private async Task ForceToScrollToCurrentPlayingLineAsync()
{
_forceToScroll = true;
await Task.Delay(1);
_forceToScroll = false;
}
private async void SettingsService_PropertyChanged( private async void SettingsService_PropertyChanged(
object? sender, object? sender,
System.ComponentModel.PropertyChangedEventArgs e System.ComponentModel.PropertyChangedEventArgs e
@@ -130,8 +85,7 @@ namespace BetterLyrics.WinUI3.Views
{ {
case nameof(SettingsService.LyricsFontSize): case nameof(SettingsService.LyricsFontSize):
case nameof(SettingsService.LyricsLineSpacingFactor): case nameof(SettingsService.LyricsLineSpacingFactor):
LayoutLyrics(); await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
await ForceToScrollToCurrentPlayingLineAsync();
break; break;
case nameof(SettingsService.IsRebuildingLyricsIndexDatabase): case nameof(SettingsService.IsRebuildingLyricsIndexDatabase):
if (!SettingsService.IsRebuildingLyricsIndexDatabase) if (!SettingsService.IsRebuildingLyricsIndexDatabase)
@@ -152,9 +106,14 @@ namespace BetterLyrics.WinUI3.Views
break; break;
case nameof(SettingsService.IsImmersiveMode): case nameof(SettingsService.IsImmersiveMode):
if (SettingsService.IsImmersiveMode) if (SettingsService.IsImmersiveMode)
BaseWindow.StackedNotificationsBehavior?.Show( WeakReferenceMessenger.Default.Send(
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"), new ShowNotificatonMessage(
AnimationHelper.StackedNotificationsShowingDuration new Notification(
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
isForeverDismissable: true,
relatedSettingsKeyName: SettingsKeys.NeverShowEnterImmersiveModeMessage
)
)
); );
break; break;
default: default:
@@ -162,7 +121,7 @@ namespace BetterLyrics.WinUI3.Views
} }
} }
private void ViewModel_PropertyChanged( private async void ViewModel_PropertyChanged(
object? sender, object? sender,
System.ComponentModel.PropertyChangedEventArgs e System.ComponentModel.PropertyChangedEventArgs e
) )
@@ -170,7 +129,17 @@ namespace BetterLyrics.WinUI3.Views
switch (e.PropertyName) switch (e.PropertyName)
{ {
case nameof(ViewModel.ShowLyricsOnly): 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; break;
default: default:
break; break;
@@ -216,11 +185,11 @@ namespace BetterLyrics.WinUI3.Views
{ {
if (sender == null) if (sender == null)
{ {
_currentTime = TimeSpan.Zero; _pureLyricsRenderer.CurrentTime = TimeSpan.Zero;
return; return;
} }
_currentTime = sender.GetTimelineProperties().Position; _pureLyricsRenderer.CurrentTime = sender.GetTimelineProperties().Position;
// _logger.LogDebug(_currentTime); // _logger.LogDebug(_currentTime);
} }
@@ -333,13 +302,7 @@ namespace BetterLyrics.WinUI3.Views
null; null;
if (_currentSession != null) if (_currentSession != null)
{ mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
try
{
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
}
ViewModel.IsAnyMusicSessionExisted = _currentSession != null; ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
@@ -347,15 +310,13 @@ namespace BetterLyrics.WinUI3.Views
await Task.Delay(AnimationHelper.StoryboardDefaultDuration); await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
( (
_lyricsLines, _pureLyricsRenderer.LyricsLines,
_coverSoftwareBitmap, _coverImageAsBackgroundRenderer.SoftwareBitmap
_coverImagePixelWidth,
_coverImagePixelHeight
) = await ViewModel.SetSongInfoAsync(mediaProps); ) = await ViewModel.SetSongInfoAsync(mediaProps);
// Force to show lyrics and scroll to current line even if the music is not playing // Force to show lyrics and scroll to current line even if the music is not playing
LyricsCanvas.Paused = false; LyricsCanvas.Paused = false;
await ForceToScrollToCurrentPlayingLineAsync(); await _pureLyricsRenderer.ForceToScrollToCurrentPlayingLineAsync();
await Task.Delay(1); await Task.Delay(1);
// Detect and recover the music state // Detect and recover the music state
CurrentSession_PlaybackInfoChanged(_currentSession, null); CurrentSession_PlaybackInfoChanged(_currentSession, null);
@@ -363,7 +324,7 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.AboutToUpdateUI = false; ViewModel.AboutToUpdateUI = false;
if (_lyricsLines.Count == 0) if (_pureLyricsRenderer.LyricsLines.Count == 0)
{ {
Grid.SetColumnSpan(SongInfoInnerGrid, 3); Grid.SetColumnSpan(SongInfoInnerGrid, 3);
} }
@@ -378,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 // Comsumes GPU related resources
private void LyricsCanvas_Draw( private void LyricsCanvas_Draw(
ICanvasAnimatedControl sender, ICanvasAnimatedControl sender,
@@ -416,16 +352,13 @@ namespace BetterLyrics.WinUI3.Views
var b = _lyricsColor.B; var b = _lyricsColor.B;
// Draw (dynamic) cover image as the very first layer // Draw (dynamic) cover image as the very first layer
if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null) _coverImageAsBackgroundRenderer.Draw(sender, ds);
{
DrawCoverImage(sender, ds);
}
// Lyrics only layer // Lyrics only layer
using var lyrics = new CanvasCommandList(sender); using var lyrics = new CanvasCommandList(sender);
using (var lyricsDs = lyrics.CreateDrawingSession()) using (var lyricsDs = lyrics.CreateDrawingSession())
{ {
DrawLyrics(sender, lyricsDs, r, g, b); _pureLyricsRenderer.Draw(sender, lyricsDs, r, g, b);
} }
using var glowedLyrics = new CanvasCommandList(sender); using var glowedLyrics = new CanvasCommandList(sender);
@@ -529,158 +462,6 @@ namespace BetterLyrics.WinUI3.Views
ds.DrawImage(maskedCombinedBlurredLyrics); 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( private void DrawGradientOpacityMask(
ICanvasAnimatedControl control, ICanvasAnimatedControl control,
CanvasDrawingSession ds, CanvasDrawingSession ds,
@@ -711,12 +492,12 @@ namespace BetterLyrics.WinUI3.Views
CanvasAnimatedUpdateEventArgs args CanvasAnimatedUpdateEventArgs args
) )
{ {
_currentTime += args.Timing.ElapsedTime; _pureLyricsRenderer.CurrentTime += args.Timing.ElapsedTime;
if (SettingsService.IsDynamicCoverOverlay) if (SettingsService.IsDynamicCoverOverlay)
{ {
_coverBitmapRotateAngle += _coverRotateSpeed; _coverImageAsBackgroundRenderer.RotateAngle += _coverRotateSpeed;
_coverBitmapRotateAngle %= MathF.PI * 2; _coverImageAsBackgroundRenderer.RotateAngle %= MathF.PI * 2;
} }
if (SettingsService.IsLyricsDynamicGlowEffectEnabled) if (SettingsService.IsLyricsDynamicGlowEffectEnabled)
{ {
@@ -724,32 +505,24 @@ namespace BetterLyrics.WinUI3.Views
_lyricsGlowEffectAngle %= MathF.PI * 2; _lyricsGlowEffectAngle %= MathF.PI * 2;
} }
if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null) _coverImageAsBackgroundRenderer.Calculate(sender);
{
var diagonal = Math.Sqrt(
Math.Pow(_lyricsAreaWidth, 2) + Math.Pow(_lyricsAreaHeight, 2)
);
_coverScaleFactor = if (_pureLyricsRenderer.LyricsLines.LastOrDefault()?.TextLayout == null)
(float)diagonal / Math.Min(_coverImagePixelWidth, _coverImagePixelHeight);
}
if (_lyricsLines.LastOrDefault()?.TextLayout == null)
{ {
LayoutLyrics(); _pureLyricsRenderer.ReLayoutAsync(sender);
} }
int currentPlayingLineIndex = GetCurrentPlayingLineIndex(); int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
UpdateScaleAndOpacity(currentPlayingLineIndex); _pureLyricsRenderer.CalculateScaleAndOpacity(currentPlayingLineIndex);
UpdatePosition(currentPlayingLineIndex); _pureLyricsRenderer.CalculatePosition(sender, currentPlayingLineIndex);
} }
private int GetCurrentPlayingLineIndex() private int GetCurrentPlayingLineIndex()
{ {
for (int i = 0; i < _lyricsLines.Count; i++) for (int i = 0; i < _pureLyricsRenderer.LyricsLines.Count; i++)
{ {
var line = _lyricsLines[i]; var line = _pureLyricsRenderer.LyricsLines[i];
if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds) if (line.EndPlayingTimestampMs < _pureLyricsRenderer.CurrentTime.TotalMilliseconds)
{ {
continue; continue;
} }
@@ -759,239 +532,6 @@ namespace BetterLyrics.WinUI3.Views
return -1; 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) private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{ {
InitMediaManager(); InitMediaManager();
@@ -1035,10 +575,8 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
) )
{ {
if (SettingsService.IsImmersiveMode) if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
( BottomCommandGrid.Opacity = .5;
(Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeInStoryboard"]
).Begin();
} }
private void BottomCommandGrid_PointerExited( private void BottomCommandGrid_PointerExited(
@@ -1046,10 +584,21 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
) )
{ {
if (SettingsService.IsImmersiveMode) if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == .5)
( BottomCommandGrid.Opacity = 0;
(Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeOutStoryboard"] }
).Begin();
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

@@ -17,7 +17,7 @@ Your smooth dynamic local lyrics display built with WinUI 3
- Dynamic blur album art as background - Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects - Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song - 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... 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) - 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) - Language (English, Simplified Chinese, Traditional Chinese)
@@ -49,22 +49,26 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
### Split view ### 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) ![alt text](Screenshots/Snipaste_2025-06-03_16-47-43.png)
### Lyrics only
![alt text](Screenshots/Snipaste_2025-06-03_17-51-22.png) ![alt text](Screenshots/Snipaste_2025-06-03_17-51-22.png)
### Fullscreen ### 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) ![alt text](Screenshots/Snipaste_2025-06-03_18-36-05.png)
### Settings ### Settings
![alt text](Screenshots/Snipaste_2025-06-03_17-51-52.png) ![alt text](Screenshots/Snipaste_2025-06-07_17-32-02.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-00.png) ![alt text](Screenshots/Snipaste_2025-06-07_17-32-17.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-05.png) ![alt text](Screenshots/Snipaste_2025-06-07_17-32-23.png)
![alt text](Screenshots/Snipaste_2025-06-03_17-52-11.png)
## Download it now ## 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) > **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"> > Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
<img src="https://pngimg.com/uploads/google_drive/google_drive_PNG9.png" width="100"/>
</a>
> .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 ## Many thanks to
@@ -102,21 +126,21 @@ Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
## Third-party libraries that this project uses ## 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.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" 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="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.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.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" /> <PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <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="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" /> <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