feat: enhance ValueTransition (support keyframes)

This commit is contained in:
Zhe Fang
2026-01-08 20:58:12 -05:00
parent 6f60952d09
commit 92e4b9468c
36 changed files with 669 additions and 373 deletions

View File

@@ -282,12 +282,6 @@
</dev:SettingsExpander.ItemsHeader> </dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander> </dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed"> <dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>

View File

@@ -58,42 +58,42 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ValueTransition<Color> _immersiveBgColorTransition = new( private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new( private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f, initialValue: 1f,
durationSeconds: 0.3f defaultTotalDuration: 0.3f
); );
private readonly ValueTransition<Color> _accentColor1Transition = new( private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor2Transition = new( private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor3Transition = new( private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor4Transition = new( private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _canvasYScrollTransition = new( private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
private readonly ValueTransition<double> _mouseYScrollTransition = new( private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
private TimeSpan _songPositionWithOffset; private TimeSpan _songPositionWithOffset;
@@ -292,7 +292,7 @@ namespace BetterLyrics.WinUI3.Controls
} }
else if (e.Property == MouseScrollOffsetProperty) else if (e.Property == MouseScrollOffsetProperty)
{ {
canvas._mouseYScrollTransition.StartTransition(Convert.ToDouble(e.NewValue)); canvas._mouseYScrollTransition.Start(Convert.ToDouble(e.NewValue));
} }
else if (e.Property == MousePositionProperty) else if (e.Property == MousePositionProperty)
{ {
@@ -318,11 +318,11 @@ namespace BetterLyrics.WinUI3.Controls
else if (e.Property == AlbumArtThemeColorsProperty) else if (e.Property == AlbumArtThemeColorsProperty)
{ {
var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue; var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue;
canvas._immersiveBgColorTransition.StartTransition(albumArtThemeColors.EnvColor); canvas._immersiveBgColorTransition.Start(albumArtThemeColors.EnvColor);
canvas._accentColor1Transition.StartTransition(albumArtThemeColors.AccentColor1); canvas._accentColor1Transition.Start(albumArtThemeColors.AccentColor1);
canvas._accentColor2Transition.StartTransition(albumArtThemeColors.AccentColor2); canvas._accentColor2Transition.Start(albumArtThemeColors.AccentColor2);
canvas._accentColor3Transition.StartTransition(albumArtThemeColors.AccentColor3); canvas._accentColor3Transition.Start(albumArtThemeColors.AccentColor3);
canvas._accentColor4Transition.StartTransition(albumArtThemeColors.AccentColor4); canvas._accentColor4Transition.Start(albumArtThemeColors.AccentColor4);
canvas._albumArtThemeColors = albumArtThemeColors; canvas._albumArtThemeColors = albumArtThemeColors;
canvas._isLayoutChanged = true; canvas._isLayoutChanged = true;
@@ -344,7 +344,6 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings; var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _gsmtcService.CurrentSongInfo.DurationMs; double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor; Color overlayColor;
double finalOpacity; double finalOpacity;
@@ -410,7 +409,7 @@ namespace BetterLyrics.WinUI3.Controls
return _synchronizer.GetLinePlayingProgress( return _synchronizer.GetLinePlayingProgress(
_songPositionWithOffset.TotalMilliseconds, _songPositionWithOffset.TotalMilliseconds,
line, line,
isForceWordByWord lyricsEffect.WordByWordEffectMode
); );
} }
); );
@@ -486,9 +485,16 @@ namespace BetterLyrics.WinUI3.Controls
var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _primaryPlayingLineIndex); var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _primaryPlayingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value; if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType); if (_isLayoutChanged)
_canvasYScrollTransition.SetDuration(lyricsEffect.LyricsScrollDuration / 1000.0); {
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset, _isLayoutChanged); _canvasYScrollTransition.JumpTo(_canvasTargetScrollOffset);
}
else
{
_canvasYScrollTransition.SetDurationMs(lyricsEffect.LyricsScrollDuration);
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType);
_canvasYScrollTransition.Start(_canvasTargetScrollOffset);
}
} }
_canvasYScrollTransition.Update(elapsedTime); _canvasYScrollTransition.Update(elapsedTime);

View File

@@ -23,6 +23,14 @@
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" /> Text="Effect" />
<dev:SettingsCard x:Uid="SettingsPageLyricsWordByWordEffectMode" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF714;}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.WordByWordEffectMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAuto" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeNever" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAlways" />
</ComboBox>
</dev:SettingsCard>
<!-- 模糊效果 --> <!-- 模糊效果 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}"> <dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" />

View File

@@ -365,10 +365,6 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- Lyrics translation --> <!-- Lyrics translation -->
<TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True"> <dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True">

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum WordByWordEffectMode
{
Auto,
Never,
Always,
}
}

View File

@@ -56,6 +56,57 @@
} }
catch (Exception) { } catch (Exception) { }
} }
/// <summary>
/// https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
/// </summary>
/// <param name="sourceDir"></param>
/// <param name="destinationDir"></param>
/// <param name="recursive"></param>
/// <exception cref="DirectoryNotFoundException"></exception>
public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
CopyLockedFile(file.FullName, targetFilePath);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
private static void CopyLockedFile(string sourcePath, string targetPath)
{
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var destStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
sourceStream.CopyTo(destStream);
}
}
} }
} }
} }

View File

