add: qq music lyrics provider

This commit is contained in:
Zhe Fang
2025-06-20 19:17:17 -04:00
parent 1fe8675743
commit f9070eed5d
40 changed files with 1037 additions and 502 deletions

View File

@@ -45,6 +45,7 @@
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />

View File

@@ -73,7 +73,7 @@ namespace BetterLyrics.WinUI3
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<ILrcLibService, LrcLibService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
// ViewModels
.AddTransient<HostWindowViewModel>()

View File

@@ -28,7 +28,7 @@
<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="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Lyricify.Lyrics.Helper" Version="0.1.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -38,6 +38,7 @@
<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="SharpZipLib" Version="1.4.2" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class LyricsSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalLrcFile"
),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalMusicFile"
),
LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
"LyricsSearchProviderLrcLib"
),
LyricsSearchProvider.QQMusic => App.ResourceLoader!.GetString(
"LyricsSearchProviderQQMusic"
),
LyricsSearchProvider.KugouMusic => App.ResourceLoader!.GetString(
"LyricsSearchProviderKugouMusic"
),
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null),
};
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -17,6 +17,10 @@ namespace BetterLyrics.WinUI3.Enums
cdet.Feed(bytes, 0, bytes.Length);
cdet.DataEnd();
var encoding = cdet.Charset;
if (encoding == null)
{
return Encoding.UTF8;
}
return Encoding.GetEncoding(encoding);
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFormat
{
Lrc,
DecryptedQrc,
DecryptedKrc,
}
public static class LyricsFormatExtensions
{
public static string ToFileExtension(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.DecryptedQrc => ".decryptedqrc",
LyricsFormat.DecryptedKrc => ".decryptedkrc",
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
};
}
public static List<string> GetSupportedLyricsFormatAsList()
{
return [.. Enum.GetValues<LyricsFormat>().Select(format => format.ToFileExtension())];
}
public static LyricsFormat FromFileExtension(string extension)
{
return extension.ToLowerInvariant() switch
{
".lrc" => LyricsFormat.Lrc,
".qrc" => LyricsFormat.DecryptedQrc,
".krc" => LyricsFormat.DecryptedKrc,
_ => throw new ArgumentException(
$"Unsupported lyrics format: {extension}",
nameof(extension)
),
};
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider
{
LocalLrcFile,
LocalMusicFile,
LrcLib,
QQMusic,
KugouMusic,
}
public static class OnlineLyricsProviderExtensions
{
public static LyricsFormat ToLyricsFormat(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
LyricsSearchProvider.LocalMusicFile => LyricsFormat.Lrc,
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
LyricsSearchProvider.QQMusic => LyricsFormat.DecryptedQrc,
LyricsSearchProvider.KugouMusic => LyricsFormat.DecryptedKrc,
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null),
};
}
}
}

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum SearchMatchMode
public enum MusicSearchMatchMode
{
TitleAndArtist,
TitleArtistAlbumAndDuration,

View File

@@ -9,5 +9,5 @@ namespace BetterLyrics.WinUI3.Events
public class IsPlayingChangedEventArgs(bool isPlaying) : EventArgs
{
public bool IsPlaying { get; set; } = isPlaying;
}
}
}

View File

@@ -9,5 +9,5 @@ namespace BetterLyrics.WinUI3.Events
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
}
}
}

View File

@@ -10,5 +10,5 @@ namespace BetterLyrics.WinUI3.Events
public class SongInfoChangedEventArgs(SongInfo? songInfo) : EventArgs
{
public SongInfo? SongInfo { get; set; } = songInfo;
}
}
}

View File

@@ -41,19 +41,21 @@ namespace BetterLyrics.WinUI3.Helper
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
// Data Files
private static string DatabaseFileName => "database.db";
public static string DatabasePath => Path.Combine(LocalFolder, DatabaseFileName);
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string OnlineLyricsCacheDirectory =>
Path.Combine(CacheFolder, "online-lyrics");
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
public static string TestMusicPath => Path.Combine(AssetsFolder, TestMusicFileName);
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(OnlineLyricsCacheDirectory);
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
{
private List<LyricsLine> _lyricsLines = [];
public List<LyricsLine> Parse(
string raw,
LyricsFormat? lyricsFormat = null,
string? title = null,
string? artist = null,
int durationMs = 0
)
{
switch (lyricsFormat)
{
case LyricsFormat.Lrc:
ParseLyricsFromLrc(raw);
break;
case LyricsFormat.DecryptedQrc:
ParseLyricsFromQrc(raw, durationMs);
break;
default:
break;
}
if (
title != null
&& artist != null
&& _lyricsLines != null
&& _lyricsLines.Count > 0
&& _lyricsLines[0].StartTimestampMs > 0
)
{
_lyricsLines.Insert(
0,
new LyricsLine
{
StartTimestampMs = 0,
EndTimestampMs = _lyricsLines[0].StartTimestampMs,
Texts = [$"{artist} - {title}"],
}
);
}
return _lyricsLines;
}
/// <summary>
/// Try to parse lyrics from the track, optionally override the raw lyrics string.
/// </summary>
/// <param name="track"></param>
/// <param name="raw"></param>
private void ParseLyricsFromLrc(string raw)
{
Track track = new() { Lyrics = new() };
track.Lyrics.ParseLRC(raw);
var lines = track.Lyrics.SynchronizedLyrics;
if (lines != null && lines.Count > 0)
{
_lyricsLines = [];
LyricsLine? lyricsLine = null;
for (int i = 0; i < lines.Count; i++)
{
var lyricsPhrase = lines[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lines.Count)
{
endTimestampMs = lines[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndTimestampMs = endTimestampMs;
_lyricsLines.Add(lyricsLine);
lyricsLine = null;
}
}
}
}
private void ParseLyricsFromQrc(string raw, int? durationMs)
{
var lines = Lyricify
.Lyrics.Parsers.QrcParser.Parse(raw)
.Lines?.Where(x => !string.IsNullOrWhiteSpace(x.Text))
.ToList();
if (lines != null && lines.Count > 0)
{
_lyricsLines = [];
for (int i = 0; i < lines.Count; i++)
{
var lineRead = lines[i];
var lineWrite = new LyricsLine
{
StartTimestampMs = lineRead.StartTime ?? 0,
Texts = [lineRead.Text],
};
if (i + 1 < lines.Count)
{
lineWrite.EndTimestampMs = lines[i + 1].StartTime ?? 0;
}
else
{
lineWrite.EndTimestampMs = (int)(durationMs ?? 0);
}
_lyricsLines.Add(lineWrite);
}
}
}
}
}

