From d1f082cc80bc2bcad23bd61e6af63a86438dc00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=9A=E7=BE=8E?= <2370337237@qq.com> Date: Fri, 14 Mar 2025 22:30:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 51 + MusicTool.sln | 24 + MusicTool/App.xaml | 43 + MusicTool/App.xaml.cs | 32 + .../Converters/BoolToHeartIconConverter.cs | 27 + .../Equalizer/EqualizerSampleProvider.cs | 56 + MusicTool/Equalizer/EqualizerWindow.xaml | 81 ++ MusicTool/Equalizer/EqualizerWindow.xaml.cs | 79 + MusicTool/MainWindow.xaml | 320 +++++ MusicTool/MainWindow.xaml.cs | 1279 +++++++++++++++++ MusicTool/Models/SongModel.cs | 101 ++ MusicTool/MusicTool.csproj | 36 + MusicTool/Playlist/InputDialog.xaml | 40 + MusicTool/Playlist/InputDialog.xaml.cs | 35 + MusicTool/Playlist/PlaylistModel.cs | 13 + MusicTool/Playlist/PlaylistWindow.xaml | 71 + MusicTool/Playlist/PlaylistWindow.xaml.cs | 192 +++ MusicTool/Playlist/SongModel.cs | 15 + MusicTool/Resources/Icons/app.ico | Bin 0 -> 163386 bytes MusicTool/Services/AudioPlayerService.cs | 149 ++ MusicTool/Services/MusicLibraryService.cs | 184 +++ MusicTool/Settings/SettingsWindow.xaml | 92 ++ MusicTool/Settings/SettingsWindow.xaml.cs | 194 +++ README.md | 61 +- 24 files changed, 3173 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 MusicTool.sln create mode 100644 MusicTool/App.xaml create mode 100644 MusicTool/App.xaml.cs create mode 100644 MusicTool/Converters/BoolToHeartIconConverter.cs create mode 100644 MusicTool/Equalizer/EqualizerSampleProvider.cs create mode 100644 MusicTool/Equalizer/EqualizerWindow.xaml create mode 100644 MusicTool/Equalizer/EqualizerWindow.xaml.cs create mode 100644 MusicTool/MainWindow.xaml create mode 100644 MusicTool/MainWindow.xaml.cs create mode 100644 MusicTool/Models/SongModel.cs create mode 100644 MusicTool/MusicTool.csproj create mode 100644 MusicTool/Playlist/InputDialog.xaml create mode 100644 MusicTool/Playlist/InputDialog.xaml.cs create mode 100644 MusicTool/Playlist/PlaylistModel.cs create mode 100644 MusicTool/Playlist/PlaylistWindow.xaml create mode 100644 MusicTool/Playlist/PlaylistWindow.xaml.cs create mode 100644 MusicTool/Playlist/SongModel.cs create mode 100644 MusicTool/Resources/Icons/app.ico create mode 100644 MusicTool/Services/AudioPlayerService.cs create mode 100644 MusicTool/Services/MusicLibraryService.cs create mode 100644 MusicTool/Settings/SettingsWindow.xaml create mode 100644 MusicTool/Settings/SettingsWindow.xaml.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab0a373 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Visual Studio files +.vs/ +bin/ +obj/ +*.user +*.suo +*.cache +*.pdb +*.exe +*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio cache/options directory +.vs/ +.vscode/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Local History +.localhistory/ + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Application specific +shortcuts.json +settings.json \ No newline at end of file diff --git a/MusicTool.sln b/MusicTool.sln new file mode 100644 index 0000000..deb1b9c --- /dev/null +++ b/MusicTool.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicTool", "MusicTool\MusicTool.csproj", "{20B3B6A3-E1F5-40CE-A210-933943F7A930}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {20B3B6A3-E1F5-40CE-A210-933943F7A930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20B3B6A3-E1F5-40CE-A210-933943F7A930}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20B3B6A3-E1F5-40CE-A210-933943F7A930}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20B3B6A3-E1F5-40CE-A210-933943F7A930}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1C2D3E4-F5G6-H7I8-J9K0-L1M2N3O4P5Q6} + EndGlobalSection +EndGlobal diff --git a/MusicTool/App.xaml b/MusicTool/App.xaml new file mode 100644 index 0000000..5b02be3 --- /dev/null +++ b/MusicTool/App.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MusicTool/App.xaml.cs b/MusicTool/App.xaml.cs new file mode 100644 index 0000000..ac780d1 --- /dev/null +++ b/MusicTool/App.xaml.cs @@ -0,0 +1,32 @@ +using System; +using System.Windows; + +namespace MusicTool +{ + /// + /// App.xaml 的交互逻辑 + /// + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + // 全局异常处理 + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + Exception ex = (Exception)args.ExceptionObject; + MessageBox.Show($"发生了未处理的异常: {ex.Message}\n\n{ex.StackTrace}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + }; + + // 应用程序级别的异常处理 + Application.Current.DispatcherUnhandledException += (sender, args) => + { + MessageBox.Show($"发生了未处理的异常: {args.Exception.Message}\n\n{args.Exception.StackTrace}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + args.Handled = true; + }; + } + } +} \ No newline at end of file diff --git a/MusicTool/Converters/BoolToHeartIconConverter.cs b/MusicTool/Converters/BoolToHeartIconConverter.cs new file mode 100644 index 0000000..bd1b820 --- /dev/null +++ b/MusicTool/Converters/BoolToHeartIconConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using MaterialDesignThemes.Wpf; + +namespace MusicTool +{ + public class BoolToHeartIconConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isFavorite && isFavorite) + { + return PackIconKind.Heart; + } + else + { + return PackIconKind.HeartOutline; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/MusicTool/Equalizer/EqualizerSampleProvider.cs b/MusicTool/Equalizer/EqualizerSampleProvider.cs new file mode 100644 index 0000000..a039576 --- /dev/null +++ b/MusicTool/Equalizer/EqualizerSampleProvider.cs @@ -0,0 +1,56 @@ +using System; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; + +namespace MusicTool.Equalizer +{ + public class EqualizerSampleProvider : ISampleProvider + { + private readonly ISampleProvider sourceProvider; + private readonly float[] bandGains; + + public EqualizerSampleProvider(ISampleProvider sourceProvider) + { + this.sourceProvider = sourceProvider; + this.bandGains = new float[10]; + } + + public WaveFormat WaveFormat => sourceProvider.WaveFormat; + + public void SetBand(int band, float gain) + { + if (band >= 0 && band < bandGains.Length) + { + // 将分贝值转换为线性增益 + bandGains[band] = (float)Math.Pow(10, gain / 20); + } + } + + public int Read(float[] buffer, int offset, int count) + { + int samplesRead = sourceProvider.Read(buffer, offset, count); + + if (samplesRead > 0) + { + // 应用均衡器增益 + for (int i = 0; i < samplesRead; i++) + { + int sampleIndex = offset + i; + float sample = buffer[sampleIndex]; + + // 对每个频段应用增益 + for (int band = 0; band < bandGains.Length; band++) + { + // 这里应该使用实际的滤波器实现 + // 为了演示,我们简单地应用增益 + sample *= bandGains[band]; + } + + buffer[sampleIndex] = sample; + } + } + + return samplesRead; + } + } +} \ No newline at end of file diff --git a/MusicTool/Equalizer/EqualizerWindow.xaml b/MusicTool/Equalizer/EqualizerWindow.xaml new file mode 100644 index 0000000..6224930 --- /dev/null +++ b/MusicTool/Equalizer/EqualizerWindow.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MusicTool/MainWindow.xaml.cs b/MusicTool/MainWindow.xaml.cs new file mode 100644 index 0000000..85af57c --- /dev/null +++ b/MusicTool/MainWindow.xaml.cs @@ -0,0 +1,1279 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using Microsoft.Win32; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using TagLib; +using File = System.IO.File; +using TagFile = TagLib.File; +using System.Windows.Forms; +using MessageBox = System.Windows.MessageBox; +using OpenFileDialog = Microsoft.Win32.OpenFileDialog; +using System.Threading.Tasks; +using WinForms = System.Windows.Forms; +using WinButton = System.Windows.Controls.Button; +using WinDataFormats = System.Windows.DataFormats; +using System.Text.Json; +using MusicTool.Equalizer; +using MusicTool.Playlist; +using MusicTool.Settings; +using System.Runtime.Versioning; + +namespace MusicTool +{ + /// + /// MainWindow.xaml 的交互逻辑 + /// + [SupportedOSPlatform("windows")] + public partial class MainWindow : Window + { + private const string PLAYLIST_FILE = "playlist.json"; + private const string SETTINGS_FILE = "settings.json"; + + private IWavePlayer? wavePlayer; + private AudioFileReader? audioFileReader; + private DispatcherTimer? timer; + private bool isPlaying = false; + private bool isDraggingSlider = false; + private ObservableCollection songs = new ObservableCollection(); + private ObservableCollection allSongs = new ObservableCollection(); + private int currentSongIndex = -1; + private PlaybackMode playbackMode = PlaybackMode.Sequential; + private float currentVolume = 0.8f; // 默认音量80% + private double[] equalizerBands = new double[10]; + private EqualizerSampleProvider? equalizerSampleProvider; + + public MainWindow() + { + InitializeComponent(); + + // 初始化播放器 + InitializePlayer(); + + // 初始化UI + SongsDataGrid.ItemsSource = songs; + + // 加载保存的播放列表和设置 + LoadPlaylist(); + LoadSettings(); + + // 绑定事件 + SongsDataGrid.SelectionChanged += SongsDataGrid_SelectionChanged; + SongsDataGrid.MouseDoubleClick += SongsDataGrid_MouseDoubleClick; + + // 初始化播放模式图标 + UpdateRepeatModeIcon(); + + // 注册窗口关闭事件 + this.Closing += MainWindow_Closing; + } + + private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) + { + // 保存播放列表和设置 + SavePlaylist(); + SaveSettings(); + + // 停止播放并释放资源 + StopPlayback(); + DisposeWavePlayer(); + } + + private void SavePlaylist() + { + try + { + var playlistData = new PlaylistData + { + Songs = songs.ToList() + }; + + string json = JsonSerializer.Serialize(playlistData, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(PLAYLIST_FILE, json); + } + catch (Exception ex) + { + MessageBox.Show($"保存播放列表失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void LoadPlaylist() + { + try + { + if (File.Exists(PLAYLIST_FILE)) + { + string json = File.ReadAllText(PLAYLIST_FILE); + var playlistData = JsonSerializer.Deserialize(json); + + if (playlistData?.Songs != null) + { + allSongs.Clear(); + songs.Clear(); + + foreach (var song in playlistData.Songs) + { + allSongs.Add(song); + songs.Add(song); + } + + //if (allSongs.Count > 0) + //{ + // MessageBox.Show($"已加载 {allSongs.Count} 首歌曲", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + //} + } + } + } + catch (Exception ex) + { + MessageBox.Show($"加载播放列表失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void SaveSettings() + { + try + { + var settings = new MusicTool.Settings.SettingsData + { + StartMinimized = WindowState == WindowState.Minimized, + ShowAlbumArt = AlbumCover.Visibility == Visibility.Visible + }; + + string json = JsonSerializer.Serialize(settings); + File.WriteAllText(SETTINGS_FILE, json); + } + catch (Exception ex) + { + MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void LoadSettings() + { + try + { + if (File.Exists(SETTINGS_FILE)) + { + string json = File.ReadAllText(SETTINGS_FILE); + var settings = JsonSerializer.Deserialize(json); + if (settings != null) + { + // 应用设置 + if (settings.StartMinimized) + { + WindowState = WindowState.Minimized; + } + + if (settings.ShowAlbumArt) + { + AlbumCover.Visibility = Visibility.Visible; + } + else + { + AlbumCover.Visibility = Visibility.Collapsed; + } + } + } + } + catch (Exception ex) + { + MessageBox.Show($"加载设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void InitializePlayer() + { + try + { + // 初始化NAudio播放器 + DisposeWavePlayer(); + wavePlayer = new WaveOutEvent(); + wavePlayer.PlaybackStopped += WavePlayer_PlaybackStopped; + + // 初始化计时器 + timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromMilliseconds(500); + timer.Tick += Timer_Tick; + } + catch (Exception ex) + { + MessageBox.Show($"初始化播放器出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void DisposeWavePlayer() + { + if (audioFileReader != null) + { + audioFileReader.Dispose(); + audioFileReader = null; + } + + if (wavePlayer != null) + { + if (wavePlayer.PlaybackState != PlaybackState.Stopped) + { + wavePlayer.Stop(); + } + wavePlayer.Dispose(); + wavePlayer = null; + } + } + + private void AddSampleData() + { + // 添加一些示例歌曲数据 + var sampleSongs = new List + { + new Playlist.SongModel + { + Title = "示例歌曲 1", + Artist = "示例艺术家", + Album = "示例专辑", + Duration = "3:45", + FilePath = "" + }, + new Playlist.SongModel + { + Title = "示例歌曲 2", + Artist = "示例艺术家", + Album = "示例专辑", + Duration = "4:20", + FilePath = "" + }, + new Playlist.SongModel + { + Title = "示例歌曲 3", + Artist = "另一个艺术家", + Album = "另一个专辑", + Duration = "5:10", + FilePath = "" + } + }; + + foreach (var song in sampleSongs) + { + songs.Add(song); + allSongs.Add(song); + } + } + + private void SongsDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + currentSongIndex = SongsDataGrid.SelectedIndex; + } + } + + private void SongsDataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + currentSongIndex = SongsDataGrid.SelectedIndex; + PlayCurrentSong(); + } + } + + private void WavePlayer_PlaybackStopped(object? sender, StoppedEventArgs e) + { + if (e.Exception == null) + { + // 正常停止,可能是歌曲播放完毕 + if (isPlaying) + { + // 根据播放模式决定下一步操作 + switch (playbackMode) + { + case PlaybackMode.Sequential: + PlayNextSong(); + break; + case PlaybackMode.Repeat: + PlayCurrentSong(); + break; + case PlaybackMode.RepeatOne: + PlayCurrentSong(); + break; + case PlaybackMode.Shuffle: + PlayRandomSong(); + break; + } + } + } + else + { + // 发生异常 + MessageBox.Show($"播放出错: {e.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void Timer_Tick(object? sender, EventArgs e) + { + if (audioFileReader != null && !isDraggingSlider) + { + // 更新进度条和时间显示 + CurrentTime.Text = audioFileReader.CurrentTime.ToString(@"mm\:ss"); + TotalTime.Text = audioFileReader.TotalTime.ToString(@"mm\:ss"); + + // 更新进度条位置 + double progress = audioFileReader.CurrentTime.TotalSeconds / audioFileReader.TotalTime.TotalSeconds; + ProgressSlider.Value = progress * 100; + } + } + + private void PlayCurrentSong() + { + if (currentSongIndex >= 0 && currentSongIndex < songs.Count) + { + var song = songs[currentSongIndex]; + + // 在实际应用中,这里应该使用真实的文件路径 + if (!string.IsNullOrEmpty(song.FilePath) && File.Exists(song.FilePath)) + { + PlaySong(song.FilePath); + + // 更新UI + NowPlayingSong.Text = song.Title; + NowPlayingArtist.Text = song.Artist; + + // TODO: 加载专辑封面 + // 这里应该从音乐文件中提取专辑封面或使用默认图片 + } + else + { + MessageBox.Show("无法播放:文件不存在或路径无效", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + private void PlaySong(string filePath) + { + try + { + // 停止当前播放并释放资源 + StopPlayback(); + DisposeWavePlayer(); + + // 确保播放器已初始化 + InitializePlayer(); + + // 创建新的音频读取器 + audioFileReader = new AudioFileReader(filePath); + audioFileReader.Volume = currentVolume; + + if (wavePlayer != null) + { + // 应用均衡器 + equalizerSampleProvider = new EqualizerSampleProvider(audioFileReader); + ApplyEqualizerSettings(equalizerBands); + + wavePlayer.Init(equalizerSampleProvider); + wavePlayer.Play(); + isPlaying = true; + + // 启动计时器 + timer?.Start(); + + // 更新播放按钮图标 + UpdatePlayPauseIcon(); + + // 尝试加载专辑封面 + LoadAlbumArt(filePath); + } + } + catch (Exception ex) + { + MessageBox.Show($"播放出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + + // 重新初始化播放器 + InitializePlayer(); + } + } + + private void LoadAlbumArt(string filePath) + { + try + { + var file = TagFile.Create(filePath); + + if (file.Tag.Pictures != null && file.Tag.Pictures.Length > 0) + { + var picture = file.Tag.Pictures[0]; + var imageData = picture.Data.Data; + + using (var stream = new MemoryStream(imageData)) + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = stream; + bitmap.EndInit(); + bitmap.Freeze(); // 重要:使图像可以跨线程访问 + + AlbumCover.Source = bitmap; + } + } + else + { + // 使用默认图片 + AlbumCover.Source = null; + } + } + catch + { + // 如果加载失败,使用默认图片 + AlbumCover.Source = null; + } + } + + private void StopPlayback() + { + if (wavePlayer != null) + { + if (wavePlayer.PlaybackState != PlaybackState.Stopped) + { + wavePlayer.Stop(); + } + } + + if (audioFileReader != null) + { + audioFileReader.Dispose(); + audioFileReader = null; + } + + if (timer != null) + { + timer.Stop(); + } + + isPlaying = false; + + // 重置UI + CurrentTime.Text = "00:00"; + TotalTime.Text = "00:00"; + ProgressSlider.Value = 0; + + // 更新播放按钮图标 + UpdatePlayPauseIcon(); + } + + private void PlayNextSong() + { + if (songs.Count > 0) + { + try + { + // 停止当前播放 + StopPlayback(); + + // 更新索引 + currentSongIndex = (currentSongIndex + 1) % songs.Count; + + // 检查文件是否存在 + var nextSong = songs[currentSongIndex]; + if (!string.IsNullOrEmpty(nextSong.FilePath) && File.Exists(nextSong.FilePath)) + { + PlayCurrentSong(); + } + else + { + // 如果文件不存在,尝试播放下一首 + MessageBox.Show($"文件 {nextSong.Title} 不存在或已被移动", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + PlayNextSong(); + } + } + catch (Exception ex) + { + MessageBox.Show($"播放下一首歌曲时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + InitializePlayer(); // 重新初始化播放器 + } + } + } + + private void PlayPreviousSong() + { + if (songs.Count > 0) + { + try + { + // 停止当前播放 + StopPlayback(); + + // 更新索引 + currentSongIndex = (currentSongIndex - 1 + songs.Count) % songs.Count; + + // 检查文件是否存在 + var prevSong = songs[currentSongIndex]; + if (!string.IsNullOrEmpty(prevSong.FilePath) && File.Exists(prevSong.FilePath)) + { + PlayCurrentSong(); + } + else + { + // 如果文件不存在,尝试播放上一首 + MessageBox.Show($"文件 {prevSong.Title} 不存在或已被移动", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + PlayPreviousSong(); + } + } + catch (Exception ex) + { + MessageBox.Show($"播放上一首歌曲时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + InitializePlayer(); // 重新初始化播放器 + } + } + } + + private void PlayRandomSong() + { + if (songs.Count > 0) + { + try + { + // 停止当前播放 + StopPlayback(); + + // 生成随机索引 + Random random = new Random(); + int oldIndex = currentSongIndex; + + // 确保不会连续播放同一首歌 + do + { + currentSongIndex = random.Next(0, songs.Count); + } while (songs.Count > 1 && currentSongIndex == oldIndex); + + // 检查文件是否存在 + var randomSong = songs[currentSongIndex]; + if (!string.IsNullOrEmpty(randomSong.FilePath) && File.Exists(randomSong.FilePath)) + { + PlayCurrentSong(); + } + else + { + // 如果文件不存在,尝试播放另一首随机歌曲 + MessageBox.Show($"文件 {randomSong.Title} 不存在或已被移动", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + PlayRandomSong(); + } + } + catch (Exception ex) + { + MessageBox.Show($"播放随机歌曲时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + InitializePlayer(); // 重新初始化播放器 + } + } + } + + private void TogglePlayPause() + { + if (wavePlayer != null) + { + if (isPlaying) + { + if (wavePlayer.PlaybackState == PlaybackState.Playing) + { + wavePlayer.Pause(); + timer?.Stop(); + isPlaying = false; + } + } + else + { + if (audioFileReader != null && wavePlayer.PlaybackState == PlaybackState.Paused) + { + wavePlayer.Play(); + timer?.Start(); + isPlaying = true; + } + else if (currentSongIndex >= 0) + { + PlayCurrentSong(); + } + else if (songs.Count > 0) + { + // 如果没有选中歌曲但列表有歌曲,播放第一首 + currentSongIndex = 0; + PlayCurrentSong(); + } + } + + // 更新播放按钮图标 + UpdatePlayPauseIcon(); + } + } + + private void UpdatePlayPauseIcon() + { + if (isPlaying) + { + PlayPauseIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.Pause; + } + else + { + PlayPauseIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.Play; + } + } + + private void UpdateRepeatModeIcon() + { + switch (playbackMode) + { + case PlaybackMode.Sequential: + RepeatIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.ArrowRightBold; + RepeatButton.ToolTip = "顺序播放"; + break; + case PlaybackMode.Repeat: + RepeatIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.Repeat; + RepeatButton.ToolTip = "列表循环"; + break; + case PlaybackMode.RepeatOne: + RepeatIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.RepeatOnce; + RepeatButton.ToolTip = "单曲循环"; + break; + case PlaybackMode.Shuffle: + RepeatIcon.Kind = MaterialDesignThemes.Wpf.PackIconKind.Shuffle; + RepeatButton.ToolTip = "随机播放"; + break; + } + + // 更新菜单项的选中状态 + SequentialMenuItem.IsChecked = (playbackMode == PlaybackMode.Sequential); + RepeatMenuItem.IsChecked = (playbackMode == PlaybackMode.Repeat); + RepeatOneMenuItem.IsChecked = (playbackMode == PlaybackMode.RepeatOne); + ShuffleMenuItem.IsChecked = (playbackMode == PlaybackMode.Shuffle); + } + + private void LoadMusicFiles() + { + OpenFileDialog openFileDialog = new OpenFileDialog + { + Filter = "所有支持的音频文件|*.mp3;*.wav;*.flac;*.m4a;*.aac;*.ogg;*.wma|" + + "MP3 文件 (*.mp3)|*.mp3|" + + "WAV 文件 (*.wav)|*.wav|" + + "FLAC 文件 (*.flac)|*.flac|" + + "M4A 文件 (*.m4a)|*.m4a|" + + "AAC 文件 (*.aac)|*.aac|" + + "OGG 文件 (*.ogg)|*.ogg|" + + "WMA 文件 (*.wma)|*.wma|" + + "所有文件 (*.*)|*.*", + Multiselect = true, + Title = "选择音乐文件" + }; + + if (openFileDialog.ShowDialog() == true) + { + int addedCount = 0; + foreach (string filePath in openFileDialog.FileNames) + { + try + { + // 使用TagLib读取音乐文件的元数据 + var file = TagFile.Create(filePath); + + var song = new Playlist.SongModel + { + Title = file.Tag.Title ?? Path.GetFileNameWithoutExtension(filePath), + Artist = string.Join(", ", file.Tag.Performers) ?? "未知艺术家", + Album = file.Tag.Album ?? "未知专辑", + Duration = TimeSpan.FromSeconds(file.Properties.Duration.TotalSeconds).ToString(@"mm\:ss"), + FilePath = filePath + }; + + songs.Add(song); + allSongs.Add(song); + addedCount++; + } + catch (Exception ex) + { + MessageBox.Show($"加载文件 {Path.GetFileName(filePath)} 时出错: {ex.Message}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + if (addedCount > 0) + { + MessageBox.Show($"已添加 {addedCount} 首歌曲", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + protected override void OnClosed(EventArgs e) + { + // 清理资源 + StopPlayback(); + DisposeWavePlayer(); + + base.OnClosed(e); + } + + // UI事件处理程序 + private void AddMusicFolder_Click(object sender, RoutedEventArgs e) + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "选择音乐文件夹"; + dialog.UseDescriptionForTitle = true; + + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + { + string folderPath = dialog.SelectedPath; + LoadMusicFilesFromFolder(folderPath); + } + } + } + + private void LoadMusicFilesFromFolder(string folderPath) + { + try + { + string[] supportedExtensions = { ".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg", ".wma" }; + var files = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories) + .Where(file => supportedExtensions.Contains(Path.GetExtension(file).ToLower())) + .ToList(); + + if (files.Count == 0) + { + MessageBox.Show("未找到支持的音乐文件", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + int addedCount = 0; + foreach (string filePath in files) + { + try + { + // 使用TagLib读取音乐文件的元数据 + var file = TagFile.Create(filePath); + + var song = new Playlist.SongModel + { + Title = file.Tag.Title ?? Path.GetFileNameWithoutExtension(filePath), + Artist = file.Tag.Performers.Length > 0 ? string.Join(", ", file.Tag.Performers) : "未知艺术家", + Album = file.Tag.Album ?? "未知专辑", + Duration = TimeSpan.FromSeconds(file.Properties.Duration.TotalSeconds).ToString(@"mm\:ss"), + FilePath = filePath + }; + + songs.Add(song); + allSongs.Add(song); + addedCount++; + } + catch (Exception ex) + { + Console.WriteLine($"加载文件 {Path.GetFileName(filePath)} 时出错: {ex.Message}"); + } + } + + MessageBox.Show($"已添加 {addedCount} 首歌曲", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + MessageBox.Show($"加载文件夹时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void PlayPauseButton_Click(object sender, RoutedEventArgs e) + { + TogglePlayPause(); + } + + private void PreviousButton_Click(object sender, RoutedEventArgs e) + { + PlayPreviousSong(); + } + + private void NextButton_Click(object sender, RoutedEventArgs e) + { + PlayNextSong(); + } + + private void ShuffleButton_Click(object sender, RoutedEventArgs e) + { + playbackMode = PlaybackMode.Shuffle; + UpdateRepeatModeIcon(); + MessageBox.Show("已切换到随机播放模式", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void RepeatButton_Click(object sender, RoutedEventArgs e) + { + RepeatButton.ContextMenu.IsOpen = true; + } + + // 播放模式菜单事件处理程序 + private void SequentialMenuItem_Click(object sender, RoutedEventArgs e) + { + playbackMode = PlaybackMode.Sequential; + UpdateRepeatModeIcon(); + MessageBox.Show("已切换到顺序播放模式", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void RepeatMenuItem_Click(object sender, RoutedEventArgs e) + { + playbackMode = PlaybackMode.Repeat; + UpdateRepeatModeIcon(); + MessageBox.Show("已切换到列表循环模式", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void RepeatOneMenuItem_Click(object sender, RoutedEventArgs e) + { + playbackMode = PlaybackMode.RepeatOne; + UpdateRepeatModeIcon(); + MessageBox.Show("已切换到单曲循环模式", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void ShuffleMenuItem_Click(object sender, RoutedEventArgs e) + { + playbackMode = PlaybackMode.Shuffle; + UpdateRepeatModeIcon(); + MessageBox.Show("已切换到随机播放模式", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + + private void ProgressSlider_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + isDraggingSlider = true; + } + + private void ProgressSlider_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (audioFileReader != null) + { + double sliderValue = ProgressSlider.Value; + double positionInSeconds = (sliderValue / 100.0) * audioFileReader.TotalTime.TotalSeconds; + audioFileReader.CurrentTime = TimeSpan.FromSeconds(positionInSeconds); + } + + isDraggingSlider = false; + } + + private void ProgressSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + // 只有在用户拖动滑块时才更新位置 + if (isDraggingSlider && audioFileReader != null) + { + double sliderValue = e.NewValue; + double positionInSeconds = (sliderValue / 100.0) * audioFileReader.TotalTime.TotalSeconds; + CurrentTime.Text = TimeSpan.FromSeconds(positionInSeconds).ToString(@"mm\:ss"); + } + } + + private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + currentVolume = (float)(e.NewValue / 100.0); + + if (audioFileReader != null) + { + audioFileReader.Volume = currentVolume; + } + } + + private void EqualizerButton_Click(object sender, RoutedEventArgs e) + { + try + { + var equalizerWindow = new EqualizerWindow(this); + equalizerWindow.ShowDialog(); + } + catch (Exception ex) + { + MessageBox.Show($"打开均衡器时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void PlaylistButton_Click(object sender, RoutedEventArgs e) + { + var window = new PlaylistWindow(this); + window.ShowDialog(); + } + + private void SettingsButton_Click(object sender, RoutedEventArgs e) + { + var window = new SettingsWindow(this); + window.ShowDialog(); + } + + public void UpdateSettings(MusicTool.Settings.SettingsData settings) + { + // 应用设置 + if (settings.StartMinimized) + { + WindowState = WindowState.Minimized; + } + + if (settings.ShowAlbumArt) + { + AlbumCover.Visibility = Visibility.Visible; + } + else + { + AlbumCover.Visibility = Visibility.Collapsed; + } + } + + // 右键菜单事件处理程序 + private void PlayMenuItem_Click(object sender, RoutedEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + currentSongIndex = SongsDataGrid.SelectedIndex; + PlayCurrentSong(); + } + } + + private void AddSongsMenuItem_Click(object sender, RoutedEventArgs e) + { + LoadMusicFiles(); + } + + private void RemoveSongMenuItem_Click(object sender, RoutedEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + var selectedSong = (Playlist.SongModel)SongsDataGrid.SelectedItem; + + // 如果正在播放这首歌,先停止播放 + if (currentSongIndex == SongsDataGrid.SelectedIndex && isPlaying) + { + StopPlayback(); + } + + // 如果删除的是当前播放的歌曲之前的歌曲,需要调整currentSongIndex + if (SongsDataGrid.SelectedIndex < currentSongIndex) + { + currentSongIndex--; + } + else if (SongsDataGrid.SelectedIndex == currentSongIndex) + { + // 如果删除的是当前播放的歌曲,重置currentSongIndex + currentSongIndex = -1; + } + + songs.Remove(selectedSong); + allSongs.Remove(selectedSong); + MessageBox.Show($"已从播放列表中移除 {selectedSong.Title}", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + + private void ClearPlaylistMenuItem_Click(object sender, RoutedEventArgs e) + { + if (songs.Count > 0) + { + if (MessageBox.Show("确定要清空播放列表吗?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) + { + StopPlayback(); + songs.Clear(); + allSongs.Clear(); + currentSongIndex = -1; + MessageBox.Show("播放列表已清空", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + private void SongInfoMenuItem_Click(object sender, RoutedEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + var selectedSong = (Playlist.SongModel)SongsDataGrid.SelectedItem; + + try + { + if (!string.IsNullOrEmpty(selectedSong.FilePath) && File.Exists(selectedSong.FilePath)) + { + var file = TagFile.Create(selectedSong.FilePath); + + string fileInfo = $"文件: {Path.GetFileName(selectedSong.FilePath)}\n" + + $"路径: {selectedSong.FilePath}\n" + + $"大小: {new FileInfo(selectedSong.FilePath).Length / 1024} KB\n\n" + + $"标题: {selectedSong.Title}\n" + + $"艺术家: {selectedSong.Artist}\n" + + $"专辑: {selectedSong.Album}\n" + + $"时长: {selectedSong.Duration}\n" + + $"比特率: {file.Properties.AudioBitrate} kbps\n" + + $"采样率: {file.Properties.AudioSampleRate} Hz\n" + + $"声道: {file.Properties.AudioChannels}"; + + MessageBox.Show(fileInfo, "歌曲信息", MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + MessageBox.Show("无法获取歌曲信息:文件不存在", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + catch (Exception ex) + { + MessageBox.Show($"获取歌曲信息时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + private void OpenFileLocationMenuItem_Click(object sender, RoutedEventArgs e) + { + if (SongsDataGrid.SelectedItem != null) + { + var selectedSong = (Playlist.SongModel)SongsDataGrid.SelectedItem; + + if (!string.IsNullOrEmpty(selectedSong.FilePath) && File.Exists(selectedSong.FilePath)) + { + try + { + // 打开文件所在的文件夹并选中该文件 + string argument = "/select, \"" + selectedSong.FilePath + "\""; + System.Diagnostics.Process.Start("explorer.exe", argument); + } + catch (Exception ex) + { + MessageBox.Show($"打开文件位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + else + { + MessageBox.Show("无法打开文件位置:文件不存在", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + string searchText = SearchTextBox.Text.Trim().ToLower(); + + if (string.IsNullOrEmpty(searchText)) + { + // 如果搜索框为空,显示所有歌曲 + songs.Clear(); + foreach (var song in allSongs) + { + songs.Add(song); + } + } + else + { + // 否则,根据搜索文本过滤歌曲 + songs.Clear(); + var filteredSongs = allSongs.Where(song => + song.Title.ToLower().Contains(searchText) || + song.Artist.ToLower().Contains(searchText) || + song.Album.ToLower().Contains(searchText) + ); + + foreach (var song in filteredSongs) + { + songs.Add(song); + } + } + } + + private void Window_DragOver(object sender, System.Windows.DragEventArgs e) + { + if (e.Data.GetDataPresent(WinDataFormats.FileDrop)) + { + e.Effects = System.Windows.DragDropEffects.Copy; + } + else + { + e.Effects = System.Windows.DragDropEffects.None; + } + e.Handled = true; + } + + private void Window_Drop(object sender, System.Windows.DragEventArgs e) + { + if (e.Data.GetDataPresent(WinDataFormats.FileDrop)) + { + string[] droppedFiles = (string[])e.Data.GetData(WinDataFormats.FileDrop); + + if (droppedFiles != null && droppedFiles.Length > 0) + { + // 检查是否有文件夹 + bool hasDirectory = droppedFiles.Any(path => Directory.Exists(path)); + + if (hasDirectory) + { + // 如果有文件夹,询问用户是否要递归导入 + if (MessageBox.Show("检测到文件夹,是否递归导入所有子文件夹中的音乐文件?", + "导入确认", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) + { + ImportDroppedItems(droppedFiles, true); + } + else + { + ImportDroppedItems(droppedFiles, false); + } + } + else + { + // 如果只有文件,直接导入 + ImportDroppedItems(droppedFiles, false); + } + } + } + } + + private void ImportDroppedItems(string[] items, bool recursive) + { + List musicFiles = new List(); + string[] supportedExtensions = { ".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg", ".wma" }; + + foreach (string item in items) + { + if (File.Exists(item)) + { + // 如果是文件,检查扩展名 + string extension = Path.GetExtension(item).ToLower(); + if (supportedExtensions.Contains(extension)) + { + musicFiles.Add(item); + } + } + else if (Directory.Exists(item)) + { + // 如果是目录,根据recursive参数决定是否递归 + SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + + try + { + var files = Directory.GetFiles(item, "*.*", searchOption) + .Where(file => supportedExtensions.Contains(Path.GetExtension(file).ToLower())); + + musicFiles.AddRange(files); + } + catch (Exception ex) + { + MessageBox.Show($"扫描目录 {item} 时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + if (musicFiles.Count > 0) + { + int addedCount = 0; + + foreach (string filePath in musicFiles) + { + try + { + // 使用TagLib读取音乐文件的元数据 + var file = TagFile.Create(filePath); + + var song = new Playlist.SongModel + { + Title = file.Tag.Title ?? Path.GetFileNameWithoutExtension(filePath), + Artist = file.Tag.Performers.Length > 0 ? string.Join(", ", file.Tag.Performers) : "未知艺术家", + Album = file.Tag.Album ?? "未知专辑", + Duration = TimeSpan.FromSeconds(file.Properties.Duration.TotalSeconds).ToString(@"mm\:ss"), + FilePath = filePath + }; + + songs.Add(song); + allSongs.Add(song); + addedCount++; + } + catch (Exception ex) + { + Console.WriteLine($"加载文件 {Path.GetFileName(filePath)} 时出错: {ex.Message}"); + } + } + + MessageBox.Show($"已添加 {addedCount} 首歌曲", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + MessageBox.Show("未找到支持的音乐文件", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + + private void FavoriteButton_Click(object sender, RoutedEventArgs e) + { + if (sender is WinButton button && button.DataContext is Playlist.SongModel song) + { + // 切换收藏状态 + song.IsFavorite = !song.IsFavorite; + + // 可以在这里添加保存收藏状态的代码 + // 例如保存到配置文件或数据库 + } + } + + public void ApplyEqualizerSettings(double[] bands) + { + // 保存均衡器设置 + if (bands.Length == equalizerBands.Length) + { + Array.Copy(bands, equalizerBands, bands.Length); + } + + // 应用到当前播放的音频 + if (equalizerSampleProvider != null) + { + for (int i = 0; i < Math.Min(bands.Length, 10); i++) + { + equalizerSampleProvider.SetBand(i, (float)bands[i]); + } + } + } + + public void GetEqualizerSettings(double[] bands) + { + if (bands.Length == equalizerBands.Length) + { + Array.Copy(equalizerBands, bands, bands.Length); + } + } + + public void LoadPlaylist(PlaylistModel playlist) + { + if (playlist != null && playlist.Songs.Count > 0) + { + // 停止当前播放 + StopPlayback(); + + // 清空当前播放列表 + songs.Clear(); + + // 添加播放列表中的歌曲 + foreach (var song in playlist.Songs) + { + songs.Add(song); + } + + // 更新allSongs集合 + allSongs.Clear(); + foreach (var song in songs) + { + allSongs.Add(song); + } + + // 设置当前歌曲索引 + currentSongIndex = 0; + + // 播放第一首歌 + PlayCurrentSong(); + + MessageBox.Show($"已加载播放列表 '{playlist.Name}'", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + public class PlaylistData + { + public List Songs { get; set; } = new List(); + } + + public enum PlaybackMode + { + Sequential, // 顺序播放 + Repeat, // 列表循环 + RepeatOne, // 单曲循环 + Shuffle // 随机播放 + } +} \ No newline at end of file diff --git a/MusicTool/Models/SongModel.cs b/MusicTool/Models/SongModel.cs new file mode 100644 index 0000000..e5f9cb4 --- /dev/null +++ b/MusicTool/Models/SongModel.cs @@ -0,0 +1,101 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace MusicTool +{ + public class SongModel : INotifyPropertyChanged + { + private string _title = string.Empty; + private string _artist = string.Empty; + private string _album = string.Empty; + private string _duration = string.Empty; + private string _filePath = string.Empty; + private bool _isFavorite; + + public string Title + { + get => _title; + set + { + if (_title != value) + { + _title = value; + OnPropertyChanged(); + } + } + } + + public string Artist + { + get => _artist; + set + { + if (_artist != value) + { + _artist = value; + OnPropertyChanged(); + } + } + } + + public string Album + { + get => _album; + set + { + if (_album != value) + { + _album = value; + OnPropertyChanged(); + } + } + } + + public string Duration + { + get => _duration; + set + { + if (_duration != value) + { + _duration = value; + OnPropertyChanged(); + } + } + } + + public string FilePath + { + get => _filePath; + set + { + if (_filePath != value) + { + _filePath = value; + OnPropertyChanged(); + } + } + } + + public bool IsFavorite + { + get => _isFavorite; + set + { + if (_isFavorite != value) + { + _isFavorite = value; + OnPropertyChanged(); + } + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/MusicTool/MusicTool.csproj b/MusicTool/MusicTool.csproj new file mode 100644 index 0000000..0369a3d --- /dev/null +++ b/MusicTool/MusicTool.csproj @@ -0,0 +1,36 @@ + + + + WinExe + net6.0-windows + enable + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MusicTool/Playlist/InputDialog.xaml b/MusicTool/Playlist/InputDialog.xaml new file mode 100644 index 0000000..a6b6c36 --- /dev/null +++ b/MusicTool/Playlist/InputDialog.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + +