diff --git a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs index 15a565fa..00da412f 100644 --- a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs @@ -116,14 +116,13 @@ public async Task SetPropertiesAsync_WithBrowserFile_ShouldSetPropertiesAsync() // Arrange var objectUrl = "blob:123"; var filename = "song.mp3"; - var inputId = "inputId"; var browserFile = new Mock(); browserFile.SetupGet(b => b.ContentType).Returns("audio/mpeg"); browserFile.SetupGet(b => b.Name).Returns(filename); var codec = Audiofile.AudioCodecs.First(x => x.FileExtension == ".mp3"); _fileInputManager.Setup(f => f.GetAudioCodec(filename, "audio/mpeg")).Returns(codec); - _fileInputManager.Setup(f => f.GetObjectUrlAsync(inputId)).ReturnsAsync(objectUrl); + _fileInputManager.Setup(f => f.GetObjectUrlAsync(browserFile.Object)).ReturnsAsync(objectUrl); _jsRuntime.Setup(js => js.InvokeAsync("getAudioDurationFromFile", It.IsAny())).Returns(new ValueTask(90.0)); var audiofile = new Audiofile(); @@ -137,7 +136,7 @@ public async Task SetPropertiesAsync_WithBrowserFile_ShouldSetPropertiesAsync() } }; // Act - await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object, inputId); + await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object); // Assert Assert.AreEqual(codec, audiofile.AudioCodec); @@ -146,7 +145,7 @@ public async Task SetPropertiesAsync_WithBrowserFile_ShouldSetPropertiesAsync() Assert.AreEqual(TimeSpan.FromSeconds(90), audiofile.Duration); Assert.AreEqual(1, audiofileChangedFired); _fileInputManager.Verify(f => f.GetAudioCodec(filename, "audio/mpeg"), Times.Once); - _fileInputManager.Verify(f => f.GetObjectUrlAsync(inputId), Times.Once); + _fileInputManager.Verify(f => f.GetObjectUrlAsync(browserFile.Object), Times.Once); _jsRuntime.Verify(js => js.InvokeAsync("getAudioDurationFromFile", It.Is(o => o[0] as string == audiofile.ObjectURL)), Times.Once); } @@ -156,14 +155,13 @@ public async Task SetProperty_AudiofileWithDuration_SetsLastTrackEndAlsoAsync() // Arrange var objectUrl = "blob:123"; var filename = "song.mp3"; - var inputId = "inputId"; var browserFile = new Mock(); browserFile.SetupGet(b => b.ContentType).Returns("audio/mpeg"); browserFile.SetupGet(b => b.Name).Returns(filename); var codec = Audiofile.AudioCodecs.First(x => x.FileExtension == ".mp3"); _fileInputManager.Setup(f => f.GetAudioCodec(filename, "audio/mpeg")).Returns(codec); - _fileInputManager.Setup(f => f.GetObjectUrlAsync(inputId)).ReturnsAsync(objectUrl); + _fileInputManager.Setup(f => f.GetObjectUrlAsync(browserFile.Object)).ReturnsAsync(objectUrl); _jsRuntime.Setup(js => js.InvokeAsync("getAudioDurationFromFile", It.IsAny())).Returns(new ValueTask(90.0)); var track1 = new Track() { @@ -189,7 +187,7 @@ public async Task SetProperty_AudiofileWithDuration_SetsLastTrackEndAlsoAsync() } }; // Act - await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object, inputId); + await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object); // Assert Assert.AreEqual(codec, audiofile.AudioCodec); @@ -199,7 +197,7 @@ public async Task SetProperty_AudiofileWithDuration_SetsLastTrackEndAlsoAsync() Assert.AreEqual(TimeSpan.FromSeconds(90), track2.End); Assert.AreEqual(1, audiofileChangedFired); _fileInputManager.Verify(f => f.GetAudioCodec(filename, "audio/mpeg"), Times.Once); - _fileInputManager.Verify(f => f.GetObjectUrlAsync(inputId), Times.Once); + _fileInputManager.Verify(f => f.GetObjectUrlAsync(browserFile.Object), Times.Once); _jsRuntime.Verify(js => js.InvokeAsync("getAudioDurationFromFile", It.Is(o => o[0] as string == audiofile.ObjectURL)), Times.Once); } @@ -209,14 +207,13 @@ public async Task SetPropertiesAsync_PreviouslySetAudiofile_ShouldClearFirstAndS // Arrange var objectUrl = "blob:123"; var filename = "song.mp3"; - var inputId = "inputId"; var browserFile = new Mock(); browserFile.SetupGet(b => b.ContentType).Returns("audio/mpeg"); browserFile.SetupGet(b => b.Name).Returns(filename); var codec = Audiofile.AudioCodecs.First(x => x.FileExtension == ".mp3"); _fileInputManager.Setup(f => f.GetAudioCodec(filename, "audio/mpeg")).Returns(codec); - _fileInputManager.Setup(f => f.GetObjectUrlAsync(inputId)).ReturnsAsync(objectUrl); + _fileInputManager.Setup(f => f.GetObjectUrlAsync(browserFile.Object)).ReturnsAsync(objectUrl); _jsRuntime.Setup(js => js.InvokeAsync("getAudioDurationFromFile", It.IsAny())).Returns(new ValueTask(90.0)); var audiofile = new Audiofile @@ -232,7 +229,7 @@ public async Task SetPropertiesAsync_PreviouslySetAudiofile_ShouldClearFirstAndS } }; // Act - await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object, inputId); + await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object); // Assert Assert.AreEqual(codec, audiofile.AudioCodec); @@ -241,7 +238,7 @@ public async Task SetPropertiesAsync_PreviouslySetAudiofile_ShouldClearFirstAndS Assert.AreEqual(TimeSpan.FromSeconds(90), audiofile.Duration); Assert.AreEqual(1, audiofileChangedFired); _fileInputManager.Verify(f => f.GetAudioCodec(filename, "audio/mpeg"), Times.Once); - _fileInputManager.Verify(f => f.GetObjectUrlAsync(inputId), Times.Once); + _fileInputManager.Verify(f => f.GetObjectUrlAsync(browserFile.Object), Times.Once); _jsRuntime.Verify(js => js.InvokeAsync("getAudioDurationFromFile", It.Is(o => o[0] as string == audiofile.ObjectURL)), Times.Once); _traceChangeManager.VerifySet(t => t.BulkEdit = true, Times.Once); _traceChangeManager.VerifySet(t => t.BulkEdit = false, Times.Once); diff --git a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs index 39a36e29..12368caa 100644 --- a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs @@ -64,6 +64,7 @@ public void SetProperty_NewValue_ChangesPropertyAndSetsTracedChange() { // Arrange var cuesheet = new Cuesheet(); + _sessionStateContainer.SetupGet(x => x.Cuesheet).Returns(cuesheet); _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(cuesheet); // Act _cuesheetManager.SetProperty(x => x.Artist, "Artist"); @@ -116,6 +117,21 @@ public void SetProperty_DeletedAudiofile_RevokesObjectURLs() _jsRuntime.Verify(x => x.InvokeAsync("revokeAudioObjectURL", It.Is(args => args.Length == 1 && args[0].Equals(audiofile2.ObjectURL))), Times.Once()); } + [TestMethod] + public void SetProperty_OnImportCuesheet_DoesNotTraceChanges() + { + // Arrange + var importCuesheet = new Cuesheet(); + var cuesheet = new Cuesheet(); + _sessionStateContainer.SetupGet(x => x.ImportCuesheet).Returns(importCuesheet); + _sessionStateContainer.SetupGet(x => x.Cuesheet).Returns(cuesheet); + _sessionStateContainer.SetupGet(x => x.ActiveCuesheet).Returns(importCuesheet); + // Act + _cuesheetManager.SetProperty(x => x.Artist, nameof(SetProperty_OnImportCuesheet_DoesNotTraceChanges)); + // Assert + _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == importCuesheet)), Times.Never); + } + [TestMethod] public void IsRecordingPossible_WithoutTracks_ReturnsSuccess() { diff --git a/AudioCuesheetEditor.Tests/Services/IO/AudioImportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/AudioImportServiceTests.cs new file mode 100644 index 00000000..751e3aaa --- /dev/null +++ b/AudioCuesheetEditor.Tests/Services/IO/AudioImportServiceTests.cs @@ -0,0 +1,120 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. +using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.IO.Audio; +using AudioCuesheetEditor.Services.AudioCuesheet; +using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AudioCuesheetEditor.Tests.Services.IO +{ + [TestClass] + public class AudioImportServiceTests + { + private readonly AudioImportService _service; + private readonly Mock _dialogManager; + private readonly Mock _sessionStateContainer; + private readonly Mock _cuesheetManager; + + public AudioImportServiceTests() + { + _dialogManager = new(); + _sessionStateContainer = new(); + _cuesheetManager = new(); + _service = new(_dialogManager.Object, _sessionStateContainer.Object, _cuesheetManager.Object); + } + + [TestMethod] + public async Task MapAudioImportAsync_WhenMappingIsNotRequired_ReturnsTrueAsync() + { + // Arrange + var cuesheet = new Cuesheet(); + var importAudiofileMapping = new Dictionary(); + _dialogManager.Setup(x => x.ShowImportAudiofilesDialogAsync()).ReturnsAsync(true); + _sessionStateContainer.SetupGet(x => x.ImportAudiofileMapping).Returns(importAudiofileMapping); + _sessionStateContainer.SetupSet(x => x.ImportAudiofileMapping = It.IsAny>()).Callback>(cs => importAudiofileMapping = cs); + _sessionStateContainer.SetupGet(x => x.ImportAudiofiles).Returns([]); + _sessionStateContainer.SetupGet(x => x.ActiveCuesheet).Returns(cuesheet); + // Act + var result = await _service.MapAudioImportAsync(); + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public async Task MapAudioImportAsync_WhenMappingIsRequiredAndDoneAutomatically_ReturnsTrueAsync() + { + // Arrange + var cuesheet = new Cuesheet() + { + Audiofiles = [ + new() + ] + }; + var importAudiofileMapping = new Dictionary + { + { new(), null } + }; + var importAudioFiles = new List + { + new() + }; + _dialogManager.Setup(x => x.ShowImportAudiofilesDialogAsync()).ReturnsAsync(true); + _sessionStateContainer.SetupGet(x => x.ImportAudiofileMapping).Returns(importAudiofileMapping); + _sessionStateContainer.SetupSet(x => x.ImportAudiofileMapping = It.IsAny>()).Callback>(cs => importAudiofileMapping = cs); + _sessionStateContainer.SetupGet(x => x.ImportAudiofiles).Returns(importAudioFiles); + _sessionStateContainer.SetupGet(x => x.ActiveCuesheet).Returns(cuesheet); + // Act + var result = await _service.MapAudioImportAsync(); + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public async Task MapAudioImportAsync_WhenMappingIsRequiredAndCannotBeDoneAutomatically_ReturnsDialogResultAsync() + { + // Arrange + var cuesheet = new Cuesheet() + { + Audiofiles = [ + new() + ] + }; + var importAudiofileMapping = new Dictionary + { + { new(), null } + }; + var importAudioFiles = new List + { + new(), new() + }; + var dialogResultAborted = true; + _dialogManager.Setup(x => x.ShowImportAudiofilesDialogAsync()).ReturnsAsync(dialogResultAborted); + _sessionStateContainer.SetupGet(x => x.ImportAudiofileMapping).Returns(importAudiofileMapping); + _sessionStateContainer.SetupSet(x => x.ImportAudiofileMapping = It.IsAny>()).Callback>(cs => importAudiofileMapping = cs); + _sessionStateContainer.SetupGet(x => x.ImportAudiofiles).Returns(importAudioFiles); + _sessionStateContainer.SetupGet(x => x.ActiveCuesheet).Returns(cuesheet); + // Act + var result = await _service.MapAudioImportAsync(); + // Assert + Assert.AreEqual(!dialogResultAborted, result); + } + } +} diff --git a/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs b/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs index 3b920a6d..90d6038e 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs @@ -232,7 +232,6 @@ public async Task CreateFileUploadsAsync_ReturnsFileUploads_WhenFileHasTextConte // Arrange var firstFile = CreateBrowserFile("Test.txt", "text/plain", "Just a test!"); var secondFile = CreateBrowserFile("Test.mp3", "audio/mpeg"); - var fileInputId = nameof(CreateFileUploadsAsync_ReturnsFileUploads_WhenFileHasTextContentAsync); IReadOnlyList browserfiles = [ firstFile, secondFile @@ -240,7 +239,7 @@ public async Task CreateFileUploadsAsync_ReturnsFileUploads_WhenFileHasTextConte var objectUrl = "Some object url!"; _jsRuntimeMock.Setup(js => js.InvokeAsync(It.IsAny(), It.IsAny())).ReturnsAsync(objectUrl); // Act - var result = await _service.CreateFileUploadsAsync(browserfiles, fileInputId); + var result = await _service.CreateFileUploadsAsync(browserfiles); // Assert Assert.HasCount(2, result); Assert.AreEqual(firstFile.Name, result.First().Name); @@ -265,7 +264,7 @@ public async Task CreateFileUploadsAsync_ReturnsEmpty_WhenFilesHaveInvalidMimeTy // Act var result = await _service.CreateFileUploadsAsync(browserfiles); // Assert - Assert.HasCount(0, result); + Assert.IsEmpty(result); } static IBrowserFile CreateBrowserFile(string name, string contentType, string? content = null) diff --git a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs index abfb859b..0336850b 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs @@ -43,6 +43,7 @@ public class ImportManagerTests private readonly Mock _fileInputManagerMock; private readonly Mock _textImportServiceMock; private readonly Mock _trackManagerMock; + private readonly Mock _audioImportService; public ImportManagerTests() { _traceChangeManagerMock = new(); @@ -105,8 +106,9 @@ public ImportManagerTests() _trackManagerMock.Object.CopyValues(track, clone, setLength: setLength); return clone; }); + _audioImportService = new(); var loggerMock = new Mock>(); - _service = new ImportManager(_sessionStateContainerMock.Object, _traceChangeManagerMock.Object, _fileInputManagerMock.Object, _textImportServiceMock.Object, _trackManagerMock.Object, loggerMock.Object); + _service = new ImportManager(_sessionStateContainerMock.Object, _traceChangeManagerMock.Object, _fileInputManagerMock.Object, _textImportServiceMock.Object, _trackManagerMock.Object, _audioImportService.Object, loggerMock.Object); } void TrackManager_SetValue(Track track, Expression> propertyExpression, TProperty value, Boolean signalTraceChangeManager = true) @@ -385,7 +387,7 @@ public async Task UploadFilesAsync_WithAudiofile_ImportsCorrectly() } [TestMethod] - public void ImportCuesheet_WithImportCuesheetAvailable_ImportsCuesheetData() + public async Task ImportCuesheetAsync_WithImportCuesheetAvailable_ImportsCuesheetDataAsync() { // Arrange var track1 = new Track() @@ -425,7 +427,7 @@ public void ImportCuesheet_WithImportCuesheetAvailable_ImportsCuesheetData() IList? sessionStateContainerImportAudiofiles = []; _sessionStateContainerMock.SetupGet(x => x.ImportAudiofiles).Returns(() => sessionStateContainerImportAudiofiles); // Act - _service.ImportCuesheet(); + await _service.ImportCuesheetAsync(); // Assert Assert.IsNotNull(sessionStateContainerCuesheet); Assert.AreEqual(sessionStateContainerImportCuesheet.Artist, sessionStateContainerCuesheet.Artist); diff --git a/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs index 5188a1de..3d00bf25 100644 --- a/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs @@ -14,10 +14,12 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Services.Options; using AudioCuesheetEditor.Services.UI; +using Microsoft.JSInterop; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; @@ -30,11 +32,13 @@ public class SessionStateContainerTests { private readonly SessionStateContainer _sessionStateContainer; private readonly Mock _localStorageOptionsProvider; + private readonly Mock _jsRuntime; public SessionStateContainerTests() { _localStorageOptionsProvider = new(); - _sessionStateContainer = new(_localStorageOptionsProvider.Object); + _jsRuntime = new(); + _sessionStateContainer = new(_localStorageOptionsProvider.Object, _jsRuntime.Object); } [TestMethod] @@ -121,7 +125,7 @@ public async Task ImportCuesheet_SetNewValueInDetailView_ShouldTriggerActiveCues } [TestMethod] - public async Task ResetImport_InitializedService_ShouldClearPropertiesAsync() + public async Task ResetImportAsync_RevokeObjectUrls_ShouldClearPropertiesAsync() { // Arrange var viewOptions = new ViewOptions(); @@ -130,15 +134,86 @@ public async Task ResetImport_InitializedService_ShouldClearPropertiesAsync() _sessionStateContainer.Importfile = Mock.Of(); _sessionStateContainer.ImportCuesheet = new Cuesheet(); _sessionStateContainer.ImportIsAnalyzed = true; + var importAudiofile1 = new Audiofile() + { + ObjectURL = "File 1" + }; + var importAudiofile2 = new Audiofile() + { + ObjectURL = "File 2" + }; + _sessionStateContainer.ImportAudiofiles.Add(importAudiofile1); + _sessionStateContainer.ImportAudiofiles.Add(importAudiofile2); + var importAudiofile3 = new Audiofile() + { + ObjectURL = "File 3" + }; + var importAudiofile4 = new Audiofile() + { + ObjectURL = "File 4" + }; + _sessionStateContainer.ImportAudiofileMapping.Add(new(), importAudiofile3); + _sessionStateContainer.ImportAudiofileMapping.Add(new(), importAudiofile4); + + // Act + await _sessionStateContainer.ResetImportAsync(true); + + // Assert + Assert.IsNull(_sessionStateContainer.Importfile); + Assert.IsEmpty(_sessionStateContainer.ImportAudiofiles); + Assert.IsNull(_sessionStateContainer.ImportCuesheet); + Assert.IsFalse(_sessionStateContainer.ImportIsAnalyzed); + Assert.IsEmpty(_sessionStateContainer.ImportAudiofileMapping); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile1.ObjectURL)), Times.Once); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile2.ObjectURL)), Times.Once); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile3.ObjectURL)), Times.Once); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile4.ObjectURL)), Times.Once); + } + + [TestMethod] + public async Task ResetImportAsync_RevokeObjectUrlsNotSet_ShouldClearPropertiesAsync() + { + // Arrange + var viewOptions = new ViewOptions(); + _localStorageOptionsProvider.Setup(x => x.GetOptionsAsync()).ReturnsAsync(viewOptions); + await _sessionStateContainer.InitializeAsync(); + _sessionStateContainer.Importfile = Mock.Of(); + _sessionStateContainer.ImportCuesheet = new Cuesheet(); + _sessionStateContainer.ImportIsAnalyzed = true; + var importAudiofile1 = new Audiofile() + { + ObjectURL = "File 1" + }; + var importAudiofile2 = new Audiofile() + { + ObjectURL = "File 2" + }; + _sessionStateContainer.ImportAudiofiles.Add(importAudiofile1); + _sessionStateContainer.ImportAudiofiles.Add(importAudiofile2); + var importAudiofile3 = new Audiofile() + { + ObjectURL = "File 3" + }; + var importAudiofile4 = new Audiofile() + { + ObjectURL = "File 4" + }; + _sessionStateContainer.ImportAudiofileMapping.Add(new(), importAudiofile3); + _sessionStateContainer.ImportAudiofileMapping.Add(new(), importAudiofile4); // Act - _sessionStateContainer.ResetImport(); + await _sessionStateContainer.ResetImportAsync(false); // Assert Assert.IsNull(_sessionStateContainer.Importfile); Assert.IsEmpty(_sessionStateContainer.ImportAudiofiles); Assert.IsNull(_sessionStateContainer.ImportCuesheet); Assert.IsFalse(_sessionStateContainer.ImportIsAnalyzed); + Assert.IsEmpty(_sessionStateContainer.ImportAudiofileMapping); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile1.ObjectURL)), Times.Never); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile2.ObjectURL)), Times.Never); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile3.ObjectURL)), Times.Never); + _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == importAudiofile4.ObjectURL)), Times.Never); } [TestMethod] diff --git a/AudioCuesheetEditor.sln b/AudioCuesheetEditor.sln index 40cbf088..73237fb1 100644 --- a/AudioCuesheetEditor.sln +++ b/AudioCuesheetEditor.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.3.11512.155 d18.3 +VisualStudioVersion = 18.3.11512.155 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudioCuesheetEditor", "AudioCuesheetEditor\AudioCuesheetEditor.csproj", "{099AE4DF-4847-460F-BBF2-207B9A35B58D}" EndProject diff --git a/AudioCuesheetEditor/Pages/Index.razor b/AudioCuesheetEditor/Pages/Index.razor index a8b75db0..35741b3e 100644 --- a/AudioCuesheetEditor/Pages/Index.razor +++ b/AudioCuesheetEditor/Pages/Index.razor @@ -23,9 +23,10 @@ along with Foobar. If not, see @inject IStringLocalizer _localizer @inject ImportManager _importManager @inject ISessionStateContainer _sessionStateContainer -@inject DialogManager _dialogManager +@inject IDialogManager _dialogManager @inject ISnackbar _snackbar @inject ICuesheetManager _cuesheetManager +@inject IAudioImportService _audioImportService @@ -97,22 +98,29 @@ along with Foobar. If not, see }); } } + var switchToImportView = false; await _dialogManager.ShowLoadingDialogAsync(); switch (_sessionStateContainer.Importfile?.FileType) { case ImportFileType.ProjectFile: await _importManager.AnalyseImportfile(); - _sessionStateContainer.ResetImport(); + await _sessionStateContainer.ResetImportAsync(false); break; case ImportFileType.Cuesheet: await _importManager.AnalyseImportfile(); - _importManager.ImportCuesheet(); - _sessionStateContainer.ResetImport(); + await _importManager.ImportCuesheetAsync(); + await _sessionStateContainer.ResetImportAsync(false); break; case ImportFileType.Textfile: + switchToImportView = true; await LocalStorageOptionsProvider.SaveOptionsValueAsync(x => x.ActiveTab, ViewMode.ImportView); break; } + //In import view we analyze the text by user and ask for mapping afterwards + if (switchToImportView == false) + { + await _audioImportService.MapAudioImportAsync(); + } } finally { diff --git a/AudioCuesheetEditor/Program.cs b/AudioCuesheetEditor/Program.cs index 52932a92..38b84b89 100644 --- a/AudioCuesheetEditor/Program.cs +++ b/AudioCuesheetEditor/Program.cs @@ -55,12 +55,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddLogging(); // Read out configuration for loglevel diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs index e2eeb63f..37acac4d 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs @@ -36,7 +36,7 @@ public class AudiofileManager(IFileInputManager fileInputManager, ITraceChangeMa public event EventHandler? AudiofileChanged; /// - public async Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile browserFile, string fileInputId) + public async Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile browserFile) { _traceChangeManager.BulkEdit = true; if (String.IsNullOrEmpty(audiofile.ObjectURL) == false) @@ -44,7 +44,7 @@ public async Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile browserFi await ClearPropertiesAsync(audiofile, false); } var codec = _fileInputManager.GetAudioCodec(browserFile.Name, browserFile.ContentType); - var objectUrl = await _fileInputManager.GetObjectUrlAsync(fileInputId); + var objectUrl = await _fileInputManager.GetObjectUrlAsync(browserFile); TimeSpan? duration = null; if (String.IsNullOrEmpty(objectUrl) == false) { diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs index 2cc9153c..3b635a50 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs @@ -36,10 +36,17 @@ public class CuesheetManager(ITraceChangeManager traceChangeManager, ISessionSta /// public void SetProperty(Expression> propertyExpression, TProperty value) { - _traceChangeManager.BulkEdit = true; + var workingOnCuesheet = _sessionStateContainer.ActiveCuesheet == _sessionStateContainer.Cuesheet; + if (workingOnCuesheet) + { + _traceChangeManager.BulkEdit = true; + } var cuesheet = _sessionStateContainer.ActiveCuesheet; - SetValue(cuesheet!, propertyExpression, value); - _traceChangeManager.BulkEdit = false; + SetValue(cuesheet!, propertyExpression, value, workingOnCuesheet); + if (workingOnCuesheet) + { + _traceChangeManager.BulkEdit = false; + } } /// @@ -282,7 +289,7 @@ public Result MoveDown(HashSet selectedAudiofiles) return Result.Success(); } - void SetValue(Cuesheet cuesheet, Expression> propertyExpression, TProperty value) + void SetValue(Cuesheet cuesheet, Expression> propertyExpression, TProperty value, Boolean traceChanges = true) { if (propertyExpression.Body is not MemberExpression memberExpression) { @@ -301,8 +308,10 @@ void SetValue(Cuesheet cuesheet, Expression } propertyInfo.SetValue(cuesheet, value); - - _traceChangeManager.AddChange(new(cuesheet, new(previousValue, propertyInfo.Name))); + if (traceChanges) + { + _traceChangeManager.AddChange(new(cuesheet, new(previousValue, propertyInfo.Name))); + } _ = RevokeObjectUrlOfRemovedAudiofilesAsync(cuesheet, propertyInfo, previousValue); } diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/IAudiofileManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/IAudiofileManager.cs index af073c46..6cf526cb 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/IAudiofileManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/IAudiofileManager.cs @@ -31,8 +31,7 @@ public interface IAudiofileManager /// /// /// - /// - Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile browserFile, string fileInputId); + Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile browserFile); /// /// Clear properties of an audiofile /// diff --git a/AudioCuesheetEditor/Services/IO/AudioImportService.cs b/AudioCuesheetEditor/Services/IO/AudioImportService.cs new file mode 100644 index 00000000..bf44fb64 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/AudioImportService.cs @@ -0,0 +1,91 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. +using AudioCuesheetEditor.Model.IO.Audio; +using AudioCuesheetEditor.Services.AudioCuesheet; +using AudioCuesheetEditor.Services.UI; +using MudBlazor; + +namespace AudioCuesheetEditor.Services.IO +{ + /// + public class AudioImportService(IDialogManager dialogManager, ISessionStateContainer sessionStateContainer, ICuesheetManager cuesheetManager) : IAudioImportService + { + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; + private readonly IDialogManager _dialogManager = dialogManager; + private readonly ICuesheetManager _cuesheetManager = cuesheetManager; + + /// + public async Task MapAudioImportAsync() + { + // Just run an import if there are audiofiles to import, otherwise this service has nothing to do + if (_sessionStateContainer.ImportAudiofiles.Count == 0) + { + return true; + } + var mappingRequired = MapImportfiles(); + var cancel = false; + if (mappingRequired) + { + cancel = await _dialogManager.ShowImportAudiofilesDialogAsync(); + } + if (cancel == false) + { + var files = new List(_sessionStateContainer.ActiveCuesheet!.Audiofiles); + foreach (var mapping in _sessionStateContainer.ImportAudiofileMapping.Where(x => x.Value != null)) + { + var audiofile = files.FirstOrDefault(x => x.Equals(mapping.Key)); + if (audiofile == null) + { + audiofile = mapping.Key; + files.Add(audiofile); + } + audiofile.ObjectURL = mapping.Value!.ObjectURL; + audiofile.AudioCodec = mapping.Value!.AudioCodec; + audiofile.Duration = mapping.Value!.Duration; + audiofile.Name = mapping.Value!.Name; + } + _cuesheetManager.SetProperty(x => x.Audiofiles, files); + } + else + { + await _sessionStateContainer.ResetAudioImportAsync(cancel); + } + return cancel == false; + } + + Boolean MapImportfiles() + { + _sessionStateContainer.ImportAudiofileMapping.Clear(); + foreach (var audiofile in _sessionStateContainer.ActiveCuesheet!.Audiofiles) + { + var importAudiofile = _sessionStateContainer.ImportAudiofiles.FirstOrDefault(); + if (string.IsNullOrEmpty(audiofile.Name) || audiofile.Name == importAudiofile?.Name) + { + if (importAudiofile != null) + { + _sessionStateContainer.ImportAudiofiles.Remove(importAudiofile); + _sessionStateContainer.ImportAudiofileMapping.Add(audiofile, importAudiofile); + } + } + else + { + _sessionStateContainer.ImportAudiofileMapping.Add(audiofile, null); + } + } + return _sessionStateContainer.ImportAudiofiles.Count > 0; + } + } +} diff --git a/AudioCuesheetEditor/Services/IO/FileInputManager.cs b/AudioCuesheetEditor/Services/IO/FileInputManager.cs index ef25fdd5..1ba191ab 100644 --- a/AudioCuesheetEditor/Services/IO/FileInputManager.cs +++ b/AudioCuesheetEditor/Services/IO/FileInputManager.cs @@ -46,9 +46,9 @@ public class FileInputManager(IJSRuntime jsRuntime, ILogger lo } /// - public async Task GetObjectUrlAsync(string fileInputId) + public async Task GetObjectUrlAsync(IBrowserFile browserFile) { - return await _jsRuntime.InvokeAsync("getObjectURLFromMudFileUpload", fileInputId); + return await _jsRuntime.InvokeAsync("getObjectURLFromMudFileUpload", browserFile.Name, browserFile.Size, browserFile.ContentType, browserFile.LastModified.ToUnixTimeMilliseconds()); } public bool IsValidAudiofile(string fileName, string? fileContentType = null) @@ -89,25 +89,22 @@ public bool CheckFileMimeType(string? fileContentType, string fileName, string m public async Task CreateAudiofileAsync(FileUpload fileUpload) { - Audiofile? audiofile = null; - if (fileUpload.ObjectUrl != null) + // Check file mime type + var codec = GetAudioCodec(fileUpload.Name, fileUpload.ContentType); + Audiofile? audiofile; + if (codec != null) { - // Check file mime type - var codec = GetAudioCodec(fileUpload.Name, fileUpload.ContentType); - if (codec != null) + TimeSpan? duration = null; + if (String.IsNullOrEmpty(fileUpload.ObjectUrl) == false) { - TimeSpan? duration = null; - if (String.IsNullOrEmpty(fileUpload.ObjectUrl) == false) - { - var durationSeconds = await _jsRuntime.InvokeAsync("getAudioDurationFromFile", fileUpload.ObjectUrl); - duration = TimeSpan.FromSeconds(durationSeconds); - } - audiofile = new Audiofile(fileUpload.Name, fileUpload.ObjectUrl, codec, duration); - } - else - { - throw new ArgumentException("The audiofile provided is not of a valid type."); + var durationSeconds = await _jsRuntime.InvokeAsync("getAudioDurationFromFile", fileUpload.ObjectUrl); + duration = TimeSpan.FromSeconds(durationSeconds); } + audiofile = new Audiofile(fileUpload.Name, fileUpload.ObjectUrl, codec, duration); + } + else + { + throw new ArgumentException("The audiofile provided is not of a valid type."); } return audiofile; } @@ -140,7 +137,7 @@ public async Task ReadFileContentAsync(IBrowserFile browserFile) } /// - public async Task> CreateFileUploadsAsync(IReadOnlyList browserFiles, string? fileInputId = null) + public async Task> CreateFileUploadsAsync(IReadOnlyList browserFiles) { List fileUploads = []; foreach (var file in browserFiles) @@ -154,7 +151,7 @@ public async Task> CreateFileUploadsAsync(IReadOnlyList< string? objectUrl = null; if (IsValidAudiofile(file.Name, file.ContentType)) { - objectUrl = await _jsRuntime.InvokeAsync("getObjectURLFromMudFileUpload", fileInputId); + objectUrl = await GetObjectUrlAsync(file); } else { diff --git a/AudioCuesheetEditor/Services/IO/IAudioImportService.cs b/AudioCuesheetEditor/Services/IO/IAudioImportService.cs new file mode 100644 index 00000000..f01d34b6 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/IAudioImportService.cs @@ -0,0 +1,29 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. +namespace AudioCuesheetEditor.Services.IO +{ + /// + /// Service for importing audio files and matching them to the cuesheet. + /// + public interface IAudioImportService + { + /// + /// Maps the import audio from session state to the active cuesheet. If mapping is not possible automatically the user gets a dialog. + /// + /// + Task MapAudioImportAsync(); + } +} diff --git a/AudioCuesheetEditor/Services/IO/IFileInputManager.cs b/AudioCuesheetEditor/Services/IO/IFileInputManager.cs index ca2a552a..95383d55 100644 --- a/AudioCuesheetEditor/Services/IO/IFileInputManager.cs +++ b/AudioCuesheetEditor/Services/IO/IFileInputManager.cs @@ -26,11 +26,11 @@ public interface IFileInputManager bool IsValidAudiofile(string fileName, string? fileContentType = null); AudioCodec? GetAudioCodec(string fileName, string? fileContentType = null); /// - /// Get object url from a mud file upload + /// Get object url for an IBrowserFile. /// - /// + /// /// - Task GetObjectUrlAsync(string fileInputId); + Task GetObjectUrlAsync(IBrowserFile browserFile); /// /// Checks if a file content type and name matches given parameters /// @@ -58,8 +58,7 @@ public interface IFileInputManager /// Generates file upload references for files /// /// - /// /// - Task> CreateFileUploadsAsync(IReadOnlyList browserFiles, string? fileInputId = null); + Task> CreateFileUploadsAsync(IReadOnlyList browserFiles); } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Services/IO/ImportManager.cs b/AudioCuesheetEditor/Services/IO/ImportManager.cs index 01ad1937..e82f031c 100644 --- a/AudioCuesheetEditor/Services/IO/ImportManager.cs +++ b/AudioCuesheetEditor/Services/IO/ImportManager.cs @@ -13,7 +13,6 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using AngleSharp.Media.Dom; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO; @@ -34,7 +33,7 @@ public enum ImportFileType Textfile, Audiofile } - public class ImportManager(ISessionStateContainer sessionStateContainer, ITraceChangeManager traceChangeManager, IFileInputManager fileInputManager, ITextImportService textImportService, ITrackManager trackManager, ILogger logger) + public class ImportManager(ISessionStateContainer sessionStateContainer, ITraceChangeManager traceChangeManager, IFileInputManager fileInputManager, ITextImportService textImportService, ITrackManager trackManager, IAudioImportService audioImportService, ILogger logger) { public event EventHandler>? UploadFilesFinished; @@ -44,6 +43,7 @@ public class ImportManager(ISessionStateContainer sessionStateContainer, ITraceC private readonly IFileInputManager _fileInputManager = fileInputManager; private readonly ITextImportService _textImportService = textImportService; private readonly ITrackManager _trackManager = trackManager; + private readonly IAudioImportService _audioImportService = audioImportService; public void ImportData(String? data) { @@ -61,7 +61,7 @@ public void ImportData(String? data) } } - public async Task AnalyseImportfile() + public async Task AnalyseImportfile() { ResetTracing(); var stopwatch = Stopwatch.StartNew(); @@ -96,14 +96,16 @@ public async Task AnalyseImportfile() break; } } + var cancelImport = await _audioImportService.MapAudioImportAsync(); stopwatch.Stop(); if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("AnalyseImportfile duration: {stopwatch.Elapsed}", stopwatch.Elapsed); } + return cancelImport; } - public void ImportCuesheet() + public async Task ImportCuesheetAsync() { var stopwatch = Stopwatch.StartNew(); ResetTracing(); @@ -115,7 +117,7 @@ public void ImportCuesheet() _sessionStateContainer.Cuesheet = newCuesheet; _traceChangeManager.AddChange(new TracedChange(_sessionStateContainer, new(previousValue, nameof(SessionStateContainer.Cuesheet)))); } - _sessionStateContainer.ResetImport(); + await _sessionStateContainer.ResetImportAsync(false); stopwatch.Stop(); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -188,10 +190,11 @@ void CopyCuesheet(Cuesheet target, ICuesheet cuesheetToCopy) target.Artist = cuesheetToCopy.Artist; target.Title = cuesheetToCopy.Title; target.Cataloguenumber = cuesheetToCopy.Cataloguenumber; + IEnumerable audiofiles = []; if (cuesheetToCopy is Cuesheet originCuesheet) { target.CDTextfile = originCuesheet.CDTextfile; - AttachClonedAudiofiles(target, originCuesheet.Audiofiles); + audiofiles = originCuesheet.Audiofiles; } if (cuesheetToCopy is ImportCuesheet importCuesheet) { @@ -199,8 +202,9 @@ void CopyCuesheet(Cuesheet target, ICuesheet cuesheetToCopy) { target.CDTextfile = new CDTextfile(importCuesheet.CDTextfile); } - AttachClonedAudiofiles(target, importCuesheet.Audiofiles); + audiofiles = importCuesheet.Audiofiles; } + AttachClonedAudiofiles(target, audiofiles); } void AttachClonedAudiofiles(Cuesheet target, IEnumerable audiofiles) diff --git a/AudioCuesheetEditor/Services/UI/DialogManager.cs b/AudioCuesheetEditor/Services/UI/DialogManager.cs index a98841fa..bf33908a 100644 --- a/AudioCuesheetEditor/Services/UI/DialogManager.cs +++ b/AudioCuesheetEditor/Services/UI/DialogManager.cs @@ -14,6 +14,7 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.UI; using AudioCuesheetEditor.Services.AudioCuesheet; using AudioCuesheetEditor.Shared.Dialogs; @@ -21,11 +22,12 @@ namespace AudioCuesheetEditor.Services.UI { - public class DialogManager(IDialogService dialogService, ITraceChangeManager traceChangeManager, ITrackManager trackManager) + public class DialogManager(IDialogService dialogService, ITraceChangeManager traceChangeManager, ITrackManager trackManager, ISessionStateContainer sessionStateContainer) : IDialogManager { private readonly IDialogService _dialogService = dialogService; private readonly ITraceChangeManager _traceChangeManager = traceChangeManager; private readonly ITrackManager _trackManager = trackManager; + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; private IDialogReference? _loadingDialog; @@ -192,7 +194,7 @@ public async Task ShowAndHandleModalEditDialogAsync(IEnumerable tracks) public async Task ShowLoadingDialogAsync() { if (_loadingDialog == null) - { + { var options = new DialogOptions() { BackdropClick = false, FullWidth = true, MaxWidth = MaxWidth.ExtraSmall, NoHeader = true }; _loadingDialog = await _dialogService.ShowAsync(options); await Task.Delay(1); @@ -204,5 +206,26 @@ public void HideLoadingDialog() _loadingDialog?.Close(); _loadingDialog = null; } + + public async Task ShowImportAudiofilesDialogAsync() + { + var parameters = new DialogParameters + { + { x => x.MappedAudiofiles, _sessionStateContainer.ImportAudiofileMapping }, + { x => x.ImportAudiofiles, _sessionStateContainer.ImportAudiofiles } + }; + var options = new DialogOptions() { BackdropClick = false, FullWidth = true }; + var dialog = await _dialogService.ShowAsync(null, parameters, options); + var result = await dialog.Result; + var canceled = result?.Canceled ?? true; + if (canceled == false) + { + if (result?.Data is Dictionary audiofileMapping) + { + _sessionStateContainer.ImportAudiofileMapping = audiofileMapping; + } + } + return canceled; + } } } diff --git a/AudioCuesheetEditor/Services/UI/IDialogManager.cs b/AudioCuesheetEditor/Services/UI/IDialogManager.cs new file mode 100644 index 00000000..b996a6b1 --- /dev/null +++ b/AudioCuesheetEditor/Services/UI/IDialogManager.cs @@ -0,0 +1,28 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. + +using AudioCuesheetEditor.Model.AudioCuesheet; + +namespace AudioCuesheetEditor.Services.UI +{ + public interface IDialogManager + { + void HideLoadingDialog(); + Task ShowAndHandleModalEditDialogAsync(IEnumerable tracks); + Task ShowImportAudiofilesDialogAsync(); + Task ShowLoadingDialogAsync(); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs index 0e68b86b..8267e2ed 100644 --- a/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs +++ b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs @@ -26,9 +26,11 @@ public interface ISessionStateContainer public Cuesheet Cuesheet { get; set; } public Cuesheet? ImportCuesheet { get; set; } public IList ImportAudiofiles { get; set; } + public Dictionary ImportAudiofileMapping { get; set; } public IImportfile? Importfile { get; set; } public Boolean ImportIsAnalyzed { get; set; } - public void ResetImport(); + public Task ResetImportAsync(Boolean revokeObjectURLs); + public Task ResetAudioImportAsync(Boolean revokeObjectURLs); /// /// Initialize this class /// diff --git a/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs index 4e5d6cb7..3a8caa84 100644 --- a/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs +++ b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs @@ -18,12 +18,14 @@ using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Services.Options; +using Microsoft.JSInterop; namespace AudioCuesheetEditor.Services.UI { public class SessionStateContainer : ISessionStateContainer, IDisposable { private readonly ILocalStorageOptionsProvider _localStorageOptionsProvider; + private readonly IJSRuntime _jsRuntime; private Cuesheet _cuesheet = new(); private Cuesheet? _importCuesheet; @@ -34,10 +36,11 @@ public class SessionStateContainer : ISessionStateContainer, IDisposable public event EventHandler? CuesheetChanged; public event EventHandler? ActiveCuesheetChanged; - public SessionStateContainer(ILocalStorageOptionsProvider localStorageOptionsProvider) + public SessionStateContainer(ILocalStorageOptionsProvider localStorageOptionsProvider, IJSRuntime jsRuntime) { _localStorageOptionsProvider = localStorageOptionsProvider; _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + _jsRuntime = jsRuntime; } public void Dispose() @@ -66,6 +69,7 @@ public Cuesheet? ImportCuesheet } } public IList ImportAudiofiles { get; set; } = []; + public Dictionary ImportAudiofileMapping { get; set; } = []; public IImportfile? Importfile { get; set; } public Boolean ImportIsAnalyzed { get; set; } = false; @@ -76,14 +80,37 @@ public async Task InitializeAsync() SetActiveCuesheet(); } - public void ResetImport() + public async Task ResetImportAsync(Boolean revokeObjectURLs) { Importfile = null; - ImportAudiofiles = []; + await ResetAudioImportAsync(revokeObjectURLs); ImportCuesheet = null; ImportIsAnalyzed = false; } + public async Task ResetAudioImportAsync(bool revokeObjectURLs) + { + if (revokeObjectURLs) + { + foreach (var audiofile in ImportAudiofiles) + { + if (!string.IsNullOrEmpty(audiofile.ObjectURL)) + { + await _jsRuntime.InvokeVoidAsync("revokeAudioObjectURL", audiofile.ObjectURL); + } + } + foreach (var audiofile in ImportAudiofileMapping.Values.Where(x => x != null)) + { + if (!string.IsNullOrEmpty(audiofile!.ObjectURL)) + { + await _jsRuntime.InvokeVoidAsync("revokeAudioObjectURL", audiofile.ObjectURL); + } + } + } + ImportAudiofileMapping = []; + ImportAudiofiles = []; + } + /// public Cuesheet? ActiveCuesheet => _activeCuesheet; @@ -131,5 +158,6 @@ void SetActiveCuesheet() } } } + } } diff --git a/AudioCuesheetEditor/Shared/Cuesheet/Audiofiles.razor b/AudioCuesheetEditor/Shared/Cuesheet/Audiofiles.razor index 7ccffe2a..d16cc16f 100644 --- a/AudioCuesheetEditor/Shared/Cuesheet/Audiofiles.razor +++ b/AudioCuesheetEditor/Shared/Cuesheet/Audiofiles.razor @@ -42,11 +42,7 @@ along with Foobar. If not, see @GetFileName(file) - @if (_fileInputIds.TryGetValue(file, out var fileInputId) == false) - { - _fileInputIds[file] = $"Audiofile_{Guid.NewGuid()}"; - } - x.MimeType))" FilesChanged="(browserFile) => FileUploaded(browserFile, file)" Hidden> + x.MimeType))" FilesChanged="(browserFile) => FileUploaded(browserFile, file)" Hidden InputClass="file-upload"> @@ -81,8 +77,6 @@ along with Foobar. If not, see [Parameter] public EventCallback> SelectedFilesChanged { get; set; } - Dictionary _fileInputIds = new(); - async Task ShowInputDialog(string? initialValue) { var parameters = new DialogParameters @@ -131,8 +125,7 @@ along with Foobar. If not, see { if (browserFile != null) { - var fileInputId = _fileInputIds[audiofile]; - await _audiofileManager.SetPropertiesAsync(audiofile, browserFile, fileInputId!); + await _audiofileManager.SetPropertiesAsync(audiofile, browserFile); } } diff --git a/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.de.resx new file mode 100644 index 00000000..ef606d12 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.de.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Abbrechen + + + Datei hinzufügen + + + Dateien zuordnen + + + Dateien + + + Keine Datei hochgeladen! + + + Ok + + + Anzahl Titel: {0} + + + Hochgeladene Dateien + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.razor new file mode 100644 index 00000000..7228b7f1 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.razor @@ -0,0 +1,171 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + + @_localizer["Assign files"] + + + + + + + + + @_localizer["Files"] + + + + + + + + + + + + @foreach (var dropzone in _dropzones) + { + + + @GetFileName(dropzone.Value) + @_localizer["Track count: {0}", @dropzone.Value.Tracks.Count] + + + + } + + + + @_localizer["Uploaded files"] + + + + + @context.Name + + + + + + @_localizer["Ok"] + @_localizer["Abort"] + + + +@code { + [CascadingParameter] + IMudDialogInstance? _mudDialog { get; set; } + + [Parameter] + [EditorRequired] + public IList ImportAudiofiles { get; set; } + + [Parameter] + [EditorRequired] + public Dictionary MappedAudiofiles { get; set; } + + List _audiofiles = []; + const string UploadedZoneIdentifier = "uploaded"; + Dictionary _dropzones = []; + string? _sourceZoneIdentifier; + + protected override void OnInitialized() + { + base.OnInitialized(); + SetDropzones(); + SetImportAudiofiles(); + } + + bool ItemsSelector(Audiofile item, string zone) + { + if (zone == UploadedZoneIdentifier) + { + return ImportAudiofiles.Contains(item); + } + var audiofile = _dropzones[zone]; + return MappedAudiofiles[audiofile] == item; + } + + void ItemPicked(MudDragAndDropItemTransaction itemTransaction) + { + _sourceZoneIdentifier = itemTransaction.SourceZoneIdentifier; + } + + void ItemDropped(MudItemDropInfo itemDropInfo) + { + if (_sourceZoneIdentifier != null && _sourceZoneIdentifier != UploadedZoneIdentifier) + { + var sourceAudiofile = _dropzones[_sourceZoneIdentifier]; + var currentlyMapped = MappedAudiofiles[sourceAudiofile]; + if (currentlyMapped != null) + { + ImportAudiofiles.Add(currentlyMapped); + } + MappedAudiofiles[sourceAudiofile] = null; + } + var targetAudiofile = _dropzones[itemDropInfo.DropzoneIdentifier]; + var currentlyMappedFile = MappedAudiofiles[targetAudiofile]; + MappedAudiofiles[targetAudiofile] = itemDropInfo.Item; + ImportAudiofiles.Remove(itemDropInfo.Item!); + if (currentlyMappedFile != null) + { + ImportAudiofiles.Add(currentlyMappedFile); + } + SetImportAudiofiles(); + _sourceZoneIdentifier = null; + } + + String GetFileName(Audiofile audiofile) + { + return String.IsNullOrEmpty(audiofile.Name) ? _localizer["No file uploaded!"] : audiofile.Name; + } + + void SetImportAudiofiles() + { + _audiofiles = [.. ImportAudiofiles, .. MappedAudiofiles.Values.Where(x => x != null).Select(x => x!)]; + } + + void SetDropzones() + { + _dropzones = MappedAudiofiles.ToDictionary(x => Guid.NewGuid().ToString(), x => x.Key); + } + + void AddButtonClicked() + { + var audiofile = new Audiofile(); + MappedAudiofiles.Add(audiofile, null); + SetDropzones(); + } + + void DeleteButtonClicked(Audiofile audiofile) + { + var currentlyMappedAudiofile = MappedAudiofiles[audiofile]; + if (currentlyMappedAudiofile != null) + { + ImportAudiofiles.Add(currentlyMappedAudiofile); + } + MappedAudiofiles.Remove(audiofile); + SetDropzones(); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.resx new file mode 100644 index 00000000..2cd88420 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ImportAudiofilesDialog.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Abort + + + Add file + + + Assign files + + + Files + + + No file uploaded! + + + Ok + + + Track count: {0} + + + Uploaded files + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Inputs/FileDropOverlay.razor b/AudioCuesheetEditor/Shared/Inputs/FileDropOverlay.razor index deb044b1..e4d4851d 100644 --- a/AudioCuesheetEditor/Shared/Inputs/FileDropOverlay.razor +++ b/AudioCuesheetEditor/Shared/Inputs/FileDropOverlay.razor @@ -22,11 +22,11 @@ along with Foobar. If not, see @inject IJSRuntime _jsRuntime @inject IStringLocalizer _localizer @inject ImportManager _importManager -@inject DialogManager _dialogManager +@inject IDialogManager _dialogManager @inject IFileInputManager _fileInputManager -