@@ -42,19 +42,23 @@ namespace BetterLyrics.WinUI3.Helper
return file; return file;
} }
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{ {
var window = WindowHook.GetWindow<T>(); var window = WindowHook.GetWindow<T>();
return await PickSaveFileAsync(window, fileTypeChoices); return await PickSaveFileAsync(window, fileTypeChoices, suggestedFileName);
} }
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{ {
if (window == null) return null; if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker(); var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.FileTypeChoices.AddRange(fileTypeChoices); picker.FileTypeChoices.AddRange(fileTypeChoices);
if (suggestedFileName != null)
{
picker.SuggestedFileName = suggestedFileName;
}
var hwnd = WindowNative.GetWindowHandle(window); var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd); InitializeWithWindow.Initialize(picker, hwnd);

View File

@@ -1,163 +1,252 @@
// 2025/6/23 by Zhe Fang using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Enums;
using System; using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
public class ValueTransition<T> public class ValueTransition<T> where T : struct
where T : struct
{ {
// 状态变量
private T _currentValue; private T _currentValue;
private double _durationSeconds;
private double _delaySeconds;
private double _delayRemaining;
private EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress;
private T _startValue; private T _startValue;
private T _targetValue; private T _targetValue;
public double DurationSeconds => _durationSeconds; // 核心队列
public double DelaySeconds => _delaySeconds; private readonly Queue<Keyframe<T>> _keyframeQueue = new Queue<Keyframe<T>>();
public bool IsTransitioning => _isTransitioning; // 时间控制
private double _stepDuration; // 当前这一段的时长 (动态变化)
private double _totalDurationForAutoSplit; // 自动均分模式的总时长
private double _configuredDelaySeconds; // 配置的延迟时长
// 动画状态
private Enums.EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress; // 当前段的进度 (0.0 ~ 1.0)
// 公开属性
public T Value => _currentValue; public T Value => _currentValue;
public T StartValue => _startValue; public bool IsTransitioning => _isTransitioning;
public T TargetValue => _targetValue; public T TargetValue => _targetValue; // 获取当前段的目标值
public EasingType? EasingType => _easingType; public Enums.EasingType? EasingType => _easingType;
public double Progress => _progress; public double DurationSeconds => _totalDurationForAutoSplit;
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0) public ValueTransition(T initialValue, double defaultTotalDuration = 0.3, EasingType? defaultEasingType = null, Func<T, T, double, T>? interpolator = null)
{ {
_currentValue = initialValue; _currentValue = initialValue;
_startValue = initialValue; _startValue = initialValue;
_targetValue = initialValue; _targetValue = initialValue;
_durationSeconds = durationSeconds; _totalDurationForAutoSplit = defaultTotalDuration;
_delaySeconds = delaySeconds;
_delayRemaining = 0; if (interpolator == null)
_progress = 1f; {
_isTransitioning = false; // 默认缓动
SetEasingType(Enums.EasingType.EaseInOutQuad);
}
else
{
_easingType = null;
_interpolator = interpolator;
}
if (interpolator != null) if (interpolator != null)
{ {
_interpolator = interpolator; _interpolator = interpolator;
_easingType = null; _easingType = null;
} }
else if (easingType.HasValue) else if (defaultEasingType != null)
{ {
_easingType = easingType; SetEasingType(defaultEasingType);
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
} }
else else
{ {
_easingType = Enums.EasingType.EaseInOutQuad; SetEasingType(Enums.EasingType.EaseInOutQuad);
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
} }
} }
#region Configuration
public void SetDuration(double seconds) public void SetDuration(double seconds)
{ {
if (seconds < 0) if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds));
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive."); _totalDurationForAutoSplit = seconds;
_durationSeconds = seconds;
} }
public void SetDurationMs(double millionSeconds) public void SetDurationMs(double millionSeconds) => SetDuration(millionSeconds / 1000.0);
{
SetDuration(millionSeconds / 1000.0);
}
public void SetDuration(TimeSpan timeSpan)
{
SetDuration(timeSpan.TotalSeconds);
}
/// <summary>
/// 设置启动延迟。
/// 原理:在动画队列最前方插入一个“数值不变”的关键帧。
/// </summary>
public void SetDelay(double seconds) public void SetDelay(double seconds)
{ {
_delaySeconds = seconds; _configuredDelaySeconds = seconds;
} }
private void JumpTo(T value) public void SetEasingType(Enums.EasingType? easingType)
{ {
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
#endregion
#region Control Methods
/// <summary>
/// 立即跳转到指定值(停止动画)
/// </summary>
public void JumpTo(T value)
{
_keyframeQueue.Clear();
_currentValue = value; _currentValue = value;
_startValue = value; _startValue = value;
_targetValue = value; _targetValue = value;
_progress = 1f;
_delayRemaining = 0;
_isTransitioning = false; _isTransitioning = false;
_progress = 0;
} }
public void Reset(T value) /// <summary>
/// 模式 A: 精确控制模式
/// 显式指定每一段的目标值和时长。
/// </summary>
public void Start(params Keyframe<T>[] keyframes)
{ {
_currentValue = value; if (keyframes == null || keyframes.Length == 0) return;
_startValue = value;
_targetValue = value;
_progress = 0f;
_delayRemaining = 0;
_isTransitioning = false;
}
public void StartTransition(T targetValue, bool jumpTo = false) PrepareStart();
{
if (jumpTo) // 1. 处理延迟 (插入静止帧)
if (_configuredDelaySeconds > 0)
{ {
JumpTo(targetValue); _keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
return;
} }
if (!targetValue.Equals(_currentValue)) // 2. 入队用户帧
foreach (var kf in keyframes)
{ {
_startValue = _currentValue; _keyframeQueue.Enqueue(kf);
_targetValue = targetValue;
_progress = 0f;
_delayRemaining = _delaySeconds;
_isTransitioning = true;
} }
MoveToNextSegment(firstStart: true);
} }
public static bool Equals(double x, double y, double tolerance) /// <summary>
/// 模式 B: 自动均分模式 (兼容旧写法)
/// 指定一串目标值,系统根据 SetDuration 的总时长平均分配。
/// </summary>
public void Start(params T[] values)
{ {
var diff = Math.Abs(x - y); if (values == null || values.Length == 0) return;
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
// 如果目标就是当前值且只有1帧直接跳过以省性能
if (values.Length == 1 && values[0].Equals(_currentValue) && _configuredDelaySeconds <= 0) return;
PrepareStart();
// 1. 处理延迟
if (_configuredDelaySeconds > 0)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
}
// 2. 计算均分时长
double autoStepDuration = _totalDurationForAutoSplit / values.Length;
// 3. 入队生成帧
foreach (var val in values)
{
_keyframeQueue.Enqueue(new Keyframe<T>(val, autoStepDuration));
}
MoveToNextSegment(firstStart: true);
}
#endregion
#region Core Logic
private void PrepareStart()
{
_keyframeQueue.Clear();
_isTransitioning = true;
}
private void MoveToNextSegment(bool firstStart = false)
{
if (_keyframeQueue.Count > 0)
{
var kf = _keyframeQueue.Dequeue();
// 起点逻辑:如果是刚开始,起点是当前值;如果是中间切换,起点是上一段的终点
_startValue = firstStart ? _currentValue : _targetValue;
_targetValue = kf.Value;
_stepDuration = kf.Duration;
if (firstStart) _progress = 0f;
// 注意:非 firstStart 时不重置 _progress保留溢出值以平滑过渡
}
else
{
// 队列耗尽,动画结束
_currentValue = _targetValue;
_isTransitioning = false;
_progress = 1f;
}
} }
public void Update(TimeSpan elapsedTime) public void Update(TimeSpan elapsedTime)
{ {
if (!_isTransitioning) return; if (!_isTransitioning) return;
if (_delayRemaining > 0) double timeStep = elapsedTime.TotalSeconds;
{
double consume = Math.Min(_delayRemaining, elapsedTime.TotalSeconds);
_delayRemaining -= consume;
if (_delayRemaining > 0)
return;
elapsedTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds - consume);
}
if (_durationSeconds <= 0) // 使用 while 处理单帧时间过长跨越多段的情况
while (timeStep > 0 && _isTransitioning)
{ {
_progress = 1f; // 计算当前帧的步进比例
} // 极小值保护防止除以0
else double progressDelta = (_stepDuration > 0.000001) ? (timeStep / _stepDuration) : 1.0;
{
_progress += elapsedTime.TotalSeconds / _durationSeconds;
}
if (_progress >= 1f) if (_progress + progressDelta >= 1.0)
{ {
_progress = 1f; // === 当前段结束 ===
_currentValue = _targetValue;
_isTransitioning = false; // 1. 计算这一段实际消耗的时间
} double timeConsumed = (1.0 - _progress) * _stepDuration;
else
{ // 2. 剩余时间留给下一段
_currentValue = _interpolator(_startValue, _targetValue, _progress); timeStep -= timeConsumed;
// 3. 修正当前值到目标值
_progress = 1.0;
_currentValue = _targetValue;
// 4. 切换到下一段
MoveToNextSegment();
// 5. 如果还有下一段,进度归零
if (_isTransitioning) _progress = 0f;
}
else
{
// === 当前段进行中 ===
_progress += progressDelta;
timeStep = 0; // 时间耗尽
// 插值计算
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
} }
} }
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type) #endregion
#region Interpolators
private Func<T, T, double, T> GetInterpolatorByEasingType(Enums.EasingType? type)
{ {
if (typeof(T) == typeof(double)) if (typeof(T) == typeof(double))
{ {
@@ -166,58 +255,32 @@ namespace BetterLyrics.WinUI3.Helper
double s = (double)(object)start; double s = (double)(object)start;
double e = (double)(object)end; double e = (double)(object)end;
double t = progress; double t = progress;
// 使用 EasingHelper (假设您的项目中已有此辅助类)
switch (type) switch (type)
{ {
case Enums.EasingType.EaseInOutSine: case Enums.EasingType.EaseInOutSine: t = EasingHelper.EaseInOutSine(t); break;
t = EasingHelper.EaseInOutSine(t); case Enums.EasingType.EaseInOutQuad: t = EasingHelper.EaseInOutQuad(t); break;
break; case Enums.EasingType.EaseInOutCubic: t = EasingHelper.EaseInOutCubic(t); break;
case Enums.EasingType.EaseInOutQuad: case Enums.EasingType.EaseInOutQuart: t = EasingHelper.EaseInOutQuart(t); break;
t = EasingHelper.EaseInOutQuad(t); case Enums.EasingType.EaseInOutQuint: t = EasingHelper.EaseInOutQuint(t); break;
break; case Enums.EasingType.EaseInOutExpo: t = EasingHelper.EaseInOutExpo(t); break;
case Enums.EasingType.EaseInOutCubic: case Enums.EasingType.EaseInOutCirc: t = EasingHelper.EaseInOutCirc(t); break;
t = EasingHelper.EaseInOutCubic(t); case Enums.EasingType.EaseInOutBack: t = EasingHelper.EaseInOutBack(t); break;
break; case Enums.EasingType.EaseInOutElastic: t = EasingHelper.EaseInOutElastic(t); break;
case Enums.EasingType.EaseInOutQuart: case Enums.EasingType.EaseInOutBounce: t = EasingHelper.EaseInOutBounce(t); break;
t = EasingHelper.EaseInOutQuart(t); case Enums.EasingType.SmoothStep: t = EasingHelper.SmoothStep(t); break;
break; case Enums.EasingType.Linear: t = EasingHelper.Linear(t); break;
case Enums.EasingType.EaseInOutQuint: default: t = EasingHelper.EaseInOutQuad(t); break;
t = EasingHelper.EaseInOutQuint(t);
break;
case Enums.EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case Enums.EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case Enums.EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case Enums.EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case Enums.EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case Enums.EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case Enums.EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
t = EasingHelper.EaseInOutQuad(t);
break;
} }
return (T)(object)(s + (e - s) * t); return (T)(object)(s + (e - s) * t);
}; };
} }
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
} }
public void SetEasingType(EasingType? easingType) #endregion
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
} }
} }

View File

@@ -95,13 +95,13 @@ namespace BetterLyrics.WinUI3.Logic
line.BlurAmountTransition.SetDuration(yScrollDuration); line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay); line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.StartTransition( line.BlurAmountTransition.Start(
(isMouseScrolling || isSecondaryLinePlaying) ? 0 : (isMouseScrolling || isSecondaryLinePlaying) ? 0 :
(lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0)); (lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
line.ScaleTransition.SetDuration(yScrollDuration); line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay); line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.StartTransition( line.ScaleTransition.Start(
isSecondaryLinePlaying ? _highlightedScale : isSecondaryLinePlaying ? _highlightedScale :
(lyricsEffect.IsLyricsOutOfSightEffectEnabled ? (lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) : (_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
@@ -109,37 +109,37 @@ namespace BetterLyrics.WinUI3.Logic
line.PhoneticOpacityTransition.SetDuration(yScrollDuration); line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay); line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.StartTransition( line.PhoneticOpacityTransition.Start(
isSecondaryLinePlaying ? phoneticOpacity : isSecondaryLinePlaying ? phoneticOpacity :
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(已播放) // 原文不透明度(已播放)
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.StartTransition( line.PlayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? 1.0 : isSecondaryLinePlaying ? 1.0 :
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(未播放) // 原文不透明度(未播放)
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.StartTransition( line.UnplayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? originalOpacity : isSecondaryLinePlaying ? originalOpacity :
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration); line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay); line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.StartTransition( line.TranslatedOpacityTransition.Start(
isSecondaryLinePlaying ? translatedOpacity : isSecondaryLinePlaying ? translatedOpacity :
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.ColorTransition.SetDuration(yScrollDuration); line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay); line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.StartTransition(isSecondaryLinePlaying ? fgColor : bgColor); line.ColorTransition.Start(isSecondaryLinePlaying ? fgColor : bgColor);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType); line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration); line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay); line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.StartTransition( line.AngleTransition.Start(
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ? (lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) : Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0); 0);
@@ -149,7 +149,7 @@ namespace BetterLyrics.WinUI3.Logic
line.YOffsetTransition.SetDelay(yScrollDelay); line.YOffsetTransition.SetDelay(yScrollDelay);
// 设计之初是当 isLayoutChanged 为真时 jumpTo // 设计之初是当 isLayoutChanged 为真时 jumpTo
// 但考虑到动画视觉,强制使用动画 // 但考虑到动画视觉,强制使用动画
line.YOffsetTransition.StartTransition(targetYScrollOffset); line.YOffsetTransition.Start(targetYScrollOffset);
} }
var maxAnimationDurationMs = Math.Max(line.EndMs - currentPositionMs, 0); var maxAnimationDurationMs = Math.Max(line.EndMs - currentPositionMs, 0);
@@ -170,10 +170,15 @@ namespace BetterLyrics.WinUI3.Logic
switch (lyricsEffect.LyricsGlowEffectScope) switch (lyricsEffect.LyricsGlowEffectScope)
{ {
case Enums.LyricsEffectScope.LineStartToCurrentChar: case Enums.LyricsEffectScope.LineStartToCurrentChar:
if (isSecondaryLinePlayingChanged) if (isSecondaryLinePlayingChanged && isSecondaryLinePlaying)
{ {
renderChar.GlowTransition.SetDurationMs(Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs)); var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.StartTransition(isSecondaryLinePlaying ? targetGlow : 0); var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetGlow, stepInOutDuration),
new Models.Keyframe<double>(targetGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
} }
break; break;
default: default:
@@ -188,12 +193,12 @@ namespace BetterLyrics.WinUI3.Logic
if (isSecondaryLinePlayingChanged) if (isSecondaryLinePlayingChanged)
{ {
renderChar.FloatTransition.StartTransition(isSecondaryLinePlaying ? targetFloat : 0); renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetFloat : 0);
} }
if (isCharPlayingChanged) if (isCharPlayingChanged)
{ {
renderChar.FloatTransition.SetDurationMs(Math.Min(lyricsEffect.LyricsFloatAnimationDuration, maxAnimationDurationMs)); renderChar.FloatTransition.SetDurationMs(Math.Min(lyricsEffect.LyricsFloatAnimationDuration, maxAnimationDurationMs));
renderChar.FloatTransition.StartTransition(0); renderChar.FloatTransition.Start(0);
} }
} }
@@ -223,8 +228,14 @@ namespace BetterLyrics.WinUI3.Logic
{ {
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration) if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{ {
renderChar.ScaleTransition.SetDurationMs(Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0); if (isSyllablePlaying)
renderChar.ScaleTransition.StartTransition(isSyllablePlaying ? targetScale : 1); {
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
} }
} }
} }
@@ -239,8 +250,14 @@ namespace BetterLyrics.WinUI3.Logic
{ {
foreach (var renderChar in syllable.ChildrenRenderLyricsChars) foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{ {
renderChar.GlowTransition.SetDurationMs(Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0); if (isSyllablePlaying)
renderChar.GlowTransition.StartTransition(isSyllablePlaying ? targetGlow : 0); {
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetGlow, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
);
}
} }
} }
break; break;
@@ -256,20 +273,10 @@ namespace BetterLyrics.WinUI3.Logic
// 更新动画 // 更新动画
foreach (var renderChar in line.PrimaryRenderChars) foreach (var renderChar in line.PrimaryRenderChars)
{ {
renderChar.ScaleTransition.Update(elapsedTime); renderChar.Update(elapsedTime);
renderChar.GlowTransition.Update(elapsedTime);
renderChar.FloatTransition.Update(elapsedTime);
} }
line.AngleTransition.Update(elapsedTime); line.Update(elapsedTime);
line.ScaleTransition.Update(elapsedTime);
line.BlurAmountTransition.Update(elapsedTime);
line.PhoneticOpacityTransition.Update(elapsedTime);
line.PlayedOriginalOpacityTransition.Update(elapsedTime);
line.UnplayedOriginalOpacityTransition.Update(elapsedTime);
line.TranslatedOpacityTransition.Update(elapsedTime);
line.YOffsetTransition.Update(elapsedTime);
line.ColorTransition.Update(elapsedTime);
} }
} }

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics; using BetterLyrics.WinUI3.Models.Lyrics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -67,7 +68,7 @@ namespace BetterLyrics.WinUI3.Logic
public LinePlaybackState GetLinePlayingProgress( public LinePlaybackState GetLinePlayingProgress(
double currentTimeMs, double currentTimeMs,
RenderLyricsLine line, RenderLyricsLine line,
bool isForceWordByWord) WordByWordEffectMode wordByWordEffectMode)
{ {
var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 }; var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 };
@@ -87,23 +88,34 @@ namespace BetterLyrics.WinUI3.Logic
return state; return state;
} }
// 逐字 switch (wordByWordEffectMode)
if (line.PrimaryRenderSyllables != null && line.PrimaryRenderSyllables.Count > 1)
{ {
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs); case WordByWordEffectMode.Auto:
} if (line.PrimaryRenderSyllables.Count > 1)
{
// 强制逐字 return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
if (isForceWordByWord && line.PrimaryText.Length > 0) }
{ else
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs); {
} state.SyllableStartIndex = line.PrimaryText.Length;
else state.SyllableProgress = 1f;
{ return state;
// 普通行 }
state.SyllableStartIndex = line.PrimaryText.Length; case WordByWordEffectMode.Never:
state.SyllableProgress = 1f; state.SyllableStartIndex = line.PrimaryText.Length;
return state; state.SyllableProgress = 1f;
return state;
case WordByWordEffectMode.Always:
if (line.PrimaryRenderSyllables.Count > 1)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
else
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
default:
return state;
} }
} }

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public struct Keyframe<T>
{
public T Value { get; }
public double Duration { get; }
public Keyframe(T value, double durationSeconds)
{
Value = value;
Duration = durationSeconds;
}
}
}

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Constants; using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using System;
using Windows.Foundation; using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models.Lyrics namespace BetterLyrics.WinUI3.Models.Lyrics
@@ -19,20 +20,28 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
{ {
ScaleTransition = new( ScaleTransition = new(
initialValue: 1.0, initialValue: 1.0,
durationSeconds: Time.AnimationDuration.TotalSeconds, defaultTotalDuration: Time.AnimationDuration.TotalSeconds,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
GlowTransition = new( GlowTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: Time.AnimationDuration.TotalSeconds, defaultTotalDuration: Time.AnimationDuration.TotalSeconds,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
FloatTransition = new( FloatTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: Time.LongAnimationDuration.TotalSeconds, defaultTotalDuration: Time.LongAnimationDuration.TotalSeconds,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
LayoutRect = layoutRect; LayoutRect = layoutRect;
} }
public void Update(TimeSpan elapsedTime)
{
ScaleTransition.Update(elapsedTime);
GlowTransition.Update(elapsedTime);
FloatTransition.Update(elapsedTime);
}
} }
} }

