This commit is contained in:
Zhe Fang
2025-06-23 13:37:35 -04:00
parent 68b7601b0f
commit 0eca011054
14 changed files with 284 additions and 78 deletions

View File

@@ -68,6 +68,10 @@
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>

View File

@@ -11,9 +11,9 @@ namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
{
private List<LyricsLine> _lyricsLines = [];
private List<List<LyricsLine>> _multiLangLyricsLines = [];
public List<LyricsLine> Parse(
public List<List<LyricsLine>> Parse(
string raw,
LyricsFormat? lyricsFormat = null,
string? title = null,
@@ -21,7 +21,7 @@ namespace BetterLyrics.WinUI3.Helper
int durationMs = 0
)
{
_lyricsLines = [];
_multiLangLyricsLines = [];
switch (lyricsFormat)
{
case LyricsFormat.Lrc:
@@ -34,21 +34,24 @@ namespace BetterLyrics.WinUI3.Helper
default:
break;
}
return _multiLangLyricsLines;
}
if (_lyricsLines.Count > 0 && _lyricsLines[0].StartMs > 0)
private void PostProcessLyricsLines(List<LyricsLine> lines)
{
if (lines.Count > 0 && lines[0].StartMs > 0)
{
_lyricsLines.Insert(
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = _lyricsLines[0].StartMs,
Texts = [""],
EndMs = lines[0].StartMs,
Text = "",
CharTimings = [],
}
);
}
return _lyricsLines;
}
private void ParseLrc(string raw, int durationMs)
@@ -66,13 +69,15 @@ namespace BetterLyrics.WinUI3.Helper
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
foreach (Match m in matches)
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 0)
@@ -105,37 +110,71 @@ namespace BetterLyrics.WinUI3.Helper
}
}
// 按时间排序
lrcLines = lrcLines.OrderBy(l => l.time).ToList();
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = grouped.Max(g => g.Count());
// 构建 LyricsLine
for (int i = 0; i < lrcLines.Count; i++)
// 初始化每种语言的歌词列表
_multiLangLyricsLines.Clear();
for (int i = 0; i < languageCount; i++)
_multiLangLyricsLines.Add(new List<LyricsLine>());
// 遍历每个时间分组
foreach (var group in grouped)
{
var (start, text, syllables) = lrcLines[i];
var line = new LyricsLine
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
StartMs = start,
EndMs = (i + 1 < lrcLines.Count) ? lrcLines[i + 1].time : durationMs,
Texts = [text],
CharTimings = [],
};
if (syllables != null && syllables.Count > 0)
{
for (int j = 0; j < syllables.Count; j++)
// 如果该语言有翻译,取对应行,否则用原文(第一行)
var (start, text, syllables) =
langIdx < linesInGroup.Count ? linesInGroup[langIdx] : linesInGroup[0];
var line = new LyricsLine
{
var (charStart, charText) = syllables[j];
int charEnd =
(j + 1 < syllables.Count) ? syllables[j + 1].Item1 : line.EndMs;
if (!string.IsNullOrEmpty(charText))
StartMs = start,
EndMs = 0, // 稍后统一修正
Text = text,
CharTimings = [],
};
if (syllables != null && syllables.Count > 0)
{
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int charEnd = (j + 1 < syllables.Count) ? syllables[j + 1].Item1 : 0;
line.CharTimings.Add(
new CharTiming { StartMs = charStart, EndMs = charEnd }
);
}
}
_multiLangLyricsLines[langIdx].Add(line);
}
_lyricsLines.Add(line);
}
// 修正 EndMs
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
var linesInSingleLang = _multiLangLyricsLines[langIdx];
for (int i = 0; i < linesInSingleLang.Count; i++)
{
if (i + 1 < linesInSingleLang.Count)
linesInSingleLang[i].EndMs = linesInSingleLang[i + 1].StartMs;
else
linesInSingleLang[i].EndMs = durationMs;
// 修正 CharTimings 的最后一个 EndMs
var timings = linesInSingleLang[i].CharTimings;
if (timings.Count > 0)
{
for (int j = 0; j < timings.Count; j++)
{
if (j + 1 < timings.Count)
timings[j].EndMs = timings[j + 1].StartMs;
else
timings[j].EndMs = linesInSingleLang[i].EndMs;
}
}
}
PostProcessLyricsLines(linesInSingleLang);
}
}
@@ -143,6 +182,7 @@ namespace BetterLyrics.WinUI3.Helper
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null)
@@ -191,16 +231,18 @@ namespace BetterLyrics.WinUI3.Helper
if (spans.Count == 0)
text = p.Value.Trim();
_lyricsLines.Add(
singleLangLyricsLine.Add(
new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
Texts = [text],
Text = text,
CharTimings = charTimings,
}
);
}
PostProcessLyricsLines(singleLangLyricsLine);
_multiLangLyricsLines.Add(singleLangLyricsLine);
}
catch
{

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsData
{
public int LanguageIndex { get; set; } = 0;
public List<LyricsLine> LyricsLines => MultiLangLyricsLines[LanguageIndex];
public List<List<LyricsLine>> MultiLangLyricsLines { get; set; } = [];
}
}

