From 4f4398acc8e318a0b223294311b4f4eaf0a150ef Mon Sep 17 00:00:00 2001 From: real-zony Date: Tue, 3 Mar 2026 12:07:28 +0800 Subject: [PATCH] feat: Implement lyrics download feature. --- src/ZonyLrcTools.Desktop/App.axaml | 1 + .../Resources/UIStrings.en-US.resx | 4 + .../Resources/UIStrings.resx | 4 + .../Services/DialogService.cs | 15 +- .../ViewModels/LyricsDownloadViewModel.cs | 172 +++++++++++++++++- .../Views/Dialogs/ConfirmDialog.axaml | 39 ++++ .../Views/Dialogs/ConfirmDialog.axaml.cs | 35 ++++ .../Views/Dialogs/MessageDialog.axaml | 32 ++++ .../Views/Dialogs/MessageDialog.axaml.cs | 29 +++ .../Views/Pages/LyricsDownloadPage.axaml | 31 +++- 10 files changed, 345 insertions(+), 17 deletions(-) create mode 100644 src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml create mode 100644 src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml.cs create mode 100644 src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml create mode 100644 src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml.cs diff --git a/src/ZonyLrcTools.Desktop/App.axaml b/src/ZonyLrcTools.Desktop/App.axaml index 7e38d66..4cce6ce 100644 --- a/src/ZonyLrcTools.Desktop/App.axaml +++ b/src/ZonyLrcTools.Desktop/App.axaml @@ -5,6 +5,7 @@ + diff --git a/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx b/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx index c4133c5..c50f557 100644 --- a/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx +++ b/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx @@ -242,4 +242,8 @@ Info + + + No music files found in the selected folder. + diff --git a/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx b/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx index 0f9e519..0a613bb 100644 --- a/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx +++ b/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx @@ -242,4 +242,8 @@ 提示 + + + 在所选文件夹中没有找到任何音乐文件。 + diff --git a/src/ZonyLrcTools.Desktop/Services/DialogService.cs b/src/ZonyLrcTools.Desktop/Services/DialogService.cs index 5a030d9..9e3706e 100644 --- a/src/ZonyLrcTools.Desktop/Services/DialogService.cs +++ b/src/ZonyLrcTools.Desktop/Services/DialogService.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; +using ZonyLrcTools.Desktop.Views.Dialogs; namespace ZonyLrcTools.Desktop.Services; @@ -47,14 +48,18 @@ public class DialogService : IDialogService public async Task ShowConfirmDialogAsync(string title, string message) { - // TODO: Implement custom confirm dialog - await Task.CompletedTask; - return true; + if (MainWindow == null) return false; + + var dialog = new ConfirmDialog(title, message); + var result = await dialog.ShowDialog(MainWindow); + return result; } public async Task ShowMessageAsync(string title, string message) { - // TODO: Implement custom message dialog - await Task.CompletedTask; + if (MainWindow == null) return; + + var dialog = new MessageDialog(title, message); + await dialog.ShowDialog(MainWindow); } } diff --git a/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs index ac0412e..bcfa179 100644 --- a/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs +++ b/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs @@ -1,7 +1,14 @@ using System.Collections.ObjectModel; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Options; +using ZonyLrcTools.Common; +using ZonyLrcTools.Common.Configuration; +using ZonyLrcTools.Common.Infrastructure.IO; +using ZonyLrcTools.Common.Infrastructure.Threading; using ZonyLrcTools.Common.Lyrics; +using ZonyLrcTools.Common.TagInfo; using ZonyLrcTools.Desktop.Infrastructure.Localization; using ZonyLrcTools.Desktop.Services; @@ -11,9 +18,14 @@ public partial class LyricsDownloadViewModel : ViewModelBase { private readonly ILyricsDownloader _lyricsDownloader; private readonly IDialogService _dialogService; + private readonly IFileScanner _fileScanner; + private readonly ITagLoader _tagLoader; + private readonly GlobalOptions _options; private readonly IUILocalizationService? _localization; private CancellationTokenSource? _downloadCts; + private Dictionary _musicFileMap = new(); + private List _scannedMusicInfos = new(); [ObservableProperty] private string? _selectedFolderPath; @@ -38,6 +50,21 @@ public partial class LyricsDownloadViewModel : ViewModelBase [NotifyPropertyChangedFor(nameof(CanStartDownload))] private bool _isDownloading; + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(StartDownloadCommand))] + [NotifyCanExecuteChangedFor(nameof(SelectFolderCommand))] + [NotifyPropertyChangedFor(nameof(CanStartDownload))] + private bool _isScanning; + + [ObservableProperty] + private int _scanProgressCount; + + [ObservableProperty] + private int _scanTotalCount; + + [ObservableProperty] + private double _scanProgressPercentage; + [ObservableProperty] private string? _currentProcessingFile; @@ -49,6 +76,7 @@ public partial class LyricsDownloadViewModel : ViewModelBase public string LyricsParallel => _localization?["Lyrics_Parallel"] ?? "Parallel:"; public string LyricsStartDownload => _localization?["Lyrics_StartDownload"] ?? "Start Download"; public string LyricsStopDownload => _localization?["Lyrics_StopDownload"] ?? "Stop Download"; + public string LyricsScanning => _localization?["Lyrics_Status_Scanning"] ?? "Scanning files..."; public string CommonTotal => _localization?["Common_Total"] ?? "Total:"; public string CommonFiles => _localization?["Common_Files"] ?? "files"; public string CommonSuccess => _localization?["Common_Success"] ?? "Success:"; @@ -58,17 +86,23 @@ public partial class LyricsDownloadViewModel : ViewModelBase public string ColumnFilePath => _localization?["Column_FilePath"] ?? "File Path"; public string ColumnStatus => _localization?["Column_Status"] ?? "Status"; - public bool CanStartDownload => !IsDownloading && !string.IsNullOrEmpty(SelectedFolderPath); + public bool CanStartDownload => !IsDownloading && !IsScanning && MusicFiles.Count > 0; public ObservableCollection MusicFiles { get; } = new(); public LyricsDownloadViewModel( ILyricsDownloader lyricsDownloader, IDialogService dialogService, + IFileScanner fileScanner, + ITagLoader tagLoader, + IOptions options, IUILocalizationService? localization = null) { _lyricsDownloader = lyricsDownloader; _dialogService = dialogService; + _fileScanner = fileScanner; + _tagLoader = tagLoader; + _options = options.Value; _localization = localization; if (_localization != null) @@ -86,6 +120,7 @@ public partial class LyricsDownloadViewModel : ViewModelBase OnPropertyChanged(nameof(LyricsParallel)); OnPropertyChanged(nameof(LyricsStartDownload)); OnPropertyChanged(nameof(LyricsStopDownload)); + OnPropertyChanged(nameof(LyricsScanning)); OnPropertyChanged(nameof(CommonTotal)); OnPropertyChanged(nameof(CommonFiles)); OnPropertyChanged(nameof(CommonSuccess)); @@ -96,32 +131,155 @@ public partial class LyricsDownloadViewModel : ViewModelBase OnPropertyChanged(nameof(ColumnStatus)); } - [RelayCommand] + [RelayCommand(CanExecute = nameof(CanSelectFolder))] private async Task SelectFolderAsync() { - var folder = await _dialogService.ShowFolderPickerAsync("Select Music Folder"); + var folder = await _dialogService.ShowFolderPickerAsync( + _localization?["Lyrics_SelectFolder"] ?? "Select Music Folder"); if (string.IsNullOrEmpty(folder)) return; SelectedFolderPath = folder; - OnPropertyChanged(nameof(CanStartDownload)); + await ScanFilesAsync(); + } + + private bool CanSelectFolder => !IsScanning && !IsDownloading; + + private async Task ScanFilesAsync() + { + if (string.IsNullOrEmpty(SelectedFolderPath)) return; + + IsScanning = true; + MusicFiles.Clear(); + _musicFileMap.Clear(); + _scannedMusicInfos.Clear(); + ScanProgressCount = 0; + ScanTotalCount = 0; + ScanProgressPercentage = 0; + TotalCount = 0; + CompletedCount = 0; + FailedCount = 0; + ProgressPercentage = 0; + + try + { + var files = (await _fileScanner.ScanMusicFilesAsync(SelectedFolderPath, _options.SupportFileExtensions)) + .ToList(); + + if (files.Count == 0) + { + await _dialogService.ShowMessageAsync( + _localization?["Common_Info"] ?? "Info", + _localization?["Error_NoMusicFiles"] ?? "No music files found in the selected folder."); + IsScanning = false; + OnPropertyChanged(nameof(CanStartDownload)); + return; + } + + ScanTotalCount = files.Count; + + using var warpTask = new WarpTask(ParallelCount); + var scanTasks = files.Select(file => + warpTask.RunAsync(async () => + { + var musicInfo = await _tagLoader.LoadTagAsync(file); + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (musicInfo != null && + (!string.IsNullOrEmpty(musicInfo.Name) || !string.IsNullOrEmpty(musicInfo.Artist))) + { + var vm = new MusicFileViewModel + { + FilePath = musicInfo.FilePath, + Name = musicInfo.Name, + Artist = musicInfo.Artist, + StatusMessage = _localization?["Status_Pending"] ?? "Pending" + }; + MusicFiles.Add(vm); + _musicFileMap[musicInfo.FilePath] = vm; + _scannedMusicInfos.Add(musicInfo); + } + + ScanProgressCount++; + ScanProgressPercentage = ScanProgressCount * 100.0 / ScanTotalCount; + }); + })); + + await Task.WhenAll(scanTasks); + + TotalCount = MusicFiles.Count; + } + catch (Exception ex) + { + await _dialogService.ShowMessageAsync( + _localization?["Common_Error"] ?? "Error", + ex.Message); + } + finally + { + IsScanning = false; + OnPropertyChanged(nameof(CanStartDownload)); + } } [RelayCommand(CanExecute = nameof(CanStartDownload))] private async Task StartDownloadAsync() { - if (string.IsNullOrEmpty(SelectedFolderPath)) return; + if (_scannedMusicInfos.Count == 0) return; IsDownloading = true; CompletedCount = 0; FailedCount = 0; ProgressPercentage = 0; + TotalCount = _scannedMusicInfos.Count; + + // Reset all row statuses + foreach (var vm in MusicFiles) + { + vm.IsProcessed = false; + vm.IsSuccessful = false; + vm.StatusMessage = _localization?["Status_Pending"] ?? "Pending"; + } _downloadCts = new CancellationTokenSource(); try { - // TODO: Implement actual download logic using _lyricsDownloader - await Task.Delay(1000, _downloadCts.Token); // Placeholder + await _lyricsDownloader.DownloadAsync( + _scannedMusicInfos, + ParallelCount, + async musicInfo => + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (_musicFileMap.TryGetValue(musicInfo.FilePath, out var vm)) + { + vm.IsProcessed = true; + vm.IsSuccessful = musicInfo.IsSuccessful; + + if (musicInfo.IsPruneMusic) + { + vm.StatusMessage = _localization?["Status_Skipped"] ?? "Skipped"; + } + else if (musicInfo.IsSuccessful) + { + vm.StatusMessage = _localization?["Status_Success"] ?? "Success"; + } + else + { + vm.StatusMessage = _localization?["Status_Failed"] ?? "Failed"; + } + } + + if (musicInfo.IsSuccessful) + CompletedCount++; + else + FailedCount++; + + ProgressPercentage = (CompletedCount + FailedCount) * 100.0 / TotalCount; + }); + }, + _downloadCts.Token); } catch (OperationCanceledException) { diff --git a/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml b/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml new file mode 100644 index 0000000..5e042f6 --- /dev/null +++ b/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + +