mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2026-03-17 06:42:57 +00:00
feat: Implement lyrics download feature.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
|
||||
<Application.Resources>
|
||||
|
||||
@@ -242,4 +242,8 @@
|
||||
<data name="Common_Info" xml:space="preserve">
|
||||
<value>Info</value>
|
||||
</data>
|
||||
<!-- Error Messages -->
|
||||
<data name="Error_NoMusicFiles" xml:space="preserve">
|
||||
<value>No music files found in the selected folder.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -242,4 +242,8 @@
|
||||
<data name="Common_Info" xml:space="preserve">
|
||||
<value>提示</value>
|
||||
</data>
|
||||
<!-- Error Messages -->
|
||||
<data name="Error_NoMusicFiles" xml:space="preserve">
|
||||
<value>在所选文件夹中没有找到任何音乐文件。</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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<bool> 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<bool>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, MusicFileViewModel> _musicFileMap = new();
|
||||
private List<MusicInfo> _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<MusicFileViewModel> MusicFiles { get; } = new();
|
||||
|
||||
public LyricsDownloadViewModel(
|
||||
ILyricsDownloader lyricsDownloader,
|
||||
IDialogService dialogService,
|
||||
IFileScanner fileScanner,
|
||||
ITagLoader tagLoader,
|
||||
IOptions<GlobalOptions> 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)
|
||||
{
|
||||
|
||||
39
src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml
Normal file
39
src/ZonyLrcTools.Desktop/Views/Dialogs/ConfirmDialog.axaml
Normal file
@@ -0,0 +1,39 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ZonyLrcTools.Desktop.Views.Dialogs.ConfirmDialog"
|
||||
Title=""
|
||||
Width="400"
|
||||
Height="200"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SizeToContent="Height">
|
||||
|
||||
<Grid Margin="24" RowDefinitions="Auto,*,Auto">
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Row="0"
|
||||
x:Name="TitleText"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- Message -->
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="MessageText"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="CancelButton"
|
||||
MinWidth="80"
|
||||
Click="OnCancelClick" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
MinWidth="80"
|
||||
Classes="accent"
|
||||
Click="OnConfirmClick" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -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<IUILocalizationService>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
32
src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml
Normal file
32
src/ZonyLrcTools.Desktop/Views/Dialogs/MessageDialog.axaml
Normal file
@@ -0,0 +1,32 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ZonyLrcTools.Desktop.Views.Dialogs.MessageDialog"
|
||||
Title=""
|
||||
Width="400"
|
||||
Height="200"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SizeToContent="Height">
|
||||
|
||||
<Grid Margin="24" RowDefinitions="Auto,*,Auto">
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Row="0"
|
||||
x:Name="TitleText"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- Message -->
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="MessageText"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- OK Button -->
|
||||
<Button Grid.Row="2"
|
||||
x:Name="OkButton"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="80"
|
||||
Click="OnOkClick" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -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<IUILocalizationService>();
|
||||
OkButton.Content = localization?["Common_OK"] ?? "OK";
|
||||
}
|
||||
|
||||
private void OnOkClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
|
||||
xmlns:loc="using:ZonyLrcTools.Desktop.Infrastructure.Localization"
|
||||
x:Class="ZonyLrcTools.Desktop.Views.Pages.LyricsDownloadPage"
|
||||
x:DataType="vm:LyricsDownloadViewModel">
|
||||
|
||||
@@ -56,6 +57,7 @@
|
||||
<Button Command="{Binding StartDownloadCommand}"
|
||||
Content="{Binding LyricsStartDownload}"
|
||||
IsVisible="{Binding !IsDownloading}"
|
||||
IsEnabled="{Binding CanStartDownload}"
|
||||
Classes="accent"
|
||||
MinWidth="100" />
|
||||
<Button Command="{Binding CancelDownloadCommand}"
|
||||
@@ -65,7 +67,21 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Progress Bar (visible during download) -->
|
||||
<!-- Progress Bar: Scanning phase -->
|
||||
<StackPanel IsVisible="{Binding IsScanning}" Spacing="8">
|
||||
<ProgressBar Value="{Binding ScanProgressPercentage}"
|
||||
Maximum="100"
|
||||
Height="6" />
|
||||
<TextBlock HorizontalAlignment="Center" FontSize="12" Opacity="0.8">
|
||||
<Run Text="{Binding LyricsScanning}" />
|
||||
<Run Text=" " />
|
||||
<Run Text="{Binding ScanProgressCount}" />
|
||||
<Run Text=" / " />
|
||||
<Run Text="{Binding ScanTotalCount}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Progress Bar: Download phase -->
|
||||
<StackPanel IsVisible="{Binding IsDownloading}" Spacing="8">
|
||||
<ProgressBar Value="{Binding ProgressPercentage}"
|
||||
Maximum="100"
|
||||
@@ -90,20 +106,25 @@
|
||||
GridLinesVisibility="Horizontal"
|
||||
BorderThickness="0">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="{Binding ColumnSongName}"
|
||||
<DataGridTextColumn x:DataType="vm:MusicFileViewModel"
|
||||
Header="{loc:Localize Column_SongName}"
|
||||
Binding="{Binding Name}"
|
||||
Width="*" />
|
||||
<DataGridTextColumn Header="{Binding ColumnArtist}"
|
||||
<DataGridTextColumn x:DataType="vm:MusicFileViewModel"
|
||||
Header="{loc:Localize Column_Artist}"
|
||||
Binding="{Binding Artist}"
|
||||
Width="150" />
|
||||
<DataGridTextColumn Header="{Binding ColumnFilePath}"
|
||||
<DataGridTextColumn x:DataType="vm:MusicFileViewModel"
|
||||
Header="{loc:Localize Column_FilePath}"
|
||||
Binding="{Binding FilePath}"
|
||||
Width="250" />
|
||||
<DataGridTextColumn Header="{Binding ColumnStatus}"
|
||||
<DataGridTextColumn x:DataType="vm:MusicFileViewModel"
|
||||
Header="{loc:Localize Column_Status}"
|
||||
Binding="{Binding StatusMessage}"
|
||||
Width="100" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</Border>
|
||||
|
||||
<!-- Status Bar -->
|
||||
|
||||
Reference in New Issue
Block a user