View File

@@ -11,5 +11,6 @@ using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Messages
{
public class ShowNotificatonMessage(Notification value)
: ValueChangedMessage<Notification>(value) { }
: ValueChangedMessage<Notification>(value)
{ }
}

View File

@@ -1,15 +0,0 @@
using SQLite;
namespace BetterLyrics.WinUI3.Models
{
public class LocalFileRecord
{
public string? Path { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
public string? Album { get; set; }
public byte[]? AlbumArt { get; set; }
public string? LyricsRaw { get; set; }
public int? Duration { get; set; }
}
}

View File

@@ -13,12 +13,12 @@ namespace BetterLyrics.WinUI3.Models
public string Text => Texts[LanguageIndex];
public int StartPlayingTimestampMs { get; set; }
public int EndPlayingTimestampMs { get; set; }
public int StartTimestampMs { get; set; }
public int EndTimestampMs { get; set; }
public LyricsPlayingState PlayingState { get; set; }
public int DurationMs => EndPlayingTimestampMs - StartPlayingTimestampMs;
public int DurationMs => EndTimestampMs - StartTimestampMs;
public float EnteringProgress { get; set; }
@@ -40,8 +40,8 @@ namespace BetterLyrics.WinUI3.Models
{
Texts = new List<string>(this.Texts),
LanguageIndex = this.LanguageIndex,
StartPlayingTimestampMs = this.StartPlayingTimestampMs,
EndPlayingTimestampMs = this.EndPlayingTimestampMs,
StartTimestampMs = this.StartTimestampMs,
EndTimestampMs = this.EndTimestampMs,
PlayingState = this.PlayingState,
EnteringProgress = this.EnteringProgress,
ExitingProgress = this.ExitingProgress,

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
namespace BetterLyrics.WinUI3.Models
{
public partial class LyricsSearchProviderInfo : ObservableObject
{
[ObservableProperty]
public partial LyricsSearchProvider Provider { get; set; }
[ObservableProperty]
public partial bool IsEnabled { get; set; }
public LyricsSearchProviderInfo() { }
public LyricsSearchProviderInfo(LyricsSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -12,19 +12,19 @@ namespace BetterLyrics.WinUI3.Models
public partial class Notification : ObservableObject
{
[ObservableProperty]
private InfoBarSeverity _severity;
public partial InfoBarSeverity Severity { get; set; }
[ObservableProperty]
private string? _message;
public partial string? Message { get; set; }
[ObservableProperty]
private bool _isForeverDismissable;
public partial bool IsForeverDismissable { get; set; }
[ObservableProperty]
private Visibility _visibility;
public partial Visibility Visibility { get; set; }
[ObservableProperty]
private string? _relatedSettingsKeyName;
public partial string? RelatedSettingsKeyName { get; set; }
public Notification(
string? message = null,

View File

@@ -1,13 +1,4 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI;
using Windows.UI;
using static ATL.LyricsInfo;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
@@ -22,87 +13,17 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string? Album { get; set; }
/// <summary>
/// In milliseconds
/// </summary>
[ObservableProperty]
public partial int? Duration { get; set; }
[ObservableProperty]
public partial LyricsStatus? LyricsStatus { get; set; }
public partial double? DurationMs { get; set; }
[ObservableProperty]
public partial string? SourceAppUserModelId { get; set; } = null;
[ObservableProperty]
public partial List<LyricsLine>? LyricsLines { get; set; } = null;
public byte[]? AlbumArt { get; set; } = null;
public SongInfo() { }
/// <summary>
/// Try to parse lyrics from the track, optionally override the raw lyrics string.
/// </summary>
/// <param name="track"></param>
/// <param name="raw"></param>
public void ParseLyrics(string? raw = null)
{
List<LyricsLine>? result = null;
Track track = new() { Lyrics = new() };
if (raw != null)
{
track.Lyrics.ParseLRC(raw);
var lyricsPhrases = track.Lyrics.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0)
{
if (lyricsPhrases[0].TimestampMs > 0)
{
var placeholder = new LyricsPhrase(0, $"{Artist} - {Title}");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result ??= [];
result.Add(lyricsLine);
lyricsLine = null;
}
}
}
if (result != null && result.Count == 0)
{
result = null;
}
LyricsLines = result;
}
}
}

View File

@@ -1,16 +0,0 @@
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface ILrcLibService
{
Task<string?> SearchLyricsAsync(
string title,
string artist,
string album,
int duration,
SearchMatchMode matchMode = SearchMatchMode.TitleAndArtist
);
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface IMusicSearchService
{
/// <summary>
///
/// </summary>
/// <param name="title"></param>
/// <param name="artist"></param>
/// <param name="album"></param>
/// <param name="durationMs"></param>
/// <param name="matchMode"></param>
/// <param name="targetProps"></param>
/// <param name="searchProviders"></param>
/// <returns>Return a tuple (raw lyrics, lyrics format, album art)</returns>
Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
);
}
}

View File

@@ -10,7 +10,6 @@ namespace BetterLyrics.WinUI3.Services
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
void ReSendingMessages();
SongInfo? SongInfo { get; }
bool IsPlaying { get; }
TimeSpan Position { get; }

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Windows.UI.Text;
@@ -12,7 +13,7 @@ namespace BetterLyrics.WinUI3.Services
// Lyrics lib
List<string> MusicLibraries { get; set; }
bool IsOnlineLyricsEnabled { get; set; }
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
// App appearance
ElementTheme ThemeType { get; set; }

View File

@@ -10,7 +10,6 @@ namespace BetterLyrics.WinUI3.Services
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BetterLyrics.WinUI3.Services;
using global::BetterLyrics.WinUI3.Events;
namespace BetterLyrics.WinUI3.Services

View File

@@ -1,61 +0,0 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Services
{
public class LrcLibService : ILrcLibService
{
private static readonly HttpClient _httpClient;
static LrcLibService()
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
}
public async Task<string?> SearchLyricsAsync(
string title,
string artist,
string album,
int duration,
SearchMatchMode matchMode = SearchMatchMode.TitleAndArtist
)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
if (matchMode == SearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&duration={Uri.EscapeDataString(duration.ToString())}";
}
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
var json = await response.Content.ReadAsStringAsync();
var jObj = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
var jArr = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
if (jArr is not null && jArr.Count > 0)
{
var syncedLyrics = jArr![0]?.syncedLyrics?.ToString();
return string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
}
return null;
}
}
}

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Providers;
namespace BetterLyrics.WinUI3.Services
{
public class MusicSearchService : IMusicSearchService
{
private readonly HttpClient _httpClient;
private readonly ISettingsService _settingsService;
public MusicSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
}
public async Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
)
{
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
case LyricsSearchProvider.QQMusic:
case LyricsSearchProvider.KugouMusic:
// Check cache first
var cachedLyrics = ReadCache(title, artist, provider.Provider);
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, provider.Provider.ToLyricsFormat());
}
break;
default:
break;
}
string? searchedLyrics = null;
switch (provider.Provider)
{
case LyricsSearchProvider.LocalMusicFile:
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
break;
case LyricsSearchProvider.LocalLrcFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
provider.Provider.ToLyricsFormat()
);
break;
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLib(
title,
artist,
album,
(int)(durationMs / 1000),
matchMode
);
break;
case LyricsSearchProvider.QQMusic:
searchedLyrics = await SearchQQMusic(
title,
artist,
album,
(int)durationMs,
matchMode
);
break;
case LyricsSearchProvider.KugouMusic:
break;
default:
break;
}
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
case LyricsSearchProvider.QQMusic:
case LyricsSearchProvider.KugouMusic:
WriteCache(title, artist, searchedLyrics, provider.Provider);
break;
default:
break;
}
return (searchedLyrics, provider.Provider.ToLyricsFormat());
}
}
return (null, null);
}
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
{
foreach (var path in _settingsService.MusicLibraries)
{
if (Directory.Exists(path))
{
foreach (
var file in Directory.GetFiles(path, $"*.*", SearchOption.AllDirectories)
)
{
if (file.Contains(title) && file.Contains(artist))
{
Track track = new(file);
if (track.Lyrics.SynchronizedLyrics.Count > 0)
{
// Get synchronized lyrics from the track (metadata)
var lrc = track.Lyrics.FormatSynchToLRC();
if (lrc != null)
{
return lrc;
}
}
}
}
}
}
return null;
}
private async Task<string?> LocalLyricsSearchInLyricsFiles(
string title,
string artist,
LyricsFormat format
)
{
foreach (var path in _settingsService.MusicLibraries)
{
if (Directory.Exists(path))
{
foreach (
var file in Directory.GetFiles(
path,
$"*{format.ToFileExtension()}",
SearchOption.AllDirectories
)
)
{
if (file.Contains(title) && file.Contains(artist))
{
string? raw = await File.ReadAllTextAsync(
file,
FileHelper.GetEncoding(file)
);
if (raw != null)
{
return raw;
}
}
}
}
}
return null;
}
private async Task<string?> SearchLrcLib(
string title,
string artist,
string album,
int duration,
MusicSearchMatchMode matchMode
)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
if (matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
var json = await response.Content.ReadAsStringAsync();
var jArr = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
if (jArr is not null && jArr.Count > 0)
{
var syncedLyrics = jArr![0]?.syncedLyrics?.ToString();
var result = string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
}
return null;
}
private async Task<string?> SearchQQMusic(
string title,
string artist,
string album,
int durationMs,
MusicSearchMatchMode matchMode
)
{
string? queryId = (
(
await new Lyricify.Lyrics.Searchers.QQMusicSearcher().SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? durationMs
: null,
Album =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? album
: null,
AlbumArtists = [artist],
Artists = [artist],
Title = title,
}
)
) as Lyricify.Lyrics.Searchers.QQMusicSearchResult
)?.Id;
if (queryId is string id)
{
return (await Lyricify.Lyrics.Decrypter.Qrc.Helper.GetLyricsAsync(id))?.Lyrics;
}
return null;
}
private void WriteCache(
string title,
string artist,
string lyrics,
LyricsSearchProvider provider
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
$"{safeArtist} - {safeTitle}{provider.ToLyricsFormat().ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
private string? ReadCache(string title, string artist, LyricsSearchProvider provider)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
$"{safeArtist} - {safeTitle}{provider.ToLyricsFormat().ToFileExtension()}"
);
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
private static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
}
}

View File

@@ -1,12 +1,14 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Windows.Media.Control;
using Windows.Storage.Streams;
@@ -28,132 +30,36 @@ namespace BetterLyrics.WinUI3.Services
public bool IsPlaying { get; private set; }
public TimeSpan Position { get; private set; }
private readonly ISettingsService _settingsService;
private readonly ILrcLibService _lrcLibService;
private readonly IMusicSearchService _musicSearchService;
public PlaybackService(ISettingsService settingsService, ILrcLibService lrcLibService)
public PlaybackService(
ISettingsService settingsService,
IMusicSearchService musicSearchService
)
{
_settingsService = settingsService;
_lrcLibService = lrcLibService;
_musicSearchService = musicSearchService;
InitMediaManager().ConfigureAwait(true);
}
private async Task RefreshSongInfoAsync()
private async Task SendSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
var mediaProps = await _currentSession?.TryGetMediaPropertiesAsync();
SongInfo = new SongInfo
{
Title = mediaProps?.Title ?? string.Empty,
Artist = mediaProps?.Artist ?? string.Empty,
Album = mediaProps?.AlbumTitle ?? string.Empty,
Duration = (int?)_currentSession?.GetTimelineProperties().EndTime.TotalSeconds,
DurationMs = _currentSession?.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = _currentSession?.SourceAppUserModelId,
LyricsStatus = LyricsStatus.Loading,
};
string? lyricsRaw = null;
byte[]? albumArt = null;
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
// Local search for lyrics only
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
(lyricsRaw, _) = await Search(SongInfo.Title, SongInfo.Artist);
}
else
{
// Local search for lyrics and album art
(lyricsRaw, albumArt) = await Search(
SongInfo.Title,
SongInfo.Artist,
targetProps: LocalSearchTargetProps.LyricsAndAlbumArt
);
SongInfo.AlbumArt = albumArt;
}
SongInfo.ParseLyrics(lyricsRaw);
// If no lyrics found locally, search online
if (_settingsService.IsOnlineLyricsEnabled && SongInfo.LyricsLines == null)
{
SongInfo.ParseLyrics(
await _lrcLibService.SearchLyricsAsync(
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album,
SongInfo.Duration ?? 0
)
);
}
if (SongInfo.LyricsLines == null || SongInfo.LyricsLines.Count == 0)
{
SongInfo.LyricsStatus = LyricsStatus.NotFound;
}
else
{
SongInfo.LyricsStatus = LyricsStatus.Found;
}
}
private async Task<(string?, byte[]?)> Search(
string title,
string artist,
LocalSearchTargetProps targetProps = LocalSearchTargetProps.LyricsOnly
)
{
string? lyricsRaw = null;
byte[]? albumArt = null;
foreach (var path in _settingsService.MusicLibraries)
{
if (Directory.Exists(path))
{
foreach (
var file in Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
)
{
if (file.Contains(title) && file.Contains(artist))
{
Track track = new(file);
albumArt ??= track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (file.EndsWith(".lrc"))
{
lyricsRaw ??= await File.ReadAllTextAsync(
file,
FileHelper.GetEncoding(file)
);
}
else if (file.EndsWith("qm.qrc"))
{
lyricsRaw ??= Lyricify.Lyrics.Decrypter.Qrc.Decrypter.DecryptLyrics(
await File.ReadAllTextAsync(file, System.Text.Encoding.UTF8)
);
}
else if (track.Lyrics.SynchronizedLyrics.Count > 0)
{
lyricsRaw ??= track.Lyrics.FormatSynchToLRC();
}
if (
lyricsRaw != null
&& (
(
targetProps == LocalSearchTargetProps.LyricsAndAlbumArt
&& albumArt != null
)
|| targetProps == LocalSearchTargetProps.LyricsOnly
)
)
{
return (lyricsRaw, albumArt);
}
}
}
}
}
return (lyricsRaw, albumArt);
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
private async Task InitMediaManager()
@@ -164,14 +70,6 @@ namespace BetterLyrics.WinUI3.Services
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
public void ReSendingMessages()
{
// Re-send messages to update UI
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
@@ -240,9 +138,11 @@ namespace BetterLyrics.WinUI3.Services
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
}
ReSendingMessages();
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
}
/// <summary>
@@ -250,23 +150,29 @@ namespace BetterLyrics.WinUI3.Services
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_MediaPropertiesChanged(
private async void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
// _logger.LogDebug("CurrentSession_MediaPropertiesChanged");
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (sender != null)
{
mediaProps = await sender.TryGetMediaPropertiesAsync();
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
async () =>
{
if (sender == null)
{
SongInfo = null;
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
else
{
await RefreshSongInfoAsync();
await SendSongInfoAsync(mediaProps);
}
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
);
}

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Newtonsoft.Json;
@@ -19,7 +20,7 @@ namespace BetterLyrics.WinUI3.Services
// Lyrics lib
private const string MusicLibrariesKey = "MusicLibraries";
private const string IsOnlineLyricsEnabledKey = "IsOnlineLyricsEnabled";
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
// App appearance
private const string ThemeTypeKey = "ThemeType";
@@ -61,10 +62,13 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(MusicLibrariesKey, JsonConvert.SerializeObject(value));
}
public bool IsOnlineLyricsEnabled
public List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo
{
get => GetValue<bool>(IsOnlineLyricsEnabledKey);
set => SetValue(IsOnlineLyricsEnabledKey, value);
get =>
JsonConvert.DeserializeObject<List<LyricsSearchProviderInfo>>(
GetValue<string>(LyricsSearchProvidersInfoKey) ?? "[]"
)!;
set => SetValue(LyricsSearchProvidersInfoKey, JsonConvert.SerializeObject(value));
}
public ElementTheme ThemeType
@@ -188,7 +192,18 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(IsFirstRunKey, true);
// Lyrics lib
SetDefault(MusicLibrariesKey, "[]");
SetDefault(IsOnlineLyricsEnabledKey, true);
SetDefault(
LyricsSearchProvidersInfoKey,
JsonConvert.SerializeObject(
new List<LyricsSearchProviderInfo>()
{
new(LyricsSearchProvider.LocalMusicFile, true),
new(LyricsSearchProvider.LocalLrcFile, true),
new(LyricsSearchProvider.LrcLib, true),
new(LyricsSearchProvider.QQMusic, true),
}
)
);
// App appearance
SetDefault(ThemeTypeKey, (int)ElementTheme.Default);
SetDefault(LanguageKey, (int)Language.FollowSystem);

View File

@@ -294,8 +294,8 @@
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>Glow effect scope</value>
</data>
<data name="SettingsPageOnlineLyrics.Header" xml:space="preserve">
<value>Search lyrics online</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>Configure lyrics search providers</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
@@ -462,4 +462,22 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>Loading lyrics...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>Local .lrc files</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>Local music files</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LrcLib</value>
</data>
<data name="LyricsSearchProviderQQMusic" xml:space="preserve">
<value>QQ Music</value>
</data>
<data name="LyricsSearchProviderKugouMusic" xml:space="preserve">
<value>Kugou Music</value>
</data>
</root>

View File

@@ -294,8 +294,8 @@
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>辉光效果作用范围</value>
</data>
<data name="SettingsPageOnlineLyrics.Header" xml:space="preserve">
<value>在线搜索歌词</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌词搜索服务</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
@@ -462,4 +462,22 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>设置</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>加载歌词中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .lrc 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LrcLib</value>
</data>
<data name="LyricsSearchProviderQQMusic" xml:space="preserve">
<value>QQ 音乐</value>
</data>
<data name="LyricsSearchProviderKugouMusic" xml:space="preserve">
<value>酷狗音乐</value>
</data>
</root>

View File

@@ -294,8 +294,8 @@
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>輝光效果作用範圍</value>
</data>
<data name="SettingsPageOnlineLyrics.Header" xml:space="preserve">
<value>在線搜尋歌詞</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌詞搜尋服務</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
@@ -462,4 +462,22 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>載入歌詞中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .lrc 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音樂文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LrcLib</value>
</data>
<data name="LyricsSearchProviderQQMusic" xml:space="preserve">
<value>QQ 音樂</value>
</data>
<data name="LyricsSearchProviderKugouMusic" xml:space="preserve">
<value>酷狗音乐</value>
</data>
</root>

View File

@@ -9,15 +9,15 @@ namespace BetterLyrics.WinUI3.Rendering
: BaseViewModel(settingsService)
{
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public TimeSpan ElapsedTime { get; set; } = TimeSpan.Zero;
public TimeSpan ElapsedTime { get; set; } = TimeSpan.Zero;
public virtual void Calculate(
ICanvasAnimatedControl control,
CanvasAnimatedUpdateEventArgs args
)
{
TotalTime += args.Timing.ElapsedTime;
ElapsedTime = args.Timing.ElapsedTime;
}
public virtual void Calculate(
ICanvasAnimatedControl control,
CanvasAnimatedUpdateEventArgs args
)
{
TotalTime += args.Timing.ElapsedTime;
ElapsedTime = args.Timing.ElapsedTime;
}
}
}

View File

@@ -11,7 +11,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using H.NotifyIcon.Interop;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;

View File

@@ -22,10 +22,8 @@ namespace BetterLyrics.WinUI3.ViewModels
: BaseViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<ObservableCollection<string>>>
IRecipient<PropertyChangedMessage<LyricsStatus>>
{
private readonly ILibWatcherService _libWatcherService;
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToDockMode;
[ObservableProperty]
@@ -43,10 +41,16 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
[ObservableProperty]
public partial LyricsStatus LyricsStatus { get; set; } = LyricsStatus.Loading;
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } =
LyricsDisplayType.SplitView;
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial bool AboutToUpdateUI { get; set; }
@@ -72,17 +76,13 @@ namespace BetterLyrics.WinUI3.ViewModels
public LyricsPageViewModel(
ISettingsService settingsService,
IPlaybackService playbackService,
ILibWatcherService libWatcherService
IPlaybackService playbackService
)
: base(settingsService)
{
LyricsFontSize = _settingsService.LyricsFontSize;
CoverImageRadius = _settingsService.CoverImageRadius;
_libWatcherService = libWatcherService;
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService = playbackService;
_playbackService.SongInfoChanged += async (_, args) =>
await UpdateSongInfoUI(args.SongInfo).ConfigureAwait(true);
@@ -92,14 +92,6 @@ namespace BetterLyrics.WinUI3.ViewModels
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
}
private void LibWatcherService_MusicLibraryFilesChanged(
object? sender,
Events.LibChangedEventArgs e
)
{
_playbackService.ReSendingMessages();
}
partial void OnCoverImageRadiusChanged(int value)
{
if (double.IsNaN(CoverImageGridActualHeight))
@@ -189,13 +181,20 @@ namespace BetterLyrics.WinUI3.ViewModels
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender.GetType() == typeof(SettingsViewModel))
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
}
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
@@ -217,22 +216,15 @@ namespace BetterLyrics.WinUI3.ViewModels
TrySwitchToPreferredDisplayType(SongInfo);
}
}
else if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.MusicLibraries))
{
_playbackService.ReSendingMessages();
}
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<string>> message)
public void Receive(PropertyChangedMessage<LyricsStatus> message)
{
if (message.Sender is SettingsViewModel)
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.MusicLibraries))
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsStatus))
{
_playbackService.ReSendingMessages();
LyricsStatus = message.NewValue;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -14,6 +15,7 @@ using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
@@ -46,7 +48,9 @@ namespace BetterLyrics.WinUI3.ViewModels
IRecipient<PropertyChangedMessage<LyricsAlignmentType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LyricsGlowEffectScope>>
IRecipient<PropertyChangedMessage<LyricsGlowEffectScope>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<string>>>
{
private protected CanvasTextFormat _textFormat = new()
{
@@ -67,6 +71,8 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
private List<LyricsLine> _lyrics = [];
private List<LyricsLine>? _lyricsForGlowEffect = [];
private SoftwareBitmap? _lastSoftwareBitmap = null;
@@ -97,6 +103,10 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsPlaying { get; set; }
[NotifyPropertyChangedRecipients]
[ObservableProperty]
public partial LyricsStatus LyricsStatus { get; set; } = LyricsStatus.Loading;
private protected Color _fontColor;
private Color _lightFontColor = Colors.White;
@@ -152,6 +162,8 @@ namespace BetterLyrics.WinUI3.ViewModels
public LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
private protected readonly IPlaybackService _playbackService;
private protected readonly IMusicSearchService _musicSearchService;
private readonly ILibWatcherService _libWatcherService;
private float _transitionAlpha = 1f;
private TimeSpan _transitionDuration = TimeSpan.FromMilliseconds(1000);
@@ -168,10 +180,16 @@ namespace BetterLyrics.WinUI3.ViewModels
public LyricsRendererViewModel(
ISettingsService settingsService,
IPlaybackService playbackService
IPlaybackService playbackService,
IMusicSearchService musicSearchService,
ILibWatcherService libWatcherService
)
: base(settingsService)
{
_libWatcherService = libWatcherService;
_musicSearchService = musicSearchService;
_playbackService = playbackService;
CoverImageRadius = _settingsService.CoverImageRadius;
IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
@@ -188,16 +206,56 @@ namespace BetterLyrics.WinUI3.ViewModels
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
_playbackService = playbackService;
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
RefreshPlaybackInfo();
UpdateFontColor();
}
private void LibWatcherService_MusicLibraryFilesChanged(
object? sender,
Events.LibChangedEventArgs e
)
{
GetLyrics().ConfigureAwait(true);
}
/// <summary>
/// Should invoke this function when:
/// 1. The song info is changed (new song is played).
/// 2. Lyrics search provider info is changed (change order, enable or disable any provider).
/// 3. Local music/lyrics files are changed (added, removed, renamed).
/// </summary>
/// <returns></returns>
private async Task GetLyrics()
{
_lyrics = [];
_isRelayoutNeeded = true;
LyricsStatus = LyricsStatus.Loading;
(var lyricsRaw, var lyricsFormat) = await _musicSearchService.SearchLyricsAsync(
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album,
SongInfo.DurationMs ?? 0
);
if (lyricsRaw == null)
{
LyricsStatus = LyricsStatus.NotFound;
}
else
{
_lyrics = new LyricsParser().Parse(lyricsRaw, lyricsFormat);
_isRelayoutNeeded = true;
LyricsStatus = LyricsStatus.Found;
}
}
public void RequestRelayout()
{
_isRelayoutNeeded = true;
@@ -211,6 +269,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
GetLyrics().ConfigureAwait(true);
}
private void PlaybackService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e)
@@ -230,9 +289,15 @@ namespace BetterLyrics.WinUI3.ViewModels
_isRelayoutNeeded = true;
}
async partial void OnSongInfoChanged(SongInfo? value)
async partial void OnSongInfoChanged(SongInfo? oldValue, SongInfo? newValue)
{
if (value?.AlbumArt is byte[] bytes)
if (newValue == oldValue)
{
_isRelayoutNeeded = true;
return;
}
if (newValue?.AlbumArt is byte[] bytes)
{
SoftwareBitmap = await (
await ImageHelper.GetDecoderFromByte(bytes)
@@ -319,10 +384,10 @@ namespace BetterLyrics.WinUI3.ViewModels
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < SongInfo?.LyricsLines?.Count; i++)
for (int i = 0; i < _lyrics?.Count; i++)
{
var line = SongInfo?.LyricsLines?[i];
if (line.EndPlayingTimestampMs < TotalTime.TotalMilliseconds)
var line = _lyrics?[i];
if (line?.EndTimestampMs < TotalTime.TotalMilliseconds)
{
continue;
}
@@ -340,12 +405,12 @@ namespace BetterLyrics.WinUI3.ViewModels
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (SongInfo == null || SongInfo.LyricsLines == null || SongInfo.LyricsLines.Count == 0)
if (SongInfo == null || _lyrics == null || _lyrics.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, SongInfo.LyricsLines.Count - 1);
return new Tuple<int, int>(0, _lyrics.Count - 1);
}
private void DrawLyrics(
@@ -621,7 +686,7 @@ namespace BetterLyrics.WinUI3.ViewModels
DrawLyrics(
control,
lyricsDs,
SongInfo?.LyricsLines,
_lyrics,
_defaultOpacity,
LyricsHighlightType.LineByLine
);
@@ -793,9 +858,9 @@ namespace BetterLyrics.WinUI3.ViewModels
float y = _topMargin;
// Init Positions
for (int i = 0; i < SongInfo?.LyricsLines?.Count; i++)
for (int i = 0; i < _lyrics?.Count; i++)
{
var line = SongInfo?.LyricsLines?[i];
var line = _lyrics?[i];
// Calculate layout bounds
using var textLayout = new CanvasTextLayout(
@@ -872,13 +937,13 @@ namespace BetterLyrics.WinUI3.ViewModels
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
CalculateLinesProps(SongInfo?.LyricsLines, currentPlayingLineIndex, _defaultOpacity);
CalculateLinesProps(_lyrics, currentPlayingLineIndex, _defaultOpacity);
CalculateCanvasYScrollOffset(control, currentPlayingLineIndex);
if (IsLyricsGlowEffectEnabled)
{
// Deep copy lyrics lines for glow effect
_lyricsForGlowEffect = SongInfo?.LyricsLines?.Select(line => line.Clone()).ToList();
_lyricsForGlowEffect = _lyrics?.Select(line => line.Clone()).ToList();
switch (LyricsGlowEffectScope)
{
case LyricsGlowEffectScope.WholeLyrics:
@@ -938,11 +1003,10 @@ namespace BetterLyrics.WinUI3.ViewModels
opacity = _highlightedOpacity;
playProgress =
((float)TotalTime.TotalMilliseconds - line.StartPlayingTimestampMs)
((float)TotalTime.TotalMilliseconds - line.StartTimestampMs)
/ line.DurationMs;
var durationFromStartMs =
TotalTime.TotalMilliseconds - line.StartPlayingTimestampMs;
var durationFromStartMs = TotalTime.TotalMilliseconds - line.StartTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering)
{
@@ -962,8 +1026,7 @@ namespace BetterLyrics.WinUI3.ViewModels
line.PlayingState = LyricsPlayingState.Played;
playProgress = 1;
var durationToEndMs =
TotalTime.TotalMilliseconds - line.EndPlayingTimestampMs;
var durationToEndMs = TotalTime.TotalMilliseconds - line.EndTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting)
{
@@ -1011,7 +1074,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = SongInfo?.LyricsLines?[currentPlayingLineIndex];
LyricsLine? currentPlayingLine = _lyrics?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
@@ -1027,13 +1090,13 @@ namespace BetterLyrics.WinUI3.ViewModels
);
var lineScrollingProgress =
(TotalTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
(TotalTime.TotalMilliseconds - currentPlayingLine.StartTimestampMs)
/ Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
float targetYScrollOffset =
(float?)(
-currentPlayingLine.Position.Y
+ SongInfo?.LyricsLines?[0].Position.Y
+ _lyrics?[0].Position.Y
- playingTextLayout.LayoutBounds.Height / 2
- _lastTotalYScroll
) ?? 0f;
@@ -1061,17 +1124,13 @@ namespace BetterLyrics.WinUI3.ViewModels
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update visible line indices
for (
int i = startLineIndex;
i >= 0 && i <= endLineIndex && i < SongInfo?.LyricsLines?.Count;
i++
)
for (int i = startLineIndex; i >= 0 && i <= endLineIndex && i < _lyrics?.Count; i++)
{
var line = SongInfo?.LyricsLines?[i];
var line = _lyrics?[i];
using var textLayout = new CanvasTextLayout(
control,
line.Text,
line?.Text,
_textFormat,
(float)LimitedLineWidth,
(float)control.Size.Height
@@ -1330,5 +1389,31 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
}
public void Receive(
PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message
)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.LyricsSearchProvidersInfo))
{
// Lyrics search providers info changed, re-fetch lyrics
GetLyrics().ConfigureAwait(true);
}
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<string>> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.MusicLibraries))
{
// Music lib changed, re-fetch lyrics
GetLyrics().ConfigureAwait(true);
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
@@ -23,10 +24,6 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsViewModel : ObservableRecipient
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsOnlineLyricsEnabled { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeType { get; set; }
@@ -43,9 +40,12 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial AutoStartWindowType AutoStartWindowType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<string> MusicLibraries { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
@@ -66,36 +66,50 @@ namespace BetterLyrics.WinUI3.ViewModels
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
partial void OnMusicLibrariesChanged(
ObservableCollection<string> oldValue,
ObservableCollection<string> newValue
)
{
if (oldValue != null)
{
oldValue.CollectionChanged -= MusicLib_CollectionChanged;
}
if (newValue != null)
{
newValue.CollectionChanged += MusicLib_CollectionChanged;
_settingsService.MusicLibraries = [.. newValue];
_libWatcherService.UpdateWatchers([.. newValue]);
}
}
private void MusicLib_CollectionChanged(
object? sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e
)
{
_settingsService.MusicLibraries = [.. MusicLibraries];
_libWatcherService.UpdateWatchers([.. MusicLibraries]);
Broadcast(MusicLibraries, MusicLibraries, nameof(MusicLibraries));
}
[ObservableProperty]
public partial Enums.Language Language { get; set; }
private readonly MediaPlayer _mediaPlayer = new();
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
public string Version { get; set; } = AppInfo.AppVersion;
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
IPlaybackService playbackService
)
{
_settingsService = settingsService;
_libWatcherService = libWatcherService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
MusicLibraries = [.. _settingsService.MusicLibraries];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
ThemeType = _settingsService.ThemeType;
BackdropType = _settingsService.BackdropType;
TitleBarType = _settingsService.TitleBarType;
AutoStartWindowType = _settingsService.AutoStartWindowType;
IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
}
partial void OnLanguageChanged(Enums.Language value)
{
switch (value)
@@ -118,55 +132,6 @@ namespace BetterLyrics.WinUI3.ViewModels
_settingsService.Language = Language;
}
private readonly MediaPlayer _mediaPlayer = new();
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
public string Version { get; set; } = AppInfo.AppVersion;
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
IPlaybackService playbackService
)
{
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
MusicLibraries = [.. _settingsService.MusicLibraries];
IsOnlineLyricsEnabled = _settingsService.IsOnlineLyricsEnabled;
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
ThemeType = _settingsService.ThemeType;
BackdropType = _settingsService.BackdropType;
TitleBarType = _settingsService.TitleBarType;
AutoStartWindowType = _settingsService.AutoStartWindowType;
IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
}
partial void OnIsOnlineLyricsEnabledChanged(bool value)
{
_settingsService.IsOnlineLyricsEnabled = value;
_playbackService.ReSendingMessages();
}
partial void OnThemeTypeChanged(ElementTheme value)
{
_settingsService.ThemeType = value;
@@ -216,6 +181,9 @@ namespace BetterLyrics.WinUI3.ViewModels
public void RemoveFolderAsync(string path)
{
MusicLibraries.Remove(path);
_settingsService.MusicLibraries = [.. MusicLibraries];
_libWatcherService.UpdateWatchers([.. MusicLibraries]);
Broadcast(MusicLibraries, MusicLibraries, nameof(MusicLibraries));
}
[RelayCommand]
@@ -265,6 +233,9 @@ namespace BetterLyrics.WinUI3.ViewModels
else
{
MusicLibraries.Add(path);
_settingsService.MusicLibraries = [.. MusicLibraries];
_libWatcherService.UpdateWatchers([.. MusicLibraries]);
Broadcast(MusicLibraries, MusicLibraries, nameof(MusicLibraries));
}
}
@@ -325,5 +296,15 @@ namespace BetterLyrics.WinUI3.ViewModels
{
OpenFolderInFileExplorer(AppInfo.LogDirectory);
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
}
}

View File

@@ -12,7 +12,10 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid" PointerMoved="RootGrid_PointerMoved" RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Grid
x:Name="RootGrid"
PointerMoved="RootGrid_PointerMoved"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Frame
x:Name="RootFrame"
@@ -24,8 +27,8 @@
Height="{x:Bind ViewModel.TitleBarHeight, Mode=OneWay}"
VerticalAlignment="Top"
Background="Transparent"
PointerMoved="TopCommandGrid_PointerMoved"
Opacity="0">
Opacity="0"
PointerMoved="TopCommandGrid_PointerMoved">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>

View File

@@ -26,7 +26,7 @@
<renderer:LyricsRenderer />
</Grid>
<Grid Margin="36">
<Grid Margin="36,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
@@ -45,24 +45,34 @@
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel
x:Name="LyricsPlaceholderStackPanel"
x:Name="LyricsNotFoundPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{StaticResource DisplayTextBlockFontSize}"
Glyph="&#xE90B;" />
<TextBlock x:Uid="MainPageLyricsNotFound" FontSize="{StaticResource TitleTextBlockFontSize}" />
<TextBlock x:Uid="MainPageLyricsNotFound" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
<StackPanel
x:Name="LyricsLoadingPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<TextBlock x:Uid="MainPageLyricsLoading" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoInnerGrid">
<Grid x:Name="SongInfoInnerGrid" Margin="0,36">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<!-- Cover area -->
@@ -416,26 +426,29 @@
<VisualStateGroup x:Name="LyricsStatus">
<VisualState x:Name="Loading">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.SongInfo.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="2" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsPlaceholderStackPanel.Opacity" Value="0" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value=".5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Found">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.SongInfo.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="1" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsPlaceholderStackPanel.Opacity" Value="0" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NotFound">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.SongInfo.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="0" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsPlaceholderStackPanel.Opacity" Value=".5" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value=".5" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -8,6 +8,7 @@
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:BetterLyrics.WinUI3.ViewModels"
mc:Ignorable="d">
@@ -129,9 +130,22 @@
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageOnlineLyrics" HeaderIcon="{ui:FontIcon Glyph=&#xF6FA;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsOnlineLyricsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLyricsSearchProvidersConfig"
HeaderIcon="{ui:FontIcon Glyph=&#xF6FA;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.LyricsSearchProvidersInfo, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo">
<controls:SettingsCard
AllowDrop="True"
CanDrag="True"
Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" Toggled="LyricsSearchProviderToggleSwitch_Toggled" />
</controls:SettingsCard>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
</controls:SettingsExpander>
</StackPanel>
</controls:Case>

View File

@@ -1,4 +1,6 @@
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
@@ -47,5 +49,16 @@ namespace BetterLyrics.WinUI3.Views
{
ViewModel.NavViewSelectedItemTag = (args.SelectedItem as NavigationViewItem)!.Tag;
}
private void LyricsSearchProviderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LyricsSearchProviderInfo providerInfo)
{
ViewModel.ToggleLyricsSearchProvider(providerInfo);
}
}
}
}
}