View File

@@ -7,11 +7,7 @@ namespace BetterLyrics.WinUI3.Models
{
public class LyricsLine
{
public List<string> Texts { get; set; } = [];
public int LanguageIndex { get; set; } = 0;
public string Text => Texts[LanguageIndex];
public string Text { get; set; } = "";
public List<CharTiming> CharTimings { get; set; } = [];
@@ -40,8 +36,7 @@ namespace BetterLyrics.WinUI3.Models
{
return new LyricsLine
{
Texts = new List<string>(this.Texts),
LanguageIndex = this.LanguageIndex,
Text = this.Text,
CharTimings = this.CharTimings,
StartMs = this.StartMs,
EndMs = this.EndMs,

View File

@@ -42,7 +42,7 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
if (file.Contains(title) && file.Contains(artist))
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
@@ -152,6 +152,52 @@ namespace BetterLyrics.WinUI3.Services
return (null, null);
}
private static int LevenshteinDistance(string a, string b)
{
if (string.IsNullOrEmpty(a))
return b.Length;
if (string.IsNullOrEmpty(b))
return a.Length;
int[,] d = new int[a.Length + 1, b.Length + 1];
for (int i = 0; i <= a.Length; i++)
d[i, 0] = i;
for (int j = 0; j <= b.Length; j++)
d[0, j] = j;
for (int i = 1; i <= a.Length; i++)
for (int j = 1; j <= b.Length; j++)
d[i, j] = Math.Min(
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
d[i - 1, j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)
);
return d[a.Length, b.Length];
}
// 判断相似度
private static bool FuzzyMatch(string fileName, string title, string artist)
{
var normFile = Normalize(fileName);
var normTarget1 = Normalize(title + artist);
var normTarget2 = Normalize(artist + title);
int dist1 = LevenshteinDistance(normFile, normTarget1);
int dist2 = LevenshteinDistance(normFile, normTarget2);
return dist1 <= 3 || dist2 <= 3; // 阈值可调整
}
private static string Normalize(string s)
{
if (string.IsNullOrWhiteSpace(s))
return "";
var sb = new StringBuilder();
foreach (var c in s.ToLowerInvariant())
{
if (char.IsLetterOrDigit(c))
sb.Append(c);
}
return sb.ToString();
}
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
@@ -166,13 +212,15 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
if (file.Contains(title) && file.Contains(artist))
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
//Track track = new(file);
//var plain = track.Lyrics.UnsynchronizedLyrics;
try
{
// TODO: replace TagLib with ATL or another library that supports AOT
string plain = TagLib.File.Create(file).Tag.Lyrics;
if (plain != string.Empty)
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (plain != null && plain != string.Empty)
{
return plain;
}
@@ -204,7 +252,7 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
if (file.Contains(title) && file.Contains(artist))
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(
file,

View File

@@ -10,6 +10,7 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Windows.ApplicationModel;
using Windows.Media.Control;
using Windows.Storage.Streams;
@@ -164,6 +165,15 @@ namespace BetterLyrics.WinUI3.Services
SourceAppUserModelId = _currentSession?.SourceAppUserModelId,
};
if (
SongInfo.SourceAppUserModelId?.Contains(Package.Current.Id.FamilyName)
?? false
)
{
SongInfo.Title = "甜度爆表";
SongInfo.Artist = "AI";
}
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);

View File

@@ -483,4 +483,7 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML files</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
</data>
</root>

View File

@@ -483,4 +483,7 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>ローカル.TTMLファイル</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください</value>
</data>
</root>

View File

@@ -483,4 +483,7 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>로컬 .TTML 파일</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.</value>
</data>
</root>

View File

@@ -483,4 +483,7 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
</root>

View File

@@ -483,4 +483,7 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾</value>
</data>
</root>

View File

@@ -63,7 +63,8 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
private List<LyricsLine> _lyrics = [];
private List<List<LyricsLine>> _multiLangLyrics = [];
private int _langIndex = 0;
private List<LyricsLine>? _lyricsForGlowEffect = [];
@@ -215,7 +216,7 @@ namespace BetterLyrics.WinUI3.ViewModels
/// <returns></returns>
private async Task RefreshLyricsAsync()
{
_lyrics = [];
_multiLangLyrics = [];
_isRelayoutNeeded = true;
LyricsStatus = LyricsStatus.Loading;
string? lyricsRaw = null;
@@ -237,7 +238,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else if (SongInfo != null)
{
_lyrics = new LyricsParser().Parse(
_multiLangLyrics = new LyricsParser().Parse(
lyricsRaw,
lyricsFormat,
SongInfo.Title,
@@ -373,9 +374,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < _lyrics?.Count; i++)
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
{
var line = _lyrics?[i];
var line = _multiLangLyrics.SafeGet(_langIndex)?[i];
if (line?.EndMs < TotalTime.TotalMilliseconds)
{
continue;
@@ -394,12 +395,16 @@ namespace BetterLyrics.WinUI3.ViewModels
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (SongInfo == null || _lyrics == null || _lyrics.Count == 0)
if (
SongInfo == null
|| _multiLangLyrics.SafeGet(_langIndex) == null
|| _multiLangLyrics[_langIndex].Count == 0
)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _lyrics.Count - 1);
return new Tuple<int, int>(0, _multiLangLyrics[_langIndex].Count - 1);
}
private void DrawLyrics(
@@ -694,7 +699,7 @@ namespace BetterLyrics.WinUI3.ViewModels
DrawLyrics(
control,
lyricsDs,
_lyrics,
_multiLangLyrics.SafeGet(_langIndex),
_defaultOpacity,
LyricsHighlightType.LineByLine
);
@@ -870,9 +875,9 @@ namespace BetterLyrics.WinUI3.ViewModels
float y = _topMargin;
// Init Positions
for (int i = 0; i < _lyrics?.Count; i++)
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
{
var line = _lyrics.SafeGet(i);
var line = _multiLangLyrics[_langIndex].SafeGet(i);
if (line == null)
{
@@ -933,13 +938,16 @@ namespace BetterLyrics.WinUI3.ViewModels
_isRelayoutNeeded = false;
}
UpdateLinesProps(_lyrics, _defaultOpacity);
UpdateLinesProps(_multiLangLyrics.SafeGet(_langIndex), _defaultOpacity);
UpdateCanvasYScrollOffset(control);
if (IsLyricsGlowEffectEnabled)
{
// Deep copy lyrics lines for glow effect
_lyricsForGlowEffect = _lyrics?.Select(line => line.Clone()).ToList();
_lyricsForGlowEffect = _multiLangLyrics
.SafeGet(_langIndex)
?.Select(line => line.Clone())
.ToList();
switch (LyricsGlowEffectScope)
{
case LyricsGlowEffectScope.WholeLyrics:
@@ -1124,7 +1132,9 @@ namespace BetterLyrics.WinUI3.ViewModels
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _lyrics?[currentPlayingLineIndex];
LyricsLine? currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
@@ -1142,7 +1152,7 @@ namespace BetterLyrics.WinUI3.ViewModels
float targetYScrollOffset =
(float?)(
-currentPlayingLine.Position.Y
+ _lyrics?[0].Position.Y
+ _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y
- playingTextLayout.LayoutBounds.Height / 2
) ?? 0f;
@@ -1159,9 +1169,14 @@ namespace BetterLyrics.WinUI3.ViewModels
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update visible line indices
for (int i = startLineIndex; i >= 0 && i <= endLineIndex && i < _lyrics?.Count; i++)
for (int i = startLineIndex; i <= endLineIndex; i++)
{
var line = _lyrics?[i];
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null)
{
continue;
}
using var textLayout = new CanvasTextLayout(
control,

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
@@ -13,8 +14,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.Core;
using Windows.Globalization;
using Windows.Media;
using Windows.Media.Playback;
using Windows.System;
using WinRT.Interop;
@@ -68,11 +71,6 @@ namespace BetterLyrics.WinUI3.ViewModels
[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]
@@ -81,6 +79,11 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
private readonly MediaPlayer _mediaPlayer = new();
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
@@ -89,6 +92,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
@@ -221,7 +225,20 @@ namespace BetterLyrics.WinUI3.ViewModels
private void AddFolderAsync(string path)
{
if (LocalLyricsFolders.Any(x => x.Path == path))
var normalizedPath =
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
if (
LocalLyricsFolders.Any(x =>
Path.GetFullPath(x.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.Equals(
normalizedPath.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase
)
)
)
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
@@ -231,8 +248,17 @@ namespace BetterLyrics.WinUI3.ViewModels
)
);
}
else if (LocalLyricsFolders.Any((item) => path.StartsWith(item.Path)))
else if (
LocalLyricsFolders.Any(item =>
normalizedPath.StartsWith(
Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase
)
)
)
{
// 添加的文件夹是现有文件夹的子文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
@@ -241,12 +267,19 @@ namespace BetterLyrics.WinUI3.ViewModels
)
);
}
else if (LocalLyricsFolders.Any((item) => item.Path.StartsWith(path)))
else if (
LocalLyricsFolders.Any(item =>
Path.GetFullPath(item.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase)
)
)
{
// 添加的文件夹是现有文件夹的父文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo")
App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo")
)
)
);

View File

@@ -39,15 +39,43 @@ Your smooth dynamic local lyrics display built with WinUI 3
We provide more than one setting item to better align with your preference
- Theme (light, dark, follow system)
- Theme
- Light
- Dark
- Follow system
- Backdrop (none, mica, acrylic, transparent)
- Backdrop
- None
- Mica
- Acrylic
- Transparent
- Album art as background (dynamic, blur amount, opacity)
- Album art as background
- Dynamic
- Blur amount
- Opacity
- Lyrics (alignment, font size, font color **(picked from album art accent color)** line spacing, opacity, blur amount, dynamic **glow** effect)
- Album art as cover
- Corner radius
- Language (English, Simplified Chinese, Traditional Chinese, Japanese, Korean)
- Lyrics
- Alignment
- Font size
- Font color **(from album art accent color)**
- Line spacing
- Opacity
- Blur amount
- Dynamic **glow** effect
- Whole lyrics
- Line by line
- Word by word
- Language
- English
- Simplified Chinese
- Traditional Chinese
- Japanese
- Korean
## Screenshots
@@ -72,7 +100,7 @@ We provide more than one setting item to better align with your preference
## Demonstration
Watch our introduction video「BetterLyrics 阶段性开发成果展示」(uploaded on 31 May 2025) on Bilibili [here](https://b23.tv/QjKkYmL).
Watch our introduction video (uploaded on 31 May 2025) on Bilibili [here](https://b23.tv/QjKkYmL).
## Try it now
@@ -109,11 +137,11 @@ To be added later.
- [LRCLIB](https://lrclib.net/)
- Online lyrics API provider
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- Local music file metadata extractor
- Used for extracting pictures in music files
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- Provide easy ways to access Win32 API regarding windowing
- [TagLib#](https://github.com/mono/taglib-sharp)
- Previously used it as metadata extractor
- Used for reading original lyrics content
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
@@ -149,7 +177,7 @@ To be added later.
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />S
```
## Star History