From e936cdaedaa9b60f5ccad30d24c4fa9a275235da Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:59:12 +0200 Subject: [PATCH 1/5] Update AudioPlayer.razor --- .../Shared/Audio/AudioPlayer.razor | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor index c3fe120a..49baffdb 100644 --- a/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor +++ b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor @@ -31,7 +31,7 @@ along with Foobar. If not, see { @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) } - + @GetSliderTimeValue() @@ -63,21 +63,21 @@ along with Foobar. If not, see @code { - double sliderValue; - HotKeysContext? hotKeysContext; + double _sliderValue; + HotKeysContext? _hotKeysContext; protected override void Dispose(bool disposing) { base.Dispose(disposing); _playbackService.CurrentPositionChanged -= PlaybackService_CurrentPositionChanged; - hotKeysContext?.DisposeAsync(); + _hotKeysContext?.DisposeAsync(); } protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); _playbackService.CurrentPositionChanged += PlaybackService_CurrentPositionChanged; - hotKeysContext = _hotKeys.CreateContext() + _hotKeysContext = _hotKeys.CreateContext() .Add(ModKey.Ctrl, Key.p, OnPlayOrPauseClicked) .Add(ModKey.Ctrl, Key.ArrowRight, PlayNextTrackAsync) .Add(ModKey.Ctrl, Key.ArrowLeft, PlayPreviousTrackAsync) @@ -121,18 +121,18 @@ along with Foobar. If not, see { if ((_playbackService.CurrentPosition.HasValue) && (_playbackService.TotalTime.HasValue)) { - sliderValue = (_playbackService.CurrentPosition.Value / _playbackService.TotalTime.Value) * 100.0; + _sliderValue = (_playbackService.CurrentPosition.Value / _playbackService.TotalTime.Value) * 100.0; } else { - sliderValue = 0.0; + _sliderValue = 0.0; } InvokeAsync(StateHasChanged); } string GetSliderTimeValue() { - var time = sliderValue * _playbackService.TotalTime / 100.0; + var time = _sliderValue * _playbackService.TotalTime / 100.0; if (time.HasValue) { return time.Value.ToString(@"hh\:mm\:ss"); From a9332badbceafca5534ddb4b067c4e411229f855 Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:58:44 +0200 Subject: [PATCH 2/5] Update PlaybackService.cs --- AudioCuesheetEditor/Services/Audio/PlaybackService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs index 04f785ea..db96491c 100644 --- a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs +++ b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs @@ -47,6 +47,7 @@ private set } } public Track? CurrentlyPlayingTrack => _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).SingleOrDefault(x => x.Begin.HasValue == true && x.End.HasValue == true && x.Begin <= CurrentPosition && x.End > CurrentPosition); + //TODO: Doesn't get reset when cuesheet or application is reset public TimeSpan? TotalTime { get @@ -63,6 +64,7 @@ public TimeSpan? TotalTime public Boolean IsPlaybackPossible => _sessionStateContainer.Cuesheet.Audiofiles.Any(x => string.IsNullOrEmpty(x.ObjectURL) == false); public Boolean IsPreviousPossible => (CurrentlyPlayingTrack != null) && _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.End <= CurrentlyPlayingTrack.Begin) != null; public Boolean IsNextPossible => (CurrentlyPlayingTrack != null) && _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.Begin >= CurrentlyPlayingTrack.End) != null; + //TODO: Doesn't stop playback when resetting cuesheet or application public Boolean IsPlaying => _currentlyPlayingAudiofile != null; public async Task InitializeAsync() @@ -271,6 +273,7 @@ async void UpdateCurrentPosition(object? state) } } CalculateDurationsBeforeCurrentlyPlayingAudiofile(); + //TODO: When audio is stopped the current position seems to be 00:00:00 and not --:--:-- var currentSecondsInCurrentlyPlayingAudiofile = await _jsRuntime.InvokeAsync("audioInterop.getAudioCurrentTime"); if (_audiofileDurationsBeforeCurrentlyPlayingAudiofile.HasValue) { From 1b83d633c685b3424fa707e39778cf5c3ffabb91 Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:41:54 +0200 Subject: [PATCH 3/5] reset playback when application gets reset --- .../Services/UI/SessionStateContainerTests.cs | 13 +++++++++---- .../Services/Audio/PlaybackService.cs | 9 +++++++-- .../Services/UI/ISessionStateContainer.cs | 1 + .../Services/UI/SessionStateContainer.cs | 2 ++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs index 0ba87be4..5188a1de 100644 --- a/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs @@ -45,14 +45,17 @@ public async Task Cuesheet_SetNewValueInDetailView_ShouldTriggerActiveCuesheetCh var viewOptions = new ViewOptions(); _localStorageOptionsProvider.Setup(x => x.GetOptionsAsync()).ReturnsAsync(viewOptions); await _sessionStateContainer.InitializeAsync(); - bool eventTriggered = false; - _sessionStateContainer.ActiveCuesheetChanged += (sender, args) => eventTriggered = true; + bool activeCuesheetChangedEvent = false; + bool cuesheetChangedEvent = false; + _sessionStateContainer.ActiveCuesheetChanged += (sender, args) => activeCuesheetChangedEvent = true; + _sessionStateContainer.CuesheetChanged += (sender, args) => cuesheetChangedEvent = true; // Act _sessionStateContainer.Cuesheet = newCuesheet; // Assert - Assert.IsTrue(eventTriggered); + Assert.IsTrue(activeCuesheetChangedEvent); + Assert.IsTrue(cuesheetChangedEvent); } [TestMethod] @@ -67,13 +70,15 @@ public async Task Cuesheet_SetNewValueInImportView_ShouldNotTriggerActiveCueshee _localStorageOptionsProvider.Setup(x => x.GetOptionsAsync()).ReturnsAsync(viewOptions); await _sessionStateContainer.InitializeAsync(); bool eventTriggered = false; + bool cuesheetChangedEvent = false; _sessionStateContainer.ActiveCuesheetChanged += (sender, args) => eventTriggered = true; - + _sessionStateContainer.CuesheetChanged += (sender, args) => cuesheetChangedEvent = true; // Act _sessionStateContainer.Cuesheet = newCuesheet; // Assert Assert.IsFalse(eventTriggered); + Assert.IsTrue(cuesheetChangedEvent); } [TestMethod] diff --git a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs index db96491c..3b6bc1ba 100644 --- a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs +++ b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs @@ -47,7 +47,6 @@ private set } } public Track? CurrentlyPlayingTrack => _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).SingleOrDefault(x => x.Begin.HasValue == true && x.End.HasValue == true && x.Begin <= CurrentPosition && x.End > CurrentPosition); - //TODO: Doesn't get reset when cuesheet or application is reset public TimeSpan? TotalTime { get @@ -64,7 +63,6 @@ public TimeSpan? TotalTime public Boolean IsPlaybackPossible => _sessionStateContainer.Cuesheet.Audiofiles.Any(x => string.IsNullOrEmpty(x.ObjectURL) == false); public Boolean IsPreviousPossible => (CurrentlyPlayingTrack != null) && _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.End <= CurrentlyPlayingTrack.Begin) != null; public Boolean IsNextPossible => (CurrentlyPlayingTrack != null) && _sessionStateContainer.Cuesheet.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.Begin >= CurrentlyPlayingTrack.End) != null; - //TODO: Doesn't stop playback when resetting cuesheet or application public Boolean IsPlaying => _currentlyPlayingAudiofile != null; public async Task InitializeAsync() @@ -74,6 +72,12 @@ public async Task InitializeAsync() _dotNetObjectReference = DotNetObjectReference.Create(this); await _jsRuntime.InvokeVoidAsync("audioInterop.register", _dotNetObjectReference); } + _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; + } + + void SessionStateContainer_CuesheetChanged(object? sender, EventArgs e) + { + _ = StopAsync(); } public async Task PlayOrPauseAsync() @@ -231,6 +235,7 @@ public void OnPlaybackPaused() public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; await _jsRuntime.InvokeVoidAsync("audioInterop.unregister"); _dotNetObjectReference?.Dispose(); } diff --git a/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs index a4ea5331..0e68b86b 100644 --- a/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs +++ b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs @@ -21,6 +21,7 @@ namespace AudioCuesheetEditor.Services.UI { public interface ISessionStateContainer { + public event EventHandler? CuesheetChanged; public event EventHandler? ActiveCuesheetChanged; public Cuesheet Cuesheet { get; set; } public Cuesheet? ImportCuesheet { get; set; } diff --git a/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs index d177360c..4e5d6cb7 100644 --- a/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs +++ b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs @@ -31,6 +31,7 @@ public class SessionStateContainer : ISessionStateContainer, IDisposable private ViewOptions? _viewOptions; private bool disposedValue; + public event EventHandler? CuesheetChanged; public event EventHandler? ActiveCuesheetChanged; public SessionStateContainer(ILocalStorageOptionsProvider localStorageOptionsProvider) @@ -52,6 +53,7 @@ public Cuesheet Cuesheet { _cuesheet = value; SetActiveCuesheet(); + CuesheetChanged?.Invoke(this, EventArgs.Empty); } } public Cuesheet? ImportCuesheet From f9616b595bd37fd153df524df657c2334aec76aa Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:18:49 +0200 Subject: [PATCH 4/5] Update PlaybackService.cs --- .../Services/Audio/PlaybackService.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs index 3b6bc1ba..be29f6a7 100644 --- a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs +++ b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs @@ -30,7 +30,7 @@ public class PlaybackService(IJSRuntime jsRuntime, ISessionStateContainer sessio private readonly Lock _timerLock = new(); private TimeSpan? _currentPosition; private DotNetObjectReference? _dotNetObjectReference; - private TimeSpan? _audiofileDurationsBeforeCurrentlyPlayingAudiofile; + private TimeSpan _durationBeforeCurrentlyPlayingAudiofile; public event Action? CurrentPositionChanged; @@ -72,6 +72,7 @@ public async Task InitializeAsync() _dotNetObjectReference = DotNetObjectReference.Create(this); await _jsRuntime.InvokeVoidAsync("audioInterop.register", _dotNetObjectReference); } + //TODO: When audiofile gets cleared playback doesnt stop but the audioplayer doesn't offer stop anymore _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; } @@ -229,7 +230,7 @@ public void OnPlaybackPaused() { IsPaused = true; StopTimer(); - UpdateCurrentPosition(null); + UpdateCurrentPosition(); } public async ValueTask DisposeAsync() @@ -245,14 +246,14 @@ async Task PlayAsync(Audiofile audiofileToPlay) await _jsRuntime.InvokeVoidAsync("audioInterop.setAudioSource", audiofileToPlay.ObjectURL); await _jsRuntime.InvokeVoidAsync("audioInterop.playAudio"); _currentlyPlayingAudiofile = audiofileToPlay; - _audiofileDurationsBeforeCurrentlyPlayingAudiofile = null; + _durationBeforeCurrentlyPlayingAudiofile = TimeSpan.Zero; } void Reset() { StopTimer(); _currentlyPlayingAudiofile = null; - _audiofileDurationsBeforeCurrentlyPlayingAudiofile = null; + _durationBeforeCurrentlyPlayingAudiofile = TimeSpan.Zero; CurrentPosition = null; IsPaused = false; } @@ -268,40 +269,42 @@ void StopTimer() _updateTimer = null; } - async void UpdateCurrentPosition(object? state) + async void UpdateCurrentPosition(object? state = null) { lock (_timerLock) { - if (_currentlyPlayingAudiofile == null) + if (IsPlaying == false) { - StopTimer(); + Reset(); + return; + } + else + { + SetDurationBeforeCurrentlyPlayingAudiofile(); } } - CalculateDurationsBeforeCurrentlyPlayingAudiofile(); - //TODO: When audio is stopped the current position seems to be 00:00:00 and not --:--:-- - var currentSecondsInCurrentlyPlayingAudiofile = await _jsRuntime.InvokeAsync("audioInterop.getAudioCurrentTime"); - if (_audiofileDurationsBeforeCurrentlyPlayingAudiofile.HasValue) - { - CurrentPosition = _audiofileDurationsBeforeCurrentlyPlayingAudiofile + TimeSpan.FromSeconds(currentSecondsInCurrentlyPlayingAudiofile); - } - else + if (IsPlaying) { - CurrentPosition = TimeSpan.FromSeconds(currentSecondsInCurrentlyPlayingAudiofile); + var currentSecondsInCurrentlyPlayingAudiofile = await _jsRuntime.InvokeAsync("audioInterop.getAudioCurrentTime"); + CurrentPosition = _durationBeforeCurrentlyPlayingAudiofile + TimeSpan.FromSeconds(currentSecondsInCurrentlyPlayingAudiofile); } } - void CalculateDurationsBeforeCurrentlyPlayingAudiofile() + void SetDurationBeforeCurrentlyPlayingAudiofile() { - if ((_audiofileDurationsBeforeCurrentlyPlayingAudiofile != null) || (_currentlyPlayingAudiofile == null)) + if (_currentlyPlayingAudiofile == null) { return; } - _audiofileDurationsBeforeCurrentlyPlayingAudiofile = TimeSpan.Zero; + _durationBeforeCurrentlyPlayingAudiofile = TimeSpan.Zero; var index = _sessionStateContainer.Cuesheet.Audiofiles.IndexOf(_currentlyPlayingAudiofile); for (int i = 0; i < index; i++) { var audiofile = _sessionStateContainer.Cuesheet.Audiofiles[i]; - _audiofileDurationsBeforeCurrentlyPlayingAudiofile += audiofile.Duration; + if (audiofile.Duration.HasValue) + { + _durationBeforeCurrentlyPlayingAudiofile += audiofile.Duration.Value; + } } } } From 9624198d9e8c96aceac6e55fcb29be035ee42a6a Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:13:13 +0200 Subject: [PATCH 5/5] fix audiofile reset bug --- .../AudioCuesheet/AudiofileManagerTests.cs | 125 ++++++++++++++++-- .../Services/Audio/PlaybackService.cs | 20 ++- .../AudioCuesheet/AudiofileManager.cs | 4 + .../AudioCuesheet/IAudiofileManager.cs | 4 + 4 files changed, 142 insertions(+), 11 deletions(-) diff --git a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs index 6f30d296..73565f58 100644 --- a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/AudiofileManagerTests.cs @@ -88,6 +88,14 @@ public async Task SetPropertiesAsync_EmptyBrowserFile_ShouldClearPropertiesAsync ObjectURL = "Just a test", Duration = TimeSpan.FromSeconds(120) }; + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate(object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; var expectedUrl = audiofile.ObjectURL; // Act await _audiofileManager.SetPropertiesAsync(audiofile, null, string.Empty); @@ -96,6 +104,7 @@ public async Task SetPropertiesAsync_EmptyBrowserFile_ShouldClearPropertiesAsync Assert.IsNull(audiofile.Name); Assert.IsNull(audiofile.ObjectURL); Assert.IsNull(audiofile.Duration); + Assert.IsTrue(audiofileChangedFired); _jsRuntime.Verify(js => js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == expectedUrl)), Times.Once); } @@ -116,7 +125,14 @@ public async Task SetPropertiesAsync_WithBrowserFile_ShouldSetPropertiesAsync() _jsRuntime.Setup(js => js.InvokeAsync("getAudioDurationFromFile", It.IsAny())).Returns(new ValueTask(90.0)); var audiofile = new Audiofile(); - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object, inputId); @@ -125,7 +141,7 @@ public async Task SetPropertiesAsync_WithBrowserFile_ShouldSetPropertiesAsync() Assert.AreEqual(filename, audiofile.Name); Assert.AreEqual(objectUrl, audiofile.ObjectURL); Assert.AreEqual(TimeSpan.FromSeconds(90), audiofile.Duration); - + Assert.IsTrue(audiofileChangedFired); _fileInputManager.Verify(f => f.GetAudioCodec("audio/mpeg", filename), Times.Once); _fileInputManager.Verify(f => f.GetObjectUrlAsync(inputId), Times.Once); _jsRuntime.Verify(js => js.InvokeAsync("getAudioDurationFromFile", It.Is(o => o[0] as string == audiofile.ObjectURL)), Times.Once); @@ -161,7 +177,14 @@ public async Task SetProperty_AudiofileWithDuration_SetsLastTrackEndAlsoAsync() { Tracks = [track1, track2] }; - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act await _audiofileManager.SetPropertiesAsync(audiofile, browserFile.Object, inputId); @@ -171,7 +194,7 @@ public async Task SetProperty_AudiofileWithDuration_SetsLastTrackEndAlsoAsync() Assert.AreEqual(objectUrl, audiofile.ObjectURL); Assert.AreEqual(TimeSpan.FromSeconds(90), audiofile.Duration); Assert.AreEqual(TimeSpan.FromSeconds(90), track2.End); - + Assert.IsTrue(audiofileChangedFired); _fileInputManager.Verify(f => f.GetAudioCodec("audio/mpeg", filename), Times.Once); _fileInputManager.Verify(f => f.GetObjectUrlAsync(inputId), Times.Once); _jsRuntime.Verify(js => js.InvokeAsync("getAudioDurationFromFile", It.Is(o => o[0] as string == audiofile.ObjectURL)), Times.Once); @@ -186,11 +209,19 @@ public async Task SetPropertiesAsync_NullBrowserFile_WithExistingObjectUrl_Shoul ObjectURL = "just a test" }; var expectedUrl = audiofile.ObjectURL; - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act await _audiofileManager.SetPropertiesAsync(audiofile, null, string.Empty); // Assert + Assert.IsTrue(audiofileChangedFired); _jsRuntime.Verify(js =>js.InvokeAsync("revokeAudioObjectURL", It.Is(args => args != null && args.Length > 0 && (args[0] as string) == expectedUrl)),Times.Once); Assert.IsNull(audiofile.ObjectURL); } @@ -203,12 +234,20 @@ public void SetProperty_ChangedName_UpdatesPropertyAndAddsTrace() { Name = "oldname.mp3" }; - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.SetProperty(audiofile, x => x.Name, "newname.mp3"); // Assert Assert.AreEqual("newname.mp3", audiofile.Name); + Assert.IsTrue(audiofileChangedFired); _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == audiofile && y.TraceableChange.PreviousValue!.Equals("oldname.mp3") && y.TraceableChange.PropertyName == nameof(Audiofile.Name))), Times.Once); } @@ -220,11 +259,19 @@ public void SetProperty_EqualValue_DoesntChangeProperty() { Name = "oldname.mp3" }; - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.SetProperty(audiofile, x => x.Name, "oldname.mp3"); // Assert + Assert.IsFalse(audiofileChangedFired); _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == audiofile && y.TraceableChange.PreviousValue!.Equals("oldname.mp3") && y.TraceableChange.PropertyName == nameof(Audiofile.Name))), Times.Never); } @@ -248,13 +295,21 @@ public void SetProperty_DurationChanged_SetsLastTrackEndAlso() Tracks = [track1, track2], }; var duration = new TimeSpan(0, 3, 37, 12); - + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.SetProperty(audiofile, x => x.Duration, duration); // Assert Assert.AreEqual(duration, audiofile.Duration); Assert.AreEqual(duration, track2.End); + Assert.IsTrue(audiofileChangedFired); } [TestMethod] @@ -268,11 +323,20 @@ public void AddTrack_FirstTrack_AddsNewTrackWithCalulatedTrackProperties() Audiofiles = [audiofile] }; _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(cuesheet); + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; var track = new Track(); // Act _audiofileManager.AddTrack(audiofile, track); // Assert Assert.HasCount(1, audiofile.Tracks); + Assert.IsTrue(audiofileChangedFired); Assert.AreEqual((ushort)1, audiofile.Tracks.First().Position); Assert.AreEqual(TimeSpan.Zero, audiofile.Tracks.First().Begin); Assert.AreEqual(duration, audiofile.Tracks.First().End); @@ -307,6 +371,14 @@ public void AddTrack_AddToPreviousTracks_AddsNewTrackWithCalulatedTrackPropertie { IsLinkedToPreviousTrack = true }; + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.AddTrack(audiofile, track); // Assert @@ -316,6 +388,7 @@ public void AddTrack_AddToPreviousTracks_AddsNewTrackWithCalulatedTrackPropertie Assert.AreEqual(duration, audiofile.Tracks.Last().End); Assert.AreEqual(cuesheet, track.Cuesheet); Assert.AreEqual(audiofile, track.Audiofile); + Assert.IsTrue(audiofileChangedFired); _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == audiofile && y.TraceableChange.PreviousValue == tracks && y.TraceableChange.PropertyName == nameof(Audiofile.Tracks))), Times.Once); _traceChangeManager.VerifySet(t => t.BulkEdit = true, Times.Once); _traceChangeManager.VerifySet(t => t.BulkEdit = false, Times.Once); @@ -348,9 +421,18 @@ public void AddTrack_DuringRecording_AddsNewTrackWithCalulatedTrackProperties() { IsLinkedToPreviousTrack = true }; + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.AddTrack(audiofile, track); // Assert + Assert.IsTrue(audiofileChangedFired); Assert.HasCount(2, audiofile.Tracks); Assert.AreEqual((ushort)2, audiofile.Tracks.Last().Position); Assert.IsNotNull(audiofile.Tracks.First().End); @@ -374,9 +456,18 @@ public void AddTrack_FirstTrackImporting_AddsNewTrackWithCalulatedTrackPropertie }; _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(importCuesheet); var track = new Track(); + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.AddTrack(audiofile, track); // Assert + Assert.IsTrue(audiofileChangedFired); Assert.HasCount(1, audiofile.Tracks); Assert.AreEqual((ushort)1, audiofile.Tracks.First().Position); Assert.AreEqual(TimeSpan.Zero, audiofile.Tracks.First().Begin); @@ -442,6 +533,14 @@ public void RemoveTracks_WithAttachedTracks_RemovesTracksAndRecalculatesRemainin track4.Cuesheet = cuesheet; track5.Cuesheet = cuesheet; _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(cuesheet); + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.RemoveTracks(audiofile, [track2, track4]); // Assert @@ -449,6 +548,7 @@ public void RemoveTracks_WithAttachedTracks_RemovesTracksAndRecalculatesRemainin Assert.Contains(track1, audiofile.Tracks); Assert.Contains(track3, audiofile.Tracks); Assert.Contains(track5, audiofile.Tracks); + Assert.IsTrue(audiofileChangedFired); Assert.AreEqual((ushort)1, track1.Position); Assert.AreEqual(TimeSpan.Zero, track1.Begin); Assert.AreEqual(track1.End, track3.Begin); @@ -520,6 +620,14 @@ public void RemoveTracks_DuringImport_RemovesTracksAndRecalculatesRemainingTrack track4.Audiofile = audiofile; track5.Audiofile = audiofile; _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(importCuesheet); + var audiofileChangedFired = false; + _audiofileManager.AudiofileChanged += delegate (object? sender, Audiofile e) + { + if (e == audiofile) + { + audiofileChangedFired = true; + } + }; // Act _audiofileManager.RemoveTracks(audiofile, [track2, track4]); // Assert @@ -533,6 +641,7 @@ public void RemoveTracks_DuringImport_RemovesTracksAndRecalculatesRemainingTrack Assert.AreEqual((ushort)2, track3.Position); Assert.AreEqual(track5.Begin, track3.End); Assert.AreEqual((ushort)3, track5.Position); + Assert.IsTrue(audiofileChangedFired); Assert.AreEqual(duration, track5.End); _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == audiofile && y.TraceableChange.PreviousValue == previousValue && y.TraceableChange.PropertyName == nameof(Audiofile.Tracks))), Times.Once); _traceChangeManager.VerifySet(t => t.BulkEdit = true, Times.Once); diff --git a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs index be29f6a7..2bd6d2f5 100644 --- a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs +++ b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs @@ -15,15 +15,17 @@ //. using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.IO.Audio; +using AudioCuesheetEditor.Services.AudioCuesheet; using AudioCuesheetEditor.Services.UI; using Microsoft.JSInterop; namespace AudioCuesheetEditor.Services.Audio { - public class PlaybackService(IJSRuntime jsRuntime, ISessionStateContainer sessionStateContainer) : IAsyncDisposable + public class PlaybackService(IJSRuntime jsRuntime, ISessionStateContainer sessionStateContainer, IAudiofileManager audiofileManager) : IAsyncDisposable { - private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; private readonly IJSRuntime _jsRuntime = jsRuntime; + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; + private readonly IAudiofileManager _audiofileManager = audiofileManager; private Audiofile? _currentlyPlayingAudiofile; private Timer? _updateTimer; @@ -72,10 +74,21 @@ public async Task InitializeAsync() _dotNetObjectReference = DotNetObjectReference.Create(this); await _jsRuntime.InvokeVoidAsync("audioInterop.register", _dotNetObjectReference); } - //TODO: When audiofile gets cleared playback doesnt stop but the audioplayer doesn't offer stop anymore + _audiofileManager.AudiofileChanged += AudiofileManager_AudiofileChanged; _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; } + void AudiofileManager_AudiofileChanged(object? sender, Audiofile audiofile) + { + if (audiofile == _currentlyPlayingAudiofile) + { + if (string.IsNullOrEmpty(audiofile.ObjectURL)) + { + _ = StopAsync(); + } + } + } + void SessionStateContainer_CuesheetChanged(object? sender, EventArgs e) { _ = StopAsync(); @@ -236,6 +249,7 @@ public void OnPlaybackPaused() public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); + _audiofileManager.AudiofileChanged -= AudiofileManager_AudiofileChanged; _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; await _jsRuntime.InvokeVoidAsync("audioInterop.unregister"); _dotNetObjectReference?.Dispose(); diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs index 5f101ac4..1c0bd31d 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/AudiofileManager.cs @@ -31,6 +31,9 @@ public class AudiofileManager(IFileInputManager fileInputManager, ITraceChangeMa private readonly IJSRuntime _jsRuntime = jsRuntime; private readonly ITrackManager _trackManager = trackManager; private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; + + /// + public event EventHandler? AudiofileChanged; /// public async Task SetPropertiesAsync(Audiofile audiofile, IBrowserFile? browserFile, string fileInputId) @@ -145,6 +148,7 @@ void SetValue(Audiofile audiofile, Expression + /// Event triggered when an audiofile is changed + /// + public event EventHandler? AudiofileChanged; /// /// Set properties from a file upload ///