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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml.cs
new file mode 100644
index 0000000..87aaeec
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml.cs
@@ -0,0 +1,35 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Microsoft.Extensions.DependencyInjection;
+using ZonyLrcTools.Desktop.Infrastructure.Localization;
+
+namespace ZonyLrcTools.Desktop.Views.Dialogs;
+
+public partial class ConfirmDialog : Window
+{
+ public ConfirmDialog()
+ {
+ InitializeComponent();
+ }
+
+ public ConfirmDialog(string title, string message) : this()
+ {
+ TitleText.Text = title;
+ MessageText.Text = message;
+ Title = title;
+
+ var localization = App.Services?.GetService();
+ ConfirmButton.Content = localization?["Common_OK"] ?? "OK";
+ CancelButton.Content = localization?["Common_Cancel"] ?? "Cancel";
+ }
+
+ private void OnConfirmClick(object? sender, RoutedEventArgs e)
+ {
+ Close(true);
+ }
+
+ private void OnCancelClick(object? sender, RoutedEventArgs e)
+ {
+ Close(false);
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml b/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml
new file mode 100644
index 0000000..6dfef93
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml.cs
new file mode 100644
index 0000000..14711dd
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml.cs
@@ -0,0 +1,29 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Microsoft.Extensions.DependencyInjection;
+using ZonyLrcTools.Desktop.Infrastructure.Localization;
+
+namespace ZonyLrcTools.Desktop.Views.Dialogs;
+
+public partial class MessageDialog : Window
+{
+ public MessageDialog()
+ {
+ InitializeComponent();
+ }
+
+ public MessageDialog(string title, string message) : this()
+ {
+ TitleText.Text = title;
+ MessageText.Text = message;
+ Title = title;
+
+ var localization = App.Services?.GetService();
+ OkButton.Content = localization?["Common_OK"] ?? "OK";
+ }
+
+ private void OnOkClick(object? sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml
index 19e11d4..9096e72 100644
--- a/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml
@@ -1,6 +1,7 @@
@@ -56,6 +57,7 @@