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...
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
if (_localSettings.Values.TryGetValue(key, out object? value))
|
||||
|
||||
@@ -369,4 +369,7 @@
|
||||
<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>
|
||||
@@ -369,4 +369,7 @@
|
||||
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
|
||||
<value>再次悬停以显示切换按钮</value>
|
||||
</data>
|
||||
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
|
||||
<value> 不再显示此消息</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -369,4 +369,7 @@
|
||||
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
|
||||
<value>再次懸停以顯示切換按鈕</value>
|
||||
</data>
|
||||
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
|
||||
<value>不再顯示此訊息</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -3,13 +3,80 @@ 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ 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;
|
||||
@@ -75,9 +77,12 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
|
||||
if (existed)
|
||||
{
|
||||
BaseWindow.StackedNotificationsBehavior?.Show(
|
||||
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"),
|
||||
Helper.AnimationHelper.StackedNotificationsShowingDuration
|
||||
WeakReferenceMessenger.Default.Send(
|
||||
new ShowNotificatonMessage(
|
||||
new Notification(
|
||||
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -23,41 +23,23 @@
|
||||
Height="{StaticResource TitleBarCompactHeight}"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent">
|
||||
|
||||
<Grid.Resources>
|
||||
|
||||
<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>
|
||||
<Grid.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</Grid.OpacityTransition>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeInStoryboard}" />
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
Binding="{x:Bind WindowModel.SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeOutStoryboard}" />
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
@@ -157,11 +139,21 @@
|
||||
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="Informational">
|
||||
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 -->
|
||||
@@ -182,7 +174,6 @@
|
||||
</Storyboard>
|
||||
</InfoBar.Resources>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
using System;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Messages;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
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.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.UI.Input;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// 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; }
|
||||
|
||||
private SettingsService SettingsService { get; set; }
|
||||
|
||||
public static StackedNotificationsBehavior? StackedNotificationsBehavior
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public BaseWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
StackedNotificationsBehavior = NotificationQueue;
|
||||
AppWindow.Changed += AppWindow_Changed;
|
||||
|
||||
WindowModel = Ioc.Default.GetService<BaseWindowModel>()!;
|
||||
|
||||
SettingsService = Ioc.Default.GetService<SettingsService>()!;
|
||||
SettingsService.PropertyChanged += SettingsService_PropertyChanged;
|
||||
WindowModel.SettingsService.PropertyChanged += SettingsService_PropertyChanged;
|
||||
|
||||
SettingsService_PropertyChanged(
|
||||
null,
|
||||
@@ -75,6 +58,11 @@ namespace BetterLyrics.WinUI3.Views
|
||||
SetTitleBar(TopCommandGrid);
|
||||
}
|
||||
|
||||
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
|
||||
{
|
||||
UpdateTitleBarWindowButtonsVisibility();
|
||||
}
|
||||
|
||||
private void SettingsService_PropertyChanged(
|
||||
object? sender,
|
||||
System.ComponentModel.PropertyChangedEventArgs e
|
||||
@@ -82,17 +70,17 @@ namespace BetterLyrics.WinUI3.Views
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Services.Settings.SettingsService.Theme):
|
||||
RootGrid.RequestedTheme = (ElementTheme)SettingsService.Theme;
|
||||
case nameof(SettingsService.Theme):
|
||||
RootGrid.RequestedTheme = (ElementTheme)WindowModel.SettingsService.Theme;
|
||||
break;
|
||||
case nameof(Services.Settings.SettingsService.BackdropType):
|
||||
case nameof(SettingsService.BackdropType):
|
||||
SystemBackdrop = null;
|
||||
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
|
||||
(BackdropType)SettingsService.BackdropType
|
||||
(BackdropType)WindowModel.SettingsService.BackdropType
|
||||
);
|
||||
break;
|
||||
case nameof(Services.Settings.SettingsService.TitleBarType):
|
||||
switch ((TitleBarType)SettingsService.TitleBarType)
|
||||
case nameof(SettingsService.TitleBarType):
|
||||
switch ((TitleBarType)WindowModel.SettingsService.TitleBarType)
|
||||
{
|
||||
case TitleBarType.Compact:
|
||||
TopCommandGrid.Height = (double)
|
||||
@@ -169,6 +157,8 @@ namespace BetterLyrics.WinUI3.Views
|
||||
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
|
||||
{
|
||||
MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible;
|
||||
FullScreenFlyoutItem.IsChecked = false;
|
||||
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
|
||||
|
||||
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
|
||||
{
|
||||
@@ -188,6 +178,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
RestoreButton.Visibility =
|
||||
AOTFlyoutItem.Visibility =
|
||||
Visibility.Collapsed;
|
||||
FullScreenFlyoutItem.IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +192,10 @@ namespace BetterLyrics.WinUI3.Views
|
||||
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
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)
|
||||
@@ -217,9 +211,14 @@ namespace BetterLyrics.WinUI3.Views
|
||||
break;
|
||||
case AppWindowPresenterKind.Overlapped:
|
||||
AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
|
||||
StackedNotificationsBehavior?.Show(
|
||||
App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
|
||||
AnimationHelper.StackedNotificationsShowingDuration
|
||||
WeakReferenceMessenger.Default.Send(
|
||||
new ShowNotificatonMessage(
|
||||
new Models.Notification(
|
||||
App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
|
||||
isForeverDismissable: true,
|
||||
relatedSettingsKeyName: SettingsKeys.NeverShowEnterFullScreenMessage
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -227,9 +226,6 @@ namespace BetterLyrics.WinUI3.Views
|
||||
}
|
||||
|
||||
UpdateTitleBarWindowButtonsVisibility();
|
||||
|
||||
FullScreenFlyoutItem.IsChecked =
|
||||
AppWindow.Presenter.Kind == AppWindowPresenterKind.FullScreen;
|
||||
}
|
||||
|
||||
private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
|
||||
@@ -15,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>
|
||||
@@ -35,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>
|
||||
@@ -113,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"
|
||||
@@ -357,25 +335,9 @@
|
||||
Opacity=".5"
|
||||
PointerEntered="BottomCommandGrid_PointerEntered"
|
||||
PointerExited="BottomCommandGrid_PointerExited">
|
||||
|
||||
<Grid.Resources>
|
||||
|
||||
<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>
|
||||
<Grid.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</Grid.OpacityTransition>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
@@ -383,13 +345,13 @@
|
||||
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" />
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0.5" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" />
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
@@ -1,33 +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 DevWinUI;
|
||||
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 Microsoft.UI.Xaml.Media.Animation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Media;
|
||||
using Windows.Media.Control;
|
||||
using Color = Windows.UI.Color;
|
||||
|
||||
@@ -44,14 +39,8 @@ namespace BetterLyrics.WinUI3.Views
|
||||
public MainViewModel ViewModel => (MainViewModel)DataContext;
|
||||
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;
|
||||
|
||||
@@ -61,33 +50,6 @@ namespace BetterLyrics.WinUI3.Views
|
||||
private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
|
||||
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 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(
|
||||
object? sender,
|
||||
System.ComponentModel.PropertyChangedEventArgs e
|
||||
@@ -130,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)
|
||||
@@ -152,9 +106,14 @@ namespace BetterLyrics.WinUI3.Views
|
||||
break;
|
||||
case nameof(SettingsService.IsImmersiveMode):
|
||||
if (SettingsService.IsImmersiveMode)
|
||||
BaseWindow.StackedNotificationsBehavior?.Show(
|
||||
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
|
||||
AnimationHelper.StackedNotificationsShowingDuration
|
||||
WeakReferenceMessenger.Default.Send(
|
||||
new ShowNotificatonMessage(
|
||||
new Notification(
|
||||
App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
|
||||
isForeverDismissable: true,
|
||||
relatedSettingsKeyName: SettingsKeys.NeverShowEnterImmersiveModeMessage
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -162,7 +121,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(
|
||||
private async void ViewModel_PropertyChanged(
|
||||
object? sender,
|
||||
System.ComponentModel.PropertyChangedEventArgs e
|
||||
)
|
||||
@@ -170,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;
|
||||
@@ -216,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);
|
||||
}
|
||||
|
||||
@@ -333,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;
|
||||
|
||||
@@ -347,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);
|
||||
@@ -363,7 +324,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
ViewModel.AboutToUpdateUI = false;
|
||||
|
||||
if (_lyricsLines.Count == 0)
|
||||
if (_pureLyricsRenderer.LyricsLines.Count == 0)
|
||||
{
|
||||
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
|
||||
private void LyricsCanvas_Draw(
|
||||
ICanvasAnimatedControl sender,
|
||||
@@ -416,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);
|
||||
@@ -529,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,
|
||||
@@ -711,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)
|
||||
{
|
||||
@@ -724,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;
|
||||
}
|
||||
@@ -759,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();
|
||||
@@ -1035,10 +575,8 @@ namespace BetterLyrics.WinUI3.Views
|
||||
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
|
||||
)
|
||||
{
|
||||
if (SettingsService.IsImmersiveMode)
|
||||
(
|
||||
(Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeInStoryboard"]
|
||||
).Begin();
|
||||
if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
|
||||
BottomCommandGrid.Opacity = .5;
|
||||
}
|
||||
|
||||
private void BottomCommandGrid_PointerExited(
|
||||
@@ -1046,10 +584,21 @@ namespace BetterLyrics.WinUI3.Views
|
||||
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
|
||||
)
|
||||
{
|
||||
if (SettingsService.IsImmersiveMode)
|
||||
(
|
||||
(Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeOutStoryboard"]
|
||||
).Begin();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
README.md
@@ -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
|
||||
|
||||

|
||||
Non-immersive mode
|
||||
|
||||

|
||||
|
||||
Immersive mode
|
||||

|
||||
|
||||
### Lyrics only
|
||||
|
||||

|
||||
|
||||
### Fullscreen
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### Settings
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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" />
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
BIN
Screenshots/Snipaste_2025-06-07_17-32-02.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
Screenshots/Snipaste_2025-06-07_17-32-17.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
Screenshots/Snipaste_2025-06-07_17-32-23.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
Screenshots/Snipaste_2025-06-07_17-36-26.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |