2025-08-21 01:13:08 -07:00
|
|
|
using System;
|
2025-08-27 16:11:06 -07:00
|
|
|
using System.Collections.Generic;
|
2025-08-27 12:44:57 -07:00
|
|
|
using System.Collections.ObjectModel;
|
2025-08-27 16:11:06 -07:00
|
|
|
using System.IO;
|
2025-08-27 12:44:57 -07:00
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
2025-08-21 01:13:08 -07:00
|
|
|
using System.Threading.Tasks;
|
2025-08-27 12:44:57 -07:00
|
|
|
using Avalonia;
|
2025-08-21 01:13:08 -07:00
|
|
|
using Avalonia.Controls;
|
2025-08-27 16:11:06 -07:00
|
|
|
using Avalonia.Data;
|
2025-08-21 01:13:08 -07:00
|
|
|
using Avalonia.Interactivity;
|
2025-08-27 16:11:06 -07:00
|
|
|
using Avalonia.Media;
|
|
|
|
|
using Avalonia.Media.Imaging;
|
|
|
|
|
using Avalonia.Platform;
|
2025-08-21 01:13:08 -07:00
|
|
|
using Avalonia.Threading;
|
2025-08-27 12:44:57 -07:00
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using MercuryConverter.Data;
|
|
|
|
|
using MercuryConverter.ExportOperation;
|
|
|
|
|
using MercuryConverter.Utility;
|
2025-08-27 16:11:06 -07:00
|
|
|
using SaturnData.Notation.Serialization;
|
2025-08-21 01:13:08 -07:00
|
|
|
|
|
|
|
|
namespace MercuryConverter.UI.Views;
|
|
|
|
|
|
2025-08-27 12:44:57 -07:00
|
|
|
public enum ExportStatus
|
|
|
|
|
{
|
|
|
|
|
NotStarted, Working, Error, Finished, FinishedWithMessages
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public partial class ExportRow : ObservableObject
|
|
|
|
|
{
|
2025-08-27 16:11:06 -07:00
|
|
|
private static Dictionary<ExportStatus, IImage?> StatusImgs = new()
|
|
|
|
|
{
|
|
|
|
|
{ ExportStatus.Working, new Bitmap(Utils.AssetPath("imgs/status/indeterminate_spinner.png")) },
|
|
|
|
|
{ ExportStatus.Error, new Bitmap(Utils.AssetPath("imgs/status/task_error.png")) },
|
|
|
|
|
{ ExportStatus.Finished, new Bitmap(Utils.AssetPath("imgs/status/task_complete.png")) },
|
|
|
|
|
{ ExportStatus.FinishedWithMessages, new Bitmap(Utils.AssetPath("imgs/status/task_alert.png")) },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private IImage? statusImg = null;
|
2025-08-27 12:44:57 -07:00
|
|
|
public required Song Song { get; set; }
|
2025-08-28 22:54:00 -07:00
|
|
|
public ExportStatus Status { set => StatusImg = StatusImgs.GetValueOrDefault(value, null); }
|
2025-08-27 12:44:57 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-21 01:13:08 -07:00
|
|
|
public partial class Export : Panel
|
|
|
|
|
{
|
2025-08-27 12:44:57 -07:00
|
|
|
public ObservableCollection<ExportRow> Rows { get; } = new();
|
|
|
|
|
|
2025-08-28 22:54:00 -07:00
|
|
|
private bool _exporting = false;
|
|
|
|
|
private bool Exporting
|
|
|
|
|
{
|
|
|
|
|
get => _exporting;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_exporting = value;
|
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
|
|
|
{
|
|
|
|
|
BtnExport.IsEnabled = !value;
|
|
|
|
|
ExportOptionsPane.IsEnabled = !value;
|
|
|
|
|
MainWindow.Instance!.TabSelection.IsEnabled = !value;
|
|
|
|
|
ExportSelectionPane.IsEnabled = !value;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-08-21 01:13:08 -07:00
|
|
|
public Export()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
2025-08-27 12:44:57 -07:00
|
|
|
DataContext = this;
|
|
|
|
|
RadioShouldAudioConvert.IsCheckedChanged += OnUIChange;
|
|
|
|
|
NumThreads.PropertyChanged += OnUIChange;
|
|
|
|
|
RadioExportAll.IsCheckedChanged += OnExportSelectionChg;
|
|
|
|
|
ListingTable.PointerPressed += OnClick;
|
|
|
|
|
|
2025-08-27 16:11:06 -07:00
|
|
|
NumThreads.Bind(TextBox.TextProperty, new Binding(nameof(Settings.ConcurrentExports))
|
|
|
|
|
{
|
|
|
|
|
Source = Settings.I!
|
|
|
|
|
});
|
2025-08-27 12:44:57 -07:00
|
|
|
|
|
|
|
|
ToolTip.SetTip(TickGroupExports,
|
|
|
|
|
"Group exported songs into subfolders named after the version they released in. For example:\n" +
|
|
|
|
|
$"\"Exports/{Consts.NUM_SOURCE[5]}/Ado - うっせぇわ\"");
|
|
|
|
|
|
|
|
|
|
Task.Run(async () =>
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
MainWindow.Instance!.TabControl.SelectionChanged += OnExportSelectionChg;
|
|
|
|
|
|
|
|
|
|
});
|
2025-08-21 01:13:08 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-27 12:44:57 -07:00
|
|
|
private void OnClick(object? sender, RoutedEventArgs e)
|
2025-08-21 01:13:08 -07:00
|
|
|
{
|
2025-08-27 12:44:57 -07:00
|
|
|
ListingTable.SelectedItems.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnUIChange(object? sender, AvaloniaPropertyChangedEventArgs e) => UpdateUIConditions();
|
|
|
|
|
private void OnUIChange(object? sender, RoutedEventArgs args) => UpdateUIConditions();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void OnExportSelectionChg(object? sender, RoutedEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
UpdateUIConditions();
|
|
|
|
|
UpdateRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnExportClick()
|
|
|
|
|
{
|
|
|
|
|
Task.Run(ExportFlow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateRows()
|
|
|
|
|
{
|
2025-08-28 22:54:00 -07:00
|
|
|
if (Exporting) return;
|
|
|
|
|
|
2025-08-27 12:44:57 -07:00
|
|
|
Console.WriteLine("Updating rows!");
|
|
|
|
|
Rows.Clear();
|
|
|
|
|
|
|
|
|
|
if ((bool)RadioExportAll.IsChecked!)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Adding DB songs to rows...");
|
|
|
|
|
Database.Songs.ToList().ForEach((s) => Rows.Add(new ExportRow { Song = s }));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Adding selections to rows...");
|
|
|
|
|
Selection.Selections.ToList().ForEach((s) => Rows.Add(new ExportRow { Song = s }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Modify UI as needed; determine if we are in an exportable state to enable the button.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void UpdateUIConditions()
|
|
|
|
|
{
|
2025-08-28 22:54:00 -07:00
|
|
|
if (Exporting) return;
|
|
|
|
|
|
2025-08-27 12:44:57 -07:00
|
|
|
ListSelectAudioConvertFormat.IsEnabled = (bool)RadioShouldAudioConvert.IsChecked!;
|
|
|
|
|
|
|
|
|
|
BtnExport.IsEnabled =
|
|
|
|
|
( // ensure we have selections
|
|
|
|
|
Selection.Selections.Count > 0
|
|
|
|
|
|| ((bool)RadioExportAll.IsChecked! && Database.Songs.Count > 0)
|
|
|
|
|
) &&
|
|
|
|
|
( // ensure audio format is set
|
|
|
|
|
!(bool)RadioShouldAudioConvert.IsChecked || ListSelectAudioConvertFormat.SelectedIndex != -1
|
|
|
|
|
) &&
|
|
|
|
|
(
|
2025-08-27 16:11:06 -07:00
|
|
|
// enabled export worker count is in good range
|
|
|
|
|
int.TryParse(NumThreads.Text, out var thr) && 1 <= thr && thr <= Environment.ProcessorCount
|
2025-08-27 12:44:57 -07:00
|
|
|
);
|
2025-08-27 23:03:30 -07:00
|
|
|
|
|
|
|
|
var ffmpegAvail = Utils.IsFFMpegAvailable();
|
|
|
|
|
if (!ffmpegAvail)
|
|
|
|
|
RadioLeaveAudioWAV.IsChecked = true;
|
|
|
|
|
RadioLeaveAudioWAV.IsEnabled = ffmpegAvail;
|
|
|
|
|
RadioShouldAudioConvert.IsEnabled = ffmpegAvail;
|
|
|
|
|
NoFFMpegMessage.IsVisible = !ffmpegAvail;
|
2025-08-27 12:44:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void ExportFlow()
|
|
|
|
|
{
|
2025-08-28 22:54:00 -07:00
|
|
|
Exporting = true;
|
2025-08-27 23:03:30 -07:00
|
|
|
var path = await Utils.BeginDirSelection("Choose your export path...", Settings.I!.ExportPath);
|
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
|
|
|
{
|
2025-08-28 22:54:00 -07:00
|
|
|
Exporting = false;
|
2025-08-27 23:03:30 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Settings.I!.ExportPath = path;
|
2025-08-27 12:44:57 -07:00
|
|
|
|
2025-08-27 16:11:06 -07:00
|
|
|
var options = await Dispatcher.UIThread.InvokeAsync(() =>
|
2025-08-27 12:44:57 -07:00
|
|
|
{
|
2025-08-27 16:11:06 -07:00
|
|
|
AudioFormat audFor;
|
|
|
|
|
if (!(bool)RadioShouldAudioConvert.IsChecked!)
|
|
|
|
|
audFor = AudioFormat.WAV;
|
|
|
|
|
else
|
|
|
|
|
audFor = (AudioFormat)ListSelectAudioConvertFormat.SelectedIndex + 1;
|
|
|
|
|
|
|
|
|
|
return new ExportOptions
|
|
|
|
|
{
|
|
|
|
|
ChartFormat = (FormatVersion)ListSelectChartFormat.SelectedIndex,
|
|
|
|
|
AudioFormat = audFor,
|
|
|
|
|
ExcludeVideo = (bool)TickExcludeVideos.IsChecked!,
|
|
|
|
|
SourceSubdir = (bool)TickGroupExports.IsChecked!
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-28 22:54:00 -07:00
|
|
|
// Reset statuses
|
|
|
|
|
Dispatcher.UIThread.Invoke(() => Rows.ToList().ForEach(row => row.Status = ExportStatus.NotStarted));
|
|
|
|
|
|
2025-08-27 16:11:06 -07:00
|
|
|
// process each song in parallel (for audio conversion)
|
2025-08-28 22:54:00 -07:00
|
|
|
// TODO: cancellable?
|
|
|
|
|
await Parallel.ForEachAsync(
|
2025-08-27 16:11:06 -07:00
|
|
|
Rows,
|
|
|
|
|
new ParallelOptions { MaxDegreeOfParallelism = Convert.ToInt32(Settings.I!.ConcurrentExports) },
|
2025-08-28 22:54:00 -07:00
|
|
|
async (row, cancelToken) =>
|
2025-08-27 16:11:06 -07:00
|
|
|
{
|
2025-08-28 22:54:00 -07:00
|
|
|
if (cancelToken.IsCancellationRequested) return;
|
|
|
|
|
|
2025-08-30 18:29:47 -07:00
|
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
row.Status = ExportStatus.Working;
|
|
|
|
|
ListingTable.ScrollIntoView(row, null);
|
|
|
|
|
});
|
2025-08-27 16:11:06 -07:00
|
|
|
Exporter.Run(path, row.Song, options);
|
2025-08-28 22:54:00 -07:00
|
|
|
await Dispatcher.UIThread.InvokeAsync(() => row.Status = ExportStatus.Finished);
|
2025-08-27 16:11:06 -07:00
|
|
|
}
|
|
|
|
|
);
|
2025-08-27 12:44:57 -07:00
|
|
|
|
2025-08-28 22:54:00 -07:00
|
|
|
Exporting = false;
|
2025-08-21 01:13:08 -07:00
|
|
|
}
|
|
|
|
|
}
|