View File

@@ -5,6 +5,7 @@ using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text; using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI; using Microsoft.UI;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -77,47 +78,47 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
{ {
AngleTransition = new( AngleTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
BlurAmountTransition = new( BlurAmountTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
PhoneticOpacityTransition = new( PhoneticOpacityTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
PlayedOriginalOpacityTransition = new( PlayedOriginalOpacityTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
UnplayedOriginalOpacityTransition = new( UnplayedOriginalOpacityTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
TranslatedOpacityTransition = new( TranslatedOpacityTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
ScaleTransition = new( ScaleTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
YOffsetTransition = new( YOffsetTransition = new(
initialValue: 0, initialValue: 0,
durationSeconds: AnimationDuration, defaultTotalDuration: AnimationDuration,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
ColorTransition = new( ColorTransition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
@@ -282,5 +283,18 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
} }
} }
public void Update(TimeSpan elapsedTime)
{
AngleTransition.Update(elapsedTime);
ScaleTransition.Update(elapsedTime);
BlurAmountTransition.Update(elapsedTime);
PhoneticOpacityTransition.Update(elapsedTime);
PlayedOriginalOpacityTransition.Update(elapsedTime);
UnplayedOriginalOpacityTransition.Update(elapsedTime);
TranslatedOpacityTransition.Update(elapsedTime);
YOffsetTransition.Update(elapsedTime);
ColorTransition.Update(elapsedTime);
}
} }
} }

View File

@@ -8,7 +8,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LanguageCode { get; set; } = ""; [ObservableProperty][NotifyPropertyChangedRecipients] public partial string LanguageCode { get; set; } = "";
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LXMusicServer { get; set; } = string.Empty; [ObservableProperty][NotifyPropertyChangedRecipients] public partial string LXMusicServer { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string AmllTtmlDbBaseUrl { get; set; } = Constants.AmllTTmlDB.BaseUrl; [ObservableProperty][NotifyPropertyChangedRecipients] public partial string AmllTtmlDbBaseUrl { get; set; } = Constants.AmllTTmlDB.BaseUrl;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsForceWordByWordEffect { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ShowOrHideLyricsWindowShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "H" }; [ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ShowOrHideLyricsWindowShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "H" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnLyricsWindowClosed { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnLyricsWindowClosed { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ListenOnNewPlaybackSource { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ListenOnNewPlaybackSource { get; set; } = true;

View File

@@ -6,6 +6,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
{ {
public partial class LyricsEffectSettings : ObservableRecipient, ICloneable public partial class LyricsEffectSettings : ObservableRecipient, ICloneable
{ {
[ObservableProperty][NotifyPropertyChangedRecipients] public partial WordByWordEffectMode WordByWordEffectMode { get; set; } = WordByWordEffectMode.Auto;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsBlurEffectEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsBlurEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsFadeOutEffectEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsFadeOutEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsOutOfSightEffectEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsOutOfSightEffectEnabled { get; set; } = true;
@@ -54,6 +56,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
{ {
return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType) return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType)
{ {
WordByWordEffectMode = this.WordByWordEffectMode,
IsLyricsBlurEffectEnabled = this.IsLyricsBlurEffectEnabled, IsLyricsBlurEffectEnabled = this.IsLyricsBlurEffectEnabled,
IsLyricsFadeOutEffectEnabled = this.IsLyricsFadeOutEffectEnabled, IsLyricsFadeOutEffectEnabled = this.IsLyricsFadeOutEffectEnabled,
IsLyricsOutOfSightEffectEnabled = this.IsLyricsOutOfSightEffectEnabled, IsLyricsOutOfSightEffectEnabled = this.IsLyricsOutOfSightEffectEnabled,

View File

@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Renderer
public CoverBackgroundRenderer() public CoverBackgroundRenderer()
{ {
_crossfadeTransition = new ValueTransition<double>(1.0, 0.7, easingType: EasingType.Linear); _crossfadeTransition = new ValueTransition<double>(1.0, 0.7, defaultEasingType: EasingType.Linear);
} }
public void SetCoverBitmap(CanvasBitmap? newBitmap) public void SetCoverBitmap(CanvasBitmap? newBitmap)
@@ -73,18 +73,18 @@ namespace BetterLyrics.WinUI3.Renderer
if (_currentBitmap == null) if (_currentBitmap == null)
{ {
_crossfadeTransition.StartTransition(1.0, jumpTo: true); _crossfadeTransition.JumpTo(1.0);
} }
else else
{ {
if (_previousBitmap == null) if (_previousBitmap == null)
{ {
_crossfadeTransition.StartTransition(1.0, jumpTo: true); _crossfadeTransition.JumpTo(1.0);
} }
else else
{ {
_crossfadeTransition.Reset(0.0); _crossfadeTransition.JumpTo(0.0);
_crossfadeTransition.StartTransition(1.0); _crossfadeTransition.Start(1.0);
} }
} }

View File

@@ -251,11 +251,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
.Where(x => x.MediaFolderId == folder.Id) .Where(x => x.MediaFolderId == folder.Id)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
// VACUUM 是 SQLite 特有的命令 await context.Database.ExecuteSqlRawAsync("VACUUM");
if (context.Database.IsSqlite())
{
await context.Database.ExecuteSqlRawAsync("VACUUM");
}
} }
finally finally
{ {

View File

@@ -8,5 +8,6 @@ namespace BetterLyrics.WinUI3.Services.LyricsCacheService
{ {
Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider); Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider);
Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result); Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result);
Task ClearCacheAsync();
} }
} }

View File

@@ -69,5 +69,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsCacheService
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task ClearCacheAsync()
{
using var context = await _contextFactory.CreateDbContextAsync();
await context.LyricsCache.ExecuteDeleteAsync();
await context.Database.ExecuteSqlRawAsync("VACUUM;");
}
} }
} }

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>فرض البقاء في المقدمة</value> <value>فرض البقاء في المقدمة</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>فرض محاكاة تأثير كلمة بكلمة حتى إذا كانت الكلمات الحالية لا تحتوي على معلومات كلمة بكلمة</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>فرض تفعيل تأثير الكلمات كلمة بكلمة</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>معدل إطارات التصيير (FPS)</value> <value>معدل إطارات التصيير (FPS)</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>دليل المستخدم</value> <value>دليل المستخدم</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Immer im Vordergrund erzwingen</value> <value>Immer im Vordergrund erzwingen</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Wort-für-Wort-Simulation erzwingen, auch wenn der aktuelle Songtext keine entsprechenden Infos enthält</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Wort-für-Wort-Effekt erzwingen</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Rendering-FPS</value> <value>Rendering-FPS</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Benutzerhandbuch</value> <value>Benutzerhandbuch</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -891,12 +891,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Force Always on Top</value> <value>Force Always on Top</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Force word-by-word simulation even if current lyrics don't have word-by-word info</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Force Word-by-Word Effect</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Render FPS</value> <value>Render FPS</value>
</data> </data>
@@ -1143,6 +1137,18 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve"> <data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Lyrics Window Status Switch Shortcut</value> <value>Lyrics Window Status Switch Shortcut</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value>Word-by-word Animation Strategy</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value>Always</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value>Auto</value>
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value>Never</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve"> <data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Adjusting this value will affect sequential search and best match search results, but will not affect search results in the manual lyrics search interface</value> <value>Adjusting this value will affect sequential search and best match search results, but will not affect search results in the manual lyrics search interface</value>
</data> </data>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Forzar siempre visible</value> <value>Forzar siempre visible</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Forzar simulación palabra por palabra incluso si la letra actual no tiene información palabra por palabra</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Forzar efecto palabra por palabra</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>FPS de renderizado</value> <value>FPS de renderizado</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Guía de usuario</value> <value>Guía de usuario</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Forcer toujours au premier plan</value> <value>Forcer toujours au premier plan</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Forcer la simulation mot à mot même si les paroles n'ont pas d'infos mot à mot</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Forcer l'effet mot à mot</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>FPS de rendu</value> <value>FPS de rendu</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Guide de l'utilisateur</value> <value>Guide de l'utilisateur</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>हमेशा शीर्ष पर रखने के लिए बाध्य करें</value> <value>हमेशा शीर्ष पर रखने के लिए बाध्य करें</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>शब्द-दर-शब्द प्रभाव का अनुकरण करने के लिए बाध्य करें, भले ही वर्तमान बोल में शब्द-दर-शब्द जानकारी न हो</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>शब्द-दर-शब्द प्रभाव सक्षम करने के लिए बाध्य करें</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>रेंडरिंग फ्रेम दर</value> <value>रेंडरिंग फ्रेम दर</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>उपयोगकर्ता गाइड</value> <value>उपयोगकर्ता गाइड</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Paksa Selalu di Atas</value> <value>Paksa Selalu di Atas</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Paksa simulasi efek kata-demi-kata meskipun lirik saat ini tidak memiliki informasi kata-demi-kata</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Paksa Aktifkan Efek Kata-demi-Kata</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Frame Rate Rendering</value> <value>Frame Rate Rendering</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Panduan Pengguna</value> <value>Panduan Pengguna</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>常にトップに強制的に表示</value> <value>常にトップに強制的に表示</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>現在の歌詞に文字単位の情報がない場合でも、文字単位の歌詞シミュレーションをします</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>文字単位の歌詞効果を強制する</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>描画 FPS</value> <value>描画 FPS</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>ユーザーガイド</value> <value>ユーザーガイド</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>항상 위에 표시 강제</value> <value>항상 위에 표시 강제</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>현재 가사에 글자 단위 정보가 없더라도 글자 단위 시뮬레이션을 강제합니다</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>단어 단위 효과 강제 적용</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>렌더링 FPS</value> <value>렌더링 FPS</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>사용자 가이드</value> <value>사용자 가이드</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Paksa Sentiasa di Atas</value> <value>Paksa Sentiasa di Atas</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Paksa simulasi kesan perkataan-demi-perkataan walaupun lirik semasa tidak mempunyai maklumat perkataan-demi-perkataan</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Paksa Dayakan Kesan Perkataan-demi-Perkataan</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Kadar Bingkai Penyajian (FPS)</value> <value>Kadar Bingkai Penyajian (FPS)</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Panduan Pengguna</value> <value>Panduan Pengguna</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Forçar Sempre no Topo</value> <value>Forçar Sempre no Topo</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Forçar a simulação do efeito palavra por palavra, mesmo que a letra atual não tenha informações palavra por palavra</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Forçar Efeito Palavra por Palavra</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Taxa de Fotogramas de Renderização</value> <value>Taxa de Fotogramas de Renderização</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Guia do Utilizador</value> <value>Guia do Utilizador</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Принудительно поверх всех окон</value> <value>Принудительно поверх всех окон</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Принудительно имитировать пословный эффект, даже если текущий текст не имеет пословной информации</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Принудительно включить пословный эффект</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>FPS рендеринга</value> <value>FPS рендеринга</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Руководство пользователя</value> <value>Руководство пользователя</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>บังคับให้อยู่บนสุดเสมอ</value> <value>บังคับให้อยู่บนสุดเสมอ</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>บังคับจำลองเอฟเฟกต์ทีละคำแม้ว่าเนื้อเพลงปัจจุบันจะไม่มีข้อมูลทีละคำ</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>บังคับเปิดใช้เอฟเฟกต์ทีละคำ</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>เฟรมเรตการเรนเดอร์</value> <value>เฟรมเรตการเรนเดอร์</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>คู่มือผู้ใช้</value> <value>คู่มือผู้ใช้</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>Buộc luôn ở trên cùng</value> <value>Buộc luôn ở trên cùng</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>Buộc mô phỏng hiệu ứng từng từ ngay cả khi lời bài hát hiện tại không có thông tin từng từ</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>Buộc bật hiệu ứng từng từ</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>Tốc độ khung hình kết xuất</value> <value>Tốc độ khung hình kết xuất</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>Hướng dẫn sử dụng</value> <value>Hướng dẫn sử dụng</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -891,12 +891,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>强制置于顶层</value> <value>强制置于顶层</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>即使当前歌词没有逐字信息,仍强制模拟逐字效果</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>强制启用逐字歌词效果</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>渲染帧率</value> <value>渲染帧率</value>
</data> </data>
@@ -1143,6 +1137,15 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve"> <data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌词窗口状态切换快捷键</value> <value>歌词窗口状态切换快捷键</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAuto.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve"> <data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>调整此值将影响顺序搜索和最佳匹配搜索结果,但不会影响手动歌词搜索界面中的搜索结果</value> <value>调整此值将影响顺序搜索和最佳匹配搜索结果,但不会影响手动歌词搜索界面中的搜索结果</value>
</data> </data>

View File

@@ -888,12 +888,6 @@
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve"> <data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>強制置於頂層</value> <value>強制置於頂層</value>
</data> </data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>即使目前歌詞沒有逐字資訊,仍強制模擬逐字效果</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Header" xml:space="preserve">
<value>強制啟用逐字歌詞效果</value>
</data>
<data name="SettingsPageFPS.Header" xml:space="preserve"> <data name="SettingsPageFPS.Header" xml:space="preserve">
<value>渲染畫面播放速率</value> <value>渲染畫面播放速率</value>
</data> </data>
@@ -1575,4 +1569,13 @@
<data name="UserGuide.Content" xml:space="preserve"> <data name="UserGuide.Content" xml:space="preserve">
<value>使用指南</value> <value>使用指南</value>
</data> </data>
<data name="SettingsPageLyricsWordByWordEffectMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeAlways.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsWordByWordEffectModeNever.Content" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -2,27 +2,35 @@
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
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 Microsoft.Data.Sqlite;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Storage;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.ViewModels namespace BetterLyrics.WinUI3.ViewModels
{ {
public partial class AboutControlViewModel : BaseViewModel public partial class AboutControlViewModel : BaseViewModel
{ {
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly ILyricsCacheService _lyricsCacheService;
[ObservableProperty] [ObservableProperty]
public partial AppSettings AppSettings { get; set; } public partial AppSettings AppSettings { get; set; }
public AboutControlViewModel(ISettingsService settingsService) public AboutControlViewModel(ISettingsService settingsService, ILyricsCacheService lyricsCacheService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_lyricsCacheService = lyricsCacheService;
AppSettings = _settingsService.AppSettings; AppSettings = _settingsService.AppSettings;
} }
@@ -47,18 +55,34 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand] [RelayCommand]
private async Task ImportSettingsAsync() private async Task ImportSettingsAsync()
{ {
var file = await PickerHelper.PickSingleFileAsync<SettingsWindow>([".json"]); var file = await PickerHelper.PickSingleFileAsync<SettingsWindow>([".zip"]);
if (file != null) if (file != null)
{ {
var succeed = _settingsService.ImportSettings(file.Path); try
if (succeed)
{ {
GC.Collect();
GC.WaitForPendingFinalizers();
SqliteConnection.ClearAllPools();
string tempExtractPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempExtractPath);
using (var stream = await file.OpenStreamForReadAsync())
{
ZipFile.ExtractToDirectory(stream, tempExtractPath);
}
DirectoryHelper.CopyDirectory(tempExtractPath, PathHelper.LocalFolder, true);
Directory.Delete(tempExtractPath, true);
WindowHook.RestartApp(); WindowHook.RestartApp();
} }
else catch (Exception ex)
{ {
ToastHelper.ShowToast("ImportSettingsFailed", null, InfoBarSeverity.Error); ToastHelper.ShowToast("ImportSettingsFailed", ex.Message, InfoBarSeverity.Error);
} }
} }
} }
@@ -66,31 +90,49 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand] [RelayCommand]
private async Task ExportSettingsAsync() private async Task ExportSettingsAsync()
{ {
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>(); try
if (folder != null)
{ {
_settingsService.ExportSettings(folder.Path); var suggestedFileName = $"{Constants.App.AppName}_{_settingsService.AppSettings.Version}_{DateTime.Now:yyyyMMdd_HHmmss}";
IDictionary<string, IList<string>> fileTypeChoices = new Dictionary<string, IList<string>>()
{
{ "Zip Archive", new List<string>() { ".zip" } }
};
var destinationFile = await PickerHelper.PickSaveFileAsync<SettingsWindow>(fileTypeChoices, suggestedFileName);
if (destinationFile == null) return;
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
DirectoryHelper.CopyDirectory(PathHelper.LocalFolder, tempDir, true);
string tempZipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
ZipFile.CreateFromDirectory(tempDir, tempZipPath);
using (var sourceStream = File.OpenRead(tempZipPath))
using (var destStream = await destinationFile.OpenStreamForWriteAsync())
{
sourceStream.CopyTo(destStream);
destStream.SetLength(sourceStream.Length);
}
Directory.Delete(tempDir, true);
File.Delete(tempZipPath);
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success); ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
} }
catch (Exception ex)
{
ToastHelper.ShowToast("Error", ex.Message, InfoBarSeverity.Error);
}
} }
[RelayCommand] [RelayCommand]
private async Task ExportPlayHistoryAsync() private async Task ClearCacheFilesAsync()
{ {
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>(); await _lyricsCacheService.ClearCacheAsync();
if (folder != null)
{
var dest = Path.Combine(folder.Path, $"BetterLyrics_Play_History_Export_{DateTime.Now:yyyyMMdd_HHmmss}.db");
await FileHelper.CopyFileAsync(PathHelper.PlayHistoryPath, dest);
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
[RelayCommand]
private void ClearCacheFiles()
{
DirectoryHelper.DeleteAllFiles(PathHelper.LogDirectory); DirectoryHelper.DeleteAllFiles(PathHelper.LogDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.LyricsCacheDirectory); DirectoryHelper.DeleteAllFiles(PathHelper.LyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.iTunesAlbumArtCacheDirectory); DirectoryHelper.DeleteAllFiles(PathHelper.iTunesAlbumArtCacheDirectory);