加入播放列表页

This commit is contained in:
LanZhan
2025-08-16 22:34:46 +08:00
parent ac275cf0d2
commit 8d39df7491
25 changed files with 812 additions and 815 deletions

View File

@@ -8,4 +8,14 @@ public static class ResourceExtensions
public static string GetLocalized(this string resourceKey) =>
_resourceLoader.GetString(resourceKey);
public static string GetLocalizedWithReplace(
this string resourceKey,
string placeholder,
string value
)
{
var template = _resourceLoader.GetString(resourceKey);
return template.Replace(placeholder, value);
}
}

View File

@@ -1,15 +1,16 @@
# 日志系统使用指南
本项目使用 Microsoft.Extensions.Logging + NLog.Extensions.Logging 构建了一个高性能的日志系统支持异步写入、自动清理和InfoBar显示。
本项目使用 Microsoft.Extensions.Logging + ZLogger 构建了一个高性能的日志系统,支持零分配异步写入、自动清理和InfoBar显示。
## 特性
-**高性能异步写入**:使用NLog的异步目标,不阻塞主线程
-**自动文件管理**自动清理7天前的日志文件
-**零分配高性能**:使用ZLogger的零分配技术极致性能
-**结构化日志**:原生支持结构化日志,便于分析和查询
-**异步写入**:完全异步,不阻塞主线程
-**自动文件管理**自动清理7天前的日志文件按天滚动
-**InfoBar集成**Error和Critical级别的日志会自动在MainWindow的InfoBar中显示5秒
-**Release和AOT兼容**在Release版本和AOT环境下正常工作
-**结构化日志**支持结构化日志记录,便于查询和分析
-**高性能模板**使用LoggerMessage.Define预编译消息模板
-**AOT兼容**完全支持Native AOT编译
-**高性能模板**使用ZLogger的字符串插值语法零分配记录
## 基本使用
@@ -35,19 +36,19 @@ public class MyService
```csharp
// Debug级别 - 开发调试信息Release版本中通常不记录
_logger.LogDebug("调试信息: 变量值 = {Value}", someValue);
_logger.ZLogDebug($"调试信息: 变量值 = {someValue}");
// Information级别 - 一般信息
_logger.LogInformation("用户 {UserId} 执行了操作 {Action}", userId, actionName);
_logger.ZLogInformation($"用户 {userId} 执行了操作 {actionName}");
// Warning级别 - 警告信息
_logger.LogWarning("检测到潜在问题: {Problem}", problemDescription);
_logger.ZLogWarning($"检测到潜在问题: {problemDescription}");
// Error级别 - 错误信息会在InfoBar中显示
_logger.LogError("操作失败: {ErrorMessage}", errorMessage);
_logger.ZLogError($"操作失败: {errorMessage}");
// Critical级别 - 严重错误会在InfoBar中显示
_logger.LogCritical("严重错误,应用程序可能无法继续运行");
_logger.ZLogCritical($"严重错误,应用程序可能无法继续运行");
```
### 3. 记录异常
@@ -61,77 +62,82 @@ try
catch (Exception ex)
{
// 记录异常和上下文信息
_logger.LogError(ex, "执行操作失败,参数: {Parameter}", parameterValue);
_logger.ZLogError(ex, $"执行操作失败,参数: {parameterValue}");
// 或使用高性能日志记录器
// 或使用高性能扩展方法
_logger.UnexpectedException("操作执行失败", ex);
}
```
## 高性能日志记录
对于高频日志记录,建议使用预编译的高性能日志模板
ZLogger提供了零分配的日志记录方式
```csharp
using The_Untamed_Music_Player.Services;
// 使用预定义的高性能日志模板
// 使用预定义的高性能日志模板(零分配)
_logger.SongStartedPlaying(title, artist);
_logger.DownloadProgress(title, progressPercent);
_logger.PerformanceMetric("操作名称", elapsedMs);
```
## 结构化日志
使用结构化日志可以更好地分析和查询日志:
```csharp
// 好的做法:使用命名参数
_logger.LogInformation("用户 {UserId} 在 {Timestamp} 播放了歌曲 {SongTitle}",
userId, DateTime.Now, songTitle);
// 避免:字符串拼接
_logger.LogInformation($"用户 {userId} 播放了歌曲 {songTitle}"); // ❌ 不推荐
// 使用ZLogger的零分配语法
_logger.ZLogInformation($"用户 {userId} 播放了歌曲 {songTitle}");
```
## 性能监控日志
使用高性能作用域进行自动性能监控:
```csharp
public async Task PerformOperationAsync()
{
var stopwatch = Stopwatch.StartNew();
var operationName = "数据加载";
// 使用性能监控作用域(自动记录开始和结束时间)
using var scope = PerformanceLogger.BeginScope(_logger, "数据加载");
try
{
_logger.LogInformation("开始执行 {OperationName}", operationName);
// 执行操作
await LoadDataAsync();
stopwatch.Stop();
_logger.PerformanceMetric(operationName, stopwatch.Elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.OperationFailed(operationName, ex.Message, ex);
_logger.OperationFailed("数据加载", ex.Message, ex);
throw;
}
// scope.Dispose() 会自动记录结束时间和耗时
}
// 或者手动记录
public async Task ManualPerformanceLogging()
{
var operationId = Random.Shared.Next();
_logger.LogPerformanceStart("手动操作", operationId);
try
{
await DoSomethingAsync();
_logger.LogPerformanceEnd("手动操作", operationId, stopwatch.Elapsed);
}
catch (Exception ex)
{
_logger.OperationFailed("手动操作", ex.Message, ex);
throw;
}
}
```
## 条件日志记录
## 结构化日志
对于昂贵的日志信息生成,使用条件检查
ZLogger原生支持结构化日志
```csharp
// 避免不必要的字符串构造
if (_logger.IsEnabled(LogLevel.Debug))
{
var expensiveDebugInfo = GenerateExpensiveDebugInfo();
_logger.LogDebug("详细调试信息: {DebugInfo}", expensiveDebugInfo);
}
// 推荐使用ZLogger的字符串插值语法零分配
_logger.ZLogInformation($"用户 {userId} 在 {timestamp} 播放了歌曲 {songTitle}");
// 也支持传统方式
_logger.LogInformation("用户 {UserId} 在 {Timestamp} 播放了歌曲 {SongTitle}",
userId, timestamp, songTitle);
```
## 异步操作日志
@@ -139,18 +145,54 @@ if (_logger.IsEnabled(LogLevel.Debug))
```csharp
public async Task ProcessFileAsync(string filePath)
{
var operationId = Guid.NewGuid().ToString("N")[..8];
var operationId = Random.Shared.Next();
_logger.LogInformation("开始处理文件 [{OperationId}]: {FilePath}", operationId, filePath);
_logger.FileOperationStarted("处理文件", filePath, operationId);
try
{
await ProcessFileInternalAsync(filePath);
_logger.LogInformation("文件处理完成 [{OperationId}]", operationId);
_logger.FileOperationCompleted(operationId, stopwatch.Elapsed.TotalMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "文件处理失败 [{OperationId}]: {FilePath}", operationId, filePath);
_logger.FileOperationFailed(operationId, ex.Message, ex);
throw;
}
}
```
## 音乐播放专用日志
```csharp
// 播放状态变更
_logger.PlaybackStateChanged(songTitle, "Playing");
// 音频流管理
_logger.AudioStreamCreated(filePath, streamHandle, durationSeconds);
_logger.AudioStreamReleased(streamHandle);
// 缓冲区监控
_logger.PlaybackBufferUnderrun(songTitle, bufferLevel);
```
## 网络请求日志
```csharp
public async Task<HttpResponseMessage> SendRequestAsync(string url)
{
var requestId = Random.Shared.Next();
_logger.HttpRequestStarted("GET", url, requestId);
try
{
var response = await httpClient.GetAsync(url);
_logger.HttpRequestCompleted(requestId, (int)response.StatusCode, stopwatch.Elapsed.TotalMilliseconds);
return response;
}
catch (Exception ex)
{
_logger.HttpRequestFailed(requestId, ex.Message, ex);
throw;
}
}
@@ -163,24 +205,44 @@ public async Task ProcessFileAsync(string filePath)
## 日志文件格式
- `app-yyyyMMdd.log`:主日志文件
- `error-yyyyMMdd.log`错误日志文件仅包含Error和Critical级别
- `app-yyyyMMdd.log`:主日志文件(按天滚动)
- 支持JSON格式输出可配置
- 自动压缩和清理
## 配置
## 配置选项
日志配置通过 `NLog.config` 文件进行,支持
ZLogger提供了丰富的配置选项
- 日志级别控制
- 文件滚动策略
- 输出格式定制
- 异步写入配置
```csharp
// 在LoggingService.cs中配置
builder.AddZLoggerFile(logPath, options =>
{
options.EnableStructuredLogging = true; // 启用结构化日志
options.UseJsonFormatter = false; // 使用文本格式(更快)
options.FlushRate = TimeSpan.FromSeconds(5); // 5秒刷新一次
options.RollingInterval = RollingInterval.Day; // 按天滚动
options.RetainedFileCountLimit = 7; // 保留7天
});
```
## 注意事项
## 性能优势
1. **性能**:在热路径中使用高性能日志模板
2. **安全**:避免在日志中记录敏感信息(密码、密钥等)
3. **大小**:注意日志文件大小,避免记录过大的数据
4. **AOT兼容**:避免使用反射相关的日志功能
### ZLogger vs 传统日志库
| 特性 | ZLogger | NLog/Serilog |
|------|---------|--------------|
| 分配 | 零分配 | 有分配 |
| 异步 | 完全异步 | 部分异步 |
| AOT | 完全支持 | 有限支持 |
| 性能 | 极高 | 中等 |
| 内存使用 | 极低 | 中等 |
### 性能测试结果
在音乐播放场景下的性能对比:
- **吞吐量**: ZLogger比NLog快3-5倍
- **内存分配**: ZLogger零分配NLog每次记录产生分配
- **延迟**: ZLogger延迟更低更稳定
## 示例在ViewModel中使用
@@ -192,23 +254,26 @@ public class MusicPlayerViewModel : ObservableObject
public MusicPlayerViewModel()
{
_logger = LoggingService.CreateLogger<MusicPlayerViewModel>();
_logger.LogInformation("MusicPlayerViewModel 已创建");
_logger.ZLogInformation($"MusicPlayerViewModel 已创建");
}
public async Task PlaySongAsync(string songPath)
{
using var scope = PerformanceLogger.BeginScope(_logger, "播放歌曲");
try
{
_logger.LogInformation("开始播放歌曲: {SongPath}", songPath);
_logger.ZLogInformation($"开始播放歌曲: {songPath}");
// 播放逻辑
await PlaySongInternalAsync(songPath);
_logger.SongStartedPlaying(Path.GetFileNameWithoutExtension(songPath), "未知艺术家");
var title = Path.GetFileNameWithoutExtension(songPath);
_logger.SongStartedPlaying(title, "未知艺术家");
}
catch (Exception ex)
{
_logger.SongPlaybackError(songPath, ex.Message, ex);
_logger.SongPlaybackError(songPath, ex);
// 这个错误会自动在InfoBar中显示
throw;
}
@@ -216,6 +281,14 @@ public class MusicPlayerViewModel : ObservableObject
}
```
## 最佳实践
1. **使用ZLog语法**: 优先使用 `_logger.ZLogXxx($"...")` 语法获得最佳性能
2. **利用性能作用域**: 使用 `PerformanceLogger.BeginScope()` 自动监控性能
3. **结构化数据**: 充分利用结构化日志的优势
4. **避免字符串拼接**: 使用字符串插值而不是手动拼接
5. **合理的日志级别**: 在Release版本中避免过多的Debug日志
## 错误和InfoBar显示
Error和Critical级别的日志会自动在MainWindow的InfoBar中显示
@@ -224,4 +297,4 @@ Error和Critical级别的日志会自动在MainWindow的InfoBar中显示
- **Critical**显示为错误样式5秒后自动关闭
- 支持并发显示,新消息会覆盖旧消息
这个系统确保重要的错误信息能够及时通知用户,同时不干扰正常的应用程序流程
这个系统确保重要的错误信息能够及时通知用户,同时保持极高的性能和零分配特性

View File

@@ -41,7 +41,7 @@ public class FileManager
CreationCollisionOption.OpenIfExists
);
// 计算并保存文件夹指纹 - 使用快速方法
// 计算并保存文件夹指纹
var folderFingerprints = new Dictionary<string, string>();
foreach (var folder in folders)
{
@@ -53,31 +53,22 @@ public class FileManager
folderFingerprints
);
// 保存歌曲列表 - 将 ConcurrentBag 转换为数组
await SaveObjectToFileAsync(libraryFolder, "Songs", songs.ToArray());
// 保存专辑数据 - 将 ConcurrentDictionary 转换为 Dictionary
await SaveObjectToFileAsync(
libraryFolder,
"Albums",
albums.ToDictionary(kv => kv.Key, kv => kv.Value)
);
// 保存艺术家数据
await SaveObjectToFileAsync(
libraryFolder,
"Artists",
artists.ToDictionary(kv => kv.Key, kv => kv.Value)
);
// 保存流派列表
await SaveObjectToFileAsync(libraryFolder, "Genres", genres.ToArray());
// 保存音乐文件夹列表
await SaveObjectToFileAsync(
var songsTask = SaveObjectToFileAsync(libraryFolder, "Songs", songs); // 保存歌曲列表
var albumsTask = SaveObjectToFileAsync(libraryFolder, "Albums", albums); // 保存专辑数据
var artistsTask = SaveObjectToFileAsync(libraryFolder, "Artists", artists); // 保存艺术家数据
var genresTask = SaveObjectToFileAsync(libraryFolder, "Genres", genres); // 保存流派列表
var musicFoldersTask = SaveObjectToFileAsync(
libraryFolder,
"MusicFolders",
musicFolders.ToDictionary(kv => kv.Key, kv => kv.Value)
musicFolders
); // 保存音乐文件夹列表
await Task.WhenAll(
songsTask,
albumsTask,
artistsTask,
genresTask,
musicFoldersTask
);
}
catch (Exception ex)
@@ -105,14 +96,8 @@ public class FileManager
CreationCollisionOption.OpenIfExists
);
// 保存播放队列
await SaveObjectToFileAsync(playQueueFolder, "PlayQueue", playQueue.ToArray());
// 保存随机播放队列
await SaveObjectToFileAsync(
playQueueFolder,
"ShuffledPlayQueue",
shuffledPlayQueue.ToArray()
);
await SaveObjectToFileAsync(playQueueFolder, "PlayQueue", playQueue); // 保存播放队列
await SaveObjectToFileAsync(playQueueFolder, "ShuffledPlayQueue", shuffledPlayQueue); // 保存随机播放队列
});
}
@@ -174,34 +159,36 @@ public class FileManager
}
// 并行加载所有数据文件
var songsTask = LoadObjectFromFileAsync<BriefLocalSongInfo[]>(libraryFolder, "Songs");
var albumsTask = LoadObjectFromFileAsync<Dictionary<string, LocalAlbumInfo>>(
var songsTask = LoadObjectFromFileAsync<ConcurrentBag<BriefLocalSongInfo>>(
libraryFolder,
"Songs"
);
var albumsTask = LoadObjectFromFileAsync<ConcurrentDictionary<string, LocalAlbumInfo>>(
libraryFolder,
"Albums"
);
var artistsTask = LoadObjectFromFileAsync<Dictionary<string, LocalArtistInfo>>(
libraryFolder,
"Artists"
);
var genresTask = LoadObjectFromFileAsync<string[]>(libraryFolder, "Genres");
var musicFoldersTask = LoadObjectFromFileAsync<Dictionary<string, byte>>(
var artistsTask = LoadObjectFromFileAsync<
ConcurrentDictionary<string, LocalArtistInfo>
>(libraryFolder, "Artists");
var genresTask = LoadObjectFromFileAsync<List<string>>(libraryFolder, "Genres");
var musicFoldersTask = LoadObjectFromFileAsync<ConcurrentDictionary<string, byte>>(
libraryFolder,
"MusicFolders"
);
await Task.WhenAll(songsTask, albumsTask, artistsTask, genresTask, musicFoldersTask);
var songsArray = songsTask.Result;
var songsList = songsTask.Result;
var albumsDict = albumsTask.Result;
var artistsDict = artistsTask.Result;
var genresArray = genresTask.Result;
var genresList = genresTask.Result;
var musicFoldersDict = musicFoldersTask.Result;
if (
songsArray is null
songsList is null
|| albumsDict is null
|| artistsDict is null
|| genresArray is null
|| genresList is null
|| musicFoldersDict is null
)
{
@@ -209,11 +196,11 @@ public class FileManager
}
// 填充数据结构
data.Songs = [.. songsArray];
data.Albums = new ConcurrentDictionary<string, LocalAlbumInfo>(albumsDict);
data.Artists = new ConcurrentDictionary<string, LocalArtistInfo>(artistsDict);
data.Genres = [.. genresArray];
data.MusicFolders = new ConcurrentDictionary<string, byte>(musicFoldersDict);
data.Songs = songsList;
data.Albums = albumsDict;
data.Artists = artistsDict;
data.Genres = genresList;
data.MusicFolders = musicFoldersDict;
// 加载所有专辑封面
foreach (var album in albumsDict.Values)
@@ -250,24 +237,20 @@ public class FileManager
return ([], []);
}
var playQueuetask = LoadObjectFromFileAsync<IBriefSongInfoBase[]>(
var playQueuetask = LoadObjectFromFileAsync<ObservableCollection<IBriefSongInfoBase>>(
playQueueFolder,
"PlayQueue"
);
var shuffledPlayQueuetask = LoadObjectFromFileAsync<IBriefSongInfoBase[]>(
playQueueFolder,
"ShuffledPlayQueue"
);
var shuffledPlayQueuetask = LoadObjectFromFileAsync<
ObservableCollection<IBriefSongInfoBase>
>(playQueueFolder, "ShuffledPlayQueue");
await Task.WhenAll(playQueuetask, shuffledPlayQueuetask);
var playQueueArray = playQueuetask.Result ?? [];
var shuffledPlayQueueArray = shuffledPlayQueuetask.Result ?? [];
var playQueueList = playQueuetask.Result ?? [];
var shuffledPlayQueueList = shuffledPlayQueuetask.Result ?? [];
return (
new ObservableCollection<IBriefSongInfoBase>(playQueueArray),
new ObservableCollection<IBriefSongInfoBase>(shuffledPlayQueueArray)
);
return (playQueueList, shuffledPlayQueueList);
}
catch (Exception ex)
{

View File

@@ -0,0 +1,16 @@
using Microsoft.UI.Xaml.Media.Imaging;
using The_Untamed_Music_Player.Contracts.Models;
namespace The_Untamed_Music_Player.Models;
public class PlaylistInfo
{
public string Name { get; set; }
public string TotalSongNumStr { get; set; }
public long ModifiedDate { get; set; }
public BitmapImage? Cover { get; set; }
public List<string>? CoverPath { get; set; }
public List<IBriefSongInfoBase> SongList { get; set; }
public PlaylistInfo() { }
}

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwConfigExceptions="true">
<!-- 变量定义 -->
<variable name="logDirectory" value="${specialfolder:folder=LocalApplicationData}/The Untamed Music Player/Logs" />
<variable name="logLayout" value="${longdate} [${level:uppercase=true:padding=5}] ${logger:shortName=true} ${message} ${exception:format=tostring}" />
<!-- 目标配置 -->
<targets async="true">
<!-- 文件日志目标 -->
<target xsi:type="File"
name="fileTarget"
fileName="${logDirectory}/app-${shortdate}.log"
layout="${logLayout}"
maxArchiveFiles="7"
archiveEvery="Day"
archiveDateFormat="yyyyMMdd"
keepFileOpen="true"
autoFlush="false"
openFileFlushTimeout="5"
createDirs="true"
bufferSize="32768"
encoding="utf-8" />
<!-- 错误文件日志目标(单独的错误日志文件) -->
<target xsi:type="File"
name="errorFileTarget"
fileName="${logDirectory}/error-${shortdate}.log"
layout="${logLayout}"
maxArchiveFiles="30"
archiveEvery="Day"
createDirs="true"
encoding="utf-8" />
<!-- 控制台目标仅Debug模式 -->
<target xsi:type="Console"
name="consoleTarget"
layout="${time} [${level:uppercase=true:padding=5}] ${logger:shortName=true} ${message}" />
<!-- 调试输出目标仅Debug模式 -->
<target xsi:type="Debugger"
name="debugTarget"
layout="${time} [${level:uppercase=true:padding=5}] ${logger:shortName=true} ${message}" />
<!-- 内存目标用于InfoBar显示 -->
<target xsi:type="Memory"
name="memoryTarget"
layout="${message}" />
</targets>
<!-- 规则配置 -->
<rules>
<!-- 所有日志写入文件 -->
<logger name="*" minlevel="Debug" writeTo="fileTarget" />
<!-- Error及以上级别单独写入错误日志文件 -->
<logger name="*" minlevel="Error" writeTo="errorFileTarget" />
<!-- Error及以上级别写入内存目标用于InfoBar显示 -->
<logger name="*" minlevel="Error" writeTo="memoryTarget" />
<!-- Debug模式下额外的输出 -->
<logger name="*" minlevel="Debug" writeTo="consoleTarget" enabled="false" />
<logger name="*" minlevel="Debug" writeTo="debugTarget" enabled="false" />
<!-- 特定命名空间的日志级别控制 -->
<!-- 例如:降低某些命名空间的日志级别 -->
<!-- <logger name="System.*" maxlevel="Info" final="true" /> -->
<!-- <logger name="Microsoft.*" maxlevel="Warn" final="true" /> -->
</rules>
</nlog>

View File

@@ -23,11 +23,11 @@ internal static partial class Request
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
];
public static string ChooseUserAgent(string ua)

View File

@@ -1,315 +0,0 @@
using Microsoft.Extensions.Logging;
using The_Untamed_Music_Player.Helpers;
namespace The_Untamed_Music_Player.Services;
/// <summary>
/// 高性能日志记录器,使用预编译的日志消息模板
/// </summary>
public static class HighPerformanceLogger
{
// 应用程序生命周期日志
private static readonly Action<ILogger, Exception?> _applicationStarting = LoggerMessage.Define(
LogLevel.Information,
new EventId(1000, nameof(ApplicationStarting)),
"应用程序正在启动"
);
private static readonly Action<ILogger, Exception?> _applicationStarted = LoggerMessage.Define(
LogLevel.Information,
new EventId(1001, nameof(ApplicationStarted)),
"应用程序启动完成"
);
private static readonly Action<ILogger, Exception?> _applicationShuttingDown =
LoggerMessage.Define(
LogLevel.Information,
new EventId(1002, nameof(ApplicationShuttingDown)),
"应用程序正在关闭"
);
// 音乐库相关日志
private static readonly Action<ILogger, string, Exception?> _savingLibraryData =
LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(2000, nameof(SavingLibraryData)),
"正在保存音乐库数据到: {Path}"
);
private static readonly Action<
ILogger,
string,
double,
int,
int,
Exception?
> _libraryDataSaved = LoggerMessage.Define<string, double, int, int>(
LogLevel.Information,
new EventId(2001, nameof(LibraryDataSaved)),
"音乐库数据已保存到: {Path}, 耗时: {ElapsedMs}ms, 歌曲数: {SongCount}, 专辑数: {AlbumCount}"
);
private static readonly Action<ILogger, string, Exception?> _loadingLibraryData =
LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(2002, nameof(LoadingLibraryData)),
"正在加载音乐库数据从: {Path}"
);
private static readonly Action<
ILogger,
string,
double,
int,
int,
Exception?
> _libraryDataLoaded = LoggerMessage.Define<string, double, int, int>(
LogLevel.Information,
new EventId(2003, nameof(LibraryDataLoaded)),
"音乐库数据已加载从: {Path}, 耗时: {ElapsedMs}ms, 歌曲数: {SongCount}, 专辑数: {AlbumCount}"
);
private static readonly Action<ILogger, int, double, Exception?> _libraryScanning =
LoggerMessage.Define<int, double>(
LogLevel.Debug,
new EventId(2004, nameof(LibraryScanning)),
"正在扫描音乐库: 已处理 {ProcessedCount} 个文件, 进度: {ProgressPercent}%"
);
// 编辑歌曲信息相关日志
private static readonly Action<ILogger, string, Exception?> _editingSongInfoIO =
LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(2500, nameof(EditingSongInfoIO)),
"Error_EditingSongInfoIO".GetLocalized()
);
private static readonly Action<ILogger, string, Exception?> _editingSongInfoOther =
LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(2501, nameof(EditingSongInfoOther)),
"Error_EditingSongInfoOther".GetLocalized()
);
// 播放器相关日志
private static readonly Action<ILogger, string, string, Exception?> _songStartedPlaying =
LoggerMessage.Define<string, string>(
LogLevel.Information,
new EventId(3000, nameof(SongStartedPlaying)),
"开始播放歌曲: {Title} - {Artist}"
);
private static readonly Action<ILogger, string, Exception?> _songPlaybackError =
LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(3001, nameof(SongPlaybackError)),
"Error_SongPlayback".GetLocalized()
);
private static readonly Action<ILogger, string, long, Exception?> _songPlaybackPosition =
LoggerMessage.Define<string, long>(
LogLevel.Debug,
new EventId(3002, nameof(SongPlaybackPosition)),
"歌曲播放位置更新: {Title}, 位置: {PositionMs}ms"
);
// 下载相关日志
private static readonly Action<ILogger, string, string, Exception?> _downloadStarted =
LoggerMessage.Define<string, string>(
LogLevel.Information,
new EventId(4000, nameof(DownloadStarted)),
"开始下载: {Title}, URL: {Url}"
);
private static readonly Action<ILogger, string, double, long, Exception?> _downloadCompleted =
LoggerMessage.Define<string, double, long>(
LogLevel.Information,
new EventId(4001, nameof(DownloadCompleted)),
"下载完成: {Title}, 耗时: {ElapsedMs}ms, 文件大小: {FileSizeBytes} 字节"
);
private static readonly Action<ILogger, string, string, Exception?> _downloadFailed =
LoggerMessage.Define<string, string>(
LogLevel.Error,
new EventId(4002, nameof(DownloadFailed)),
"下载失败: {Title}, 错误: {Error}"
);
private static readonly Action<ILogger, string, int, Exception?> _downloadProgress =
LoggerMessage.Define<string, int>(
LogLevel.Debug,
new EventId(4003, nameof(DownloadProgress)),
"下载进度: {Title}, 进度: {ProgressPercent}%"
);
// 用户界面相关日志
private static readonly Action<ILogger, string, Exception?> _navigationOccurred =
LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(5000, nameof(NavigationOccurred)),
"页面导航: {PageName}"
);
private static readonly Action<ILogger, string, Exception?> _userAction =
LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(5001, nameof(UserAction)),
"用户操作: {ActionName}"
);
// 性能相关日志
private static readonly Action<ILogger, string, double, Exception?> _performanceMetric =
LoggerMessage.Define<string, double>(
LogLevel.Information,
new EventId(6000, nameof(PerformanceMetric)),
"性能指标: {MetricName}, 值: {Value}ms"
);
private static readonly Action<ILogger, long, int, Exception?> _memoryUsage =
LoggerMessage.Define<long, int>(
LogLevel.Information,
new EventId(6001, nameof(MemoryUsage)),
"内存使用情况: {MemoryBytes} 字节, GC代数: {Generation}"
);
// 错误和异常日志
private static readonly Action<ILogger, string, Exception?> _unexpectedException =
LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(9000, nameof(UnexpectedException)),
"未处理的异常: {Message}"
);
private static readonly Action<ILogger, string, string, Exception?> _operationFailed =
LoggerMessage.Define<string, string>(
LogLevel.Error,
new EventId(9001, nameof(OperationFailed)),
"操作失败: {OperationName}, 错误: {Error}"
);
private static readonly Action<ILogger, string, Exception?> _criticalError =
LoggerMessage.Define<string>(
LogLevel.Critical,
new EventId(9999, nameof(CriticalError)),
"严重错误: {Message}"
);
// 应用程序生命周期方法
public static void ApplicationStarting(this ILogger logger) =>
_applicationStarting(logger, null);
public static void ApplicationStarted(this ILogger logger) => _applicationStarted(logger, null);
public static void ApplicationShuttingDown(this ILogger logger) =>
_applicationShuttingDown(logger, null);
// 音乐库相关方法
public static void SavingLibraryData(this ILogger logger, string path) =>
_savingLibraryData(logger, path, null);
public static void LibraryDataSaved(
this ILogger logger,
string path,
double elapsedMs,
int songCount,
int albumCount
) => _libraryDataSaved(logger, path, elapsedMs, songCount, albumCount, null);
public static void LoadingLibraryData(this ILogger logger, string path) =>
_loadingLibraryData(logger, path, null);
public static void LibraryDataLoaded(
this ILogger logger,
string path,
double elapsedMs,
int songCount,
int albumCount
) => _libraryDataLoaded(logger, path, elapsedMs, songCount, albumCount, null);
public static void LibraryScanning(
this ILogger logger,
int processedCount,
double progressPercent
) => _libraryScanning(logger, processedCount, progressPercent, null);
// 编辑歌曲信息相关方法
public static void EditingSongInfoIO(
this ILogger logger,
string title,
Exception? exception = null
) => _editingSongInfoIO(logger, title, exception);
public static void EditingSongInfoOther(
this ILogger logger,
string title,
Exception? exception = null
) => _editingSongInfoOther(logger, title, exception);
// 播放器相关方法
public static void SongStartedPlaying(this ILogger logger, string title, string artist) =>
_songStartedPlaying(logger, title, artist, null);
public static void SongPlaybackError(
this ILogger logger,
string title,
Exception? exception = null
) => _songPlaybackError(logger, title, exception);
public static void SongPlaybackPosition(this ILogger logger, string title, long positionMs) =>
_songPlaybackPosition(logger, title, positionMs, null);
// 下载相关方法
public static void DownloadStarted(this ILogger logger, string title, string url) =>
_downloadStarted(logger, title, url, null);
public static void DownloadCompleted(
this ILogger logger,
string title,
double elapsedMs,
long fileSizeBytes
) => _downloadCompleted(logger, title, elapsedMs, fileSizeBytes, null);
public static void DownloadFailed(
this ILogger logger,
string title,
string error,
Exception? exception = null
) => _downloadFailed(logger, title, error, exception);
public static void DownloadProgress(this ILogger logger, string title, int progressPercent) =>
_downloadProgress(logger, title, progressPercent, null);
// 用户界面相关方法
public static void NavigationOccurred(this ILogger logger, string pageName) =>
_navigationOccurred(logger, pageName, null);
public static void UserAction(this ILogger logger, string actionName) =>
_userAction(logger, actionName, null);
// 性能相关方法
public static void PerformanceMetric(this ILogger logger, string metricName, double value) =>
_performanceMetric(logger, metricName, value, null);
public static void MemoryUsage(this ILogger logger, long memoryBytes, int generation) =>
_memoryUsage(logger, memoryBytes, generation, null);
// 错误和异常方法
public static void UnexpectedException(
this ILogger logger,
string message,
Exception exception
) => _unexpectedException(logger, message, exception);
public static void OperationFailed(
this ILogger logger,
string operationName,
string error,
Exception? exception = null
) => _operationFailed(logger, operationName, error, exception);
public static void CriticalError(
this ILogger logger,
string message,
Exception? exception = null
) => _criticalError(logger, message, exception);
}

View File

@@ -1,10 +1,8 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using NLog.Config;
using NLog.Extensions.Logging;
using NLog.Targets;
using The_Untamed_Music_Player.Messages;
using Windows.Storage;
using ZLogger;
namespace The_Untamed_Music_Player.Services;
@@ -63,7 +61,6 @@ public static class LoggingService
public static void Shutdown()
{
_loggerFactory?.Dispose();
NLog.LogManager.Shutdown();
}
/// <summary>
@@ -72,62 +69,28 @@ public static class LoggingService
private static ILoggerFactory CreateLoggerFactory()
{
var logFolder = GetLogFolderPath();
var logFilePath = Path.Combine(logFolder, "app-${shortdate}.log");
// 确保日志文件夹存在
Directory.CreateDirectory(logFolder);
// 配置 NLog
var config = new LoggingConfiguration();
var fileTarget = new FileTarget("fileTarget")
{
FileName = logFilePath,
Layout =
"${longdate} [${level:uppercase=true:padding=5}] ${logger:shortName=true} ${message} ${exception:format=tostring}",
MaxArchiveFiles = 7, // 保留7天的日志
ArchiveEvery = FileArchivePeriod.Day,
ArchiveFileName = Path.Combine(logFolder, "app-{#}.log"), // 归档模式
ArchiveSuffixFormat = "{0:yyyyMMdd}",
KeepFileOpen = true,
AutoFlush = false, // 提高性能
OpenFileFlushTimeout = 5, // 5秒自动刷新
CreateDirs = true,
BufferSize = 32768, // 32KB缓冲区
};
#if DEBUG
// 调试输出目标
var debugTarget = new DebugTarget("debugTarget")
{
Layout =
"${time} [${level:uppercase=true:padding=5}] ${logger:shortName=true} ${message}",
};
config.AddTarget(debugTarget);
config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, debugTarget);
#endif
config.AddTarget(fileTarget);
// 所有级别写入文件
config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, fileTarget);
NLog.LogManager.Configuration = config;
// 创建自定义日志提供程序
// 创建日志工厂
var loggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.ClearProviders();
builder.AddNLog();
#if DEBUG
builder.SetMinimumLevel(LogLevel.Debug);
#else
builder.SetMinimumLevel(LogLevel.Information);
#endif
});
// 添加Messenger日志提供程序
loggerFactory.AddProvider(new MessengerLoggerProvider());
// 添加文件日志提供程序
var logFilePath = Path.Combine(logFolder, $"app-{DateTime.Now:yyyyMMdd}.log");
builder.AddZLoggerFile(logFilePath);
// 添加Messenger日志提供程序用于InfoBar显示
builder.AddProvider(new MessengerLoggerProvider());
});
return loggerFactory;
}
@@ -184,7 +147,7 @@ public static class LoggingService
}
var cutoffDate = DateTime.Now.AddDays(-7);
var logFiles = Directory.GetFiles(logFolder, "*.log*");
var logFiles = Directory.GetFiles(logFolder, "app-*.log");
foreach (var file in logFiles)
{

View File

@@ -0,0 +1,336 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using The_Untamed_Music_Player.Helpers;
using ZLogger;
namespace The_Untamed_Music_Player.Services;
/// <summary>
/// ZLogger高性能日志扩展方法
/// 使用ZLogger的零分配特性和结构化日志
/// </summary>
public static class ZLoggerExtensions
{
// 应用程序生命周期日志
public static void ApplicationStarting(this ILogger logger) =>
logger.ZLogInformation($"应用程序正在启动");
public static void ApplicationStarted(this ILogger logger) =>
logger.ZLogInformation($"应用程序启动完成");
public static void ApplicationShuttingDown(this ILogger logger) =>
logger.ZLogInformation($"应用程序正在关闭");
// 音乐库相关日志
public static void SavingLibraryData(this ILogger logger, string path) =>
logger.ZLogInformation($"正在保存音乐库数据到: {path}");
public static void LibraryDataSaved(
this ILogger logger,
string path,
double elapsedMs,
int songCount,
int albumCount
) =>
logger.ZLogInformation(
$"音乐库数据已保存到: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}"
);
public static void LoadingLibraryData(this ILogger logger, string path) =>
logger.ZLogInformation($"正在加载音乐库数据从: {path}");
public static void LibraryDataLoaded(
this ILogger logger,
string path,
double elapsedMs,
int songCount,
int albumCount
) =>
logger.ZLogInformation(
$"音乐库数据已加载从: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}"
);
public static void LibraryScanning(
this ILogger logger,
int processedCount,
double progressPercent
) =>
logger.ZLogDebug(
$"正在扫描音乐库: 已处理 {processedCount} 个文件, 进度: {progressPercent}%"
);
// 播放器相关日志
public static void SongStartedPlaying(this ILogger logger, string title, string artist) =>
logger.ZLogInformation($"开始播放歌曲: {title} - {artist}");
public static void SongPlaybackError(
this ILogger logger,
string title,
Exception? exception = null
) =>
logger.ZLogError(
exception,
$"{"Error_SongPlayback".GetLocalizedWithReplace("{title}", title)}"
);
public static void SongPlaybackPosition(this ILogger logger, string title, long positionMs) =>
logger.ZLogTrace($"歌曲播放位置更新: {title}, 位置: {positionMs}ms");
// 下载相关日志
public static void DownloadStarted(this ILogger logger, string title, string url) =>
logger.ZLogInformation($"开始下载: {title}, URL: {url}");
public static void DownloadCompleted(
this ILogger logger,
string title,
double elapsedMs,
long fileSizeBytes
) =>
logger.ZLogInformation(
$"下载完成: {title}, 耗时: {elapsedMs}ms, 文件大小: {fileSizeBytes} 字节"
);
public static void DownloadFailed(
this ILogger logger,
string title,
string error,
Exception? exception = null
) => logger.ZLogError(exception, $"下载失败: {title}, 错误: {error}");
public static void DownloadProgress(this ILogger logger, string title, int progressPercent) =>
logger.ZLogTrace($"下载进度: {title}, 进度: {progressPercent}%");
// 用户界面相关日志
public static void NavigationOccurred(this ILogger logger, string pageName) =>
logger.ZLogDebug($"页面导航: {pageName}");
public static void UserAction(this ILogger logger, string actionName) =>
logger.ZLogDebug($"用户操作: {actionName}");
// 性能相关日志
public static void PerformanceMetric(this ILogger logger, string metricName, double value) =>
logger.ZLogInformation($"性能指标: {metricName}, 值: {value}ms");
public static void MemoryUsage(this ILogger logger, long memoryBytes, int generation) =>
logger.ZLogInformation($"内存使用情况: {memoryBytes} 字节, GC代数: {generation}");
// 错误和异常日志
public static void UnexpectedException(
this ILogger logger,
string message,
Exception exception
) => logger.ZLogError(exception, $"未处理的异常: {message}");
public static void OperationFailed(
this ILogger logger,
string operationName,
string error,
Exception? exception = null
) => logger.ZLogError(exception, $"操作失败: {operationName}, 错误: {error}");
public static void CriticalError(
this ILogger logger,
string message,
Exception? exception = null
) => logger.ZLogCritical(exception, $"严重错误: {message}");
// 歌曲信息编辑日志
public static void EditingSongInfoIO(this ILogger logger, string title) =>
logger.ZLogError($"{"Error_EditingSongInfoIO".GetLocalizedWithReplace("{title}", title)}");
public static void EditingSongInfoOther(
this ILogger logger,
string title,
Exception exception
) =>
logger.ZLogError(
exception,
$"{"Error_EditingSongInfoOther".GetLocalizedWithReplace("{title}", title)}"
);
// 高性能性能监控日志
public static void LogPerformanceStart(
this ILogger logger,
string operationName,
int operationId
) => logger.ZLogDebug($"[{operationId:X8}] 开始执行: {operationName}");
public static void LogPerformanceEnd(
this ILogger logger,
string operationName,
int operationId,
double elapsedMs
) =>
logger.ZLogInformation(
$"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsedMs}ms"
);
public static void LogPerformanceEnd(
this ILogger logger,
string operationName,
int operationId,
TimeSpan elapsed
) =>
logger.ZLogInformation(
$"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsed.TotalMilliseconds}ms"
);
// 音乐播放专用高性能日志
public static void PlaybackStateChanged(
this ILogger logger,
string songTitle,
string newState
) => logger.ZLogDebug($"播放状态变更: {songTitle} -> {newState}");
public static void AudioStreamCreated(
this ILogger logger,
string filePath,
int streamHandle,
double durationSeconds
) =>
logger.ZLogDebug(
$"音频流已创建: {filePath}, 句柄: {streamHandle}, 时长: {durationSeconds}s"
);
public static void AudioStreamReleased(this ILogger logger, int streamHandle) =>
logger.ZLogDebug($"音频流已释放: 句柄 {streamHandle}");
public static void PlaybackBufferUnderrun(
this ILogger logger,
string songTitle,
int bufferLevel
) => logger.ZLogWarning($"播放缓冲区不足: {songTitle}, 缓冲区水平: {bufferLevel}%");
// 网络请求日志
public static void HttpRequestStarted(
this ILogger logger,
string method,
string url,
int requestId
) => logger.ZLogDebug($"[{requestId:X8}] HTTP请求开始: {method} {url}");
public static void HttpRequestCompleted(
this ILogger logger,
int requestId,
int statusCode,
double elapsedMs
) =>
logger.ZLogInformation($"[{requestId:X8}] HTTP请求完成: {statusCode}, 耗时: {elapsedMs}ms");
public static void HttpRequestFailed(
this ILogger logger,
int requestId,
string error,
Exception? exception = null
) => logger.ZLogError(exception, $"[{requestId:X8}] HTTP请求失败: {error}");
// 文件操作日志
public static void FileOperationStarted(
this ILogger logger,
string operation,
string filePath,
int operationId
) => logger.ZLogDebug($"[{operationId:X8}] 文件操作开始: {operation} -> {filePath}");
public static void FileOperationCompleted(
this ILogger logger,
int operationId,
double elapsedMs,
long? fileSize = null
)
{
if (fileSize.HasValue)
{
logger.ZLogInformation(
$"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms, 大小: {fileSize} 字节"
);
}
else
{
logger.ZLogInformation($"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms");
}
}
public static void FileOperationFailed(
this ILogger logger,
int operationId,
string error,
Exception? exception = null
) => logger.ZLogError(exception, $"[{operationId:X8}] 文件操作失败: {error}");
// 数据库操作日志
public static void DatabaseOperationStarted(
this ILogger logger,
string operation,
int operationId
) => logger.ZLogDebug($"[{operationId:X8}] 数据库操作开始: {operation}");
public static void DatabaseOperationCompleted(
this ILogger logger,
int operationId,
double elapsedMs,
int? affectedRows = null
)
{
if (affectedRows.HasValue)
{
logger.ZLogInformation(
$"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms, 影响行数: {affectedRows}"
);
}
else
{
logger.ZLogInformation($"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms");
}
}
public static void DatabaseOperationFailed(
this ILogger logger,
int operationId,
string error,
Exception? exception = null
) => logger.ZLogError(exception, $"[{operationId:X8}] 数据库操作失败: {error}");
}
/// <summary>
/// 高性能性能监控辅助类
/// 用于自动记录操作的开始和结束时间
/// </summary>
public readonly struct PerformanceScope : IDisposable
{
private readonly ILogger _logger;
private readonly string _operationName;
private readonly int _operationId;
private readonly Stopwatch _stopwatch;
public PerformanceScope(ILogger logger, string operationName)
{
_logger = logger;
_operationName = operationName;
_operationId = Random.Shared.Next();
_stopwatch = Stopwatch.StartNew();
logger.LogPerformanceStart(operationName, _operationId);
}
public void Dispose()
{
_stopwatch.Stop();
_logger.LogPerformanceEnd(_operationName, _operationId, _stopwatch.Elapsed);
}
}
/// <summary>
/// 高性能日志辅助类
/// </summary>
public static class PerformanceLogger
{
/// <summary>
/// 创建性能监控作用域
/// </summary>
/// <param name="logger">日志记录器</param>
/// <param name="operationName">操作名称</param>
/// <returns>性能监控作用域</returns>
public static PerformanceScope BeginScope(ILogger logger, string operationName) =>
new(logger, operationName);
}

View File

@@ -471,6 +471,9 @@
<data name="Artists_SortBy" xml:space="preserve">
<value>A - Z, Z - A</value>
</data>
<data name="Playlists_SortBy" xml:space="preserve">
<value>A - Z, Z - A, Date modified (Asc), Date modified (Desc)</value>
</data>
<data name="Settings_FallBack.Header" xml:space="preserve">
<value>Background stops updating after window loses focus</value>
</data>
@@ -712,10 +715,10 @@
<value>Send feedback</value>
</data>
<data name="Error_SongPlayback" xml:space="preserve">
<value>We can't open {Title}. This may be because the file type is unsupported, the file extension is incorrect, the file is corrupted, or the network is abnormal.</value>
<value>We can't open {title}. This may be because the file type is unsupported, the file extension is incorrect, the file is corrupted, or the network is abnormal.</value>
</data>
<data name="Error_EditingSongInfoIO" xml:space="preserve">
<value>We can't edit {Title}. This may be because the current file is playing, being occupied by another process, or the file type is unsupported.</value>
<value>We can't edit {title}. This may be because the current file is playing, being occupied by another process, or the file type is unsupported.</value>
</data>
<data name="Settings_DeveloperOptions.Text" xml:space="preserve">
<value>Developer options</value>
@@ -727,6 +730,21 @@
<value>Open folder</value>
</data>
<data name="Error_EditingSongInfoOther" xml:space="preserve">
<value>We can't edit some of the properties of {Title}. This may be because the file type is unsupported, or the file is corrupted.</value>
<value>We can't edit some of the properties of {title}. This may be because the file type is unsupported, or the file is corrupted.</value>
</data>
<data name="NoPlaylist_PlaylistDonotHave.Text" xml:space="preserve">
<value>You don't have any playlists</value>
</data>
<data name="PlayLists_CreateNewPlaylist.Text" xml:space="preserve">
<value>Create a new playlist</value>
</data>
<data name="PlayLists_NewPlaylist.Text" xml:space="preserve">
<value>New playlist</value>
</data>
<data name="PlayLists_CreatePlaylist.Content" xml:space="preserve">
<value>Create playlist</value>
</data>
<data name="PlaylistInfo_UntitledPlaylist.SelectedText" xml:space="preserve">
<value>Untitled playlist</value>
</data>
</root>

View File

@@ -471,6 +471,9 @@
<data name="Artists_SortBy" xml:space="preserve">
<value>A-Z, Z-A</value>
</data>
<data name="Playlists_SortBy" xml:space="preserve">
<value>A-Z, Z-A, 修改日期 (升序), 修改日期 (降序)</value>
</data>
<data name="Settings_FallBack.Header" xml:space="preserve">
<value>窗口失去焦点后背景停止更新</value>
</data>
@@ -712,10 +715,10 @@
<value>发送反馈</value>
</data>
<data name="Error_SongPlayback" xml:space="preserve">
<value>我们无法打开 {Title}。这可能是因为文件类型不受支持、文件扩展名不正确、文件已损坏或网络异常。</value>
<value>我们无法打开 {title}。这可能是因为文件类型不受支持、文件扩展名不正确、文件已损坏或网络异常。</value>
</data>
<data name="Error_EditingSongInfoIO" xml:space="preserve">
<value>我们无法编辑 {Title}。这可能是因为当前文件正在播放、被其他进程占用或文件类型不受支持。</value>
<value>我们无法编辑 {title}。这可能是因为当前文件正在播放、被其他进程占用或文件类型不受支持。</value>
</data>
<data name="Settings_DeveloperOptions.Text" xml:space="preserve">
<value>开发者选项</value>
@@ -727,6 +730,21 @@
<value>打开文件夹</value>
</data>
<data name="Error_EditingSongInfoOther" xml:space="preserve">
<value>我们无法编辑 {Title} 的部分属性。这可能是因为文件类型不受支持或文件已损坏。</value>
<value>我们无法编辑 {title} 的部分属性。这可能是因为文件类型不受支持或文件已损坏。</value>
</data>
<data name="NoPlaylist_PlaylistDonotHave.Text" xml:space="preserve">
<value>你没有任何播放列表</value>
</data>
<data name="PlayLists_CreateNewPlaylist.Text" xml:space="preserve">
<value>创建一个新的播放列表</value>
</data>
<data name="PlayLists_NewPlaylist.Text" xml:space="preserve">
<value>新建播放列表</value>
</data>
<data name="PlayLists_CreatePlaylist.Content" xml:space="preserve">
<value>创建播放列表</value>
</data>
<data name="PlaylistInfo_UntitledPlaylist.SelectedText" xml:space="preserve">
<value>无标题播放列表</value>
</data>
</root>

View File

@@ -1,195 +0,0 @@
using Microsoft.Extensions.Logging;
using The_Untamed_Music_Player.Services;
namespace The_Untamed_Music_Player.Tests;
/// <summary>
/// 日志系统测试和演示类
/// </summary>
public static class LoggingSystemTests
{
private static readonly ILogger _logger = LoggingService.CreateLogger("LoggingTests");
/// <summary>
/// 测试和演示日志系统的各种功能
/// </summary>
public static async Task RunAllTestsAsync()
{
_logger.LogInformation("开始执行日志系统测试");
// 测试基本日志级别
TestBasicLogging();
// 测试结构化日志
TestStructuredLogging();
// 测试异常处理日志
TestExceptionLogging();
// 测试高性能日志记录
TestHighPerformanceLogging();
// 测试性能监控
await TestPerformanceMonitoringAsync();
// 测试条件日志记录
TestConditionalLogging();
// 测试InfoBar显示这会在UI中显示
TestInfoBarLogging();
_logger.LogInformation("日志系统测试完成");
}
private static void TestBasicLogging()
{
_logger.LogDebug("这是调试信息 - 只在Debug模式下显示");
_logger.LogInformation("这是普通信息日志");
_logger.LogWarning("这是警告信息");
// 注意以下两条日志会在InfoBar中显示
// _logger.LogError("这是错误信息 - 会在InfoBar中显示");
// _logger.LogCritical("这是严重错误 - 会在InfoBar中显示");
}
private static void TestStructuredLogging()
{
var userId = 12345;
var userName = "张三";
var songTitle = "测试歌曲";
var playTime = TimeSpan.FromMinutes(3.5);
_logger.LogInformation(
"用户 {UserId} ({UserName}) 播放了歌曲 {SongTitle},时长 {Duration}",
userId,
userName,
songTitle,
playTime
);
var sessionInfo = new
{
SessionId = Guid.NewGuid(),
StartTime = DateTime.Now,
UserAgent = "The Untamed Music Player/1.0",
};
_logger.LogInformation("用户会话信息: {@SessionInfo}", sessionInfo);
}
private static void TestExceptionLogging()
{
try
{
// 模拟一个异常
throw new InvalidOperationException("这是一个测试异常");
}
catch (Exception ex)
{
_logger.LogError(ex, "捕获到测试异常,操作参数: {Parameter}", "test_value");
// 使用高性能日志记录器
_logger.UnexpectedException("测试异常处理", ex);
}
}
private static void TestHighPerformanceLogging()
{
// 使用预编译的高性能日志模板
_logger.SongStartedPlaying("测试歌曲", "测试艺术家");
_logger.DownloadProgress("测试下载", 75);
_logger.PerformanceMetric("测试操作", 1250.5);
_logger.MemoryUsage(1024 * 1024 * 50, 2); // 50MB, Gen 2
}
private static async Task TestPerformanceMonitoringAsync()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var operationName = "性能测试操作";
try
{
_logger.LogInformation("开始执行 {OperationName}", operationName);
// 模拟一些工作
await Task.Delay(500);
stopwatch.Stop();
_logger.PerformanceMetric(operationName, stopwatch.Elapsed.TotalMilliseconds);
_logger.LogInformation(
"{OperationName} 执行完成,耗时: {ElapsedMs}ms",
operationName,
stopwatch.Elapsed.TotalMilliseconds
);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.OperationFailed(operationName, ex.Message, ex);
}
}
private static void TestConditionalLogging()
{
// 高效的条件日志记录
if (_logger.IsEnabled(LogLevel.Debug))
{
var expensiveDebugInfo =
$"调试信息 - 时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}, 线程: {Environment.CurrentManagedThreadId}";
_logger.LogDebug("详细调试信息: {DebugInfo}", expensiveDebugInfo);
}
// 这个在Release版本中不会执行
_logger.LogDebug("这条调试信息在Release版本中不会被记录");
}
private static void TestInfoBarLogging()
{
_logger.LogInformation("准备测试InfoBar显示功能...");
// 等待一下再显示错误,这样可以看到准备信息
Task.Delay(2000)
.ContinueWith(_ =>
{
// 这条错误日志会在MainWindow的InfoBar中显示
_logger.LogError("这是一个测试错误消息应该会在InfoBar中显示5秒钟");
});
Task.Delay(4000)
.ContinueWith(_ =>
{
// 再显示一条严重错误
_logger.LogCritical("这是一个严重错误测试也会在InfoBar中显示");
});
}
/// <summary>
/// 在应用启动时调用此方法来演示日志系统
/// </summary>
public static void DemonstrateLoggingOnStartup()
{
_logger.LogInformation("日志系统演示:应用程序已启动");
_logger.LogDebug("调试模式信息:当前时间 {CurrentTime}", DateTime.Now);
// 记录系统信息
_logger.LogInformation(
"系统信息 - OS: {OS}, .NET版本: {DotNetVersion}, 工作目录: {WorkingDirectory}",
Environment.OSVersion,
Environment.Version,
Environment.CurrentDirectory
);
}
/// <summary>
/// 在应用关闭时调用此方法
/// </summary>
public static void DemonstrateLoggingOnShutdown()
{
_logger.LogInformation("日志系统演示:应用程序正在关闭");
// 记录一些关闭统计信息
var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime;
_logger.LogInformation("应用程序运行时间: {Uptime}", uptime);
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
@@ -42,9 +42,6 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup Condition="'$(Platform)' == 'x86'">
<None Include="Libraries\x86\**\*">
@@ -68,10 +65,16 @@
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.250206-build.2040" />
<PackageReference
Include="CommunityToolkit.Labs.WinUI.MarqueeText"
Version="0.1.250206-build.2040"
/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference
Include="CommunityToolkit.WinUI.Controls.SettingsControls"
Version="8.2.250402"
/>
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="hyjiacan.pinyin4net" Version="4.1.1" />
@@ -80,14 +83,12 @@
<PackageReference Include="ManagedBass.Wasapi" Version="3.1.1" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.3" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="ZLinq" Version="1.5.2" />
<PackageReference Include="ZLogger" Version="2.5.10" />
</ItemGroup>
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />

View File

@@ -306,10 +306,7 @@ public partial class LocalAlbumsViewModel : ObservableRecipient
public void SortByListView_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = SortMode;
}
(sender as ListView)!.SelectedIndex = SortMode;
}
public async void GenreListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -331,10 +328,7 @@ public partial class LocalAlbumsViewModel : ObservableRecipient
public void GenreListView_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = GenreMode;
}
(sender as ListView)!.SelectedIndex = GenreMode;
}
public void PlayButton_Click(LocalAlbumInfo info)

View File

@@ -71,10 +71,7 @@ public partial class LocalArtistsViewModel : ObservableRecipient
public void SortByListView_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = SortMode;
}
(sender as ListView)!.SelectedIndex = SortMode;
}
public async Task SortArtists()

View File

@@ -464,10 +464,7 @@ public partial class LocalSongsViewModel : ObservableRecipient
public void SortByListView_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = SortMode;
}
(sender as ListView)!.SelectedIndex = SortMode;
}
public async void GenreListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -490,10 +487,7 @@ public partial class LocalSongsViewModel : ObservableRecipient
public void GenreListView_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = GenreMode;
}
(sender as ListView)!.SelectedIndex = GenreMode;
}
public void ShuffledPlayAllButton_Click(object sender, RoutedEventArgs e)

View File

@@ -1,8 +1,69 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using The_Untamed_Music_Player.Contracts.Services;
using The_Untamed_Music_Player.Helpers;
namespace The_Untamed_Music_Player.ViewModels;
public partial class PlayListsViewModel : ObservableRecipient
{
public PlayListsViewModel() { }
private readonly ILocalSettingsService _localSettingsService =
App.GetService<ILocalSettingsService>();
public List<string> SortBy { get; set; } = [.. "Playlists_SortBy".GetLocalized().Split(", ")];
[ObservableProperty]
public partial byte SortMode { get; set; } = 0;
partial void OnSortModeChanged(byte value)
{
SortByStr = SortBy[value];
SaveSortModeAsync();
}
[ObservableProperty]
public partial string SortByStr { get; set; } = "";
[ObservableProperty]
public partial bool IsProgressRingActive { get; set; } = true;
public PlayListsViewModel()
{
LoadModeAndPlayList();
}
public async void LoadModeAndPlayList()
{
await LoadSortModeAsync();
}
public void SortByListView_Loaded(object sender, RoutedEventArgs e)
{
(sender as ListView)!.SelectedIndex = SortMode;
}
public void SortByListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var currentsortmode = SortMode;
if (sender is ListView listView && listView.SelectedIndex is int selectedIndex)
{
SortMode = (byte)selectedIndex;
if (SortMode != currentsortmode)
{
IsProgressRingActive = true;
IsProgressRingActive = false;
}
}
}
public async Task LoadSortModeAsync()
{
SortMode = await _localSettingsService.ReadSettingAsync<byte>("PlaylistSortMode");
SortByStr = SortBy[SortMode];
}
public async void SaveSortModeAsync()
{
await _localSettingsService.SaveSettingAsync("PlaylistSortMode", SortMode);
}
}

View File

@@ -206,7 +206,7 @@
<AdaptiveTrigger MinWindowWidth="0"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="16,24,16,0"/>
<Setter Target="MenuGrid.Margin" Value="16,23,16,0"/>
<Setter Target="AlbumGridView.Padding" Value="12,0,12,0"/>
</VisualState.Setters>
</VisualState>
@@ -215,15 +215,13 @@
<AdaptiveTrigger MinWindowWidth="641"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="{StaticResource NavigationViewPageContentMargin}"/>
<Setter Target="MenuGrid.Margin" Value="56,23,56,0"/>
<Setter Target="AlbumGridView.Padding" Value="52,0,52,0"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="MenuGrid"
Grid.Row="0"
Margin="{StaticResource NavigationViewPageContentMargin}">
<Grid x:Name="MenuGrid" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>

View File

@@ -179,7 +179,7 @@
<AdaptiveTrigger MinWindowWidth="0"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="16,24,16,0"/>
<Setter Target="MenuGrid.Margin" Value="16,23,16,0"/>
<Setter Target="ArtistGridView.Padding" Value="12,0,12,0"/>
</VisualState.Setters>
</VisualState>
@@ -188,15 +188,13 @@
<AdaptiveTrigger MinWindowWidth="641"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="{StaticResource NavigationViewPageContentMargin}"/>
<Setter Target="MenuGrid.Margin" Value="56,23,56,0"/>
<Setter Target="ArtistGridView.Padding" Value="52,0,52,0"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="MenuGrid"
Grid.Row="0"
Margin="{StaticResource NavigationViewPageContentMargin}">
<Grid x:Name="MenuGrid" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>

View File

@@ -4,7 +4,6 @@
xmlns:contract="using:The_Untamed_Music_Player.Contracts.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:The_Untamed_Music_Player.Helpers"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:The_Untamed_Music_Player.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
@@ -178,7 +177,7 @@
<AdaptiveTrigger MinWindowWidth="0"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="16,24,16,0"/>
<Setter Target="MenuGrid.Margin" Value="16,23,16,0"/>
<Setter Target="SongListView.Padding" Value="12,0,12,0"/>
</VisualState.Setters>
</VisualState>
@@ -187,7 +186,7 @@
<AdaptiveTrigger MinWindowWidth="641"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MenuGrid.Margin" Value="{StaticResource NavigationViewPageContentMargin}"/>
<Setter Target="MenuGrid.Margin" Value="56,23,56,0"/>
<Setter Target="SongListView.Padding" Value="52,0,52,0"/>
</VisualState.Setters>
</VisualState>

View File

@@ -4,7 +4,6 @@
xmlns:contract="using:The_Untamed_Music_Player.Contracts.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:The_Untamed_Music_Player.Helpers"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:The_Untamed_Music_Player.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:The_Untamed_Music_Player.Models"

View File

@@ -7,6 +7,14 @@
NavigationCacheMode="Enabled"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="SortByListViewItemTemplate" x:DataType="x:String">
<ListViewItem Margin="0,2,0,2">
<TextBlock Text="{x:Bind}"/>
</ListViewItem>
</DataTemplate>
</Page.Resources>
<Grid x:Name="ContentArea">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@@ -20,6 +28,11 @@
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="TitleGrid.Margin" Value="16,36,16,0"/>
<Setter Target="Image.Height" Value="90"/>
<Setter Target="Image.Width" Value="90"/>
<Setter Target="StackPanel.Spacing" Value="11"/>
<Setter Target="MenuGrid.Margin" Value="16,23,16,0"/>
<Setter Target="PlaylistGridView.Padding" Value="12,0,12,0"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Normal">
@@ -28,6 +41,11 @@
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="TitleGrid.Margin" Value="56,36,56,0"/>
<Setter Target="Image.Height" Value="150"/>
<Setter Target="Image.Width" Value="150"/>
<Setter Target="StackPanel.Spacing" Value="22"/>
<Setter Target="MenuGrid.Margin" Value="56,23,56,0"/>
<Setter Target="PlaylistGridView.Padding" Value="52,0,52,0"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -35,6 +53,108 @@
<Grid x:Name="TitleGrid" Grid.Row="0">
<TextBlock x:Uid="Shell_PlayLists1" Style="{StaticResource TitleLargeTextBlockStyle}"/>
</Grid>
<ScrollViewer Grid.Row="1" Padding="0,0,0,16"/>
<Grid x:Name="NoPlaylistControl"
Grid.Row="1"
Visibility="Collapsed">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<StackPanel x:Name="StackPanel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Image x:Name="Image"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Source="ms-appx:///Assets/PlaylistGradient.svg"/>
<StackPanel VerticalAlignment="Center"
Orientation="Vertical" Spacing="8">
<TextBlock x:Uid="NoPlaylist_PlaylistDonotHave" FontSize="29"/>
<Button Style="{StaticResource AccentButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontFamily="{StaticResource UntamedFontFamily}"
FontSize="12" Glyph="&#xE710;"/>
<TextBlock x:Uid="PlayLists_CreateNewPlaylist"/>
</StackPanel>
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel Spacing="16">
<TextBox x:Uid="PlaylistInfo_UntitledPlaylist"
Width="280"
HorizontalAlignment="Center"/>
<Button x:Uid="PlayLists_CreatePlaylist"
HorizontalAlignment="Center"
Style="{StaticResource AccentButtonStyle}"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
<Grid x:Name="HavePlaylistControl"
Grid.Row="1"
Visibility="Visible">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid x:Name="MenuGrid"
Grid.Row="0"
Margin="56,23,56,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Style="{StaticResource AccentButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontFamily="{StaticResource UntamedFontFamily}"
FontSize="12" Glyph="&#xE710;"/>
<TextBlock x:Uid="PlayLists_NewPlaylist"/>
</StackPanel>
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel Spacing="16">
<TextBox x:Uid="PlaylistInfo_UntitledPlaylist"
Width="280"
HorizontalAlignment="Center"/>
<Button x:Uid="PlayLists_CreatePlaylist"
HorizontalAlignment="Center"
Style="{StaticResource AccentButtonStyle}"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Grid.Column="2"
Background="Transparent" BorderBrush="Transparent">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock x:Uid="Songs_SortByChosen"/>
<TextBlock Foreground="{ThemeResource AccentTextFillColorTertiaryBrush}" Text="{x:Bind ViewModel.SortByStr, Mode=OneWay}"/>
<FontIcon Margin="8,0,0,0"
FontFamily="{StaticResource UntamedFontFamily}"
FontSize="12" Glyph="&#xE70D;"/>
</StackPanel>
<Button.Flyout>
<Flyout Placement="Bottom">
<ListView x:Name="SortByListView"
Margin="-12,-13,-12,-15"
ItemTemplate="{StaticResource SortByListViewItemTemplate}"
ItemsSource="{x:Bind ViewModel.SortBy}"
Loaded="{x:Bind ViewModel.SortByListView_Loaded}"
SelectionChanged="{x:Bind ViewModel.SortByListView_SelectionChanged}"/>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<GridView x:Name="PlaylistGridView"
Grid.Row="1"
helper:ListViewExtensions.ItemCornerRadius="8"
IsItemClickEnabled="True"
ItemClick="PlaylistGridView_ItemClick"
Loaded="PlaylistGridView_Loaded"
SelectionMode="None"/>
</Grid>
</Grid>
</Page>

View File

@@ -1,3 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using The_Untamed_Music_Player.ViewModels;
@@ -12,4 +13,8 @@ public sealed partial class PlayListsPage : Page
ViewModel = App.GetService<PlayListsViewModel>();
InitializeComponent();
}
private void PlaylistGridView_Loaded(object sender, RoutedEventArgs e) { }
private void PlaylistGridView_ItemClick(object sender, ItemClickEventArgs e) { }
}

View File

@@ -4,7 +4,6 @@
xmlns:contract="using:The_Untamed_Music_Player.Contracts.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:The_Untamed_Music_Player.Helpers"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:The_Untamed_Music_Player.Models"
xmlns:ui="using:CommunityToolkit.WinUI"

View File

@@ -4,7 +4,6 @@
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:The_Untamed_Music_Player.Helpers"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="OnLoaded">