diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 0232fc35..b6dfd7d9 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -2,7 +2,6 @@ name: Run unit tests # Run workflow on every push to the master branch on: - push: pull_request: jobs: @@ -10,14 +9,14 @@ jobs: # use ubuntu-latest image to run steps on runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.2.2 # sets up .NET - # version can be found here https://dotnet.microsoft.com/en-us/download/dotnet/8.0 + # version can be found here https://dotnet.microsoft.com/en-us/download/dotnet/9.0 - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.203' + dotnet-version: '9.0.101' - name: Install wasm-tools run: dotnet workload install wasm-tools diff --git a/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj b/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj index da02ce7b..d8af8a05 100644 --- a/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj +++ b/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj @@ -1,17 +1,17 @@ - net8.0 + net9.0 enable false - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AudioCuesheetEditor.Tests/Extensions/SessionStateContainerTests.cs b/AudioCuesheetEditor.Tests/Extensions/SessionStateContainerTests.cs deleted file mode 100644 index 07641a59..00000000 --- a/AudioCuesheetEditor.Tests/Extensions/SessionStateContainerTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -//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.UI; -using AudioCuesheetEditor.Tests.Utility; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace AudioCuesheetEditor.Extensions.Tests -{ - [TestClass()] - public class SessionStateContainerTests - { - [TestMethod()] - public void SessionStateContainerTest() - { - var helper = new TestHelper(); - var manager = new TraceChangeManager(TestHelper.CreateLogger()); - var container = new SessionStateContainer(manager); - var importCuesheetChangedFired = false; - container.ImportCuesheetChanged += delegate - { - importCuesheetChangedFired = true; - }; - container.ImportCuesheet = null; - Assert.IsFalse(importCuesheetChangedFired); - container.ImportCuesheet = new Cuesheet(); - Assert.IsTrue(importCuesheetChangedFired); - } - - [TestMethod()] - public void SessionStateContainerFireCuesheetChangedTest() - { - var helper = new TestHelper(); - var manager = new TraceChangeManager(TestHelper.CreateLogger()); - var container = new SessionStateContainer(manager); - var cuesheetChangedFired = false; - container.CuesheetChanged += delegate - { - cuesheetChangedFired = true; - }; - Assert.IsFalse(cuesheetChangedFired); - container.FireCuesheetImported(); - Assert.IsTrue(cuesheetChangedFired); - } - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CataloguenumberTests.cs b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CataloguenumberTests.cs deleted file mode 100644 index 4488015a..00000000 --- a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CataloguenumberTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -//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.Entity; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace AudioCuesheetEditor.Tests.Model.AudioCuesheet -{ - [TestClass()] - public class CataloguenumberTests - { - [TestMethod()] - public void CatalogueNumberTest() - { - var catalogueNumber = new Cataloguenumber(); - Assert.AreEqual(ValidationStatus.NoValidation, catalogueNumber.Validate().Status); - catalogueNumber.Value = "Testvalue"; - Assert.AreEqual(ValidationStatus.Error, catalogueNumber.Validate().Status); - catalogueNumber.Value = "01234567891234567890"; - Assert.AreEqual(ValidationStatus.Error, catalogueNumber.Validate().Status); - catalogueNumber.Value = "1234567890123"; - Assert.AreEqual(ValidationStatus.Success, catalogueNumber.Validate().Status); - } - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs index dd8dbb9b..4123b697 100644 --- a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs +++ b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs @@ -14,14 +14,15 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Extensions; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO.Audio; +using AudioCuesheetEditor.Model.IO.Export; using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.UI; +using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; using AudioCuesheetEditor.Tests.Properties; using AudioCuesheetEditor.Tests.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -31,7 +32,6 @@ using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace AudioCuesheetEditor.Tests.Model.AudioCuesheet { @@ -41,183 +41,38 @@ public class CuesheetTests [TestMethod()] public void AddTrackTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); - AutoResetEvent tracksAddedEvent = new(false); - cuesheet.TracksAdded += (object? sender, TracksAddedRemovedEventArgs args) => tracksAddedEvent.Set(); - Assert.AreEqual(cuesheet.Tracks.Count, 0); - cuesheet.AddTrack(new Track(), testHelper.ApplicationOptions); - Assert.AreEqual(cuesheet.Tracks.Count, 1); - Assert.IsTrue(tracksAddedEvent.WaitOne(1000)); + Assert.AreEqual(0, cuesheet.Tracks.Count); + cuesheet.AddTrack(new Track()); + Assert.AreEqual(1, cuesheet.Tracks.Count); } [TestMethod()] public void CuesheetTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); Assert.IsNull(cuesheet.Audiofile); - var validationErrorAudioFile = cuesheet.Validate(x => x.Audiofile); + var validationErrorAudioFile = cuesheet.Validate(nameof(Cuesheet.Audiofile)); Assert.AreEqual(ValidationStatus.Error, validationErrorAudioFile.Status); cuesheet.Audiofile = new Audiofile("AudioFile01.ogg"); - validationErrorAudioFile = cuesheet.Validate(x => x.Audiofile); + validationErrorAudioFile = cuesheet.Validate(nameof(Cuesheet.Audiofile)); Assert.AreEqual(ValidationStatus.Success, validationErrorAudioFile.Status); } [TestMethod()] public void EmptyCuesheetTracksValidationTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); - Assert.AreEqual(cuesheet.Tracks.Count, 0); - var validationErrorTracks = cuesheet.Validate(x => x.Tracks); + Assert.AreEqual(0, cuesheet.Tracks.Count); + var validationErrorTracks = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Error, validationErrorTracks.Status); - cuesheet.AddTrack(new Track(), testHelper.ApplicationOptions); - validationErrorTracks = cuesheet.Validate(x => x.Tracks); + cuesheet.AddTrack(new Track()); + validationErrorTracks = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Success, validationErrorTracks.Status); } [TestMethod()] - public void MoveTrackTest() - { - var testHelper = new TestHelper(); - var cuesheet = new Cuesheet(); - var track1 = new Track(); - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - var track2 = new Track(); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - var track3 = new Track(); - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); - Assert.AreEqual(cuesheet.Tracks.Count, 3); - Assert.AreEqual((uint)1, track1.Position); - cuesheet.MoveTrack(track1, MoveDirection.Up); - Assert.AreEqual((uint)1, track1.Position); - Assert.AreEqual((uint)3, track3.Position); - cuesheet.MoveTrack(track3, MoveDirection.Down); - Assert.AreEqual((uint)3, track3.Position); - Assert.AreEqual((uint)2, track2.Position); - cuesheet.MoveTrack(track2, MoveDirection.Up); - Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(0)); - Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(1)); - cuesheet.MoveTrack(track2, MoveDirection.Down); - cuesheet.MoveTrack(track2, MoveDirection.Down); - Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(2)); - Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(0)); - Assert.AreEqual(track3, cuesheet.Tracks.ElementAt(1)); - } - - [TestMethod()] - public void MoveAndDeleteTrackTest() - { - var testHelper = new TestHelper(); - var cuesheet = new Cuesheet(); - var track1 = new Track - { - Artist = "Track 1", - Title = "Track 1" - }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - var track2 = new Track - { - Title = "Track 2", - Artist = "Track 2", - Begin = new TimeSpan(0, 5, 0) - }; - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - var track3 = new Track - { - Artist = "Track 3", - Title = "Track 3", - Begin = new TimeSpan(0, 10, 0) - }; - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); - var track4 = new Track - { - Artist = "Track 4", - Title = "Track 4", - Begin = new TimeSpan(0, 15, 0) - }; - cuesheet.AddTrack(track4, testHelper.ApplicationOptions); - var track5 = new Track - { - Artist = "Track 5", - Title = "Track 5", - Begin = new TimeSpan(0, 20, 0) - }; - cuesheet.AddTrack(track5, testHelper.ApplicationOptions); - cuesheet.RemoveTrack(track2); - cuesheet.RemoveTrack(track4); - Assert.AreEqual(3, cuesheet.Tracks.Count); - track1.IsLinkedToPreviousTrack = true; - track3.IsLinkedToPreviousTrack = true; - track5.IsLinkedToPreviousTrack = true; - var track1End = track1.End; - testHelper.ApplicationOptions.LinkTracksWithPreviousOne = true; - cuesheet.MoveTrack(track3, MoveDirection.Up); - Assert.AreEqual((uint)1, track3.Position); - Assert.AreEqual(track3, cuesheet.Tracks.ElementAt(0)); - Assert.AreEqual(track1, cuesheet.GetPreviousLinkedTrack(track5)); - Assert.AreEqual(TimeSpan.Zero, track3.Begin.Value); - Assert.AreEqual(track1End, track1.Begin); - cuesheet.MoveTrack(track5, MoveDirection.Up); - Assert.AreEqual((uint)2, track5.Position); - Assert.AreEqual(track5, cuesheet.Tracks.ElementAt(1)); - Assert.AreEqual(track5, cuesheet.GetPreviousLinkedTrack(track1)); - Assert.IsNull(cuesheet.Tracks.Last().End); - //Reset for move down - cuesheet.RemoveTracks(cuesheet.Tracks); - track1 = new Track - { - Artist = "Track 1", - Title = "Track 1" - }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - track2 = new Track - { - Title = "Track 2", - Artist = "Track 2", - Begin = new TimeSpan(0, 5, 0) - }; - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - track3 = new Track - { - Artist = "Track 3", - Title = "Track 3", - Begin = new TimeSpan(0, 10, 0) - }; - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); - track4 = new Track - { - Artist = "Track 4", - Title = "Track 4", - Begin = new TimeSpan(0, 15, 0) - }; - cuesheet.AddTrack(track4, testHelper.ApplicationOptions); - track5 = new Track - { - Artist = "Track 5", - Title = "Track 5", - Begin = new TimeSpan(0, 20, 0) - }; - cuesheet.AddTrack(track5, testHelper.ApplicationOptions); - cuesheet.RemoveTrack(track2); - cuesheet.RemoveTrack(track4); - Assert.AreEqual(3, cuesheet.Tracks.Count); - testHelper.ApplicationOptions.LinkTracksWithPreviousOne = true; - track3.IsLinkedToPreviousTrack = true; - track5.IsLinkedToPreviousTrack = true; - track1End = track1.End; - cuesheet.MoveTrack(track1, MoveDirection.Down); - Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(1)); - Assert.AreEqual(track3, cuesheet.GetPreviousLinkedTrack(track1)); - Assert.AreEqual((uint)2, track1.Position); - Assert.AreEqual((uint)3, track5.Position); - Assert.AreEqual(track1, cuesheet.GetPreviousLinkedTrack(track5)); - Assert.AreEqual(track1End, track3.End); - } - - [TestMethod()] - public async Task ImportTestAsync() + public void ImportTest() { // Arrange var fileContent = new List @@ -236,20 +91,18 @@ public async Task ImportTestAsync() var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); var testHelper = new TestHelper(); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsNull(sessionStateContainer.ImportCuesheet?.CDTextfile); @@ -265,7 +118,7 @@ public async Task ImportTestAsync() } [TestMethod()] - public async Task ImportTestCalculateEndCorrectlyAsync() + public void ImportTestCalculateEndCorrectly() { // Arrange var textImportMemoryStream = new MemoryStream(Resources.Textimport_Bug_54); @@ -280,13 +133,18 @@ public async Task ImportTestCalculateEndCorrectlyAsync() var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions(); - importOptions.TextImportScheme.SchemeCuesheet = null; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); + var options = new ApplicationOptions(); + var textImportScheme = new TextImportScheme() + { + SchemeCuesheet = null, + SchemeTracks = TextImportScheme.DefaultSchemeTracks + }; + options.ImportScheme = textImportScheme; + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); + var timeSpanFormat = new TimeSpanFormat(); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); Assert.IsNotNull(sessionStateContainer.ImportCuesheet); @@ -298,54 +156,48 @@ public async Task ImportTestCalculateEndCorrectlyAsync() [TestMethod()] public void RecordTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); Assert.IsFalse(cuesheet.IsRecording); - Assert.IsNull(cuesheet.RecordingTime); + Assert.IsNull(cuesheet.RecordingStart); cuesheet.StartRecording(); Assert.IsTrue(cuesheet.IsRecording); - Assert.IsNotNull(cuesheet.RecordingTime); + Assert.IsNotNull(cuesheet.RecordingStart); var track = new Track(); Assert.IsNull(track.Begin); Assert.IsNull(track.End); - cuesheet.AddTrack(track, testHelper.ApplicationOptions, testHelper.RecordOptions); + cuesheet.AddTrack(track); Assert.AreEqual(TimeSpan.Zero, track.Begin); Assert.IsNull(track.End); var track2 = new Track(); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions, testHelper.RecordOptions); + cuesheet.AddTrack(track2); Assert.IsNotNull(track.End); Assert.AreNotEqual(TimeSpan.Zero, track.End); - //Now lets test with another RecordTimeSensitivity cuesheet = new Cuesheet(); cuesheet.StartRecording(); track = new Track(); - testHelper.RecordOptions.RecordTimeSensitivity = TimeSensitivityMode.Seconds; - cuesheet.AddTrack(track, testHelper.ApplicationOptions, testHelper.RecordOptions); + cuesheet.AddTrack(track); Assert.AreEqual(TimeSpan.Zero, track.Begin); Assert.IsNull(track.End); Thread.Sleep(3000); track2 = new Track(); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions, testHelper.RecordOptions); + cuesheet.AddTrack(track2); Assert.IsNotNull(track.End); Assert.AreNotEqual(TimeSpan.Zero, track.End); - Assert.AreEqual(0, track.End.Value.Milliseconds); - cuesheet.StopRecording(testHelper.RecordOptions); + cuesheet.StopRecording(); Assert.IsNotNull(track.End); Assert.AreEqual(track.End, track2.Begin); - Assert.AreEqual(0, track2.End?.Milliseconds); } [TestMethod()] public void TrackRecalculationTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); var track1 = new Track(); var track2 = new Track(); var track3 = new Track(); - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); Assert.AreEqual((uint)1, track1.Position); Assert.AreEqual((uint)2, track2.Position); Assert.AreEqual((uint)3, track3.Position); @@ -373,13 +225,13 @@ public void TrackOverlappingTest() var track1 = new Track(); var track2 = new Track(); var track3 = new Track(); - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); Assert.AreEqual((uint)1, track1.Position); Assert.AreEqual((uint)2, track2.Position); Assert.AreEqual((uint)3, track3.Position); - var validationResult = cuesheet.Validate(x => x.Tracks); + var validationResult = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); track1.Position = 1; track2.Position = 1; @@ -389,7 +241,7 @@ public void TrackOverlappingTest() track2.End = new TimeSpan(0, 5, 30); track3.Begin = new TimeSpan(0, 4, 54); track3.End = new TimeSpan(0, 8, 12); - validationResult = cuesheet.Validate(x => x.Tracks); + validationResult = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Error, validationResult.Status); Assert.AreEqual(7, validationResult.ValidationMessages?.Count); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0} {1} '{2}' is used also by {3}({4},{5},{6},{7},{8}). Positions must be unique!" && x.Parameter != null && x.Parameter[2].Equals(track1.Position) && x.Parameter[7].Equals(track1.Begin) && x.Parameter[8].Equals(track1.End))); @@ -400,14 +252,14 @@ public void TrackOverlappingTest() Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track2.Position) && x.Parameter[4].Equals(track2.Begin) && x.Parameter[5].Equals(track2.End) && x.Parameter[6].Equals(track3.Position) && x.Parameter[9].Equals(track3.Begin) && x.Parameter[10].Equals(track3.End))); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track3.Position) && x.Parameter[4].Equals(track3.Begin) && x.Parameter[5].Equals(track3.End) && x.Parameter[6].Equals(track2.Position) && x.Parameter[9].Equals(track2.Begin) && x.Parameter[10].Equals(track2.End))); track2.End = new TimeSpan(0, 5, 15); - validationResult = cuesheet.Validate(x => x.Tracks); + validationResult = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Error, validationResult.Status); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track2.Position) && x.Parameter[4].Equals(track2.Begin) && x.Parameter[5].Equals(track2.End) && x.Parameter[6].Equals(track3.Position) && x.Parameter[9].Equals(track3.Begin) && x.Parameter[10].Equals(track3.End))); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track3.Position) && x.Parameter[4].Equals(track3.Begin) && x.Parameter[5].Equals(track3.End) && x.Parameter[6].Equals(track2.Position) && x.Parameter[9].Equals(track2.Begin) && x.Parameter[10].Equals(track2.End))); track1.Position = 1; track2.Position = 2; track3.Position = 3; - validationResult = cuesheet.Validate(x => x.Tracks); + validationResult = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Error, validationResult.Status); Assert.AreEqual(4, validationResult.ValidationMessages?.Count); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track1.Position) && x.Parameter[4].Equals(track1.Begin) && x.Parameter[5].Equals(track1.End) && x.Parameter[6].Equals(track2.Position) && x.Parameter[9].Equals(track2.Begin) && x.Parameter[10].Equals(track2.End))); @@ -416,81 +268,85 @@ public void TrackOverlappingTest() Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!" && x.Parameter != null && x.Parameter[1].Equals(track3.Position) && x.Parameter[4].Equals(track3.Begin) && x.Parameter[5].Equals(track3.End) && x.Parameter[6].Equals(track2.Position) && x.Parameter[9].Equals(track2.Begin) && x.Parameter[10].Equals(track2.End))); track2.Begin = new TimeSpan(0, 2, 30); track3.Begin = new TimeSpan(0, 5, 15); - validationResult = cuesheet.Validate(x => x.Tracks); + validationResult = cuesheet.Validate(nameof(Cuesheet.Tracks)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); - } [TestMethod()] - public void RemoveTrackTest() + public void RemoveTrack_ShouldReCalculate_WithPreviouslyAddedTracks() { - var testHelper = new TestHelper(); - testHelper.ApplicationOptions.LinkTracksWithPreviousOne = true; - AutoResetEvent tracksRemovedEvent = new(false); - var cuesheet = new Cuesheet(); - cuesheet.TracksRemoved += (sender, trackAddRemoveEventArgs) => tracksRemovedEvent.Set(); - var track1 = new Track() { Artist = "1", Title = "1" }; - var track2 = new Track() { Artist = "2", Title = "2" }; - var track3 = new Track() { Artist = "3", Title = "3" }; - var track4 = new Track() { Artist = "4", Title = "4" }; - var track5 = new Track() { Artist = "5", Title = "5" }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); - cuesheet.AddTrack(track4, testHelper.ApplicationOptions); - cuesheet.AddTrack(track5, testHelper.ApplicationOptions); + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track() { Artist = "1", Title = "1", IsLinkedToPreviousTrack = true }; + var track2 = new Track() { Artist = "2", Title = "2", IsLinkedToPreviousTrack = true }; + var track3 = new Track() { Artist = "3", Title = "3" , IsLinkedToPreviousTrack = true }; + var track4 = new Track() { Artist = "4", Title = "4" , IsLinkedToPreviousTrack = true }; + var track5 = new Track() { Artist = "5", Title = "5" , IsLinkedToPreviousTrack = true }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + cuesheet.AddTrack(track4); + cuesheet.AddTrack(track5); track1.End = new TimeSpan(0, 5, 0); track2.End = new TimeSpan(0, 10, 0); track3.End = new TimeSpan(0, 15, 0); track4.End = new TimeSpan(0, 20, 0); track5.End = new TimeSpan(0, 25, 0); - Assert.AreEqual(5, cuesheet.Tracks.Count); + // Act cuesheet.RemoveTrack(track2); - Assert.AreEqual(true, tracksRemovedEvent.WaitOne(1000)); + // Assert Assert.AreEqual((uint)2, track3.Position); Assert.AreEqual((uint)3, track4.Position); Assert.AreEqual((uint)4, track5.Position); - testHelper = new TestHelper(); - testHelper.ApplicationOptions.LinkTracksWithPreviousOne = true; - cuesheet = new Cuesheet(); - cuesheet.TracksRemoved += (sender, trackAddRemoveEventArgs) => tracksRemovedEvent.Set(); - track1 = new Track + } + + [TestMethod()] + public void RemoveTracks_ShouldRemoveTracks_WithPreviouslyAddedTracks() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Artist = "Track 1", - Title = "Track 1" + Title = "Track 1", + IsLinkedToPreviousTrack = true }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - track2 = new Track + cuesheet.AddTrack(track1); + var track2 = new Track { Title = "Track 2", Artist = "Track 2", - Begin = new TimeSpan(0, 5, 0) + Begin = new TimeSpan(0, 5, 0), + IsLinkedToPreviousTrack = true }; - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - track3 = new Track + cuesheet.AddTrack(track2); + var track3 = new Track { Artist = "Track 3", Title = "Track 3", - Begin = new TimeSpan(0, 10, 0) + Begin = new TimeSpan(0, 10, 0), + IsLinkedToPreviousTrack = true }; - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); - track4 = new Track + cuesheet.AddTrack(track3); + var track4 = new Track { Artist = "Track 4", Title = "Track 4", - Begin = new TimeSpan(0, 15, 0) + Begin = new TimeSpan(0, 15, 0), + IsLinkedToPreviousTrack = true }; - cuesheet.AddTrack(track4, testHelper.ApplicationOptions); - track5 = new Track + cuesheet.AddTrack(track4); + var track5 = new Track { Artist = "Track 5", Title = "Track 5", - Begin = new TimeSpan(0, 20, 0) + Begin = new TimeSpan(0, 20, 0), IsLinkedToPreviousTrack = true }; - cuesheet.AddTrack(track5, testHelper.ApplicationOptions); + cuesheet.AddTrack(track5); var list = new List() { track2, track4 }; + // Act cuesheet.RemoveTracks(list.AsReadOnly()); - Assert.AreEqual(true, tracksRemovedEvent.WaitOne(1000)); + // Assert Assert.AreEqual(3, cuesheet.Tracks.Count); Assert.AreEqual(new TimeSpan(0, 5, 0), track3.Begin); Assert.AreEqual(new TimeSpan(0, 15, 0), track5.Begin); @@ -503,7 +359,6 @@ public void RemoveTrackTest() public void UnlickedTrackTest() { var cuesheet = new Cuesheet(); - var testHelper = new TestHelper(); var track1 = new Track { Position = 1, @@ -517,9 +372,9 @@ public void UnlickedTrackTest() { Position = 3 }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - cuesheet.AddTrack(track3, testHelper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); Assert.AreEqual(track1.End, track2.Begin); Assert.IsNull(track2.End); Assert.IsNull(track3.Begin); @@ -530,7 +385,6 @@ public void UnlickedTrackTest() [TestMethod()] public void TrackPositionChangedTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); var track1 = new Track { @@ -543,8 +397,8 @@ public void TrackPositionChangedTest() Begin = track1.End, End = new TimeSpan(0, 7, 30) }; - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track1); Assert.AreEqual(track2, cuesheet.Tracks.First()); Assert.AreEqual(track1, cuesheet.Tracks.Last()); track1.Position = 1; @@ -555,12 +409,11 @@ public void TrackPositionChangedTest() [TestMethod()] public void TrackLengthChangedWithIsLinkedToPreivousTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); var track1 = new Track(); var track2 = new Track(); - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); track1.IsLinkedToPreviousTrack = true; track2.IsLinkedToPreviousTrack = true; Assert.IsNull(track2.Begin); @@ -572,7 +425,7 @@ public void TrackLengthChangedWithIsLinkedToPreivousTest() Assert.AreEqual(editedTrack.End, track2.Begin); } [TestMethod()] - public async Task ImportSamplesTestAsync() + public void ImportSamplesTest() { // Arrange var fileContent = File.ReadAllLines("../../../../AudioCuesheetEditor/wwwroot/samples/Sample_Inputfile.txt"); @@ -580,19 +433,17 @@ public async Task ImportSamplesTestAsync() var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); Assert.IsNotNull(sessionStateContainer.ImportCuesheet); @@ -603,7 +454,7 @@ public async Task ImportSamplesTestAsync() } [TestMethod()] - public async Task ImportSamples2TestAsync() + public void ImportSamples2Test() { // Arrange var fileContent = File.ReadAllLines("../../../../AudioCuesheetEditor/wwwroot/samples/Sample_Inputfile2.txt"); @@ -611,13 +462,20 @@ public async Task ImportSamples2TestAsync() var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions(); - importOptions.TextImportScheme.SchemeCuesheet = null; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); + var textImportScheme = new TextImportScheme() + { + SchemeCuesheet = null, + SchemeTracks = TextImportScheme.DefaultSchemeTracks + }; + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions + { + ImportScheme = textImportScheme + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); Assert.IsNotNull(sessionStateContainer.ImportCuesheet); @@ -631,22 +489,18 @@ public async Task ImportSamples2TestAsync() public void ValidateTest() { var cuesheet = new Cuesheet(); - Assert.AreEqual(ValidationStatus.Error, cuesheet.Validate(x => x.Artist).Status); + Assert.AreEqual(ValidationStatus.Error, cuesheet.Validate(nameof(Cuesheet.Artist)).Status); cuesheet.Artist = "Testartist"; - Assert.AreEqual(ValidationStatus.Success, cuesheet.Validate(x => x.Artist).Status); - Assert.AreEqual(ValidationStatus.Error, cuesheet.Validate(x => x.Title).Status); + Assert.AreEqual(ValidationStatus.Success, cuesheet.Validate(nameof(Cuesheet.Artist)).Status); + Assert.AreEqual(ValidationStatus.Error, cuesheet.Validate(nameof(Cuesheet.Title)).Status); cuesheet.Title = "Testtitle"; - Assert.AreEqual(ValidationStatus.Success, cuesheet.Validate(x => x.Title).Status); + Assert.AreEqual(ValidationStatus.Success, cuesheet.Validate(nameof(Cuesheet.Title)).Status); } [TestMethod()] public void IsLinkedToPreviousTrack_ChangedLastTrackBegin_SetsTrackProperties() { //Arrange - var applicationOptions = new ApplicationOptions() - { - LinkTracksWithPreviousOne = false - }; var cuesheet = new Cuesheet(); var track1 = new Track() { @@ -659,8 +513,8 @@ public void IsLinkedToPreviousTrack_ChangedLastTrackBegin_SetsTrackProperties() Title = "Track2 Title", End = new TimeSpan(0, 9, 12) }; - cuesheet.AddTrack(track1, applicationOptions); - cuesheet.AddTrack(track2, applicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); track2.Begin = new TimeSpan(0, 4, 23); //Act track2.IsLinkedToPreviousTrack = true; @@ -668,5 +522,489 @@ public void IsLinkedToPreviousTrack_ChangedLastTrackBegin_SetsTrackProperties() Assert.AreEqual((uint)2, track2.Position); Assert.AreEqual(track2.Begin, track1.End); } + + [TestMethod()] + public void IsRecordingPossible_WhenRecordingIsAlreadyRunning_ReturnsError() + { + // Arrange + var cuesheet = new Cuesheet(); + cuesheet.StartRecording(); + + // Act + var errors = cuesheet.IsRecordingPossible.ToList(); + + // Assert + Assert.Contains("Record is already running!", errors); + } + + [TestMethod()] + public void IsRecordingPossible_WhenCuesheetContainsTracks_ReturnsError() + { + // Arrange + var cuesheet = new Cuesheet(); + cuesheet.AddTrack(new Track()); + + // Act + var errors = cuesheet.IsRecordingPossible.ToList(); + + // Assert + Assert.Contains("Cuesheet already contains tracks!", errors); + } + + [TestMethod()] + public void IsRecordingPossible_WhenRecordingIsAlreadyAvailable_ReturnsError() + { + // Arrange + var cuesheet = new Cuesheet + { + Audiofile = new Audiofile("test", isRecorded: true) + }; + + // Act + var errors = cuesheet.IsRecordingPossible.ToList(); + + // Assert + Assert.Contains("A recording is already available!", errors); + } + + [TestMethod()] + public void IsRecordingPossible_WhenNoErrors_ReturnsEmpty() + { + // Arrange + var cuesheet = new Cuesheet(); + + // Act + var errors = cuesheet.IsRecordingPossible.ToList(); + + // Assert + Assert.IsEmpty(errors); + } + + [TestMethod] + public void AddSection_WithValidData_FiresEvents() + { + // Arrange + var cuesheet = new Cuesheet(); + bool eventFired = false; + cuesheet.TraceablePropertyChanged += (sender, args) => + { + if (args.TraceableChange.PropertyName == nameof(Cuesheet.Sections)) + { + eventFired = true; + } + }; + + // Act + var section = cuesheet.AddSection(); + + // Assert + Assert.IsTrue(eventFired); + Assert.IsNotNull(section); + Assert.AreEqual(1, cuesheet.Sections.Count); + Assert.AreEqual(cuesheet, section.Cuesheet); + } + + [TestMethod()] + public void RemoveSections_RemovesSpecifiedSections() + { + // Arrange + var cuesheet = new Cuesheet(); + var section1 = cuesheet.AddSection(); + var section2 = cuesheet.AddSection(); + var section3 = cuesheet.AddSection(); + var sectionsToRemove = new List { section1, section3 }; + bool eventFired = false; + cuesheet.TraceablePropertyChanged += (sender, args) => + { + if (args.TraceableChange.PropertyName == nameof(Cuesheet.Sections)) + { + eventFired = true; + } + }; + + // Act + cuesheet.RemoveSections(sectionsToRemove); + + // Assert + Assert.IsTrue(eventFired); + Assert.AreEqual(1, cuesheet.Sections.Count); + Assert.IsTrue(cuesheet.Sections.Contains(section2)); + Assert.IsFalse(cuesheet.Sections.Contains(section1)); + Assert.IsFalse(cuesheet.Sections.Contains(section3)); + } + + [TestMethod()] + public void GetSection_ReturnsCorrectSection() + { + // Arrange + var cuesheet = new Cuesheet(); + var section1 = cuesheet.AddSection(); + section1.Begin = TimeSpan.Zero; + section1.End = TimeSpan.FromSeconds(120); + cuesheet.AddSection(); + var track = new Track { Begin = TimeSpan.Zero, End = TimeSpan.FromSeconds(83) }; + cuesheet.AddTrack(track); + + // Act + var result = cuesheet.GetSection(track); + + // Assert + Assert.AreEqual(section1, result); + } + + [TestMethod()] + public void GetSection_ReturnsNullIfNoMatchingSection() + { + // Arrange + var cuesheet = new Cuesheet(); + var section1 = cuesheet.AddSection(); + section1.Begin = TimeSpan.Zero; + section1.End = TimeSpan.FromHours(1.5); + var track = new Track { Begin = section1.End + TimeSpan.FromSeconds(1), End = section1.End + TimeSpan.FromSeconds(2) }; + cuesheet.AddTrack(track); + + // Act + var result = cuesheet.GetSection(track); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public void MoveTracksPossible_ShouldReturnFalse_WhenNoTracksToMove() + { + // Arrange + var cuesheet = new Cuesheet(); + var tracksToMove = new List(); + + // Act + var result = cuesheet.MoveTracksPossible(tracksToMove, MoveDirection.Up); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void MoveTracksPossible_ShouldReturnFalse_WhenMovingUpAndFirstTrack() + { + // Arrange + var cuesheet = new Cuesheet(); + var track = new Track(); + cuesheet.AddTrack(track); + var tracksToMove = new List { track }; + + // Act + var result = cuesheet.MoveTracksPossible(tracksToMove, MoveDirection.Up); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void MoveTracksPossible_ShouldReturnTrue_WhenMovingUpAndNotFirstTrack() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track(); + var track2 = new Track(); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + var tracksToMove = new List { track2 }; + + // Act + var result = cuesheet.MoveTracksPossible(tracksToMove, MoveDirection.Up); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public void MoveTracksPossible_ShouldReturnFalse_WhenMovingDownAndLastTrack() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track(); + var track2 = new Track(); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + var tracksToMove = new List { track2 }; + + // Act + var result = cuesheet.MoveTracksPossible(tracksToMove, MoveDirection.Down); + + // Assert + Assert.IsFalse(result); + } + + + [TestMethod] + public void MoveTracksPossible_ShouldReturnTrue_WhenMovingDownAndNotLastTrack() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track(); + var track2 = new Track(); + var track3 = new Track(); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + var tracksToMove = new List { track2 }; + + // Act + var result = cuesheet.MoveTracksPossible(tracksToMove, MoveDirection.Down); + + // Assert + Assert.IsTrue(result); + } + + + [TestMethod] + public void MoveTracks_Up_Success() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1 }; + var track2 = new Track { Position = 2 }; + var track3 = new Track { Position = 3 }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + + // Act + cuesheet.MoveTracks([track2], MoveDirection.Up); + + // Assert + Assert.AreEqual((uint)2, cuesheet.Tracks.ElementAt(0).Position); + Assert.AreEqual((uint)1, cuesheet.Tracks.ElementAt(1).Position); + Assert.AreEqual((uint)3, cuesheet.Tracks.ElementAt(2).Position); + Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(0)); + Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(1)); + Assert.AreEqual(track3, cuesheet.Tracks.ElementAt(2)); + } + + [TestMethod] + public void MoveTracks_Down_Success() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1 }; + var track2 = new Track { Position = 2 }; + var track3 = new Track { Position = 3 }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + + // Act + cuesheet.MoveTracks([track2], MoveDirection.Down); + + // Assert + Assert.AreEqual((uint)1, cuesheet.Tracks.ElementAt(0).Position); + Assert.AreEqual((uint)3, cuesheet.Tracks.ElementAt(1).Position); + Assert.AreEqual((uint)2, cuesheet.Tracks.ElementAt(2).Position); + Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(0)); + Assert.AreEqual(track3, cuesheet.Tracks.ElementAt(1)); + Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(2)); + } + + [TestMethod] + public void MoveTracks_Up_NotPossible() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1 }; + var track2 = new Track { Position = 2 }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + + // Act + cuesheet.MoveTracks([track1], MoveDirection.Up); + + // Assert + Assert.AreEqual((uint)1, cuesheet.Tracks.ElementAt(0).Position); + Assert.AreEqual((uint)2, cuesheet.Tracks.ElementAt(1).Position); + Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(0)); + Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(1)); + } + + [TestMethod] + public void MoveTracks_Down_NotPossible() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1 }; + var track2 = new Track { Position = 2 }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + + // Act + cuesheet.MoveTracks([track2], MoveDirection.Down); + + // Assert + Assert.AreEqual((uint)1, cuesheet.Tracks.ElementAt(0).Position); + Assert.AreEqual((uint)2, cuesheet.Tracks.ElementAt(1).Position); + Assert.AreEqual(track1, cuesheet.Tracks.ElementAt(0)); + Assert.AreEqual(track2, cuesheet.Tracks.ElementAt(1)); + } + + [TestMethod] + public void StartRecording_WithValidData_ShouldStartRecording() + { + // Arrange + var cuesheet = new Cuesheet(); + + // Act + cuesheet.StartRecording(); + + // Assert + Assert.IsTrue(cuesheet.IsRecording); + Assert.IsNotNull(cuesheet.RecordingStart); + } + + [TestMethod] + public void StopRecording_WithRecordingRunning_ShouldStopRecording() + { + // Arrange + var cuesheet = new Cuesheet(); + cuesheet.StartRecording(); + var track = new Track(); + cuesheet.AddTrack(track); + bool isRecordingChangedEventFired = false; + cuesheet.IsRecordingChanged += (sender, args) => isRecordingChangedEventFired = true; + + // Act + cuesheet.StopRecording(); + + // Assert + Assert.IsFalse(cuesheet.IsRecording); + Assert.IsNull(cuesheet.RecordingStart); + Assert.IsTrue(isRecordingChangedEventFired); + Assert.IsNotNull(track.End); + } + + [TestMethod] + public void StartRecording_ShouldNotStartIfAlreadyRecording() + { + // Arrange + var cuesheet = new Cuesheet(); + cuesheet.StartRecording(); + + // Act + cuesheet.StartRecording(); + + // Assert + Assert.IsTrue(cuesheet.IsRecording); + Assert.IsNotNull(cuesheet.RecordingStart); + } + + [TestMethod] + public void StartRecording_WithTrackAlreadyAdded_ShouldNotStartRecording() + { + // Arrange + var cuesheet = new Cuesheet(); + cuesheet.AddTrack(new Track()); + + // Act + cuesheet.StartRecording(); + + // Assert + Assert.IsFalse(cuesheet.IsRecording); + } + + [TestMethod] + public void StartRecording_WithAudiofile_ShouldNotStartRecording() + { + // Arrange + var cuesheet = new Cuesheet + { + Audiofile = new Audiofile("test", isRecorded: true) + }; + + // Act + cuesheet.StartRecording(); + + // Assert + Assert.IsFalse(cuesheet.IsRecording); + } + + [TestMethod] + public void RecalculateLastTrackEnd_SingleTrackWithAudiofile_EndSetToAudiofileDuration() + { + // Arrange + var audiofileMock = new Mock(); + audiofileMock.SetupGet(a => a.Duration).Returns(TimeSpan.FromMinutes(5)); + + var cuesheet = new Cuesheet + { + Audiofile = audiofileMock.Object + }; + var track = new Track { Position = 1, Begin = TimeSpan.Zero }; + cuesheet.AddTrack(track); + + // Act + cuesheet.RecalculateLastTrackEnd(); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(5), track.End); + } + + [TestMethod] + public void RecalculateLastTrackEnd_MultipleTracks_EndSetCorrectly() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1, Begin = TimeSpan.Zero, End = TimeSpan.FromMinutes(2) }; + var track2 = new Track { Position = 2, Begin = TimeSpan.FromMinutes(2) }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + + // Act + cuesheet.RecalculateLastTrackEnd(); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(2), track1.End); + Assert.IsNull(track2.End); + } + + [TestMethod] + public void RecalculateLastTrackEnd_MultipleTracksWithAudiofile_LastTrackEndSetToAudiofileDuration() + { + // Arrange + var audiofileMock = new Mock(); + audiofileMock.SetupGet(a => a.Duration).Returns(TimeSpan.FromMinutes(5)); + + var cuesheet = new Cuesheet + { + Audiofile = audiofileMock.Object + }; + var track1 = new Track { Position = 1, Begin = TimeSpan.Zero, End = TimeSpan.FromMinutes(2) }; + var track2 = new Track { Position = 2, Begin = TimeSpan.FromMinutes(2) }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + + // Act + cuesheet.RecalculateLastTrackEnd(); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(2), track1.End); + Assert.AreEqual(TimeSpan.FromMinutes(5), track2.End); + } + + [TestMethod] + public void RecalculateLastTrackEnd_TracksWithLinkedPreviousTrack_PropertiesRecalculatedCorrectly() + { + // Arrange + var cuesheet = new Cuesheet(); + var track1 = new Track { Position = 1, Begin = TimeSpan.Zero, End = TimeSpan.FromMinutes(2) }; + var track2 = new Track { Position = 2, IsLinkedToPreviousTrack = true }; + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + + // Act + cuesheet.RecalculateLastTrackEnd(); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(2), track1.End); + Assert.AreEqual(TimeSpan.FromMinutes(2), track2.Begin); + } } } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/TrackTests.cs b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/TrackTests.cs index 2d3becf5..fe4c93ca 100644 --- a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/TrackTests.cs +++ b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/TrackTests.cs @@ -97,7 +97,6 @@ public void LinkTrackTest() [TestMethod()] public void CheckPositionInCuesheetTest() { - var testHelper = new TestHelper(); var cuesheet = new Cuesheet(); var track1 = new Track { @@ -110,22 +109,22 @@ public void CheckPositionInCuesheetTest() Begin = track1.End, End = new TimeSpan(0, 8, 23) }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); - var validationResult = track1.Validate(x => x.Position); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + var validationResult = track1.Validate(nameof(Track.Position)); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!" && x.Parameter != null && x.Parameter[0].Equals(track1.Position) && x.Parameter[3].Equals(track1.Begin) && x.Parameter[4].Equals(track1.End) && x.Parameter[5].Equals(1))); track1.Position = 1; - validationResult = track1.Validate(x => x.Position); + validationResult = track1.Validate(nameof(Track.Position)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); - validationResult = track2.Validate(x => x.Position); + validationResult = track2.Validate(nameof(Track.Position)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); track1.Position = 3; track2.Position = 5; - validationResult = track1.Validate(x => x.Position); + validationResult = track1.Validate(nameof(Track.Position)); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!" && x.Parameter != null && x.Parameter[0].Equals(track1.Position) && x.Parameter[3].Equals(track1.Begin) && x.Parameter[4].Equals(track1.End) && x.Parameter[5].Equals(1))); - validationResult = track2.Validate(x => x.Position); + validationResult = track2.Validate(nameof(Track.Position)); Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Message == "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!" && x.Parameter != null && x.Parameter[0].Equals(track2.Position) && x.Parameter[3].Equals(track2.Begin) && x.Parameter[4].Equals(track2.End) && x.Parameter[5].Equals(2))); cuesheet = new Cuesheet(); @@ -134,20 +133,20 @@ public void CheckPositionInCuesheetTest() Artist = "Testartist 1", Title = "Testtitle 1" }; - cuesheet.AddTrack(track1, testHelper.ApplicationOptions); + cuesheet.AddTrack(track1); track2 = new Track() { Artist = "Testartist 2", Title = "Testtitle 2" }; - cuesheet.AddTrack(track2, testHelper.ApplicationOptions); + cuesheet.AddTrack(track2); Assert.IsNotNull(track1.Begin); Assert.IsNull(track1.End); Assert.IsNull(track2.Begin); Assert.IsNull(track2.End); - validationResult = track1.Validate(x => x.Position); + validationResult = track1.Validate(nameof(Track.Position)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); - validationResult = track2.Validate(x => x.Position); + validationResult = track2.Validate(nameof(Track.Position)); Assert.AreEqual(ValidationStatus.Success, validationResult.Status); } @@ -235,7 +234,7 @@ public void Validate_InvalidEnd_ReturnsValidationStatusError() End = new TimeSpan(0, 1, 15) }; // Act - var endValidationResult = track.Validate(x => x.End); + var endValidationResult = track.Validate(nameof(Track.End)); // Assert Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); } @@ -250,7 +249,7 @@ public void Validate_InvalidBegin_ReturnsValidationStatusError() End = new TimeSpan(0, 3, 15) }; // Act - var endValidationResult = track.Validate(x => x.Begin); + var endValidationResult = track.Validate(nameof(Track.Begin)); // Assert Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); } diff --git a/AudioCuesheetEditor.Tests/Model/Entity/ValidateableTests.cs b/AudioCuesheetEditor.Tests/Model/Entity/ValidateableTests.cs index 75913b47..be481b15 100644 --- a/AudioCuesheetEditor.Tests/Model/Entity/ValidateableTests.cs +++ b/AudioCuesheetEditor.Tests/Model/Entity/ValidateableTests.cs @@ -19,7 +19,7 @@ namespace AudioCuesheetEditor.Tests.Model.Entity { - public class ValidateableTestClass : Validateable + public class ValidateableTestClass : Validateable { private string? testProperty; private int? testProperty2; @@ -42,7 +42,7 @@ public int? TestProperty2 OnValidateablePropertyChanged(); } } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; diff --git a/AudioCuesheetEditor.Tests/Model/Entity/ValidationResultTests.cs b/AudioCuesheetEditor.Tests/Model/Entity/ValidationResultTests.cs index c0b1e4e3..69d9eda1 100644 --- a/AudioCuesheetEditor.Tests/Model/Entity/ValidationResultTests.cs +++ b/AudioCuesheetEditor.Tests/Model/Entity/ValidationResultTests.cs @@ -13,58 +13,106 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. +using AudioCuesheetEditor.Model.Entity; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; +using System.Linq; -namespace AudioCuesheetEditor.Model.Entity.Tests +namespace AudioCuesheetEditor.Tests.Model.Entity { [TestClass()] public class ValidationResultTests { - [TestMethod()] - public void CreateTest() + [TestMethod] + public void Create_NoValidation_ReturnsExpectedResult() { + // Arrange & Act var validationResult = ValidationResult.Create(ValidationStatus.NoValidation); + + // Assert Assert.AreEqual(ValidationStatus.NoValidation, validationResult.Status); - Assert.IsNull(validationResult.ValidationMessages); - validationResult = ValidationResult.Create(ValidationStatus.Success); + Assert.AreEqual(0, validationResult.ValidationMessages.Count); + } + + [TestMethod] + public void Create_Success_ReturnsExpectedResult() + { + // Arrange & Act + var validationResult = ValidationResult.Create(ValidationStatus.Success); + + // Assert Assert.AreEqual(ValidationStatus.Success, validationResult.Status); - Assert.IsNull(validationResult.ValidationMessages); - validationResult = ValidationResult.Create(ValidationStatus.Error); + Assert.AreEqual(0, validationResult.ValidationMessages.Count); + } + + [TestMethod] + public void Create_Error_ReturnsExpectedResult() + { + // Arrange & Act + var validationResult = ValidationResult.Create(ValidationStatus.Error); + + // Assert Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsNull(validationResult.ValidationMessages); - var validationMessages = new List() { new ValidationMessage("Testmessage!") }; - validationResult = ValidationResult.Create(ValidationStatus.Success, validationMessages); + Assert.AreEqual(0, validationResult.ValidationMessages.Count); + } + + [TestMethod] + public void Create_SuccessWithMessages_ReturnsErrorStatus() + { + // Arrange + var validationMessages = new List { new("Testmessage!") }; + + // Act + var validationResult = ValidationResult.Create(ValidationStatus.Success, validationMessages); + + // Assert Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsNotNull(validationResult.ValidationMessages); - validationResult = ValidationResult.Create(ValidationStatus.NoValidation, validationMessages); + CollectionAssert.AreEqual(validationMessages.ToList(), validationResult.ValidationMessages.ToList()); + } + + [TestMethod] + public void Create_NoValidationWithMessages_ReturnsErrorStatus() + { + // Arrange + var validationMessages = new List { new("Testmessage!") }; + + // Act + var validationResult = ValidationResult.Create(ValidationStatus.NoValidation, validationMessages); + + // Assert Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsNotNull(validationResult.ValidationMessages); + CollectionAssert.AreEqual(validationMessages.ToList(), validationResult.ValidationMessages.ToList()); } - [TestMethod()] - public void StatusTest() + [TestMethod] + public void Default_Status_IsNoValidation() { - var result = new ValidationResult(); - Assert.IsNull(result.ValidationMessages); - Assert.AreEqual(ValidationStatus.NoValidation, result.Status); - result.Status = ValidationStatus.Success; - Assert.IsNull(result.ValidationMessages); - Assert.AreEqual(ValidationStatus.Success, result.Status); + // Arrange & Act + var validationResult = new ValidationResult(); + + // Assert + Assert.AreEqual(ValidationStatus.NoValidation, validationResult.Status); + Assert.AreEqual(0, validationResult.ValidationMessages.Count); } - [TestMethod()] - public void ErrorTest() + [TestMethod] + public void Setting_ValidationMessages_SetsStatusToError() { - var result = new ValidationResult(); - Assert.IsNull(result.ValidationMessages); - Assert.AreEqual(ValidationStatus.NoValidation, result.Status); - result.Status = ValidationStatus.Success; - Assert.IsNull(result.ValidationMessages); - Assert.AreEqual(ValidationStatus.Success, result.Status); - result.ValidationMessages = new List() { new ValidationMessage("Unit Test error1"), new ValidationMessage("Unit Test error2") }; - Assert.IsNotNull(result.ValidationMessages); - Assert.AreEqual(ValidationStatus.Error, result.Status); + // Arrange + var validationResult = new ValidationResult(); + var messages = new List + { + new("Unit Test error1"), + new("Unit Test error2") + }; + + // Act + validationResult.ValidationMessages = messages; + + // Assert + CollectionAssert.AreEqual(messages.ToList(), validationResult.ValidationMessages.ToList()); + Assert.AreEqual(ValidationStatus.Error, validationResult.Status); } } } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/IO/Audio/AudiofileTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Audio/AudiofileTests.cs index 1da1b44e..7b0bea1b 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/Audio/AudiofileTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/Audio/AudiofileTests.cs @@ -13,14 +13,11 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Text; using AudioCuesheetEditor.Model.IO.Audio; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; -namespace AudioCuesheetEditor.Model.IO.Audio.Tests +namespace AudioCuesheetEditor.Tests.Model.IO.Audio { [TestClass()] public class AudiofileTests @@ -34,18 +31,17 @@ public void AudioFileTest() Assert.IsNotNull(audioFile.Name); Assert.AreEqual(audioFile.AudioFileType, "MP3"); audioFile = new Audiofile("Test"); - Assert.AreEqual(audioFile.AudioFileType, String.Empty); + Assert.AreEqual(audioFile.AudioFileType, string.Empty); Assert.IsNotNull(audioFile.Name); var codec = Audiofile.AudioCodecs.Single(x => x.FileExtension == ".ogg"); - var httpClient = new System.Net.Http.HttpClient(); - audioFile = new Audiofile("test", "TestobjectURL", codec, httpClient); + audioFile = new Audiofile("test", "TestobjectURL", codec); Assert.IsNotNull(audioFile.Name); Assert.AreEqual("test.ogg", audioFile.Name); Assert.AreEqual(audioFile.AudioFileType, "OGG"); Assert.IsNotNull(audioFile.ObjectURL); Assert.IsTrue(audioFile.PlaybackPossible); codec = Audiofile.AudioCodecs.Single(x => x.FileExtension == ".mp3"); - var audioFile2 = new Audiofile(audioFile.Name, "TestObjectURL2", codec, httpClient); + var audioFile2 = new Audiofile(audioFile.Name, "TestObjectURL2", codec); Assert.AreEqual("test.mp3", audioFile2.Name); } } diff --git a/AudioCuesheetEditor.Tests/Model/IO/Export/CuesheetSectionTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Export/CuesheetSectionTests.cs index 36c343e7..0629a434 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/Export/CuesheetSectionTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/Export/CuesheetSectionTests.cs @@ -14,36 +14,36 @@ public void ValidationTest() var cuesheet = new Cuesheet(); var section = new CuesheetSection(cuesheet); Assert.IsNull(section.Begin); - var beginValidationResult = section.Validate(x => x.Begin); - var endValidationResult = section.Validate(x => x.End); + var beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + var endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Error, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); section.Begin = new TimeSpan(0, 0, 0); - beginValidationResult = section.Validate(x => x.Begin); - endValidationResult = section.Validate(x => x.End); + beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Success, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); cuesheet.AddTrack(new Track() { Position = 1, Begin = new TimeSpan(0, 0, 10) }); cuesheet.AddTrack(new Track() { Position = 2, Begin = new TimeSpan(0, 8, 43), End = new TimeSpan(0, 15, 43) }); - beginValidationResult = section.Validate(x => x.Begin); - endValidationResult = section.Validate(x => x.End); + beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Error, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); section.Begin = new TimeSpan(0, 0, 10); - beginValidationResult = section.Validate(x => x.Begin); - endValidationResult = section.Validate(x => x.End); + beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Success, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); section.Begin = new TimeSpan(0, 10, 0); section.End = new TimeSpan(0, 5, 0); - beginValidationResult = section.Validate(x => x.Begin); - endValidationResult = section.Validate(x => x.End); + beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Error, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Error, endValidationResult.Status); section.Begin = new TimeSpan(0, 10, 0); section.End = new TimeSpan(0, 15, 0); - beginValidationResult = section.Validate(x => x.Begin); - endValidationResult = section.Validate(x => x.End); + beginValidationResult = section.Validate(nameof(CuesheetSection.Begin)); + endValidationResult = section.Validate(nameof(CuesheetSection.End)); Assert.AreEqual(ValidationStatus.Success, beginValidationResult.Status); Assert.AreEqual(ValidationStatus.Success, endValidationResult.Status); } diff --git a/AudioCuesheetEditor.Tests/Model/IO/Export/ExportfileGeneratorTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Export/ExportfileGeneratorTests.cs deleted file mode 100644 index fbebfaf5..00000000 --- a/AudioCuesheetEditor.Tests/Model/IO/Export/ExportfileGeneratorTests.cs +++ /dev/null @@ -1,723 +0,0 @@ -//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.Entity; -using AudioCuesheetEditor.Model.IO.Audio; -using AudioCuesheetEditor.Model.IO.Export; -using AudioCuesheetEditor.Tests.Utility; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.IO; -using System.Linq; - -namespace AudioCuesheetEditor.Tests.Model.IO.Export -{ - [TestClass()] - public class ExportfileGeneratorTests - { - - [TestMethod()] - public void GenerateCuesheetFilesTest() - { - var testHelper = new TestHelper(); - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - } - var generator = new ExportfileGenerator(ExportType.Cuesheet, cuesheet, applicationOptions: testHelper.ApplicationOptions); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - Assert.AreEqual(Exportfile.DefaultCuesheetFilename, generatedFiles.First().Name); - var content = generatedFiles.First().Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - Assert.AreEqual(fileContent[0], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, cuesheet.Title)); - Assert.AreEqual(fileContent[1], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, cuesheet.Artist)); - Assert.AreEqual(fileContent[2], string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, cuesheet.Audiofile.Name, cuesheet.Audiofile.AudioFileType)); - var position = 1; - for (int i = 3; i < fileContent.Length; i += 4) - { - var track = cuesheet.Tracks.Single(x => x.Position == position); - position++; - Assert.AreEqual(fileContent[i], string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position, CuesheetConstants.CuesheetTrackAudio)); - Assert.AreEqual(fileContent[i + 1], string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title)); - Assert.AreEqual(fileContent[i + 2], string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist)); - var trackBegin = track.Begin; - Assert.IsNotNull(trackBegin); - Assert.AreEqual(fileContent[i + 3], string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(trackBegin.Value.TotalMinutes), trackBegin.Value.Seconds, trackBegin.Value.Milliseconds / 75)); - } - File.Delete(fileName); - cuesheet.CDTextfile = new CDTextfile("Testfile.cdt"); - cuesheet.Cataloguenumber.Value = "0123456789123"; - generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - content = generatedFiles.First().Content; - Assert.IsNotNull(content); - fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - fileContent = File.ReadAllLines(fileName); - Assert.AreEqual(fileContent[0], string.Format("{0} {1}", CuesheetConstants.CuesheetCatalogueNumber, cuesheet.Cataloguenumber.Value)); - Assert.AreEqual(fileContent[1], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetCDTextfile, cuesheet.CDTextfile.Name)); - File.Delete(fileName); - cuesheet.CDTextfile = new CDTextfile("Testfile.cdt"); - cuesheet.Cataloguenumber.Value = "Testvalue"; - Assert.AreEqual(ValidationStatus.Error, generator.Validate().Status); - generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(0, generatedFiles.Count); - } - - [TestMethod()] - public void GenerateCuesheetFilesWithPreGapAndPostGapTest() - { - var testHelper = new TestHelper(); - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - var rand = new Random(); - var flagsToAdd = rand.Next(1, 3); - for (int x = 0; x < flagsToAdd; x++) - { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); - } - track.PostGap = new TimeSpan(0, 0, 2); - track.PreGap = new TimeSpan(0, 0, 3); - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - } - var generator = new ExportfileGenerator(ExportType.Cuesheet, cuesheet, applicationOptions: testHelper.ApplicationOptions); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - var content = generatedFiles.First().Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - Assert.AreEqual(fileContent[0], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, cuesheet.Title)); - Assert.AreEqual(fileContent[1], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, cuesheet.Artist)); - Assert.AreEqual(fileContent[2], string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, cuesheet.Audiofile.Name, cuesheet.Audiofile.AudioFileType)); - var position = 1; - for (int i = 3; i < fileContent.Length; i += 7) - { - var track = cuesheet.Tracks.Single(x => x.Position == position); - position++; - Assert.AreEqual(string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position, CuesheetConstants.CuesheetTrackAudio), fileContent[i]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title), fileContent[i + 1]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist), fileContent[i + 2]); - Assert.AreEqual(string.Format("{0}{1}{2} {3}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackFlags, string.Join(" ", track.Flags.Select(x => x.CuesheetLabel))), fileContent[i + 3]); - var preGap = track.PreGap; - var trackBegin = track.Begin; - var postGap = track.PostGap; - Assert.IsNotNull(preGap); - Assert.IsNotNull(trackBegin); - Assert.IsNotNull(postGap); - Assert.AreEqual(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPreGap, Math.Floor(preGap.Value.TotalMinutes), preGap.Value.Seconds, preGap.Value.Milliseconds / 75), fileContent[i + 4]); - Assert.AreEqual(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(trackBegin.Value.TotalMinutes), trackBegin.Value.Seconds, trackBegin.Value.Milliseconds / 75), fileContent[i + 5]); - Assert.AreEqual(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPostGap, Math.Floor(postGap.Value.TotalMinutes), postGap.Value.Seconds, postGap.Value.Milliseconds / 75), fileContent[i + 6]); - } - File.Delete(fileName); - } - - [TestMethod()] - public void GenerateCuesheetFilesWithTrackFlagsTest() - { - var testHelper = new TestHelper(); - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - var rand = new Random(); - var flagsToAdd = rand.Next(1, 3); - for (int x = 0; x < flagsToAdd; x++) - { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); - } - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - } - var generator = new ExportfileGenerator(ExportType.Cuesheet, cuesheet, applicationOptions: testHelper.ApplicationOptions); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - var content = generatedFiles.First().Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - Assert.AreEqual(fileContent[0], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, cuesheet.Title)); - Assert.AreEqual(fileContent[1], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, cuesheet.Artist)); - Assert.AreEqual(fileContent[2], string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, cuesheet.Audiofile.Name, cuesheet.Audiofile.AudioFileType)); - var position = 1; - for (int i = 3; i < fileContent.Length; i += 5) - { - var track = cuesheet.Tracks.Single(x => x.Position == position); - position++; - Assert.AreEqual(fileContent[i], string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position, CuesheetConstants.CuesheetTrackAudio)); - Assert.AreEqual(fileContent[i + 1], string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title)); - Assert.AreEqual(fileContent[i + 2], string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist)); - Assert.AreEqual(fileContent[i + 3], string.Format("{0}{1}{2} {3}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackFlags, string.Join(" ", track.Flags.Select(x => x.CuesheetLabel)))); - var trackBegin = track.Begin; - Assert.IsNotNull(trackBegin); - Assert.AreEqual(fileContent[i + 4], string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(trackBegin.Value.TotalMinutes), trackBegin.Value.Seconds, trackBegin.Value.Milliseconds / 75)); - } - File.Delete(fileName); - } - - [TestMethod()] - public void GenerateCuesheetFilesWithIncorrectTrackPositionsTest() - { - var testHelper = new TestHelper(); - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - var random = new Random(); - for (int i = 1; i < 6; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin, - Position = (uint)(i + random.Next(1, 10)) - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - } - var generator = new ExportfileGenerator(ExportType.Cuesheet, cuesheet, applicationOptions: testHelper.ApplicationOptions); - Assert.AreEqual(ValidationStatus.Error, generator.Validate().Status); - //Rearrange positions - cuesheet.Tracks.ElementAt(0).Position = 1; - cuesheet.Tracks.ElementAt(1).Position = 2; - cuesheet.Tracks.ElementAt(2).Position = 3; - cuesheet.Tracks.ElementAt(3).Position = 4; - cuesheet.Tracks.ElementAt(4).Position = 5; - Assert.AreEqual(ValidationStatus.Success, generator.Validate().Status); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - var content = generatedFiles.First().Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - Assert.AreEqual(fileContent[0], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, cuesheet.Title)); - Assert.AreEqual(fileContent[1], string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, cuesheet.Artist)); - Assert.AreEqual(fileContent[2], string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, cuesheet.Audiofile.Name, cuesheet.Audiofile.AudioFileType)); - var position = 1; - for (int i = 3; i < fileContent.Length; i += 4) - { - var track = cuesheet.Tracks.ElementAt(position - 1); - Assert.AreEqual(string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, position, CuesheetConstants.CuesheetTrackAudio), fileContent[i]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title), fileContent[i + 1]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist), fileContent[i + 2]); - var trackBegin = track.Begin; - Assert.IsNotNull(trackBegin); - Assert.AreEqual(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(trackBegin.Value.TotalMinutes), trackBegin.Value.Seconds, trackBegin.Value.Milliseconds / 75), fileContent[i + 3]); - position++; - } - File.Delete(fileName); - } - - [TestMethod()] - public void GenerateCuesheetFilesWithSectionsTest() - { - var testHelper = new TestHelper(); - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - } - var section = cuesheet.AddSection(); - section.Begin = new TimeSpan(1, 30, 0); - section.End = new TimeSpan(2, 0, 0); - section.Title = "Last part"; - section.AudiofileName = "Last part.mp3"; - section = cuesheet.AddSection(); - section.Begin = new TimeSpan(0, 0, 0); - section.End = new TimeSpan(0, 30, 0); - section.AudiofileName = "First part.mp3"; - section = cuesheet.AddSection(); - section.Begin = new TimeSpan(0, 30, 0); - section.End = new TimeSpan(1, 0, 0); - section.Artist = "Demo Artist Part2"; - section = cuesheet.AddSection(); - section.Artist = "Artist 3"; - section.Title = "Title 3"; - section.AudiofileName = "Part 3.mp3"; - section.Begin = new TimeSpan(1, 0, 0); - section.End = new TimeSpan(1, 30, 0); - testHelper.ApplicationOptions.CuesheetFilename = "Unit test.cue"; - var generator = new ExportfileGenerator(ExportType.Cuesheet, cuesheet, applicationOptions: testHelper.ApplicationOptions); - Assert.AreEqual(ValidationStatus.Success, generator.Validate().Status); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(4, generatedFiles.Count); - var position = 1; - var counter = 1; - //Check split according to sections - Assert.AreEqual(new TimeSpan(0, 0, 0), generatedFiles.First().Begin); - Assert.AreEqual(new TimeSpan(0, 30, 0), generatedFiles.First().End); - Assert.AreEqual(new TimeSpan(1, 0, 0), generatedFiles.ElementAt(1).End); - Assert.AreEqual(new TimeSpan(1, 0, 0), generatedFiles.ElementAt(2).Begin); - Assert.AreEqual(new TimeSpan(1, 30, 0), generatedFiles.ElementAt(2).End); - Assert.AreEqual(new TimeSpan(1, 30, 0), generatedFiles.Last().Begin); - Assert.AreEqual(new TimeSpan(2, 0, 0), generatedFiles.Last().End); - foreach (var generatedFile in generatedFiles) - { - Assert.AreEqual(string.Format("Unit test({0}).cue", counter), generatedFile.Name); - counter++; - var content = generatedFile.Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - File.Delete(fileName); - int positionDifference = 1 - position; - // Check cuesheet header for artist and title - var sectionForThisFile = cuesheet.Sections.FirstOrDefault(x => x.Begin == generatedFile.Begin); - if (sectionForThisFile != null) - { - Assert.AreEqual(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, sectionForThisFile.Title), fileContent.First()); - Assert.AreEqual(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, sectionForThisFile.Artist), fileContent[1]); - Assert.AreEqual(string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, sectionForThisFile.AudiofileName, cuesheet.Audiofile?.AudioFileType), fileContent[2]); - } - else - { - Assert.AreEqual(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, cuesheet.Title), fileContent.First()); - Assert.AreEqual(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, cuesheet.Artist), fileContent[1]); - var cuesheetFileName = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(cuesheet.Audiofile?.Name), counter - 1, Path.GetExtension(cuesheet.Audiofile?.Name)); - Assert.AreEqual(string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, cuesheetFileName, cuesheet.Audiofile?.AudioFileType), fileContent[2]); - } - //Check for start from position 1 and begin = 00:00:00 - for (int i = 3; i < fileContent.Length; i += 4) - { - var track = cuesheet.Tracks.Single(x => x.Position == position); - position++; - Assert.AreEqual(string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position + positionDifference, CuesheetConstants.CuesheetTrackAudio), fileContent[i]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title), fileContent[i + 1]); - Assert.AreEqual(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist), fileContent[i + 2]); - var trackBegin = track.Begin; - if (generatedFile.Begin != null) - { - if (generatedFile.Begin >= track.Begin) - { - trackBegin = TimeSpan.Zero; - } - else - { - trackBegin = track.Begin - generatedFile.Begin; - } - } - Assert.IsNotNull(trackBegin); - Assert.AreEqual(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(trackBegin.Value.TotalMinutes), trackBegin.Value.Seconds, trackBegin.Value.Milliseconds / 75), fileContent[i + 3]); - } - position--; - } - } - - [TestMethod()] - public void GenerateExportfilesTest() - { - var testHelper = new TestHelper(); - //Prepare cuesheet - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - var rand = new Random(); - var flagsToAdd = rand.Next(0, 3); - if (flagsToAdd > 0) - { - for (int x = 0; x < flagsToAdd; x++) - { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); - } - } - } - - cuesheet.Cataloguenumber.Value = "0123456789123"; - cuesheet.CDTextfile = new CDTextfile("Testfile.cdt"); - - //Test class - var exportProfile = new Exportprofile - { - SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%;%Cuesheet.Cataloguenumber%;%Cuesheet.CDTextfile%", - SchemeTracks = "%Track.Position%;%Track.Artist%;%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%", - SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor" - }; - Assert.AreEqual(ValidationStatus.Success, exportProfile.Validate().Status); - var generator = new ExportfileGenerator(ExportType.Exportprofile, cuesheet, exportprofile: exportProfile); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - Assert.AreEqual(exportProfile.Filename, generatedFiles.First().Name); - var fileContent = generatedFiles.First().Content; - Assert.IsNotNull(fileContent); - var tempFile = Path.GetTempFileName(); - File.WriteAllBytes(tempFile, fileContent); - var content = File.ReadAllLines(tempFile); - Assert.AreEqual("Demo Artist;Demo Title;0123456789123;Testfile.cdt", content[0]); - for (int i = 1; i < content.Length - 1; i++) - { - Assert.IsFalse(string.IsNullOrEmpty(content[i])); - Assert.AreNotEqual(content[i], ";;;;;"); - Assert.IsTrue(content[i].StartsWith(cuesheet.Tracks.ToList()[i - 1].Position + ";")); - } - Assert.AreEqual(content[^1], "Exported Demo Title from Demo Artist using AudioCuesheetEditor"); - - File.Delete(tempFile); - - exportProfile.SchemeHead = "%Track.Position%;%Cuesheet.Artist%;"; - var validationResult = exportProfile.Validate(x => x.SchemeHead); - Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Parameter != null && x.Parameter.Contains("%Track.Position%"))); - - //Check multiline export - exportProfile = new Exportprofile - { - SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%", - SchemeTracks = string.Format("%Track.Position%{0}%Track.Artist%{1}%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%", Environment.NewLine, Environment.NewLine), - SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor" - }; - Assert.AreEqual(ValidationStatus.Success, exportProfile.Validate().Status); - generator.Exportprofile = exportProfile; - generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - Assert.AreEqual(exportProfile.Filename, generatedFiles.First().Name); - fileContent = generatedFiles.First().Content; - Assert.IsNotNull(fileContent); - tempFile = Path.GetTempFileName(); - File.WriteAllBytes(tempFile, fileContent); - content = File.ReadAllLines(tempFile); - Assert.AreEqual(content[0], "Demo Artist;Demo Title"); - var trackPosition = 0; - for (int i = 1; i < content.Length - 1; i += 3) - { - Assert.IsFalse(string.IsNullOrEmpty(content[i])); - Assert.AreNotEqual(content[i], ";;;;;"); - var track = cuesheet.Tracks.ToList()[trackPosition]; - Assert.IsNotNull(track.Position); - var position = track.Position.ToString(); - Assert.IsNotNull(position); - Assert.IsTrue(content[i].StartsWith(position)); - trackPosition++; - } - Assert.AreEqual(content[^1], "Exported Demo Title from Demo Artist using AudioCuesheetEditor"); - - File.Delete(tempFile); - - //Test flags - exportProfile = new Exportprofile - { - SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%;%Cuesheet.Cataloguenumber%;%Cuesheet.CDTextfile%", - SchemeTracks = "%Track.Position%;%Track.Flags%;%Track.Artist%;%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%", - SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor" - }; - Assert.AreEqual(ValidationStatus.Success, exportProfile.Validate().Status); - generator.Exportprofile = exportProfile; - generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - Assert.AreEqual(exportProfile.Filename, generatedFiles.First().Name); - fileContent = generatedFiles.First().Content; - Assert.IsNotNull(fileContent); - tempFile = Path.GetTempFileName(); - File.WriteAllBytes(tempFile, fileContent); - content = File.ReadAllLines(tempFile); - Assert.AreEqual("Demo Artist;Demo Title;0123456789123;Testfile.cdt", content[0]); - for (int i = 1; i < content.Length - 1; i++) - { - Assert.IsFalse(string.IsNullOrEmpty(content[i])); - Assert.AreNotEqual(content[i], ";;;;;"); - Assert.IsTrue(content[i].StartsWith(cuesheet.Tracks.ToList()[i - 1].Position + ";")); - if (cuesheet.Tracks.ElementAt(i - 1).Flags.Count > 0) - { - var flags = cuesheet.Tracks.ElementAt(i - 1).Flags; - Assert.IsTrue(content[i].Contains(string.Join(" ", flags.Select(x => x.CuesheetLabel)))); - } - } - Assert.AreEqual(content[^1], "Exported Demo Title from Demo Artist using AudioCuesheetEditor"); - File.Delete(tempFile); - } - - [TestMethod()] - public void GenerateExportfilesWithPregapAndPostgapTest() - { - var testHelper = new TestHelper(); - //Prepare cuesheet - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin, - PostGap = new TimeSpan(0, 0, 1), - PreGap = new TimeSpan(0, 0, 3) - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - var rand = new Random(); - var flagsToAdd = rand.Next(0, 3); - if (flagsToAdd > 0) - { - for (int x = 0; x < flagsToAdd; x++) - { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); - } - } - } - - cuesheet.Cataloguenumber.Value = "0123456789123"; - cuesheet.CDTextfile = new CDTextfile("Testfile.cdt"); - - var exportProfile = new Exportprofile - { - SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%;%Cuesheet.Cataloguenumber%;%Cuesheet.CDTextfile%", - SchemeTracks = "%Track.Position%;%Track.Artist%;%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%;%Track.PreGap%;%Track.PostGap%", - SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor at %Date%" - }; - Assert.AreEqual(ValidationStatus.Success, exportProfile.Validate().Status); - var generator = new ExportfileGenerator(ExportType.Exportprofile, cuesheet, exportprofile: exportProfile); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(1, generatedFiles.Count); - Assert.AreEqual(exportProfile.Filename, generatedFiles.First().Name); - var fileContent = generatedFiles.First().Content; - Assert.IsNotNull(fileContent); - var tempFile = Path.GetTempFileName(); - File.WriteAllBytes(tempFile, fileContent); - var content = File.ReadAllLines(tempFile); - Assert.AreEqual("Demo Artist;Demo Title;0123456789123;Testfile.cdt", content[0]); - for (int i = 1; i < content.Length - 1; i++) - { - Assert.IsFalse(string.IsNullOrEmpty(content[i])); - Assert.AreNotEqual(content[i], ";;;;;"); - Assert.IsTrue(content[i].StartsWith(cuesheet.Tracks.ToList()[i - 1].Position + ";")); - } - Assert.AreEqual(content[^1], string.Format("Exported Demo Title from Demo Artist using AudioCuesheetEditor at {0}", DateTime.Now.ToShortDateString())); - File.Delete(tempFile); - } - - [TestMethod()] - public void GenerateExportfilesWithSectionsTest() - { - var testHelper = new TestHelper(); - //Prepare cuesheet - Cuesheet cuesheet = new() - { - Artist = "Demo Artist", - Title = "Demo Title", - Audiofile = new Audiofile("Testfile.mp3") - }; - var begin = TimeSpan.Zero; - for (int i = 1; i < 25; i++) - { - var track = new Track - { - Artist = string.Format("Demo Track Artist {0}", i), - Title = string.Format("Demo Track Title {0}", i), - Begin = begin - }; - begin = begin.Add(new TimeSpan(0, i, i)); - track.End = begin; - cuesheet.AddTrack(track, testHelper.ApplicationOptions); - var rand = new Random(); - var flagsToAdd = rand.Next(0, 3); - if (flagsToAdd > 0) - { - for (int x = 0; x < flagsToAdd; x++) - { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); - } - } - } - - cuesheet.Cataloguenumber.Value = "0123456789123"; - cuesheet.CDTextfile = new CDTextfile("Testfile.cdt"); - var section = cuesheet.AddSection(); - section.Begin = new TimeSpan(1, 30, 0); - section.End = new TimeSpan(2, 0, 0); - section.Title = "Last part"; - section.AudiofileName = "Last part.mp3"; - section = cuesheet.AddSection(); - section.Begin = new TimeSpan(0, 0, 0); - section.End = new TimeSpan(0, 30, 0); - section.AudiofileName = "First part.mp3"; - section = cuesheet.AddSection(); - section.Begin = new TimeSpan(0, 30, 0); - section.End = new TimeSpan(1, 0, 0); - section.Artist = "Demo Artist Part2"; - section = cuesheet.AddSection(); - section.Artist = "Artist 3"; - section.Title = "Title 3"; - section.AudiofileName = "Part 3.mp3"; - section.Begin = new TimeSpan(1, 0, 0); - section.End = new TimeSpan(1, 30, 0); - //Test export - var exportProfile = new Exportprofile - { - SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%;%Cuesheet.Audiofile%;%Cuesheet.Cataloguenumber%;%Cuesheet.CDTextfile%", - SchemeTracks = "%Track.Position%;%Track.Artist%;%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%", - SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor" - }; - var generator = new ExportfileGenerator(ExportType.Exportprofile, cuesheet, exportProfile); - Assert.AreEqual(ValidationStatus.Success, generator.Validate().Status); - var generatedFiles = generator.GenerateExportfiles(); - Assert.AreEqual(4, generatedFiles.Count); - - //Check split according to sections - Assert.AreEqual(new TimeSpan(0, 0, 0), generatedFiles.First().Begin); - Assert.AreEqual(new TimeSpan(0, 30, 0), generatedFiles.First().End); - Assert.AreEqual(new TimeSpan(1, 0, 0), generatedFiles.ElementAt(1).End); - Assert.AreEqual(new TimeSpan(1, 0, 0), generatedFiles.ElementAt(2).Begin); - Assert.AreEqual(new TimeSpan(1, 30, 0), generatedFiles.ElementAt(2).End); - Assert.AreEqual(new TimeSpan(1, 30, 0), generatedFiles.Last().Begin); - Assert.AreEqual(new TimeSpan(2, 0, 0), generatedFiles.Last().End); - var counter = 1; - var position = 1; - foreach (var generatedFile in generatedFiles) - { - Assert.AreEqual(string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(exportProfile.Filename), counter, Path.GetExtension(exportProfile.Filename)), generatedFile.Name); - counter++; - var content = generatedFile.Content; - Assert.IsNotNull(content); - var fileName = Path.GetTempFileName(); - File.WriteAllBytes(fileName, content); - var fileContent = File.ReadAllLines(fileName); - File.Delete(fileName); - int positionDifference = 1 - position; - //Check cuesheet header for artist and title - var sectionForThisFile = cuesheet.Sections.FirstOrDefault(x => x.Begin == generatedFile.Begin); - if (sectionForThisFile != null) - { - Assert.AreEqual(string.Format("{0};{1};{2};0123456789123;Testfile.cdt", sectionForThisFile.Artist, sectionForThisFile.Title, sectionForThisFile.AudiofileName), fileContent[0]); - } - else - { - var audiofileName = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(cuesheet.Audiofile?.Name), counter - 1, Path.GetExtension(cuesheet.Audiofile?.Name)); - Assert.AreEqual(string.Format("{0};{1};{2};0123456789123;Testfile.cdt", cuesheet.Artist, cuesheet.Title, audiofileName), fileContent[0]); - } - //Check for start from position 1 and begin = 00:00:00 - for (int i = 1; i < fileContent.Length - 1; i++) - { - var track = cuesheet.Tracks.Single(x => x.Position == position); - position++; - var trackBegin = track.Begin; - var trackEnd = track.End; - if (generatedFile.Begin != null) - { - if (generatedFile.Begin >= track.Begin) - { - trackBegin = TimeSpan.Zero; - } - else - { - trackBegin = track.Begin - generatedFile.Begin; - } - trackEnd = track.End - generatedFile.Begin; - } - Assert.AreEqual(string.Format("{0};{1};{2};{3};{4};{5}", track.Position + positionDifference, track.Artist, track.Title, trackBegin, trackEnd, trackEnd - trackBegin), fileContent[i]); - } - if (sectionForThisFile != null) - { - Assert.AreEqual(string.Format("Exported {0} from {1} using AudioCuesheetEditor", sectionForThisFile.Title, sectionForThisFile.Artist), fileContent.Last()); - } - else - { - Assert.AreEqual(string.Format("Exported {0} from {1} using AudioCuesheetEditor", cuesheet.Title, cuesheet.Artist), fileContent.Last()); - } - position--; - } - } - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/IO/Export/ExportprofileTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Export/ExportprofileTests.cs index b976c7c0..2cdd8009 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/Export/ExportprofileTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/Export/ExportprofileTests.cs @@ -30,17 +30,17 @@ public void ValidateTest() { Filename = string.Empty }; - Assert.AreEqual(ValidationStatus.Error, exportprofile.Validate(x => x.Filename).Status); + Assert.AreEqual(ValidationStatus.Error, exportprofile.Validate(nameof(Exportprofile.Filename)).Status); exportprofile.Filename = "Test123"; - Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(x => x.Filename).Status); + Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(nameof(Exportprofile.Filename)).Status); exportprofile.SchemeHead = "%Cuesheet.Artist%;%Cuesheet.Title%;%Cuesheet.Cataloguenumber%;%Cuesheet.CDTextfile%"; - Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(x => x.SchemeHead).Status); + Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(nameof(Exportprofile.SchemeHead)).Status); exportprofile.SchemeTracks = "%Track.Position%;%Track.Artist%;%Track.Title%;%Track.Begin%;%Track.End%;%Track.Length%;%Track.PreGap%;%Track.PostGap%"; - Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(x => x.SchemeTracks).Status); + Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(nameof(Exportprofile.SchemeTracks)).Status); exportprofile.SchemeFooter = "Exported %Cuesheet.Title% from %Cuesheet.Artist% using AudioCuesheetEditor at %Date%"; - Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(x => x.SchemeFooter).Status); + Assert.AreEqual(ValidationStatus.Success, exportprofile.Validate(nameof(Exportprofile.SchemeFooter)).Status); exportprofile.SchemeFooter = "Exported %Track.Title% from %Cuesheet.Artist% using AudioCuesheetEditor at %Date%"; - Assert.AreEqual(ValidationStatus.Error, exportprofile.Validate(x => x.SchemeFooter).Status); + Assert.AreEqual(ValidationStatus.Error, exportprofile.Validate(nameof(Exportprofile.SchemeFooter)).Status); } } } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs index 2efbed05..17be72f0 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs @@ -16,52 +16,153 @@ using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO.Import; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace AudioCuesheetEditorTests.Model.IO.Import +namespace AudioCuesheetEditor.Tests.Model.IO.Import { [TestClass()] public class TextImportSchemeTests { - [TestMethod()] - public void TextImportSchemeValidationTest() + [TestMethod] + public void Validate_SchemeCuesheet_WithValidPlaceholders_ShouldReturnSuccess() { - var importScheme = new TextImportScheme + // Arrange + var scheme = new TextImportScheme { - SchemeTracks = String.Empty, - SchemeCuesheet = String.Empty + SchemeCuesheet = "(?'Artist'.+) - (?'Title'.+)\\t+(?'Audiofile'.+)" }; - Assert.AreEqual(ValidationStatus.Success, importScheme.Validate().Status); - importScheme.SchemeCuesheet = "(?'Track.Begin'\\w{1,}) - (?'Cuesheet.Artist'\\w{1})"; - var validationResult = importScheme.Validate(x => x.SchemeCuesheet); - Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Parameter != null && x.Parameter.Last().Equals("(?'Track.Begin'\\w{1,})"))); - importScheme.SchemeCuesheet = "(?'Track.Begin'\\w{1,}) - (?'Cuesheet.Artist'\\w{1}) - (?'Track.Artist'[A-z0-9]{1,})"; - validationResult = importScheme.Validate(x => x.SchemeCuesheet); - Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsTrue(validationResult.ValidationMessages?.Count == 2); - Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Parameter != null && x.Parameter.Last().Equals("(?'Track.Begin'\\w{1,})"))); - Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Parameter != null && x.Parameter.Last().Equals("(?'Track.Artist'[A-z0-9]{1,})"))); - Boolean eventFiredCorrect = false; - importScheme.SchemeChanged += delegate (object? sender, String property) + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); + } + + [TestMethod] + public void Validate_SchemeCuesheet_WithSimplePlaceholders_ShouldReturnSuccess() + { + // Arrange + var scheme = new TextImportScheme { - if (property == nameof(TextImportScheme.SchemeTracks)) - { - eventFiredCorrect = true; - } + SchemeCuesheet = "Artist - Title" }; - importScheme.SchemeTracks = "(?'Cuesheet.Title'[a-zA-Z0-9_ .;äöü&:,]{1,}) - (?'Track.End'\\w{1,})"; - Assert.AreEqual(true, eventFiredCorrect); - validationResult = importScheme.Validate(x => x.SchemeTracks); - Assert.AreEqual(ValidationStatus.Error, validationResult.Status); - Assert.IsTrue(validationResult.ValidationMessages?.Any(x => x.Parameter != null && x.Parameter.Last().Equals("(?'Cuesheet.Title'[a-zA-Z0-9_ .;äöü&:,]{1,})"))); - importScheme.SchemeCuesheet = "(?'Cuesheet.Artist'\\w{1,}) - (?'Cuesheet.AudioFile'[a-zA-Z0-9_. ;äöü&:,\\]{1,})"; - importScheme.SchemeTracks = "(?'Track.Artist'[A-z0-9]{1,}) - (?'Track.Title'.{1,})"; - Assert.AreEqual(ValidationStatus.Success, importScheme.Validate().Status); + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); + } + + [TestMethod] + public void Validate_SchemeCuesheet_WithoutPlaceholders_ShouldReturnError() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeCuesheet = "InvalidPattern" + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); + + // Assert + Assert.AreEqual(ValidationStatus.Error, result.Status); + Assert.AreEqual(1, result.ValidationMessages.Count); + var message = result.ValidationMessages.Single(); + Assert.AreEqual("{0} contains no placeholder!", message.Message); + Assert.AreEqual(nameof(TextImportScheme.SchemeCuesheet), message.Parameter?.FirstOrDefault()); + } + + [TestMethod] + public void Validate_SchemeTracks_WithValidPlaceholders_ShouldReturnSuccess() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeTracks = "(?'Artist'.+) - (?'Title'.+)(?:...\\t)(?'End'.+)" + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); + } + + [TestMethod] + public void Validate_SchemeTracks_WithSimplePlaceholders_ShouldReturnSuccess() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeTracks = "Artist - Title\tEnd" + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); + } + + [TestMethod] + public void Validate_SchemeTracks_WithoutPlaceholders_ShouldReturnError() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeTracks = "InvalidPattern" + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); + + // Assert + Assert.AreEqual(ValidationStatus.Error, result.Status); + Assert.AreEqual(1, result.ValidationMessages.Count); + var message = result.ValidationMessages.Single(); + Assert.AreEqual("{0} contains no placeholder!", message.Message); + Assert.AreEqual(nameof(TextImportScheme.SchemeTracks), message.Parameter?.FirstOrDefault()); + } + + [TestMethod] + public void Validate_SchemeCuesheetEmpty_ShouldReturnSuccess() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeCuesheet = string.Empty + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); + } + + [TestMethod] + public void Validate_SchemeTrackstEmpty_ShouldReturnSuccess() + { + // Arrange + var scheme = new TextImportScheme + { + SchemeTracks = string.Empty + }; + + // Act + var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); + + // Assert + Assert.AreEqual(ValidationStatus.Success, result.Status); + Assert.AreEqual(0, result.ValidationMessages.Count); } } } diff --git a/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs b/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs index 1f8fde61..16695b6a 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs @@ -14,51 +14,58 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.IO; using AudioCuesheetEditor.Model.IO.Audio; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; -namespace AudioCuesheetEditor.Model.IO.Tests +namespace AudioCuesheetEditor.Tests.Model.IO { [TestClass()] public class ProjectfileTests { [TestMethod()] - public void GenerateFileTest() + public void GenerateFile_WithoutSections_GeneratesOneFile() { + // Arrange var cuesheet = new Cuesheet { Artist = "CuesheetArtist", Title = "CuesheetTitle", Audiofile = new Audiofile("AudioFile.mp3"), CDTextfile = new CDTextfile("CDTextfile.cdt"), + Cataloguenumber = "A123" }; - cuesheet.Cataloguenumber.Value = "A123"; var begin = TimeSpan.Zero; for (int i = 1; i <= 10; i++) { var track = new Track { Position = (uint)i, - Artist = String.Format("Artist {0}", i), - Title = String.Format("Title {0}", i), + Artist = string.Format("Artist {0}", i), + Title = string.Format("Title {0}", i), Begin = begin }; var rand = new Random(); var flagsToAdd = rand.Next(1, 3); + var flags = new List(); for (int x = 0; x < flagsToAdd; x++) { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); + flags.Add(Flag.AvailableFlags.ElementAt(x)); } + track.Flags = flags; begin = begin.Add(new TimeSpan(0, i, i)); track.End = begin; - cuesheet.AddTrack(track, new Options.ApplicationOptions() { LinkTracksWithPreviousOne = true }); + cuesheet.AddTrack(track); } + // Act var projectFile = new Projectfile(cuesheet); + // Assert var generatedFile = projectFile.GenerateFile(); Assert.IsNotNull(generatedFile); var fileName = Path.GetTempFileName(); @@ -70,42 +77,47 @@ public void GenerateFileTest() } [TestMethod()] - public void GenerateFileWithSectionsTest() + public void GenerateFile_WithSections_GeneratesOneFile() { + // Arrange var cuesheet = new Cuesheet { Artist = "CuesheetArtist", Title = "CuesheetTitle", Audiofile = new Audiofile("AudioFile.mp3"), CDTextfile = new CDTextfile("CDTextfile.cdt"), + Cataloguenumber = "A123" }; - cuesheet.Cataloguenumber.Value = "A123"; var begin = TimeSpan.Zero; for (int i = 1; i <= 10; i++) { var track = new Track { Position = (uint)i, - Artist = String.Format("Artist {0}", i), - Title = String.Format("Title {0}", i), + Artist = string.Format("Artist {0}", i), + Title = string.Format("Title {0}", i), Begin = begin }; var rand = new Random(); var flagsToAdd = rand.Next(1, 3); + var flags = new List(); for (int x = 0; x < flagsToAdd; x++) { - track.SetFlag(Flag.AvailableFlags.ElementAt(x), SetFlagMode.Add); + flags.Add(Flag.AvailableFlags.ElementAt(x)); } + track.Flags = flags; begin = begin.Add(new TimeSpan(0, i, i)); track.End = begin; - cuesheet.AddTrack(track, new Options.ApplicationOptions() { LinkTracksWithPreviousOne = true }); + cuesheet.AddTrack(track); } var section = cuesheet.AddSection(); section.Begin = new TimeSpan(0, 30, 0); section = cuesheet.AddSection(); section.Begin = new TimeSpan(1, 0, 0); var projectFile = new Projectfile(cuesheet); + // Act var generatedFile = projectFile.GenerateFile(); + // Assert Assert.IsNotNull(generatedFile); var fileName = Path.GetTempFileName(); File.WriteAllBytes(fileName, generatedFile); @@ -116,47 +128,53 @@ public void GenerateFileWithSectionsTest() } [TestMethod()] - public void ImportFileTest() + public void ImportFile_ValidProjectfile_ShouldImportFile() { - var fileContent = Encoding.UTF8.GetBytes("{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"},\"Cataloguenumber\":{\"Value\":\"A123\"}}"); + // Arrange + var fileContent = Encoding.UTF8.GetBytes("{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Cataloguenumber\":\"A123\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"}}"); + // Act var cuesheet = Projectfile.ImportFile(fileContent); + // Assert Assert.IsNotNull(cuesheet); Assert.IsTrue(cuesheet.Tracks.All(x => x.Cuesheet == cuesheet)); Assert.AreEqual("CuesheetArtist", cuesheet.Artist); Assert.AreEqual("CuesheetTitle", cuesheet.Title); Assert.AreEqual("AudioFile.mp3", cuesheet.Audiofile?.Name); Assert.IsFalse(cuesheet.Audiofile?.IsRecorded); - Assert.AreEqual("A123", cuesheet.Cataloguenumber.Value); - Assert.IsTrue(cuesheet.Cataloguenumber.Validate().ValidationMessages?.Count == 2); - Assert.IsTrue(cuesheet.Tracks.Count == 10); + Assert.AreEqual("A123", cuesheet.Cataloguenumber); + Assert.AreEqual(2, cuesheet.Validate(nameof(Cuesheet.Cataloguenumber)).ValidationMessages?.Count); + Assert.AreEqual(10, cuesheet.Tracks.Count); Assert.IsTrue(cuesheet.Tracks.ElementAt(3).Flags.Contains(Flag.DCP)); Assert.IsTrue(cuesheet.Tracks.ElementAt(3).Flags.Contains(Flag.FourCH)); Assert.AreEqual("Artist 10", cuesheet.Tracks.Last().Artist); Assert.AreEqual(new TimeSpan(0, 55, 55), cuesheet.Tracks.Last().End); - Assert.IsTrue(Object.ReferenceEquals(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1)))); + Assert.IsTrue(ReferenceEquals(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1)))); Assert.AreEqual(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1))); Assert.AreEqual((uint)10, cuesheet.Tracks.Last().Position); } [TestMethod()] - public void ImportFileWithSectionsTest() + public void ImportFile_ValidProjectfileWithSections_ShouldImportFile() { - var fileContent = Encoding.UTF8.GetBytes("{\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"},\"Cataloguenumber\":{\"Value\":\"A123\"},\"Sections\":[{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"00:30:00\"},{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"01:00:00\"}]}"); + //Arrange + var fileContent = Encoding.UTF8.GetBytes("{\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"},\"Cataloguenumber\":\"A123\",\"Sections\":[{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"00:30:00\"},{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"01:00:00\"}]}"); + // Act var cuesheet = Projectfile.ImportFile(fileContent); + // Assert Assert.IsNotNull(cuesheet); Assert.IsTrue(cuesheet.Tracks.All(x => x.Cuesheet == cuesheet)); Assert.AreEqual("CuesheetArtist", cuesheet.Artist); Assert.AreEqual("CuesheetTitle", cuesheet.Title); Assert.AreEqual("AudioFile.mp3", cuesheet.Audiofile?.Name); Assert.IsFalse(cuesheet.Audiofile?.IsRecorded); - Assert.AreEqual("A123", cuesheet.Cataloguenumber.Value); - Assert.IsTrue(cuesheet.Cataloguenumber.Validate().ValidationMessages?.Count == 2); - Assert.IsTrue(cuesheet.Tracks.Count == 10); + Assert.AreEqual("A123", cuesheet.Cataloguenumber); + Assert.AreEqual(2, cuesheet.Validate(nameof(Cuesheet.Cataloguenumber)).ValidationMessages?.Count); + Assert.AreEqual(10, cuesheet.Tracks.Count); Assert.IsTrue(cuesheet.Tracks.ElementAt(3).Flags.Contains(Flag.DCP)); Assert.IsTrue(cuesheet.Tracks.ElementAt(3).Flags.Contains(Flag.FourCH)); Assert.AreEqual("Artist 10", cuesheet.Tracks.Last().Artist); Assert.AreEqual(new TimeSpan(0, 55, 55), cuesheet.Tracks.Last().End); - Assert.IsTrue(Object.ReferenceEquals(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1)))); + Assert.IsTrue(ReferenceEquals(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1)))); Assert.AreEqual(cuesheet.Tracks.First(), cuesheet.GetPreviousLinkedTrack(cuesheet.Tracks.ElementAt(1))); Assert.AreEqual((uint)10, cuesheet.Tracks.Last().Position); Assert.AreEqual(2, cuesheet.Sections.Count); diff --git a/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanFormatTests.cs b/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanFormatTests.cs new file mode 100644 index 00000000..9e0e5c1e --- /dev/null +++ b/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanFormatTests.cs @@ -0,0 +1,39 @@ +//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.Utility; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + +namespace AudioCuesheetEditor.Tests.Model.Utility +{ + [TestClass()] + public class TimeSpanFormatTests + { + [TestMethod()] + public void ParseTimeSpan_WithScheme_ReturnsTimeSpan() + { + //Arrange + var format = new TimeSpanFormat() + { + Scheme = $"{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}" + }; + //Act + var timespan = format.ParseTimeSpan("63:12"); + //Assert + Assert.AreEqual(new TimeSpan(1,3,12), timespan); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanUtilityTests.cs b/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanUtilityTests.cs index 22dac02a..fdcd8cbd 100644 --- a/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanUtilityTests.cs +++ b/AudioCuesheetEditor.Tests/Model/Utility/TimeSpanUtilityTests.cs @@ -13,50 +13,161 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. +using AudioCuesheetEditor.Model.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -namespace AudioCuesheetEditor.Model.Utility.Tests +namespace AudioCuesheetEditor.Tests.Model.Utility { [TestClass()] public class TimeSpanUtilityTests { [TestMethod()] - public void ParseTimeSpanTest() + public void ParseTimeSpan_Valid_HoursMinutesSeconds() { - var timespan = TimeSpanUtility.ParseTimeSpan("01:23:45"); + // Arrange + string input = "01:23:45"; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(1, 23, 45), timespan); - var format = new TimeSpanFormat() { Scheme = "(?'Minutes'\\d{1,})[:](?'Seconds'\\d{1,})" }; - timespan = TimeSpanUtility.ParseTimeSpan("3:12", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_MinutesSeconds() + { + // Arrange + string input = "3:12"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(0, 3, 12), timespan); - timespan = TimeSpanUtility.ParseTimeSpan("63:12", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_OverflowMinutes() + { + // Arrange + string input = "63:12"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(1, 3, 12), timespan); - format.Scheme = "(?'Hours'\\d{1,})[:](?'Minutes'\\d{1,})"; - timespan = TimeSpanUtility.ParseTimeSpan("23:12", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_HoursMinutes() + { + // Arrange + string input = "23:12"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(23, 12, 0), timespan); - format.Scheme = "(?'Hours'\\d{1,})[:](?'Minutes'\\d{1,})[:](?'Seconds'\\d{1,})"; - timespan = TimeSpanUtility.ParseTimeSpan("23:45:56", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_FullFormat() + { + // Arrange + string input = "23:45:56"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(0, 23, 45, 56), timespan); - format.Scheme = "(?'Days'\\d{1,})[.](?'Hours'\\d{1,})[:](?'Minutes'\\d{1,})[:](?'Seconds'\\d{1,})"; - timespan = TimeSpanUtility.ParseTimeSpan("2.23:45:56", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_WithDays() + { + // Arrange + string input = "2.23:45:56"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Days}.{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(2, 23, 45, 56), timespan); - format.Scheme = "(?'Hours'\\d{1,})[:](?'TimeSpanFormat.Minutes'\\d{1,})[:](?'TimeSpanFormat.Seconds'\\d{1,})[.](?'TimeSpanFormat.Milliseconds'\\d{1,})"; - timespan = TimeSpanUtility.ParseTimeSpan("23:45:56.599", format); + } + + [TestMethod()] + public void ParseTimeSpan_Valid_WithMilliseconds() + { + // Arrange + string input = "23:45:56.599"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}.{TimeSpanFormat.Milliseconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNotNull(timespan); Assert.AreEqual(new TimeSpan(0, 23, 45, 56, 599), timespan); - timespan = TimeSpanUtility.ParseTimeSpan("1.2e:45:87.h3a", format); + } + + [TestMethod()] + public void ParseTimeSpan_Invalid_Format() + { + // Arrange + string input = "1.2e:45:87.h3a"; + + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}.{TimeSpanFormat.Milliseconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNull(timespan); - timespan = TimeSpanUtility.ParseTimeSpan("Test", format); + } + + [TestMethod()] + public void ParseTimeSpan_Invalid_Text() + { + // Arrange + string input = "Test"; + var format = new TimeSpanFormat() { Scheme = $"{TimeSpanFormat.Hours}:{TimeSpanFormat.Minutes}:{TimeSpanFormat.Seconds}.{TimeSpanFormat.Milliseconds}" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNull(timespan); - format.Scheme = "this is a test"; - timespan = TimeSpanUtility.ParseTimeSpan("Test", format); + } + + [TestMethod()] + public void ParseTimeSpan_Invalid_Scheme() + { + // Arrange + string input = "Test"; + var format = new TimeSpanFormat() { Scheme = "this is a test" }; + + // Act + var timespan = TimeSpanUtility.ParseTimeSpan(input, format); + + // Assert Assert.IsNull(timespan); } } + } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/IO/CuesheetExportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/CuesheetExportServiceTests.cs new file mode 100644 index 00000000..f32d6765 --- /dev/null +++ b/AudioCuesheetEditor.Tests/Services/IO/CuesheetExportServiceTests.cs @@ -0,0 +1,225 @@ +//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.Model.IO.Export; +using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Linq; +using System.Text; + +namespace AudioCuesheetEditor.Tests.Services.IO +{ + [TestClass()] + public class CuesheetExportServiceTests + { + private readonly Mock sessionStateContainerMock; + private readonly CuesheetExportService cuesheetExportService; + + public CuesheetExportServiceTests() + { + sessionStateContainerMock = new Mock(); + cuesheetExportService = new CuesheetExportService(sessionStateContainerMock.Object); + } + + [TestMethod] + public void CanGenerateExportfiles_InvalidExtension_ReturnsValidationMessage() + { + // Arrange + string invalidFilename = "test.txt"; + sessionStateContainerMock.SetupProperty(x => x.Cuesheet, new Cuesheet()); + + // Act + var result = cuesheetExportService.CanGenerateExportfiles(invalidFilename); + + // Assert + Assert.IsTrue(result.Any(vm => vm.Message.Contains("File extension is not"))); + } + + [TestMethod] + public void CanGenerateExportfiles_ValidExtension_ReturnsEmpty() + { + // Arrange + var cuesheet = new Cuesheet() + { + Artist = "Test Artist", + Title = "Test Title", + Audiofile = new Audiofile("Audio.mp3") + }; + cuesheet.AddTrack(new Track()); + sessionStateContainerMock.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = cuesheetExportService.CanGenerateExportfiles("test.cue"); + + // Assert + Assert.AreEqual(0, result.Count()); + } + + [TestMethod] + public void GenerateExportfiles_WithoutSections_ReturnsExportFile() + { + // Arrange + var filename = "Test valid Filename.cue"; + var cuesheet = new Cuesheet() + { + Artist = "Test artist cuesheet", + Title = "Test title cuesheet", + Audiofile = new Audiofile("Test audiofile.mp3") + }; + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 1", + Title = "Test title 1", + Begin = TimeSpan.Zero, + End = new TimeSpan(0, 4, 12), + Position = 1 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 2", + Title = "Test title 2", + End = new TimeSpan(0, 8, 32), + Position = 2 + }); + sessionStateContainerMock.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = cuesheetExportService.GenerateExportfiles(filename); + + // Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual(filename, result.First().Name); + Assert.AreEqual(TimeSpan.Zero, result.First().Begin); + Assert.AreEqual(new TimeSpan(0, 8, 32), result.First().End); + var content = result.First().Content; + Assert.IsNotNull(content); + var contentString = Encoding.UTF8.GetString(content); + Assert.AreEqual(@"TITLE ""Test title cuesheet"" +PERFORMER ""Test artist cuesheet"" +FILE ""Test audiofile.mp3"" MP3 + TRACK 01 AUDIO + TITLE ""Test title 1"" + PERFORMER ""Test artist 1"" + INDEX 01 00:00:00 + TRACK 02 AUDIO + TITLE ""Test title 2"" + PERFORMER ""Test artist 2"" + INDEX 01 04:12:00 +", contentString); + } + + [TestMethod] + public void GenerateExportfiles_WithSections_ReturnsExportFiles() + { + // Arrange + var filename = "Test valid Filename.cue"; + var exportProfile = new Exportprofile + { + Name = "TestProfile", + SchemeHead = "%Cuesheet.Artist% - %Cuesheet.Title%", + SchemeTracks = "%Track.Position% %Track.Artist% - %Track.Title%", + Filename = "TestExport.txt" + }; + var cuesheet = new Cuesheet() + { + Artist = "Test artist cuesheet", + Title = "Test title cuesheet", + Audiofile = new Audiofile("Test audiofile.mp3") + }; + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 1", + Title = "Test title 1", + Begin = TimeSpan.Zero, + End = new TimeSpan(0, 4, 12), + Position = 1 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 2", + Title = "Test title 2", + End = new TimeSpan(0, 8, 32), + Position = 2 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 3", + Title = "Test title 3", + End = new TimeSpan(0, 12, 31), + Position = 3 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 4", + Title = "Test title 4", + End = new TimeSpan(0, 16, 8), + Position = 4 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 5", + Title = "Test title 5", + End = new TimeSpan(0, 21, 54), + Position = 5 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 6", + Title = "Test title 6", + End = new TimeSpan(0, 31, 32), + Position = 6 + }); + var section1 = cuesheet.AddSection(); + section1.Begin = TimeSpan.Zero; + section1.End = new TimeSpan(0, 10, 0); + var section2 = cuesheet.AddSection(); + section2.Begin = section1.End; + section2.End = new TimeSpan(0, 20, 0); + var section3 = cuesheet.AddSection(); + section3.Begin = section2.End; + section3.End = new TimeSpan(0, 30, 0); + sessionStateContainerMock.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = cuesheetExportService.GenerateExportfiles(filename); + + // Assert + Assert.AreEqual(3, result.Count); + Assert.AreEqual("Test valid Filename(3).cue", result.Last().Name); + Assert.AreEqual(section3.Begin, result.Last().Begin); + Assert.AreEqual(section3.End, result.Last().End); + var content = result.Last().Content; + Assert.IsNotNull(content); + var contentString = Encoding.UTF8.GetString(content); + Assert.AreEqual(@"TITLE ""Test title cuesheet"" +PERFORMER ""Test artist cuesheet"" +FILE ""Test audiofile.mp3"" MP3 + TRACK 01 AUDIO + TITLE ""Test title 5"" + PERFORMER ""Test artist 5"" + INDEX 01 00:00:00 + TRACK 02 AUDIO + TITLE ""Test title 6"" + PERFORMER ""Test artist 6"" + INDEX 01 01:54:00 +", contentString); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs index a31aa3eb..2b373da0 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs @@ -207,8 +207,8 @@ public void Analyse_WithCDTextFileCatalogueNumberAndPreAndPostGap_CreatesValidCu Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "PREGAP 00:04:00")), importFile.FileContentRecognized?.ElementAt(35)); Assert.AreEqual(8, importFile.AnalysedCuesheet.Tracks.Count); Assert.IsNotNull(importFile.AnalysedCuesheet.CDTextfile); - Assert.AreEqual(4, importFile.AnalysedCuesheet.Tracks.ElementAt(0).Flags.Count); - Assert.AreEqual(2, importFile.AnalysedCuesheet.Tracks.ElementAt(1).Flags.Count); + Assert.AreEqual(4, importFile.AnalysedCuesheet.Tracks.ElementAt(0).Flags.Count()); + Assert.AreEqual(2, importFile.AnalysedCuesheet.Tracks.ElementAt(1).Flags.Count()); Assert.IsNotNull(importFile.AnalysedCuesheet.Tracks.ElementAt(1).Flags.SingleOrDefault(x => x.CuesheetLabel == "DCP")); Assert.IsNotNull(importFile.AnalysedCuesheet.Tracks.ElementAt(1).Flags.SingleOrDefault(x => x.CuesheetLabel == "PRE")); Assert.AreEqual(new TimeSpan(0, 0, 2), importFile.AnalysedCuesheet.Tracks.ElementAt(4).PostGap); diff --git a/AudioCuesheetEditor.Tests/Services/IO/ExportfileGeneratorTests.cs b/AudioCuesheetEditor.Tests/Services/IO/ExportfileGeneratorTests.cs new file mode 100644 index 00000000..9e2309c5 --- /dev/null +++ b/AudioCuesheetEditor.Tests/Services/IO/ExportfileGeneratorTests.cs @@ -0,0 +1,224 @@ +//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.Model.IO.Export; +using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Linq; + +namespace AudioCuesheetEditor.Tests.Services.IO +{ + [TestClass] + public class ExportfileGeneratorTests + { + private readonly Mock mockSessionStateContainer; + private readonly ExportfileGenerator exportfileGenerator; + + public ExportfileGeneratorTests() + { + mockSessionStateContainer = new Mock(); + exportfileGenerator = new ExportfileGenerator(mockSessionStateContainer.Object); + } + + [TestMethod] + public void GenerateExportFile_ShouldGenerateExportfile_WithoutSections() + { + // Arrange + var exportProfile = new Exportprofile + { + Name = "TestProfile", + SchemeHead = "%Cuesheet.Artist% - %Cuesheet.Title%", + SchemeTracks = "%Track.Position% %Track.Artist% - %Track.Title%", + Filename = "TestExport.txt" + }; + var cuesheet = new Cuesheet() + { + Artist = "Test artist cuesheet", + Title = "Test title cuesheet", + Audiofile = new Audiofile("Test audiofile.mp3") + }; + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 1", + Title = "Test title 1", + Begin = TimeSpan.Zero, + End = new TimeSpan(0, 4, 12), + Position = 1 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 2", + Title = "Test title 2", + End = new TimeSpan(0, 8, 32), + Position = 2 + }); + mockSessionStateContainer.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = exportfileGenerator.GenerateExportfiles(exportProfile); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(exportProfile.Filename, result.First().Name); + Assert.AreEqual(TimeSpan.Zero, result.First().Begin); + Assert.AreEqual(new TimeSpan(0, 8, 32), result.First().End); + var content = result.First().Content; + Assert.IsNotNull(content); + var contentString = System.Text.Encoding.UTF8.GetString(content); + Assert.AreEqual(@"Test artist cuesheet - Test title cuesheet +1 Test artist 1 - Test title 1 +2 Test artist 2 - Test title 2 + +", contentString); + } + + [TestMethod] + public void GenerateExportFile_ShouldGenerateExportfiles_WithSections() + { + // Arrange + var exportProfile = new Exportprofile + { + Name = "TestProfile", + SchemeHead = "%Cuesheet.Artist% - %Cuesheet.Title%", + SchemeTracks = "%Track.Position% %Track.Artist% - %Track.Title%", + Filename = "TestExport.txt" + }; + var cuesheet = new Cuesheet() + { + Artist = "Test artist cuesheet", + Title = "Test title cuesheet", + Audiofile = new Audiofile("Test audiofile.mp3") + }; + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 1", + Title = "Test title 1", + Begin = TimeSpan.Zero, + End = new TimeSpan(0, 4, 12), + Position = 1 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 2", + Title = "Test title 2", + End = new TimeSpan(0, 8, 32), + Position = 2 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 3", + Title = "Test title 3", + End = new TimeSpan(0, 12, 31), + Position = 3 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 4", + Title = "Test title 4", + End = new TimeSpan(0, 16, 8), + Position = 4 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 5", + Title = "Test title 5", + End = new TimeSpan(0, 21, 54), + Position = 5 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 6", + Title = "Test title 6", + End = new TimeSpan(0, 31, 32), + Position = 6 + }); + var section1 = cuesheet.AddSection(); + section1.Begin = TimeSpan.Zero; + section1.End = new TimeSpan(0, 10, 0); + var section2 = cuesheet.AddSection(); + section2.Begin = section1.End; + section2.End = new TimeSpan(0, 20, 0); + var section3 = cuesheet.AddSection(); + section3.Begin = section2.End; + section3.End = new TimeSpan(0, 30, 0); + mockSessionStateContainer.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = exportfileGenerator.GenerateExportfiles(exportProfile); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Count); + Assert.AreEqual("TestExport(3).txt", result.Last().Name); + Assert.AreEqual(section3.Begin, result.Last().Begin); + Assert.AreEqual(section3.End, result.Last().End); + var content = result.Last().Content; + Assert.IsNotNull(content); + var contentString = System.Text.Encoding.UTF8.GetString(content); + Assert.AreEqual(@"Test artist cuesheet - Test title cuesheet +1 Test artist 5 - Test title 5 +2 Test artist 6 - Test title 6 + +", contentString); + } + + [TestMethod] + public void GenerateExportFile_ShouldHandleEmptyProfile() + { + // Arrange + var exportProfile = new Exportprofile(); + var cuesheet = new Cuesheet() + { + Artist = "Test artist cuesheet", + Title = "Test title cuesheet", + Audiofile = new Audiofile("Test audiofile.mp3") + }; + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 1", + Title = "Test title 1", + Begin = TimeSpan.Zero, + End = new TimeSpan(0, 4, 12), + Position = 1 + }); + cuesheet.AddTrack(new Track() + { + Artist = "Test artist 2", + Title = "Test title 2", + End = new TimeSpan(0, 8, 32), + Position = 2 + }); + mockSessionStateContainer.SetupProperty(x => x.Cuesheet, cuesheet); + + // Act + var result = exportfileGenerator.GenerateExportfiles(exportProfile); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.AreEqual(Exportprofile.DefaultFileName, result.First().Name); + Assert.AreEqual(TimeSpan.Zero, result.First().Begin); + Assert.AreEqual(new TimeSpan(0, 8, 32), result.First().End); + Assert.IsNotNull(result.First().Content); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs index 76382dd6..1ba96928 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs @@ -15,28 +15,28 @@ //. using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Extensions; using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.UI; +using AudioCuesheetEditor.Model.Utility; +using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; using AudioCuesheetEditor.Tests.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -namespace AudioCuesheetEditor.Services.IO.Tests +namespace AudioCuesheetEditor.Tests.Services.IO { [TestClass()] public class ImportManagerTests { [TestMethod()] - public async Task ImportTextAsync_TextfileWithStartDateTime_CreatesValidCuesheetAsync() + public void ImportTextAsync_TextfileWithStartDateTime_CreatesValidCuesheet() { // Arrange - var fileContent = new List + var fileContent = new List { "Innellea~The Golden Fort~02.08.2024 20:10:48", "Nora En Pure~Diving with Whales (Daniel Portman Remix)~02.08.2024 20:15:21", @@ -73,20 +73,18 @@ public async Task ImportTextAsync_TextfileWithStartDateTime_CreatesValidCuesheet var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = @"(?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,'*-?:]{1,})~(?'Track.Title'[a-zA-Z0-9_ .();äöü&'*-?:Ü]{1,})~(?'Track.StartDateTime'.{1,})" - } + SchemeCuesheet = null, + SchemeTracks = @"(?'Artist'[a-zA-Z0-9_ .();äöü&:,'*-?:]{1,})~(?'Title'[a-zA-Z0-9_ .();äöü&'*-?:Ü]{1,})~(?'StartDateTime'.{1,})" }; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); var testHelper = new TestHelper(); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); Assert.IsNotNull(sessionStateContainer.ImportCuesheet); diff --git a/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs index 06ef45b5..e95cfd6c 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs @@ -15,7 +15,6 @@ //. using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.IO; using AudioCuesheetEditor.Tests.Properties; @@ -46,17 +45,13 @@ public void Analyse_SampleCuesheet_CreatesValidCuesheet() "Sample Artist 7 - Sample Title 7 00:45:54", "Sample Artist 8 - Sample Title 8 01:15:54" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -85,17 +80,13 @@ public void Analyse_InvalidSchemeTracks_CreatesAnalyseException() "7|Sample Artist 7 - Sample Title 7 00:45:54", "8|Sample Artist 8 - Sample Title 8 01:15:54" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'.{1,})", - SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" - } + SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'.{1,})", + SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNotNull(importfile.AnalyseException); } @@ -116,17 +107,49 @@ public void Analyse_InputfileWithExtraSeperator_CreatesValidCuesheet() "7|Sample Artist 7 - Sample Title 7 00:45:54", "8|Sample Artist 8 - Sample Title 8 01:15:54" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'.{1,})", - SchemeTracks = @"(?'Track.Position'\d{1,})[|](?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" - } + SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'.{1,})", + SchemeTracks = @"(?'Position'\d{1,})[|](?'Artist'.{1,}) - (?'Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'End'.{1,})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); + // Assert + Assert.IsNull(importfile.AnalyseException); + Assert.IsNotNull(importfile.AnalysedCuesheet); + Assert.AreEqual("CuesheetArtist", importfile.AnalysedCuesheet.Artist); + Assert.AreEqual("CuesheetTitle", importfile.AnalysedCuesheet.Title); + Assert.AreEqual("c:\\tmp\\TestTextFile.cdt", importfile.AnalysedCuesheet.CDTextfile); + Assert.AreEqual(8, importfile.AnalysedCuesheet.Tracks.Count); + Assert.AreEqual((uint)6, importfile.AnalysedCuesheet.Tracks.ElementAt(5).Position); + Assert.AreEqual("Sample Artist 1", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Artist); + Assert.AreEqual("Sample Title 1", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Title); + Assert.AreEqual(new TimeSpan(0, 5, 0), importfile.AnalysedCuesheet.Tracks.ElementAt(0).End); + } + + [TestMethod()] + public void Analyse_InputfileWithSimplifiedScheme_CreatesValidCuesheet() + { + // Arrange + var fileContent = new List + { + "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt", + "1|Sample Artist 1 - Sample Title 1 00:05:00", + "2|Sample Artist 2 - Sample Title 2 00:09:23", + "3|Sample Artist 3 - Sample Title 3 00:15:54", + "4|Sample Artist 4 - Sample Title 4 00:20:13", + "5|Sample Artist 5 - Sample Title 5 00:24:54", + "6|Sample Artist 6 - Sample Title 6 00:31:54", + "7|Sample Artist 7 - Sample Title 7 00:45:54", + "8|Sample Artist 8 - Sample Title 8 01:15:54" + }; + var textImportScheme = new TextImportScheme() + { + SchemeCuesheet = @"Artist|Title CDTextfile", + SchemeTracks = @"Position|Artist - Title End" + }; + // Act + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -164,17 +187,13 @@ public void Analyse_InvalidScheme_CreatesAnalyseException() string.Empty, string.Empty }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cuesheet.Cataloguenumber'.{1,})", - SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" - } + SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cuesheet.Cataloguenumber'.{1,})", + SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNotNull(importfile.AnalyseException); } @@ -203,17 +222,13 @@ public void Analyse_CuesheetWithTextfileAndCatalogueNumber_CreatesValidCuesheet( string.Empty, string.Empty }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cuesheet.Cataloguenumber'.{1,})", - SchemeTracks = @"(?'Track.Position'.{1,})[|](?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" - } + SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cataloguenumber'.{1,})", + SchemeTracks = @"(?'Position'.{1,})[|](?'Artist'.{1,}) - (?'Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'End'.{1,})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -244,17 +259,13 @@ public void Analyse_CuesheeTracksOnly_CreatesValidCuesheet() "Sample Artist 8 - Sample Title 8 01:15:54", "Sample Artist 9 - Sample Title 9" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = null, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -276,18 +287,14 @@ public void Analyse_CuesheetBug213_CreatesValidCuesheet() lines.Add(reader.ReadLine()); } var fileContent = lines.AsReadOnly(); - var importService = new TextImportService(); - var importOptions = new ImportOptions() + var timeSpanFormat = new TimeSpanFormat() { Scheme = "Minutes:Seconds" }; + var textImportScheme = new TextImportScheme() { - TimeSpanFormat = new TimeSpanFormat() { Scheme = "(?'TimeSpanFormat.Minutes'\\d{1,})[:](?'TimeSpanFormat.Seconds'\\d{1,})" }, - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = null, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent, timeSpanFormat); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -310,17 +317,13 @@ public void Analyse_CuesheetWithFlags_CreatesValidCuesheet() "Sample Artist 7 - Sample Title 7 00:45:54", "Sample Artist 8 - Sample Title 8 01:15:54 PRE DCP 4CH SCMS" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = "(?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Track.Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'Track.End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'Track.Flags'[a-zA-Z 0-9,]{1,})" - } + SchemeCuesheet = null, + SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'Flags'[a-zA-Z 0-9,]{1,})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -353,17 +356,13 @@ public void Analyse_CuesheetWithPreGapAndPostGap_CreatesValidCuesheet() "Sample Artist 7 - Sample Title 7 00:00:00 00:45:54 00:00:00", "Sample Artist 8 - Sample Title 8 00:00:02 01:15:54 00:00:00" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = "(?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Track.Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'Track.PreGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'Track.End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'Track.PostGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})" - } + SchemeCuesheet = null, + SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'PreGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'PostGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})" }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -402,17 +401,13 @@ public void Analyse_SchemeCuesheetOnly_CreatesFileContentRecognizedOnlyForCueshe "Sample Artist 7 - Sample Title 7 00:45:54", "Sample Artist 8 - Sample Title 8 01:15:54" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = null - } + SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, + SchemeTracks = null }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -451,17 +446,13 @@ public void Analyse_CuesheetWithoutTracks_CreatesValidFileContentRecognized() "Sample Artist 7 - Sample Title 7 00:45:54", "Sample Artist 8 - Sample Title 8 01:15:54" }; - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -488,17 +479,13 @@ public void Analyse_TextfileBug233_CreatesValidFileContentRecognized() lines.Add(reader.ReadLine()); } var fileContent = lines.AsReadOnly(); - var importService = new TextImportService(); - var importOptions = new ImportOptions + var textImportScheme = new TextImportScheme() { - TextImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - } + SchemeCuesheet = null, + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; // Act - var importfile = importService.Analyse(importOptions, fileContent); + var importfile = TextImportService.Analyse(textImportScheme, fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); diff --git a/AudioCuesheetEditor.Tests/Services/UI/ApplicationOptionsTimeSpanParserTests.cs b/AudioCuesheetEditor.Tests/Services/UI/ApplicationOptionsTimeSpanParserTests.cs new file mode 100644 index 00000000..ed16bb62 --- /dev/null +++ b/AudioCuesheetEditor.Tests/Services/UI/ApplicationOptionsTimeSpanParserTests.cs @@ -0,0 +1,118 @@ +//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.Data.Options; +using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.Options; +using AudioCuesheetEditor.Services.UI; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace AudioCuesheetEditor.Tests.Services.UI +{ + [TestClass()] + public class ApplicationOptionsTimeSpanParserTests + { + [TestMethod] + public async Task TimespanTextChanged_ValidInput_SetsPropertyCorrectly() + { + // Arrange + var options = new ApplicationOptions() + { + TimeSpanFormat = new() + { + Scheme = "Minutes:Seconds" + } + }; + var mockOptionsProvider = new Mock(); + mockOptionsProvider + .Setup(p => p.GetOptions()) + .ReturnsAsync(options); + var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object); + await Task.Delay(50); + var track = new Track(); + // Act + await parser.TimespanTextChanged(track, x => x.Begin, "92:12"); + // Assert + Assert.AreEqual(new TimeSpan(1, 32, 12), track.Begin); + } + + [TestMethod] + public async Task TimespanTextChanged_InvalidInput_SetsNull() + { + var options = new ApplicationOptions() + { + TimeSpanFormat = new() + { + Scheme = "Minutes:Seconds" + } + }; + var mockOptionsProvider = new Mock(); + mockOptionsProvider + .Setup(p => p.GetOptions()) + .ReturnsAsync(options); + var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object); + await Task.Delay(50); + var track = new Track(); + // Act + await parser.TimespanTextChanged(track, x => x.End, "not a time"); + // Assert + Assert.IsNull(track.End); + } + + [TestMethod()] + public async Task GetTimespanFormatted_ValidFormat_ReturnsCorrectString() + { + // Arrange + var options = new ApplicationOptions() + { + DisplayTimeSpanFormat = @"hh\:mm\:ss" + }; + var mockOptionsProvider = new Mock(); + mockOptionsProvider + .Setup(p => p.GetOptions()) + .ReturnsAsync(options); + var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object); + await Task.Delay(50); + // Act + var result = parser.GetTimespanFormatted(new TimeSpan(0, 1, 30, 27, 200, 103)); + // Assert + Assert.AreEqual("01:30:27", result); + } + + [TestMethod()] + public async Task GetTimespanFormatted_InvalidFormat_FallbackToDefault() + { + // Arrange + var options = new ApplicationOptions() + { + DisplayTimeSpanFormat = "INVALID_FORMAT" + }; + var mockOptionsProvider = new Mock(); + mockOptionsProvider + .Setup(p => p.GetOptions()) + .ReturnsAsync(options); + var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object); + await Task.Delay(50); + // Act + var result = parser.GetTimespanFormatted(new TimeSpan(0, 1, 30, 27, 200, 103)); + // Assert + Assert.AreEqual("01:30:27.2001030", result); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs new file mode 100644 index 00000000..a5169e7d --- /dev/null +++ b/AudioCuesheetEditor.Tests/Services/UI/SessionStateContainerTests.cs @@ -0,0 +1,103 @@ +//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.Model.IO.Import; +using AudioCuesheetEditor.Services.UI; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace AudioCuesheetEditor.Tests.Services.UI +{ + [TestClass] + public class SessionStateContainerTests + { + private readonly Mock traceChangeManagerMock; + private readonly SessionStateContainer sessionStateContainer; + + public SessionStateContainerTests() + { + traceChangeManagerMock = new Mock(); + sessionStateContainer = new SessionStateContainer(traceChangeManagerMock.Object); + } + + [TestMethod] + public void Cuesheet_SetNewValue_ShouldTriggerCuesheetChangedEvent() + { + // Arrange + var newCuesheet = new Cuesheet(); + bool eventTriggered = false; + sessionStateContainer.CuesheetChanged += (sender, args) => eventTriggered = true; + + // Act + sessionStateContainer.Cuesheet = newCuesheet; + + // Assert + Assert.IsTrue(eventTriggered); + traceChangeManagerMock.Verify(m => m.Reset(), Times.Exactly(2)); + traceChangeManagerMock.Verify(m => m.TraceChanges(newCuesheet), Times.Once); + } + + [TestMethod] + public void ImportCuesheet_SetNewValue_ShouldTriggerImportCuesheetChangedEvent() + { + // Arrange + var newImportCuesheet = new Cuesheet(); + bool eventTriggered = false; + sessionStateContainer.ImportCuesheetChanged += (sender, args) => eventTriggered = true; + + // Act + sessionStateContainer.ImportCuesheet = newImportCuesheet; + + // Assert + Assert.IsTrue(eventTriggered); + } + + [TestMethod] + public void ImportAudiofile_SetNewValue_ShouldUpdateImportCuesheetAndTriggerEvent() + { + // Arrange + var importCuesheet = new Cuesheet(); + var audioFile = new Audiofile("Test audio file.mp3"); + sessionStateContainer.ImportCuesheet = importCuesheet; + bool eventTriggered = false; + sessionStateContainer.ImportCuesheetChanged += (sender, args) => eventTriggered = true; + + // Act + sessionStateContainer.ImportAudiofile = audioFile; + + // Assert + Assert.AreEqual(audioFile, importCuesheet.Audiofile); + Assert.IsTrue(eventTriggered); + } + + [TestMethod] + public void ResetImport_ShouldClearImportProperties() + { + // Arrange + sessionStateContainer.Importfile = Mock.Of(); + sessionStateContainer.ImportCuesheet = new Cuesheet(); + + // Act + sessionStateContainer.ResetImport(); + + // Assert + Assert.IsNull(sessionStateContainer.Importfile); + Assert.IsNull(sessionStateContainer.ImportAudiofile); + Assert.IsNull(sessionStateContainer.ImportCuesheet); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/UI/TraceChangeManagerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs similarity index 76% rename from AudioCuesheetEditor.Tests/Model/UI/TraceChangeManagerTests.cs rename to AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs index c083c37c..09a0c60f 100644 --- a/AudioCuesheetEditor.Tests/Model/UI/TraceChangeManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs @@ -14,13 +14,13 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Extensions; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.UI; +using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.IO; +using AudioCuesheetEditor.Services.UI; using AudioCuesheetEditor.Tests.Properties; using AudioCuesheetEditor.Tests.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -29,9 +29,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; -namespace AudioCuesheetEditor.Tests.Model.UI +namespace AudioCuesheetEditor.Tests.Services.UI { [TestClass()] public class TraceChangeManagerTests @@ -151,12 +150,11 @@ public void UndoRedoCombinationTest() [TestMethod()] public void TrackListTest() { - var testhelper = new TestHelper(); var manager = new TraceChangeManager(TestHelper.CreateLogger()); var cuesheet = new Cuesheet(); manager.TraceChanges(cuesheet); Assert.IsFalse(manager.CanUndo); - cuesheet.AddTrack(new Track(), testhelper.ApplicationOptions); + cuesheet.AddTrack(new Track()); Assert.IsTrue(manager.CanUndo); manager.Undo(); Assert.AreEqual(0, cuesheet.Tracks.Count); @@ -167,10 +165,9 @@ public void TrackListTest() } [TestMethod()] - public async Task Import_ValidTextfile_IsUndoable() + public void Import_ValidTextfile_IsUndoable() { // Arrange - var testhelper = new TestHelper(); var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); @@ -182,34 +179,30 @@ public async Task Import_ValidTextfile_IsUndoable() } var fileContent = lines.AsReadOnly(); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions(); - importOptions.TextImportScheme.SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})"; - importOptions.TextImportScheme.SchemeTracks = TextImportScheme.DefaultSchemeTracks; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); - bool eventFired = false; - sessionStateContainer.Cuesheet.TracksAdded += delegate + var textImportScheme = new TextImportScheme() { - eventFired = true; + SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); // Act - await importManager.ImportTextAsync(fileContent); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); // Assert Assert.IsFalse(traceChangeManager.CanUndo); Assert.IsFalse(traceChangeManager.CanRedo); Assert.IsNotNull(sessionStateContainer.ImportCuesheet); Assert.AreEqual("DJFreezeT", sessionStateContainer.ImportCuesheet.Artist); - Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet.Cataloguenumber.Value); + Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet.Cataloguenumber); Assert.AreNotEqual(0, sessionStateContainer.ImportCuesheet.Tracks.Count); - Assert.IsFalse(eventFired); } [TestMethod()] - public async Task UndoImport_ValidTextfile_ResetsToEmptyCuesheet() + public void UndoImport_ValidTextfile_ResetsToEmptyCuesheet() { // Arrange - var testhelper = new TestHelper(); var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); @@ -221,32 +214,29 @@ public async Task UndoImport_ValidTextfile_ResetsToEmptyCuesheet() } var fileContent = lines.AsReadOnly(); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions(); - importOptions.TextImportScheme.SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})"; - importOptions.TextImportScheme.SchemeTracks = TextImportScheme.DefaultSchemeTracks; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); - bool eventFired = false; - sessionStateContainer.Cuesheet.TracksAdded += delegate + var textImportScheme = new TextImportScheme() { - eventFired = true; + SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; - await importManager.ImportTextAsync(fileContent); - await importManager.ImportCuesheetAsync(); + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); + importManager.ImportCuesheet(); // Act traceChangeManager.Undo(); // Assert Assert.AreEqual(0, sessionStateContainer.Cuesheet.Tracks.Count); Assert.IsTrue(string.IsNullOrEmpty(sessionStateContainer.Cuesheet.Artist)); - Assert.IsTrue(string.IsNullOrEmpty(sessionStateContainer.Cuesheet.Cataloguenumber.Value)); + Assert.IsTrue(string.IsNullOrEmpty(sessionStateContainer.Cuesheet.Cataloguenumber)); Assert.IsFalse(traceChangeManager.CanUndo); Assert.IsTrue(traceChangeManager.CanRedo); - Assert.IsFalse(eventFired); } [TestMethod()] - public async Task UndoAndRedoImport_ValidTextfile_ResetsTextfileValues() + public void UndoAndRedoImport_ValidTextfile_ResetsTextfileValues() { // Arrange var testhelper = new TestHelper(); @@ -261,34 +251,28 @@ public async Task UndoAndRedoImport_ValidTextfile_ResetsTextfileValues() } var fileContent = lines.AsReadOnly(); var localStorageOptionsProviderMock = new Mock(); - var importOptions = new ImportOptions(); - importOptions.TextImportScheme.SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})"; - importOptions.TextImportScheme.SchemeTracks = TextImportScheme.DefaultSchemeTracks; - localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(importOptions); - var textImportService = new TextImportService(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, textImportService, traceChangeManager); - bool eventFired = false; - sessionStateContainer.Cuesheet.TracksAdded += delegate + var textImportScheme = new TextImportScheme() { - eventFired = true; + SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", + SchemeTracks = TextImportScheme.DefaultSchemeTracks }; - await importManager.ImportTextAsync(fileContent); + var timeSpanFormat = new TimeSpanFormat(); + var options = new ApplicationOptions(); + localStorageOptionsProviderMock.Setup(x => x.GetOptions()).ReturnsAsync(options); + var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager); + importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); traceChangeManager.Undo(); // Act traceChangeManager.Redo(); // Assert Assert.AreEqual("DJFreezeT", sessionStateContainer.ImportCuesheet?.Artist); - Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet?.Cataloguenumber.Value); + Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet?.Cataloguenumber); Assert.AreEqual(39, sessionStateContainer.ImportCuesheet?.Tracks.Count); - Assert.IsFalse(eventFired); - } [TestMethod()] public void RemoveTracksTest() { - var testhelper = new TestHelper(); - testhelper.ApplicationOptions.LinkTracksWithPreviousOne = true; var manager = new TraceChangeManager(TestHelper.CreateLogger()); var cuesheet = new Cuesheet(); manager.TraceChanges(cuesheet); @@ -316,10 +300,10 @@ public void RemoveTracksTest() Title = "Track 4 Title", End = new TimeSpan(0, 9, 12) }; - cuesheet.AddTrack(track1, testhelper.ApplicationOptions); - cuesheet.AddTrack(track2, testhelper.ApplicationOptions); - cuesheet.AddTrack(track3, testhelper.ApplicationOptions); - cuesheet.AddTrack(track4, testhelper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + cuesheet.AddTrack(track4); Assert.AreEqual(ValidationStatus.Success, track1.Validate().Status); Assert.AreEqual(ValidationStatus.Success, track2.Validate().Status); Assert.AreEqual(ValidationStatus.Success, track3.Validate().Status); @@ -344,51 +328,9 @@ public void RemoveTracksTest() Assert.AreEqual(ValidationStatus.Success, track4.Validate().Status); } - [TestMethod()] - public void MoveTracksTest() - { - var testhelper = new TestHelper(); - var manager = new TraceChangeManager(TestHelper.CreateLogger()); - var cuesheet = new Cuesheet(); - manager.TraceChanges(cuesheet); - var track1 = new Track() - { - Artist = "Track 1 Artist", - Title = "Track 1 Title", - End = new TimeSpan(0, 2, 30) - }; - var track2 = new Track() - { - Artist = "Track 2 Artist", - Title = "Track 2 Title", - End = new TimeSpan(0, 4, 20) - }; - var track3 = new Track() - { - Artist = "Track 3 Artist", - Title = "Track 3 Title", - End = new TimeSpan(0, 7, 12) - }; - var track4 = new Track() - { - Artist = "Track 4 Artist", - Title = "Track 4 Title", - End = new TimeSpan(0, 9, 12) - }; - cuesheet.AddTrack(track1, testhelper.ApplicationOptions); - cuesheet.AddTrack(track2, testhelper.ApplicationOptions); - cuesheet.AddTrack(track3, testhelper.ApplicationOptions); - cuesheet.AddTrack(track4, testhelper.ApplicationOptions); - cuesheet.MoveTrack(track2, MoveDirection.Up); - Assert.AreEqual(track2, cuesheet.Tracks.First()); - manager.Undo(); - Assert.AreEqual(track1, cuesheet.Tracks.First()); - } - [TestMethod()] public void ResetTest() { - var testhelper = new TestHelper(); var manager = new TraceChangeManager(TestHelper.CreateLogger()); Assert.IsFalse(manager.CanUndo); Assert.IsFalse(manager.CanRedo); @@ -400,7 +342,7 @@ public void ResetTest() Title = "Track 1 Title", End = new TimeSpan(0, 2, 30) }; - cuesheet.AddTrack(track1, testhelper.ApplicationOptions); + cuesheet.AddTrack(track1); Assert.IsTrue(manager.CanUndo); manager.Reset(); Assert.IsFalse(manager.CanUndo); @@ -410,7 +352,6 @@ public void ResetTest() [TestMethod()] public void BulkEditTracksTest() { - var helper = new TestHelper(); var manager = new TraceChangeManager(TestHelper.CreateLogger()); var cuesheet = new Cuesheet(); manager.TraceChanges(cuesheet); @@ -427,10 +368,10 @@ public void BulkEditTracksTest() manager.TraceChanges(track2); manager.TraceChanges(track3); manager.TraceChanges(track4); - cuesheet.AddTrack(track1, helper.ApplicationOptions); - cuesheet.AddTrack(track2, helper.ApplicationOptions); - cuesheet.AddTrack(track3, helper.ApplicationOptions); - cuesheet.AddTrack(track4, helper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + cuesheet.AddTrack(track4); track1.IsLinkedToPreviousTrack = true; track2.IsLinkedToPreviousTrack = true; track3.IsLinkedToPreviousTrack = true; @@ -476,7 +417,6 @@ public void BulkEditTracksTest() public void BulkEditTracksLengthTest() { //We do an edit on 4 tracks to set the length, undo, redo and again undo. - var helper = new TestHelper(); var manager = new TraceChangeManager(TestHelper.CreateLogger()); var cuesheet = new Cuesheet(); manager.TraceChanges(cuesheet); @@ -488,10 +428,10 @@ public void BulkEditTracksLengthTest() manager.TraceChanges(track2); manager.TraceChanges(track3); manager.TraceChanges(track4); - cuesheet.AddTrack(track1, helper.ApplicationOptions); - cuesheet.AddTrack(track2, helper.ApplicationOptions); - cuesheet.AddTrack(track3, helper.ApplicationOptions); - cuesheet.AddTrack(track4, helper.ApplicationOptions); + cuesheet.AddTrack(track1); + cuesheet.AddTrack(track2); + cuesheet.AddTrack(track3); + cuesheet.AddTrack(track4); track1.IsLinkedToPreviousTrack = true; track2.IsLinkedToPreviousTrack = true; track3.IsLinkedToPreviousTrack = true; diff --git a/AudioCuesheetEditor.Tests/Utility/TestHelper.cs b/AudioCuesheetEditor.Tests/Utility/TestHelper.cs index 3b382b6c..b1900bb0 100644 --- a/AudioCuesheetEditor.Tests/Utility/TestHelper.cs +++ b/AudioCuesheetEditor.Tests/Utility/TestHelper.cs @@ -22,17 +22,7 @@ namespace AudioCuesheetEditor.Tests.Utility { internal class TestHelper { - public TestHelper() - { - ApplicationOptions = new ApplicationOptions - { - LinkTracksWithPreviousOne = false - }; - RecordOptions = new(); - } - - public ApplicationOptions ApplicationOptions { get; private set; } - public RecordOptions RecordOptions { get; private set; } + public ApplicationOptions ApplicationOptions { get; private set; } = new(); public static ILogger CreateLogger() { var serviceProvider = new ServiceCollection() diff --git a/AudioCuesheetEditor/AudioCuesheetEditor.csproj b/AudioCuesheetEditor/AudioCuesheetEditor.csproj index 9190636c..19b796bb 100644 --- a/AudioCuesheetEditor/AudioCuesheetEditor.csproj +++ b/AudioCuesheetEditor/AudioCuesheetEditor.csproj @@ -1,153 +1,36 @@ - net8.0 + net9.0 true enable enable https://github.com/NeoCoderMatrix86/AudioCuesheetEditor 3.0 - 8.0.0 + 9.0.0 false true true service-worker-assets.js - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + + + + + + diff --git a/AudioCuesheetEditor/Data/Options/ILocalStorageOptionsProvider.cs b/AudioCuesheetEditor/Data/Options/ILocalStorageOptionsProvider.cs index 938d0b4c..2702fda6 100644 --- a/AudioCuesheetEditor/Data/Options/ILocalStorageOptionsProvider.cs +++ b/AudioCuesheetEditor/Data/Options/ILocalStorageOptionsProvider.cs @@ -23,6 +23,7 @@ public interface ILocalStorageOptionsProvider event EventHandler? OptionSaved; Task GetOptions() where T : IOptions; Task SaveOptions(IOptions options); - Task SaveOptionsValue(Expression> propertyExpression, object value) where T : class, IOptions, new(); + Task SaveOptionsValue(Expression> propertyExpression, object? value) where T : class, IOptions, new(); + Task SaveNestedOptionValue(Expression> nestedPropertyExpression, Expression> valuePropertyExpression, TValue value) where T : class, IOptions, new(); } } diff --git a/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs b/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs index 743a4cf1..f254cc40 100644 --- a/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs +++ b/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs @@ -37,18 +37,18 @@ public async Task GetOptions() where T : IOptions { var type = typeof(T); IOptions? options = (IOptions?)Activator.CreateInstance(type); - String optionsJson = await _jsRuntime.InvokeAsync(String.Format("{0}.get", type.Name)); + String optionsJson = await _jsRuntime.InvokeAsync("AppSettings.get", type.Name); if (String.IsNullOrEmpty(optionsJson) == false) { try { - options = (IOptions?)JsonSerializer.Deserialize(optionsJson, typeof(T)); - options ??= (IOptions?)Activator.CreateInstance(typeof(T)); + options = JsonSerializer.Deserialize(optionsJson); + options ??= Activator.CreateInstance(); } catch (JsonException) { //nothing to do, we can not deserialize - options = (IOptions?)Activator.CreateInstance(typeof(T)); + options = Activator.CreateInstance(); } } if (options != null) @@ -63,51 +63,94 @@ public async Task GetOptions() where T : IOptions public async Task SaveOptions(IOptions options) { - var optionsJson = JsonSerializer.Serialize(options, SerializerOptions); - await _jsRuntime.InvokeVoidAsync(String.Format("{0}.set", options.GetType().Name), optionsJson); - OptionSaved?.Invoke(this, options); + bool saveOptions = true; + if (options is IValidateable validateable) + { + saveOptions = validateable.Validate().Status != ValidationStatus.Error; + } + if (saveOptions) + { + var optionsJson = JsonSerializer.Serialize(options, SerializerOptions); + await _jsRuntime.InvokeVoidAsync("AppSettings.set", options.GetType().Name, optionsJson); + OptionSaved?.Invoke(this, options); + } } - public async Task SaveOptionsValue(Expression> propertyExpression, object value) where T : class, IOptions, new() + public async Task SaveOptionsValue(Expression> propertyExpression, object? value) where T : class, IOptions, new() { var options = await GetOptions(); + PropertyInfo? propertyInfo = null; + object? targetObject = options; + if (propertyExpression.Body is MemberExpression memberExpression) { - var propertyInfo = memberExpression.Member as PropertyInfo; - if (propertyInfo != null) - { - propertyInfo.SetValue(options, Convert.ChangeType(value, propertyInfo.PropertyType)); - } - else - { - throw new ArgumentException("The provided expression does not reference a valid property."); - } + propertyInfo = ResolveNestedProperty(memberExpression, ref targetObject); } else if (propertyExpression.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression unaryMemberExpression) { - var propertyInfo = unaryMemberExpression.Member as PropertyInfo; - if (propertyInfo != null) - { - propertyInfo.SetValue(options, Convert.ChangeType(value, propertyInfo.PropertyType)); - } - else - { - throw new ArgumentException("The provided expression does not reference a valid property."); - } + propertyInfo = ResolveNestedProperty(unaryMemberExpression, ref targetObject); + } + + if (propertyInfo != null && targetObject != null) + { + propertyInfo.SetValue(targetObject, Convert.ChangeType(value, propertyInfo.PropertyType)); } else { throw new ArgumentException("The provided expression does not reference a valid property."); } - Boolean saveOptions = true; - if (options is IValidateable validateable) + await SaveOptions(options); + } + + public async Task SaveNestedOptionValue(Expression> nestedPropertyExpression, Expression> valuePropertyExpression, TValue value) where T : class, IOptions, new() + { + var options = await GetOptions(); + + if (nestedPropertyExpression.Body is not MemberExpression memberExpression) { - saveOptions = validateable.Validate(propertyExpression).Status != ValidationStatus.Error; + throw new ArgumentException("The provided nested property expression does not reference a member!"); } - if (saveOptions) + + var nestedProperty = typeof(T).GetProperty(memberExpression.Member.Name) ?? throw new ArgumentException("The provided nested property expression does not reference a valid property."); + var nestedInstance = nestedProperty.GetValue(options) ?? throw new InvalidOperationException("The nested property is null."); + var valueProperty = ResolveNestedProperty(valuePropertyExpression.Body as MemberExpression, ref nestedInstance) ?? throw new ArgumentException("The provided value property expression does not reference a valid property."); + valueProperty.SetValue(nestedInstance, Convert.ChangeType(value, valueProperty.PropertyType)); + + await SaveOptions(options); + } + + private static PropertyInfo? ResolveNestedProperty(MemberExpression? memberExpression, ref object? targetObject) + { + PropertyInfo? propertyInfo = null; + var members = new Stack(); + + while (memberExpression != null) + { + members.Push(memberExpression); + if (memberExpression.Expression is MemberExpression parentMember) + { + memberExpression = parentMember; + } + else + { + memberExpression = null; + } + } + + while (members.Count > 0 && targetObject != null) { - await SaveOptions(options); + memberExpression = members.Pop(); + propertyInfo = targetObject.GetType().GetProperty(memberExpression.Member.Name); + if (propertyInfo != null) + { + if (members.Count > 0) + { + targetObject = propertyInfo.GetValue(targetObject); + } + } } + + return propertyInfo; } } } diff --git a/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs b/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs index 834c5df1..a7350ff9 100644 --- a/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs +++ b/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs @@ -16,21 +16,19 @@ using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; -using Microsoft.JSInterop; -using System.Collections.Immutable; using System.Reflection; namespace AudioCuesheetEditor.Data.Services { public class MusicBrainzArtist { - public Guid Id { get; init; } + public Guid? Id { get; init; } public String? Name { get; init; } public String? Disambiguation { get; init; } } public class MusicBrainzTrack { - public Guid Id { get; init; } + public Guid? Id { get; init; } public String? Artist { get; init; } public String? Title { get; init; } public TimeSpan? Length { get; init; } @@ -45,7 +43,7 @@ public class MusicBrainzDataProvider(ILogger logger) private String? applicationVersion = null; - public async Task> SearchArtistAsync(String searchString) + public async Task> SearchArtistAsync(String? searchString, CancellationToken token) { List artistSearchResult = []; try @@ -53,7 +51,7 @@ public async Task> SearchArtistAsync(Stri if (String.IsNullOrEmpty(searchString) == false) { using var query = new Query(Application, ApplicationVersion, ProjectUrl); - var findArtistsResult = await query.FindArtistsAsync(searchString, simple: true); + var findArtistsResult = await query.FindArtistsAsync(searchString, simple: true, cancellationToken: token); artistSearchResult = findArtistsResult.Results.ToList().ConvertAll(x => new MusicBrainzArtist() { Id = x.Item.Id, Name = x.Item.Name, Disambiguation = x.Item.Disambiguation }); } } @@ -64,7 +62,7 @@ public async Task> SearchArtistAsync(Stri return artistSearchResult.AsReadOnly(); } - public async Task> SearchTitleAsync(String searchString, String? artist = null) + public async Task> SearchTitleAsync(String? searchString, String? artist, CancellationToken token) { List titleSearchResult = []; try @@ -75,11 +73,11 @@ public async Task> SearchTitleAsync(String ISearchResults> findRecordingsResult; if (String.IsNullOrEmpty(artist)) { - findRecordingsResult = await query.FindRecordingsAsync(searchString, simple: true); + findRecordingsResult = await query.FindRecordingsAsync(searchString, simple: true, cancellationToken: token); } else { - findRecordingsResult = await query.FindRecordingsAsync(String.Format("{0} AND artistname:{1}", searchString, artist)); + findRecordingsResult = await query.FindRecordingsAsync(String.Format("{0} AND artistname:{1}", searchString, artist), cancellationToken: token); } foreach (var result in findRecordingsResult.Results) { @@ -106,45 +104,11 @@ public async Task> SearchTitleAsync(String } } } - catch(HttpRequestException hre) - { - _logger.LogError(hre, "Error getting response from MusicBrainz"); - } - return titleSearchResult.AsReadOnly(); - } - - public async Task GetDetailsAsync(Guid id) - { - MusicBrainzTrack? track = null; - try - { - if (id != Guid.Empty) - { - var query = new Query(Application, ApplicationVersion, ProjectUrl); - var recording = await query.LookupRecordingAsync(id, Include.Artists); - if (recording != null) - { - String artist = String.Empty; - if (recording.ArtistCredit != null) - { - foreach (var artistCredit in recording.ArtistCredit) - { - artist += artistCredit.Name; - if (String.IsNullOrEmpty(artistCredit.JoinPhrase) == false) - { - artist += artistCredit.JoinPhrase; - } - } - } - track = new MusicBrainzTrack() { Id = recording.Id, Title = recording.Title, Artist = artist, Length = recording.Length }; - } - } - } catch (HttpRequestException hre) { _logger.LogError(hre, "Error getting response from MusicBrainz"); } - return track; + return titleSearchResult.AsReadOnly(); } private String? ApplicationVersion diff --git a/AudioCuesheetEditor/Extensions/FlagJsonConverter.cs b/AudioCuesheetEditor/Extensions/FlagJsonConverter.cs index e42d74ac..941943fc 100644 --- a/AudioCuesheetEditor/Extensions/FlagJsonConverter.cs +++ b/AudioCuesheetEditor/Extensions/FlagJsonConverter.cs @@ -14,12 +14,8 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Model.AudioCuesheet; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace AudioCuesheetEditor.Extensions { diff --git a/AudioCuesheetEditor/Extensions/InterfaceConverter.cs b/AudioCuesheetEditor/Extensions/InterfaceConverter.cs new file mode 100644 index 00000000..f5ab2bfb --- /dev/null +++ b/AudioCuesheetEditor/Extensions/InterfaceConverter.cs @@ -0,0 +1,40 @@ +//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 System.Text.Json.Serialization; +using System.Text.Json; + +namespace AudioCuesheetEditor.Extensions +{ + public class InterfaceConverter : JsonConverter where TImplementation : TInterface + { + public override TInterface? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options) + { + if (value != null) + { + JsonSerializer.Serialize(writer, (TImplementation)value, options); + } + else + { + writer.WriteNullValue(); + } + } + } +} diff --git a/AudioCuesheetEditor/Extensions/WebAssemblyHostExtension.cs b/AudioCuesheetEditor/Extensions/WebAssemblyHostExtension.cs index bdba0ae4..11e24d17 100644 --- a/AudioCuesheetEditor/Extensions/WebAssemblyHostExtension.cs +++ b/AudioCuesheetEditor/Extensions/WebAssemblyHostExtension.cs @@ -13,22 +13,17 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Model.Options; +using AudioCuesheetEditor.Services.UI; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using System.Globalization; namespace AudioCuesheetEditor.Extensions { public static class WebAssemblyHostExtension { - public async static Task SetDefaultCulture(this WebAssemblyHost host) + public async static Task SetCultureFromConfigurationAsync(this WebAssemblyHost host) { - var localStorageOptionsProvider = host.Services.GetRequiredService(); - var options = await localStorageOptionsProvider.GetOptions(); - - CultureInfo.DefaultThreadCurrentCulture = options.Culture; - CultureInfo.DefaultThreadCurrentUICulture = options.Culture; + var localizationService = host.Services.GetRequiredService(); + await localizationService.SetCultureFromConfigurationAsync(); } } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/CDTextfile.cs b/AudioCuesheetEditor/Model/AudioCuesheet/CDTextfile.cs index c4f53e36..38994110 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/CDTextfile.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/CDTextfile.cs @@ -13,26 +13,37 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using AudioCuesheetEditor.Model.IO; namespace AudioCuesheetEditor.Model.AudioCuesheet { public class CDTextfile { - public const String MimeType = "text/*"; - public const String FileExtension = ".cdt"; - + private string name; public CDTextfile(String name) { if (String.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } - Name = name; + this.name = name; + } + public String Name + { + get => name; + set + { + if (String.IsNullOrEmpty(value)) + { + throw new ArgumentNullException(nameof(value)); + } + var extension = Path.GetExtension(value); + if (extension.Equals(FileExtensions.CDTextfile, StringComparison.CurrentCultureIgnoreCase) == false) + { + value = $"{value}{FileExtensions.CDTextfile}"; + } + name = value; + } } - public String Name { get; private set; } } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/CatalogueNumber.cs b/AudioCuesheetEditor/Model/AudioCuesheet/CatalogueNumber.cs deleted file mode 100644 index 41c35d76..00000000 --- a/AudioCuesheetEditor/Model/AudioCuesheet/CatalogueNumber.cs +++ /dev/null @@ -1,100 +0,0 @@ -//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.Entity; -using AudioCuesheetEditor.Model.UI; - -namespace AudioCuesheetEditor.Model.AudioCuesheet -{ - public class Cataloguenumber : Validateable, ITraceable - { - private String? value; - - public event EventHandler? TraceablePropertyChanged; - - public String? Value - { - get => value; - set - { - var oldValue = this.value; - this.value = value; - FireEvents(oldValue, propertyName: nameof(Value)); - } - } - protected override ValidationResult Validate(string property) - { - ValidationStatus validationStatus = ValidationStatus.NoValidation; - List? validationMessages = null; - switch (property) - { - case nameof(Value): - if (String.IsNullOrEmpty(Value) == false) - { - validationStatus = ValidationStatus.Success; - if (Value.All(Char.IsDigit) == false) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must only contain numbers!", nameof(Value))); - } - if (Value.Length != 13) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} has an invalid length. Allowed length is {1}!", nameof(Value), 13)); - } - } - break; - } - return ValidationResult.Create(validationStatus, validationMessages); - } - - private void OnTraceablePropertyChanged(object? previousValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") - { - TraceablePropertyChanged?.Invoke(this, new TraceablePropertiesChangedEventArgs(new TraceableChange(previousValue, propertyName))); - } - - /// - /// Method for checking if fire of events should be done - /// - /// Previous value of the property firing events - /// Fire OnValidateablePropertyChanged? - /// Fire TraceablePropertyChanged? - /// Property firing the event - /// If propertyName can not be found, an exception is thrown. - private void FireEvents(object? previousValue, Boolean fireValidateablePropertyChanged = true, Boolean fireTraceablePropertyChanged = true, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") - { - var propertyInfo = GetType().GetProperty(propertyName); - if (propertyInfo != null) - { - var propertyValue = propertyInfo.GetValue(this); - if (Equals(propertyValue, previousValue) == false) - { - if (fireValidateablePropertyChanged) - { - OnValidateablePropertyChanged(propertyName); - } - if (fireTraceablePropertyChanged) - { - OnTraceablePropertyChanged(previousValue, propertyName); - } - } - } - else - { - throw new NullReferenceException(String.Format("Property {0} could not be found!", propertyName)); - } - } - } -} diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs b/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs index 0d70dca3..a105b2e4 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs @@ -16,7 +16,6 @@ using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Export; -using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.UI; using System.Text.Json.Serialization; @@ -33,31 +32,22 @@ public class TracksAddedRemovedEventArgs(IEnumerable tracks) : EventArgs public IEnumerable Tracks { get; } = tracks; } - public class CuesheetSectionAddRemoveEventArgs(CuesheetSection section) : EventArgs + public class Cuesheet() : Validateable, ITraceable, ICuesheet { - public CuesheetSection Section { get; } = section; - } - - public class Cuesheet(TraceChangeManager? traceChangeManager = null) : Validateable, ITraceable, ICuesheet - { - private readonly object syncLock = new(); + private readonly Lock syncLock = new(); private List tracks = []; private String? artist; private String? title; - private Audiofile? audiofile; + private IAudiofile? audiofile; private CDTextfile? cDTextfile; - private Cataloguenumber catalogueNumber = new(); - private DateTime? recordingStart; + private String? catalogueNumber; private readonly List> currentlyHandlingLinkedTrackPropertyChange = []; private List sections = []; - public event EventHandler? AudioFileChanged; public event EventHandler? TraceablePropertyChanged; - public event EventHandler? TracksAdded; - public event EventHandler? TracksRemoved; - public event EventHandler? SectionAdded; - public event EventHandler? SectionRemoved; + public event EventHandler? IsRecordingChanged; + public event EventHandler? AudiofileChanged; [JsonInclude] public IReadOnlyCollection Tracks @@ -101,29 +91,15 @@ public String? Title FireEvents(previousValue, propertyName: nameof(Title)); } } - public Audiofile? Audiofile + public IAudiofile? Audiofile { get => audiofile; set { var previousValue = audiofile; - if (audiofile != null) - { - audiofile.ContentStreamLoaded -= Audiofile_ContentStreamLoaded; - } audiofile = value; - if (audiofile != null) - { - if (audiofile.IsContentStreamLoaded == false) - { - audiofile.ContentStreamLoaded += Audiofile_ContentStreamLoaded; - } - else - { - RecalculateLastTrackEnd(); - } - } - FireEvents(previousValue, fireAudioFileChanged: true, propertyName: nameof(Audiofile)); + FireEvents(previousValue, propertyName: nameof(Audiofile)); + AudiofileChanged?.Invoke(this, EventArgs.Empty); } } @@ -138,35 +114,42 @@ public CDTextfile? CDTextfile } } - public Cataloguenumber Cataloguenumber + public String? Cataloguenumber { get => catalogueNumber; set { var previousValue = catalogueNumber; catalogueNumber = value; - FireEvents(previousValue, fireValidateablePropertyChanged: false, propertyName: nameof(Cataloguenumber)); + FireEvents(previousValue, propertyName: nameof(Cataloguenumber)); } } [JsonIgnore] - public bool IsRecording - { - get { return RecordingTime.HasValue; } - } + public bool IsRecording => RecordingStart.HasValue; + + [JsonIgnore] + public DateTime? RecordingStart { get; set; } - public TimeSpan? RecordingTime + [JsonIgnore] + public IEnumerable IsRecordingPossible { - get - { - if (recordingStart.HasValue == true) + get + { + var errors = new List(); + if (IsRecording) { - return DateTime.UtcNow - recordingStart; + errors.Add("Record is already running!"); } - else + if (Tracks.Count != 0) + { + errors.Add("Cuesheet already contains tracks!"); + } + if (Audiofile?.IsRecorded == true) { - return null; + errors.Add("A recording is already available!"); } + return errors; } } @@ -187,30 +170,24 @@ private set } } - [JsonIgnore] - public TraceChangeManager? TraceChangeManager { get; } = traceChangeManager; - public CuesheetSection AddSection() { var previousValue = new List(sections); var section = new CuesheetSection(this); sections.Add(section); - SectionAdded?.Invoke(this, new CuesheetSectionAddRemoveEventArgs(section)); OnTraceablePropertyChanged(previousValue, nameof(Sections)); return section; } - public void RemoveSection(CuesheetSection section) + public void RemoveSections(IEnumerable sectionsToRemove) { var previousValue = new List(sections); - if (sections.Remove(section)) - { - OnTraceablePropertyChanged(previousValue, nameof(Sections)); - SectionRemoved?.Invoke(this, new CuesheetSectionAddRemoveEventArgs(section)); - } + var intersection = sections.Intersect(sectionsToRemove); + sections = [.. sections.Except(intersection)]; + OnTraceablePropertyChanged(previousValue, nameof(Sections)); } - public CuesheetSection? GetSectionAtTrack(Track track) + public CuesheetSection? GetSection(Track track) { return Sections?.FirstOrDefault(x => track.Begin <= x.Begin && track.End >= x.Begin); } @@ -234,7 +211,7 @@ public void RemoveSection(CuesheetSection section) return previousLinkedTrack; } - public void AddTrack(Track track, ApplicationOptions? applicationOptions = null, RecordOptions? recordOptions = null) + public void AddTrack(Track track) { if (track.IsCloned) { @@ -242,25 +219,17 @@ public void AddTrack(Track track, ApplicationOptions? applicationOptions = null, } var previousValue = new List(tracks); track.IsLinkedToPreviousTrackChanged += Track_IsLinkedToPreviousTrackChanged; - if (IsRecording && recordingStart.HasValue) - { - ArgumentNullException.ThrowIfNull(recordOptions); - track.Begin = CalculateTimeSpanWithSensitivity(DateTime.UtcNow - recordingStart.Value, recordOptions.RecordTimeSensitivity); - } - //When no applications are available (because of used by import for example) we don't try to calculate properties - if (applicationOptions != null) + if (IsRecording && RecordingStart.HasValue) { - track.IsLinkedToPreviousTrack = applicationOptions.LinkTracksWithPreviousOne; + track.Begin = DateTime.UtcNow - RecordingStart.Value; } + //Fire the event manually since we don't know if the track is already linked to previous one + Track_IsLinkedToPreviousTrackChanged(track, EventArgs.Empty); tracks.Add(track); track.Cuesheet = this; - ReCalculateTrackProperties(track); + RecalculateTrackProperties(track); track.RankPropertyValueChanged += Track_RankPropertyValueChanged; OnTraceablePropertyChanged(previousValue, nameof(Tracks)); - if (IsImporting == false) - { - TracksAdded?.Invoke(this, new TracksAddedRemovedEventArgs([track])); - } } public void RemoveTrack(Track track) @@ -299,7 +268,6 @@ public void RemoveTrack(Track track) } RecalculateLastTrackEnd(); OnTraceablePropertyChanged(previousValue, nameof(Tracks)); - TracksRemoved?.Invoke(this, new TracksAddedRemovedEventArgs([track])); } /// @@ -313,7 +281,7 @@ public void RemoveTracks(IReadOnlyCollection tracksToRemove) tracks.ForEach(x => x.RankPropertyValueChanged -= Track_RankPropertyValueChanged); tracks.ForEach(x => x.IsLinkedToPreviousTrackChanged -= Track_IsLinkedToPreviousTrackChanged); var intersection = tracks.Intersect(tracksToRemove); - tracks = tracks.Except(intersection).ToList(); + tracks = [.. tracks.Except(intersection)]; foreach (var track in tracks) { if (track.IsLinkedToPreviousTrack) @@ -333,79 +301,100 @@ public void RemoveTracks(IReadOnlyCollection tracksToRemove) tracks.ForEach(x => x.IsLinkedToPreviousTrackChanged += Track_IsLinkedToPreviousTrackChanged); RecalculateLastTrackEnd(); OnTraceablePropertyChanged(previousValue, nameof(Tracks)); - TracksRemoved?.Invoke(this, new TracksAddedRemovedEventArgs(intersection)); } - - public Boolean MoveTrackPossible(Track track, MoveDirection moveDirection) + public Boolean MoveTracksPossible(IEnumerable tracksToMove, MoveDirection moveDirection) { - Boolean movePossible = false; lock (syncLock) { - var index = Tracks.ToList().IndexOf(track); + var trackIndices = tracksToMove.Select(t => tracks.IndexOf(t)).Where(i => i >= 0).OrderBy(i => i).ToList(); + + if (trackIndices.Count == 0) + { + return false; + } + if (moveDirection == MoveDirection.Up) { - if (index > 0) - { - movePossible = true; - } + return trackIndices.First() > 0; } if (moveDirection == MoveDirection.Down) { - if ((index + 1) < Tracks.Count) - { - movePossible = true; - } + return trackIndices.Last() < Tracks.Count - 1; } + + return false; } - return movePossible; } - - public void MoveTrack(Track track, MoveDirection moveDirection) + public void MoveTracks(IEnumerable tracksToMove, MoveDirection moveDirection) { - var index = tracks.IndexOf(track); - Track? currentTrack = null; - switch (moveDirection) + lock (syncLock) { - case MoveDirection.Up: - if (index > 0) + if (!MoveTracksPossible(tracksToMove, moveDirection)) + { + return; + } + var trackIndices = tracksToMove.Select(t => tracks.IndexOf(t)).Where(i => i >= 0).OrderBy(i => i).ToList(); + + var previousValue = new List(Tracks); + + if (moveDirection == MoveDirection.Up) + { + foreach (var index in trackIndices) { - currentTrack = tracks.ElementAt(index - 1); + if (index > 0) + { + SwitchTracks(tracks[index], tracks[index - 1]); + } } - break; - case MoveDirection.Down: - if ((index + 1) < Tracks.Count) + } + else if (moveDirection == MoveDirection.Down) + { + for (int i = trackIndices.Count - 1; i >= 0; i--) { - currentTrack = tracks.ElementAt(index + 1); + int index = trackIndices[i]; + if (index < Tracks.Count - 1) + { + SwitchTracks(tracks[index], tracks[index + 1]); + } } - break; - default: - throw new ArgumentException("Invalid enum value for MoveDirection!", nameof(moveDirection)); - } - if (currentTrack != null) - { - var previousValue = new List(tracks); - SwitchTracks(track, currentTrack); + } + OnTraceablePropertyChanged(previousValue, nameof(Tracks)); } } public void StartRecording() { - recordingStart = DateTime.UtcNow; + if (IsRecordingPossible.Any() == false) + { + RecordingStart = DateTime.UtcNow; + IsRecordingChanged?.Invoke(this, EventArgs.Empty); + } } - public void StopRecording(RecordOptions recordOptions) + public void StopRecording() { //Set end of last track var lastTrack = Tracks.LastOrDefault(); - if ((lastTrack != null) && (recordingStart.HasValue)) + if ((lastTrack != null) && (RecordingStart.HasValue)) + { + lastTrack.End = DateTime.UtcNow - RecordingStart.Value; + } + RecordingStart = null; + IsRecordingChanged?.Invoke(this, EventArgs.Empty); + } + + public void RecalculateLastTrackEnd() + { + //Try to recalculate length by recalculating last track + var lastTrack = tracks.LastOrDefault(); + if (lastTrack != null) { - lastTrack.End = CalculateTimeSpanWithSensitivity(DateTime.UtcNow - recordingStart.Value, recordOptions.RecordTimeSensitivity); + RecalculateTrackProperties(lastTrack); } - recordingStart = null; } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; @@ -475,16 +464,31 @@ protected override ValidationResult Validate(string property) validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(Title))); } break; + case nameof(Cataloguenumber): + validationStatus = ValidationStatus.Success; + if (String.IsNullOrEmpty(Cataloguenumber) == false) + { + if (Cataloguenumber.All(Char.IsDigit) == false) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} must only contain numbers!", nameof(Cataloguenumber))); + } + if (Cataloguenumber.Length != 13) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} has an invalid length. Allowed length is {1}!", nameof(Cataloguenumber), 13)); + } + } + break; } return ValidationResult.Create(validationStatus, validationMessages); } - private void ReCalculateTrackProperties(Track trackToCalculate) + private void RecalculateTrackProperties(Track trackToCalculate) { if ((Audiofile != null) && (Audiofile.Duration.HasValue) && (trackToCalculate.End.HasValue == false)) { trackToCalculate.End = Audiofile.Duration; - TraceChangeManager?.MergeLastEditWithEdit(x => x.Changes.All(y => y.TraceableObject == this && y.TraceableChange.PropertyName == nameof(Audiofile))); } if (Tracks.Count > 1) { @@ -625,11 +629,6 @@ private void OnTraceablePropertyChanged(object? previousValue, [System.Runtime.C TraceablePropertyChanged?.Invoke(this, new TraceablePropertiesChangedEventArgs(new TraceableChange(previousValue, propertyName))); } - private void Audiofile_ContentStreamLoaded(object? sender, EventArgs e) - { - RecalculateLastTrackEnd(); - } - private void SwitchTracks(Track track1, Track track2) { var indexTrack1 = tracks.IndexOf(track1); @@ -676,52 +675,15 @@ private void SwitchTracks(Track track1, Track track2) } } - private static TimeSpan CalculateTimeSpanWithSensitivity(TimeSpan inputTimeSpan, TimeSensitivityMode sensitivityMode) - { - TimeSpan timeSpan; - switch (sensitivityMode) - { - default: - case TimeSensitivityMode.Full: - timeSpan = inputTimeSpan; - break; - case TimeSensitivityMode.Seconds: - timeSpan = new TimeSpan(inputTimeSpan.Days, inputTimeSpan.Hours, inputTimeSpan.Minutes, inputTimeSpan.Seconds); - break; - case TimeSensitivityMode.Minutes: - if (inputTimeSpan.Seconds >= 30) - { - timeSpan = new TimeSpan(inputTimeSpan.Days, inputTimeSpan.Hours, inputTimeSpan.Minutes + 1, 0); - } - else - { - timeSpan = new TimeSpan(inputTimeSpan.Days, inputTimeSpan.Hours, inputTimeSpan.Minutes, 0); - } - break; - } - return timeSpan; - } - - private void RecalculateLastTrackEnd() - { - //Try to recalculate length by recalculating last track - var lastTrack = tracks.LastOrDefault(); - if (lastTrack != null) - { - ReCalculateTrackProperties(lastTrack); - } - } - /// /// Method for checking if fire of events should be done /// /// Previous value of the property firing events - /// Fire AudioFileChanged? /// Fire OnValidateablePropertyChanged? /// Fire TraceablePropertyChanged? /// Property firing the event /// If propertyName can not be found, an exception is thrown. - private void FireEvents(object? previousValue, Boolean fireAudioFileChanged = false, Boolean fireValidateablePropertyChanged = true, Boolean fireTraceablePropertyChanged = true, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + private void FireEvents(object? previousValue, Boolean fireValidateablePropertyChanged = true, Boolean fireTraceablePropertyChanged = true, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { var propertyInfo = GetType().GetProperty(propertyName); if (propertyInfo != null) @@ -729,10 +691,6 @@ private void FireEvents(object? previousValue, Boolean fireAudioFileChanged = fa var propertyValue = propertyInfo.GetValue(this); if (Equals(propertyValue, previousValue) == false) { - if (fireAudioFileChanged) - { - AudioFileChanged?.Invoke(this, EventArgs.Empty); - } if (fireValidateablePropertyChanged) { OnValidateablePropertyChanged(propertyName); diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/Flag.cs b/AudioCuesheetEditor/Model/AudioCuesheet/Flag.cs index 9e2f747c..5bb24da0 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/Flag.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/Flag.cs @@ -14,11 +14,7 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace AudioCuesheetEditor.Model.AudioCuesheet { @@ -28,19 +24,19 @@ public class Flag /// /// 4CH (Four channel audio) /// - public static readonly Flag FourCH = new("4CH", "4CH"); + public static readonly Flag FourCH = new("4 channel audio (4CH)", "4CH"); /// /// DCP (Digital copy permitted) /// - public static readonly Flag DCP = new("DCP", "DCP"); + public static readonly Flag DCP = new("Digital copy permitted (DCP)", "DCP"); /// /// PRE (Pre-emphasis enabled) /// - public static readonly Flag PRE = new("PRE", "PRE"); + public static readonly Flag PRE = new("Pre-emphasis enabled (PRE)", "PRE"); /// /// SCMS (Serial copy management system) /// - public static readonly Flag SCMS = new("SCMS", "SCMS"); + public static readonly Flag SCMS = new("Serial copy management system (SCMS)", "SCMS"); public static readonly IReadOnlyCollection AvailableFlags; static Flag() diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/ICuesheet.cs b/AudioCuesheetEditor/Model/AudioCuesheet/ICuesheet.cs index 02a2e007..8cdc8c4d 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/ICuesheet.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/ICuesheet.cs @@ -20,5 +20,6 @@ public interface ICuesheet { string? Artist { get; set; } string? Title { get; set; } + string? Cataloguenumber { get; set; } } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/ITrack.cs b/AudioCuesheetEditor/Model/AudioCuesheet/ITrack.cs index 6b0d6192..4fa6341e 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/ITrack.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/ITrack.cs @@ -23,9 +23,8 @@ public interface ITrack TimeSpan? Begin { get; set; } TimeSpan? End { get; set; } TimeSpan? Length { get; set; } - IReadOnlyCollection Flags { get; } + IEnumerable Flags { get; set; } TimeSpan? PreGap { get; set; } TimeSpan? PostGap { get; set; } - void SetFlags(IEnumerable flags); } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/Import/ImportTrack.cs b/AudioCuesheetEditor/Model/AudioCuesheet/Import/ImportTrack.cs index 88f9033d..3614e292 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/Import/ImportTrack.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/Import/ImportTrack.cs @@ -18,21 +18,15 @@ namespace AudioCuesheetEditor.Model.AudioCuesheet.Import { public class ImportTrack : ITrack { - private readonly List flags = []; public string? Artist { get; set; } public string? Title { get; set; } public uint? Position { get; set; } public TimeSpan? Begin { get; set; } public TimeSpan? End { get; set; } public TimeSpan? Length { get; set; } - public IReadOnlyCollection Flags => flags; + public IEnumerable Flags { get; set; } = []; public TimeSpan? PreGap { get; set; } public TimeSpan? PostGap { get; set; } public DateTime? StartDateTime { get; set; } - public void SetFlags(IEnumerable flags) - { - this.flags.Clear(); - this.flags.AddRange(flags); - } } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/Track.cs b/AudioCuesheetEditor/Model/AudioCuesheet/Track.cs index 24353e82..be873fd5 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/Track.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/Track.cs @@ -25,7 +25,7 @@ public enum SetFlagMode Remove } - public class Track : Validateable, ITraceable, ITrack + public class Track : Validateable, ITraceable, ITrack { public static readonly List AllPropertyNames = [nameof(IsLinkedToPreviousTrack), nameof(Position), nameof(Artist), nameof(Title), nameof(Begin), nameof(End), nameof(Flags), nameof(PreGap), nameof(PostGap), nameof(Length)]; @@ -34,7 +34,7 @@ public class Track : Validateable, ITraceable, ITrack private String? title; private TimeSpan? begin; private TimeSpan? end; - private TimeSpan? _length; + private TimeSpan? length; private List flags = []; private Boolean isLinkedToPreviousTrack; private Cuesheet? cuesheet; @@ -115,7 +115,7 @@ public TimeSpan? Length } else { - return _length; + return length; } } set @@ -149,18 +149,20 @@ public TimeSpan? Length } else { - _length = value; + length = value; } FireEvents(previousValue, fireRankPropertyValueChanged: false, fireTraceablePropertyChanged: false); } } [JsonInclude] - public IReadOnlyCollection Flags + public IEnumerable Flags { get { return flags.AsReadOnly(); } - private set + set { + var previousValue = flags; flags = [.. value]; + FireEvents(previousValue, fireValidateablePropertyChanged: false, fireRankPropertyValueChanged: false, propertyName: nameof(Flags)); } } [JsonIgnore] @@ -322,7 +324,7 @@ public void CopyValues(ITrack track, Boolean setCuesheet = true, Boolean setIsLi } else { - SetFlags(track.Flags); + Flags = track.Flags; } } if (setPreGap) @@ -360,29 +362,7 @@ public void CopyValues(ITrack track, Boolean setCuesheet = true, Boolean setIsLi } } - /// - public void SetFlag(Flag flag, SetFlagMode flagMode) - { - var previousValue = flags; - if ((flagMode == SetFlagMode.Add) && (Flags.Contains(flag) == false)) - { - flags.Add(flag); - } - if ((flagMode == SetFlagMode.Remove) && Flags.Contains(flag)) - { - flags.Remove(flag); - } - OnTraceablePropertyChanged(previousValue); - } - - /// - public void SetFlags(IEnumerable flags) - { - this.flags.Clear(); - this.flags.AddRange(flags); - } - - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; diff --git a/AudioCuesheetEditor/Model/Entity/IValidateable.cs b/AudioCuesheetEditor/Model/Entity/IValidateable.cs index acb54ae5..4d88ec34 100644 --- a/AudioCuesheetEditor/Model/Entity/IValidateable.cs +++ b/AudioCuesheetEditor/Model/Entity/IValidateable.cs @@ -13,13 +13,6 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using AudioCuesheetEditor.Model.AudioCuesheet; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; - namespace AudioCuesheetEditor.Model.Entity { public interface IValidateable @@ -29,20 +22,15 @@ public interface IValidateable /// /// Validation result. ValidationResult Validate(); - } - public interface IValidateable : IValidateable - { /// - /// Validate a property and return the result of validation. + /// Validates the property and return the result of validation. /// - /// Property type - /// Property selector - /// Validation result. - ValidationResult Validate(Expression> expression); + /// + /// + ValidationResult Validate(String property); - public event EventHandler? ValidateablePropertyChanged; + event EventHandler? ValidateablePropertyChanged; } - public enum ValidationStatus { NoValidation, @@ -51,21 +39,26 @@ public enum ValidationStatus } public class ValidationResult { - private List? validationMessages; + private List validationMessages = []; public static ValidationResult Create(ValidationStatus validationStatus, IEnumerable? validationMessages = null) { - return new ValidationResult() { Status = validationStatus, ValidationMessages = validationMessages?.ToList() }; + var result = new ValidationResult() { Status = validationStatus }; + if (validationMessages != null) + { + result.ValidationMessages = [.. validationMessages]; + } + return result; } public ValidationStatus Status { get; set; } - public List? ValidationMessages + public ICollection ValidationMessages { get => validationMessages; set { - validationMessages = value; - if ((validationMessages != null) && validationMessages.Count != 0) + validationMessages = [.. value]; + if (validationMessages.Count > 0) { Status = ValidationStatus.Error; } diff --git a/AudioCuesheetEditor/Model/Entity/Validateable.cs b/AudioCuesheetEditor/Model/Entity/Validateable.cs index d0c78b3d..5d48e438 100644 --- a/AudioCuesheetEditor/Model/Entity/Validateable.cs +++ b/AudioCuesheetEditor/Model/Entity/Validateable.cs @@ -13,42 +13,22 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using System.Linq.Expressions; using System.Reflection; namespace AudioCuesheetEditor.Model.Entity { - public abstract class Validateable : IValidateable + public abstract class Validateable : IValidateable { public event EventHandler? ValidateablePropertyChanged; - public ValidationResult Validate(Expression> expression) - { - if (expression.Body is MemberExpression memberExpression) - { - return Validate(memberExpression.Member.Name); - } - else if (expression.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression unaryMemberExpression) - { - return Validate(unaryMemberExpression.Member.Name); - } - else - { - throw new ArgumentException("The provided expression does not reference a valid property."); - } - } - public ValidationResult Validate() { ValidationResult validationResult = new() { Status = ValidationStatus.NoValidation }; - foreach (var property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) + List validationMessages = []; + foreach (var property in this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) { var result = Validate(property.Name); - if (result.ValidationMessages != null) - { - validationResult.ValidationMessages ??= []; - validationResult.ValidationMessages.AddRange(result.ValidationMessages); - } + validationMessages.AddRange(result.ValidationMessages); switch (validationResult.Status) { case ValidationStatus.NoValidation: @@ -66,14 +46,15 @@ public ValidationResult Validate() break; } } + validationResult.ValidationMessages = validationMessages; return validationResult; } + public abstract ValidationResult Validate(String property); + protected void OnValidateablePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { ValidateablePropertyChanged?.Invoke(this, propertyName); } - - protected abstract ValidationResult Validate(String property); } } diff --git a/AudioCuesheetEditor/Model/Entity/ValidationMessage.cs b/AudioCuesheetEditor/Model/Entity/ValidationMessage.cs index 54f01681..06694604 100644 --- a/AudioCuesheetEditor/Model/Entity/ValidationMessage.cs +++ b/AudioCuesheetEditor/Model/Entity/ValidationMessage.cs @@ -13,7 +13,7 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using Blazorise.Localization; +using Microsoft.Extensions.Localization; namespace AudioCuesheetEditor.Model.Entity { @@ -30,8 +30,8 @@ public ValidationMessage(String message, params object[]? args) } public String Message { get; private set; } public object[]? Parameter { get; private set; } - - public String GetMessageLocalized(ITextLocalizer localizer) + + public String GetMessageLocalized(IStringLocalizer localizer) { object[]? arguments = null; if (Parameter != null) @@ -48,8 +48,9 @@ public String GetMessageLocalized(ITextLocalizer localizer) arguments[i] = localizer[(String)arguments[i]]; } } + return localizer[Message, arguments]; } - return localizer[Message, arguments]; + return localizer[Message]; } } } diff --git a/AudioCuesheetEditor/Model/Entity/ValidationMessage.de.resx b/AudioCuesheetEditor/Model/Entity/ValidationMessage.de.resx new file mode 100644 index 00000000..aa9e152e --- /dev/null +++ b/AudioCuesheetEditor/Model/Entity/ValidationMessage.de.resx @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Künstler + + + Audiodatei + + + Beginn + + + Katalognummer + + + Cuesheetdateiname + + + Ende + + + Dateiname + + + Länge + + + Name + + + Position + + + Schema + + + Schema Cuesheet + + + Schema Fuß + + + Schema Kopf + + + Schema Titel + + + Titel + + + Titel ({0},{1},{2},{3},{4}) hat nicht die korrekte Position '{5}'! + + + Titel + + + {0} enthält keine Platzhalter! + + + {0} enthält Platzhalter '{1}' die nicht verwendet werden können! + + + {0} hat eine ungültige Länge. Erlaubte Länge ist {1}! + + + {0} hat ungültige Anzahl ({1})! + + + {0} hat keinen Wert! + + + {0} darf nicht 0 sein! + + + {0} muss größer oder gleich 0 sein! + + + {0} muss mit '{1}' enden! + + + {0} muss einen Dateinamen haben! + + + {0} darf nur Zahlen enthalten! + + + {0} muss größer oder gleich '{1}' sein! + + + {0} muss kleiner oder gleich '{1}' sein! + + + {0} {1} '{2}' wird auch von {3}({4},{5},{6},{7},{8}) genutzt. Positionen müssen eindeutig sein! + + + {0}({1},{2},{3},{4},{5}) überlappt mit {0}({6},{7},{8},{9},{10}). Bitte stellen Sie sicher, dass die Zeitspanne nur einmal genutzt wird! + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Model/Entity/ValidationMessage.resx b/AudioCuesheetEditor/Model/Entity/ValidationMessage.resx new file mode 100644 index 00000000..4e807726 --- /dev/null +++ b/AudioCuesheetEditor/Model/Entity/ValidationMessage.resx @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Artist + + + Audiofile + + + Begin + + + Cataloguenumber + + + CuesheetFilename + + + End + + + Filename + + + Length + + + Name + + + Position + + + Scheme + + + SchemeCuesheet + + + SchemeFooter + + + SchemeHead + + + SchemeTracks + + + Title + + + Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'! + + + Tracks + + + {0} contains no placeholder! + + + {0} contains placeholder '{1}' that can not be resolved! + + + {0} has an invalid length. Allowed length is {1}! + + + {0} has invalid Count ({1})! + + + {0} has no value! + + + {0} may not be 0! + + + {0} must be equal or greater zero! + + + {0} must end with '{1}'! + + + {0} must have a filename! + + + {0} must only contain numbers! + + + {0} should be greater than or equal '{1}'! + + + {0} should be less than or equal '{1}'! + + + {0} {1} '{2}' is used also by {3}({4},{5},{6},{7},{8}). Positions must be unique! + + + {0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once! + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Model/IO/Audio/Audiofile.cs b/AudioCuesheetEditor/Model/IO/Audio/Audiofile.cs index 67568106..a4b960b7 100644 --- a/AudioCuesheetEditor/Model/IO/Audio/Audiofile.cs +++ b/AudioCuesheetEditor/Model/IO/Audio/Audiofile.cs @@ -18,9 +18,9 @@ namespace AudioCuesheetEditor.Model.IO.Audio { [method: JsonConstructor] - public class Audiofile(String name, Boolean isRecorded = false) : IDisposable + public class Audiofile(String name, Boolean isRecorded = false) : IDisposable, IAudiofile { - public static readonly String RecordingFileName = "Recording.webm"; + public static readonly String RecordingFileName = $"Recording-{Guid.NewGuid()}.webm"; public static readonly AudioCodec AudioCodecWEBM = new("audio/webm", ".webm", "AudioCodec WEBM"); public static readonly List AudioCodecs = @@ -36,11 +36,13 @@ public class Audiofile(String name, Boolean isRecorded = false) : IDisposable ]; private AudioCodec? audioCodec; + private Stream? contentStream; + private String name = name; private bool disposedValue; public event EventHandler? ContentStreamLoaded; - public Audiofile(String name, String objectURL, AudioCodec? audioCodec, HttpClient httpClient, Boolean isRecorded = false) : this(name, isRecorded) + public Audiofile(String name, String objectURL, AudioCodec? audioCodec, Boolean isRecorded = false) : this(name, isRecorded) { if (String.IsNullOrEmpty(objectURL)) { @@ -48,17 +50,32 @@ public Audiofile(String name, String objectURL, AudioCodec? audioCodec, HttpClie } ObjectURL = objectURL; AudioCodec = audioCodec; - HttpClient = httpClient; } - public String Name { get; private set; } = name; + public String Name + { + get => name; + set + { + if (String.IsNullOrEmpty(value)) + { + throw new ArgumentNullException(nameof(value)); + } + var extension = Path.GetExtension(value); + if (extension.Equals(audioCodec?.FileExtension, StringComparison.CurrentCultureIgnoreCase) == false) + { + value = $"{value}{audioCodec?.FileExtension}"; + } + name = value; + } + } [JsonIgnore] public String? ObjectURL { get; private set; } /// /// Boolean indicating if the stream has fully been loaded /// [JsonIgnore] - public Boolean IsContentStreamLoaded + public Boolean IsContentStreamLoaded { get { return ContentStream != null; } } @@ -66,17 +83,28 @@ public Boolean IsContentStreamLoaded /// File content stream. Be carefull, this stream is loaded asynchronously. Connect to the StreamLoaded for checking if loading has already been done! /// [JsonIgnore] - public Stream? ContentStream { get; private set; } + public Stream? ContentStream + { + get => contentStream; + set + { + contentStream = value; + if ((ContentStream != null) && (AudioCodec != null)) + { + var track = new ATL.Track(ContentStream, AudioCodec.MimeType); + Duration = new TimeSpan(0, 0, track.Duration); + ContentStreamLoaded?.Invoke(this, EventArgs.Empty); + } + } + } [JsonIgnore] public Boolean IsRecorded { get; private set; } = isRecorded; /// /// Duration of the audio file /// public TimeSpan? Duration { get; private set; } - [JsonIgnore] - public HttpClient? HttpClient { get; set; } - public AudioCodec? AudioCodec + public AudioCodec? AudioCodec { get { return audioCodec; } private set @@ -93,7 +121,7 @@ private set [JsonIgnore] public String? AudioFileType { - get + get { String? audioFileType = null; if (AudioCodec != null) @@ -120,17 +148,6 @@ public Boolean PlaybackPossible } } - public async Task LoadContentStream() - { - if ((ContentStream == null) && (String.IsNullOrEmpty(ObjectURL) == false) && (AudioCodec != null) && (HttpClient != null)) - { - ContentStream = await HttpClient.GetStreamAsync(ObjectURL); - var track = new ATL.Track(ContentStream, AudioCodec.MimeType); - Duration = new TimeSpan(0, 0, track.Duration); - ContentStreamLoaded?.Invoke(this, EventArgs.Empty); - } - } - protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/AudioCuesheetEditor/Model/Options/ImportOptions.cs b/AudioCuesheetEditor/Model/IO/Audio/IAudiofile.cs similarity index 60% rename from AudioCuesheetEditor/Model/Options/ImportOptions.cs rename to AudioCuesheetEditor/Model/IO/Audio/IAudiofile.cs index 5131d11e..d3d88436 100644 --- a/AudioCuesheetEditor/Model/Options/ImportOptions.cs +++ b/AudioCuesheetEditor/Model/IO/Audio/IAudiofile.cs @@ -13,15 +13,20 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. - -using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Utility; - -namespace AudioCuesheetEditor.Model.Options +namespace AudioCuesheetEditor.Model.IO.Audio { - public class ImportOptions : IOptions + public interface IAudiofile { - public TextImportScheme TextImportScheme { get; set; } = TextImportScheme.DefaultTextImportScheme; - public TimeSpanFormat TimeSpanFormat { get; set; } = new(); + AudioCodec? AudioCodec { get; } + string? AudioFileType { get; } + Stream? ContentStream { get; set; } + TimeSpan? Duration { get; } + bool IsContentStreamLoaded { get; } + bool IsRecorded { get; } + string Name { get; set; } + string? ObjectURL { get; } + bool PlaybackPossible { get; } + + event EventHandler? ContentStreamLoaded; } -} +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs b/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs index 7107f45a..534b65d3 100644 --- a/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs +++ b/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs @@ -20,7 +20,7 @@ namespace AudioCuesheetEditor.Model.IO.Export { - public class CuesheetSection : Validateable, ITraceable + public class CuesheetSection : Validateable, ITraceable { private Cuesheet? cuesheet; private TimeSpan? begin; @@ -131,7 +131,7 @@ public void CopyValues(CuesheetSection splitPoint) Begin = splitPoint.Begin; } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; diff --git a/AudioCuesheetEditor/Model/IO/Export/ExportfileGenerator.cs b/AudioCuesheetEditor/Model/IO/Export/ExportfileGenerator.cs deleted file mode 100644 index 8190dc1f..00000000 --- a/AudioCuesheetEditor/Model/IO/Export/ExportfileGenerator.cs +++ /dev/null @@ -1,338 +0,0 @@ -//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.Entity; -using AudioCuesheetEditor.Model.IO.Audio; -using AudioCuesheetEditor.Model.Options; -using System.Data; -using System.Diagnostics.Metrics; -using System.Text; -using System.Xml.Linq; - -namespace AudioCuesheetEditor.Model.IO.Export -{ - public enum ExportType - { - Cuesheet = 0, - Exportprofile - } - - public class ExportfileGenerator(ExportType exportType, Cuesheet cuesheet, Exportprofile? exportprofile = null, ApplicationOptions? applicationOptions = null) : Validateable - { - public Cuesheet Cuesheet { get; } = cuesheet; - public Exportprofile? Exportprofile { get; set; } = exportprofile; - public ApplicationOptions? ApplicationOptions { get; set; } = applicationOptions; - public ExportType ExportType { get; set; } = exportType; - - public IReadOnlyCollection GenerateExportfiles() - { - List exportfiles = []; - if (Validate().Status != ValidationStatus.Error) - { - if (Cuesheet.Sections.Count != 0) - { - var counter = 1; - String? content = null; - String filename = String.Empty; - String? audioFileName = null; - foreach (var section in Cuesheet.Sections.OrderBy(x => x.Begin)) - { - audioFileName = section.AudiofileName; - if (section.Validate().Status == ValidationStatus.Success) - { - switch (ExportType) - { - case ExportType.Cuesheet: - content = WriteCuesheet(audioFileName, section); - filename = String.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(ApplicationOptions?.CuesheetFilename), counter, FileExtensions.Cuesheet); - break; - case ExportType.Exportprofile: - if (Exportprofile != null) - { - content = WriteExport(audioFileName, section); - filename = String.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(Exportprofile.Filename), counter, Path.GetExtension(Exportprofile.Filename)); - } - break; - } - if (content != null) - { - exportfiles.Add(new Exportfile() { Name = filename, Content = Encoding.UTF8.GetBytes(content), Begin = section.Begin, End = section.End }); - } - counter++; - } - } - } - else - { - String filename = String.Empty; - String? content = null; - switch (ExportType) - { - case ExportType.Cuesheet: - var cuesheetfilename = ApplicationOptions?.CuesheetFilename; - if (String.IsNullOrEmpty(cuesheetfilename) == false) - { - filename = cuesheetfilename; - } - else - { - filename = Exportfile.DefaultCuesheetFilename; - } - if (Cuesheet.Audiofile != null) - { - content = WriteCuesheet(Cuesheet.Audiofile.Name); - } - break; - case ExportType.Exportprofile: - if (Exportprofile != null) - { - filename = Exportprofile.Filename; - if (Cuesheet.Audiofile != null) - { - content = WriteExport(Cuesheet.Audiofile.Name); - } - } - break; - } - if (content != null) - { - var begin = Cuesheet.Tracks.Min(x => x.Begin); - var end = Cuesheet.Tracks.Max(x => x.End); - exportfiles.Add(new Exportfile() { Name = filename, Content = Encoding.UTF8.GetBytes(content), Begin = begin, End = end }); - } - } - } - return exportfiles; - } - - private string WriteCuesheet(String? audiofileName, CuesheetSection? section = null) - { - var builder = new StringBuilder(); - if (Cuesheet.Cataloguenumber != null && string.IsNullOrEmpty(Cuesheet.Cataloguenumber.Value) == false && Cuesheet.Cataloguenumber.Validate().Status != ValidationStatus.Error) - { - builder.AppendLine(string.Format("{0} {1}", CuesheetConstants.CuesheetCatalogueNumber, Cuesheet.Cataloguenumber.Value)); - } - if (Cuesheet.CDTextfile != null) - { - builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetCDTextfile, Cuesheet.CDTextfile.Name)); - } - builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, section != null ? section.Title : Cuesheet.Title)); - builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, section != null ? section.Artist : Cuesheet.Artist)); - builder.AppendLine(string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, audiofileName, Cuesheet.Audiofile?.AudioFileType)); - IEnumerable tracks = Cuesheet.Tracks.OrderBy(x => x.Position); - if (section != null) - { - tracks = Cuesheet.Tracks.Where(x => x.Begin <= section.End && x.End >= section.Begin).OrderBy(x => x.Position); - } - if (tracks.Any()) - { - //Position and begin should always start from 0 even with splitpoints - int positionDifference = 1 - Convert.ToInt32(tracks.First().Position); - foreach (var track in tracks) - { - builder.AppendLine(string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position + positionDifference, CuesheetConstants.CuesheetTrackAudio)); - builder.AppendLine(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title)); - builder.AppendLine(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist)); - if (track.Flags.Count > 0) - { - builder.AppendLine(string.Format("{0}{1}{2} {3}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackFlags, string.Join(" ", track.Flags.Select(x => x.CuesheetLabel)))); - } - if (track.PreGap.HasValue) - { - builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPreGap, Math.Floor(track.PreGap.Value.TotalMinutes), track.PreGap.Value.Seconds, track.PreGap.Value.Milliseconds * 75 / 1000)); - } - if (track.Begin.HasValue) - { - var begin = track.Begin.Value; - if ((section != null) && (section.Begin.HasValue)) - { - if (section.Begin >= track.Begin) - { - begin = TimeSpan.Zero; - } - else - { - begin = track.Begin.Value - section.Begin.Value; - } - } - builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(begin.TotalMinutes), begin.Seconds, begin.Milliseconds * 75 / 1000)); - } - else - { - throw new NullReferenceException(string.Format("{0} may not be null!", nameof(Track.Begin))); - } - if (track.PostGap.HasValue) - { - builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPostGap, Math.Floor(track.PostGap.Value.TotalMinutes), track.PostGap.Value.Seconds, track.PostGap.Value.Milliseconds * 75 / 1000)); - } - } - } - return builder.ToString(); - } - - private String WriteExport(String? audiofileName, CuesheetSection? section = null) - { - var builder = new StringBuilder(); - if (Exportprofile != null) - { - var header = Exportprofile.SchemeHead - .Replace(Exportprofile.SchemeCuesheetArtist, section != null ? section.Artist : Cuesheet.Artist) - .Replace(Exportprofile.SchemeCuesheetTitle, section != null ? section.Title : Cuesheet.Title) - .Replace(Exportprofile.SchemeCuesheetAudiofile, audiofileName) - .Replace(Exportprofile.SchemeCuesheetCDTextfile, Cuesheet.CDTextfile?.Name) - .Replace(Exportprofile.SchemeCuesheetCatalogueNumber, Cuesheet.Cataloguenumber?.Value) - .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) - .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) - .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); - builder.AppendLine(header); - IEnumerable tracks = Cuesheet.Tracks.OrderBy(x => x.Position); - if (section != null) - { - tracks = Cuesheet.Tracks.Where(x => x.Begin <= section.End && x.End >= section.Begin).OrderBy(x => x.Position); - } - if (tracks.Any()) - { - //Position, Begin and End should always start from 0 even with splitpoints - int positionDifference = 1 - Convert.ToInt32(tracks.First().Position); - foreach (var track in tracks) - { - TimeSpan begin; - var end = track.End; - if (track.Begin.HasValue) - { - begin = track.Begin.Value; - if (section?.Begin != null) - { - if (section.Begin >= track.Begin) - { - begin = TimeSpan.Zero; - } - else - { - begin = track.Begin.Value - section.Begin.Value; - } - end = track.End - section.Begin.Value; - } - } - else - { - throw new NullReferenceException(string.Format("{0} may not be null!", nameof(Track.Begin))); - } - var trackLine = Exportprofile.SchemeTracks - .Replace(Exportprofile.SchemeTrackArtist, track.Artist) - .Replace(Exportprofile.SchemeTrackTitle, track.Title) - .Replace(Exportprofile.SchemeTrackPosition, (track.Position + positionDifference).ToString()) - .Replace(Exportprofile.SchemeTrackBegin, begin.ToString()) - .Replace(Exportprofile.SchemeTrackEnd, end.ToString()) - .Replace(Exportprofile.SchemeTrackLength, (end - begin).ToString()) - .Replace(Exportprofile.SchemeTrackFlags, String.Join(" ", track.Flags.Select(x => x.CuesheetLabel))) - .Replace(Exportprofile.SchemeTrackPreGap, track.PreGap != null ? track.PreGap.Value.ToString() : String.Empty) - .Replace(Exportprofile.SchemeTrackPostGap, track.PostGap != null ? track.PostGap.Value.ToString() : String.Empty) - .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) - .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) - .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); - builder.AppendLine(trackLine); - } - } - var footer = Exportprofile.SchemeFooter - .Replace(Exportprofile.SchemeCuesheetArtist, section != null ? section.Artist : Cuesheet.Artist) - .Replace(Exportprofile.SchemeCuesheetTitle, section != null ? section.Title : Cuesheet.Title) - .Replace(Exportprofile.SchemeCuesheetAudiofile, audiofileName) - .Replace(Exportprofile.SchemeCuesheetCDTextfile, Cuesheet.CDTextfile?.Name) - .Replace(Exportprofile.SchemeCuesheetCatalogueNumber, Cuesheet.Cataloguenumber?.Value) - .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) - .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) - .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); - builder.AppendLine(footer); - } - return builder.ToString(); - } - - protected override ValidationResult Validate(string property) - { - ValidationResult validationResult; - switch (property) - { - case nameof(Cuesheet): - var validationResults = new Dictionary - { - { Cuesheet, Cuesheet.Validate() }, - { Cuesheet.Cataloguenumber, Cuesheet.Cataloguenumber.Validate() } - }; - foreach (var track in Cuesheet.Tracks) - { - validationResults.Add(track, track.Validate()); - } - if (validationResults.Any(x => x.Value.Status == ValidationStatus.Error)) - { - var messages = validationResults.Values.Where(x => x.ValidationMessages != null).SelectMany(x => x.ValidationMessages!); - validationResult = ValidationResult.Create(ValidationStatus.Error, messages); - } - else - { - validationResult = ValidationResult.Create(ValidationStatus.Success); - } - break; - case nameof(ApplicationOptions): - if (ExportType == ExportType.Cuesheet) - { - if (ApplicationOptions == null) - { - var validationMessages = new List() - { - new("{0} has no value!", nameof(ApplicationOptions)) - }; - validationResult = ValidationResult.Create(ValidationStatus.Error, validationMessages); - } - else - { - validationResult = ApplicationOptions.Validate(x => x.CuesheetFilename); - } - } - else - { - validationResult = ValidationResult.Create(ValidationStatus.NoValidation); - } - break; - case nameof(Exportprofile): - if (ExportType == ExportType.Exportprofile) - { - if (Exportprofile != null) - { - validationResult = Exportprofile.Validate(); - } - else - { - var validationMessages = new List() - { - new("{0} has no value!", nameof(Exportprofile)) - }; - validationResult = ValidationResult.Create(ValidationStatus.Error, validationMessages); - } - } - else - { - validationResult = ValidationResult.Create(ValidationStatus.NoValidation); - } - break; - default: - validationResult = ValidationResult.Create(ValidationStatus.NoValidation); - break; - } - return validationResult; - } - } -} diff --git a/AudioCuesheetEditor/Model/IO/Export/Exportprofile.cs b/AudioCuesheetEditor/Model/IO/Export/Exportprofile.cs index a7477a50..1b044244 100644 --- a/AudioCuesheetEditor/Model/IO/Export/Exportprofile.cs +++ b/AudioCuesheetEditor/Model/IO/Export/Exportprofile.cs @@ -18,7 +18,7 @@ namespace AudioCuesheetEditor.Model.IO.Export { - public class Exportprofile : Validateable + public class Exportprofile : Validateable { public static readonly String DefaultFileName = "Export.txt"; @@ -136,7 +136,7 @@ public String Filename set { filename = value; OnValidateablePropertyChanged(); } } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; diff --git a/AudioCuesheetEditor/Model/IO/FileExtensions.cs b/AudioCuesheetEditor/Model/IO/FileExtensions.cs index d4b76535..439bf6df 100644 --- a/AudioCuesheetEditor/Model/IO/FileExtensions.cs +++ b/AudioCuesheetEditor/Model/IO/FileExtensions.cs @@ -20,5 +20,6 @@ public class FileExtensions public const string Text = ".txt"; public const string Projectfile = ".ace"; public const string Cuesheet = ".cue"; + public const string CDTextfile = ".cdt"; } } diff --git a/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs b/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs index a6edaf11..4970cb24 100644 --- a/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs +++ b/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs @@ -20,5 +20,6 @@ public static class FileMimeTypes public const string Text = "text/plain"; public const string Projectfile = "text/*"; public const string Cuesheet = "text/*"; + public const string CDTextfile = "text/*"; } } diff --git a/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs b/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs index f5d3e865..e63c3fce 100644 --- a/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs +++ b/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs @@ -16,64 +16,22 @@ using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.Entity; -using Blazorise.Localization; namespace AudioCuesheetEditor.Model.IO.Import { - public class TextImportScheme : Validateable + public class TextImportScheme : Validateable { - public const String EnterRegularExpressionHere = "ENTER REGULAR EXPRESSION HERE"; - - public static readonly IReadOnlyDictionary AvailableSchemeCuesheet; - public static readonly IReadOnlyDictionary AvailableSchemesTrack; - - public static ITextLocalizer? TextLocalizer { get; set; } + public static readonly IEnumerable AvailableSchemeCuesheet; + public static readonly IEnumerable AvailableSchemesTrack; static TextImportScheme() { - var schemeCuesheetArtist = String.Format("(?'{0}.{1}'{2})", nameof(Cuesheet), nameof(Cuesheet.Artist), EnterRegularExpressionHere); - var schemeCuesheetTitle = String.Format("(?'{0}.{1}'{2})", nameof(Cuesheet), nameof(Cuesheet.Title), EnterRegularExpressionHere); - var schemeCuesheetAudioFile = String.Format("(?'{0}.{1}'{2})", nameof(Cuesheet), nameof(Cuesheet.Audiofile), EnterRegularExpressionHere); - var schemeCuesheetCDTextfile = String.Format("(?'{0}.{1}'{2})", nameof(Cuesheet), nameof(Cuesheet.CDTextfile), EnterRegularExpressionHere); - var schemeCuesheetCatalogueNumber = String.Format("(?'{0}.{1}'{2})", nameof(Cuesheet), nameof(Cuesheet.Cataloguenumber), EnterRegularExpressionHere); - - AvailableSchemeCuesheet = new Dictionary - { - {nameof(Cuesheet.Artist), schemeCuesheetArtist }, - {nameof(Cuesheet.Title), schemeCuesheetTitle }, - {nameof(Cuesheet.Audiofile), schemeCuesheetAudioFile }, - {nameof(Cuesheet.Cataloguenumber), schemeCuesheetCatalogueNumber }, - {nameof(Cuesheet.CDTextfile), schemeCuesheetCDTextfile } - }; - - var schemeTrackArtist = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Artist), EnterRegularExpressionHere); - var schemeTrackTitle = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Title), EnterRegularExpressionHere); - var schemeTrackBegin = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Begin), EnterRegularExpressionHere); - var schemeTrackEnd = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.End), EnterRegularExpressionHere); - var schemeTrackLength = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Length), EnterRegularExpressionHere); - var schemeTrackPosition = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Position), EnterRegularExpressionHere); - var schemeTrackFlags = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.Flags), EnterRegularExpressionHere); - var schemeTrackPreGap = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.PreGap), EnterRegularExpressionHere); - var schemeTrackPostGap = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(Track.PostGap), EnterRegularExpressionHere); - var schemeTrackStartDateTime = String.Format("(?'{0}.{1}'{2})", nameof(Track), nameof(ImportTrack.StartDateTime), EnterRegularExpressionHere); - - AvailableSchemesTrack = new Dictionary - { - { nameof(Track.Position), schemeTrackPosition }, - { nameof(Track.Artist), schemeTrackArtist }, - { nameof(Track.Title), schemeTrackTitle }, - { nameof(Track.Begin), schemeTrackBegin }, - { nameof(Track.End), schemeTrackEnd }, - { nameof(Track.Length), schemeTrackLength }, - { nameof(Track.Flags), schemeTrackFlags }, - { nameof(Track.PreGap), schemeTrackPreGap }, - { nameof(Track.PostGap), schemeTrackPostGap }, - { nameof(ImportTrack.StartDateTime), schemeTrackStartDateTime } - }; + AvailableSchemeCuesheet = [nameof(Cuesheet.Artist), nameof(Cuesheet.Title), nameof(Cuesheet.Audiofile), nameof(Cuesheet.CDTextfile), nameof(Cuesheet.Cataloguenumber)]; + AvailableSchemesTrack = [nameof(Track.Artist), nameof(Track.Title), nameof(Track.Begin), nameof(Track.End), nameof(Track.Length), nameof(Track.Position), nameof(Track.Flags), nameof(Track.PreGap), nameof(Track.PostGap), nameof(ImportTrack.StartDateTime)]; } - public static readonly String DefaultSchemeCuesheet = @"(?'Cuesheet.Artist'\A.*) - (?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.Audiofile'.{1,})"; - public static readonly String DefaultSchemeTracks = @"(?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,'*-?:]{1,}) - (?'Track.Title'[a-zA-Z0-9_ .();äöü&'*-?:]{1,})\t{0,}(?'Track.End'.{1,})"; + public static readonly String DefaultSchemeCuesheet = @"(?'Artist'\w*) - (?'Title'\w*)\t{1,}(?'Audiofile'.*)"; + public static readonly String DefaultSchemeTracks = @"(?'Artist'.+?) - (?'Title'.+?)\s*\t+(?'End'.+)"; public static readonly TextImportScheme DefaultTextImportScheme = new() { @@ -107,52 +65,51 @@ public String? SchemeCuesheet OnValidateablePropertyChanged(); } } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; - String enterRegularExpression = EnterRegularExpressionHere; - if (TextLocalizer != null) - { - enterRegularExpression = TextLocalizer[EnterRegularExpressionHere]; - } switch (property) { case nameof(SchemeCuesheet): validationStatus = ValidationStatus.Success; - foreach (var availableScheme in AvailableSchemesTrack) + if (String.IsNullOrEmpty(SchemeCuesheet) == false) { - if (SchemeCuesheet?.Contains(availableScheme.Value.Substring(0, availableScheme.Value.IndexOf(EnterRegularExpressionHere))) == true) + var containsPlaceHolder = false; + var enumerator = AvailableSchemeCuesheet.GetEnumerator(); + if (enumerator.MoveNext()) + { + do + { + containsPlaceHolder = SchemeCuesheet?.Contains(enumerator.Current) == true; + } while ((containsPlaceHolder == false) && (enumerator.MoveNext())); + } + if (containsPlaceHolder == false) { - var startIndex = SchemeCuesheet.IndexOf(availableScheme.Value.Substring(0, availableScheme.Value.IndexOf(EnterRegularExpressionHere))); - var realRegularExpression = SchemeCuesheet.Substring(startIndex, (SchemeCuesheet.IndexOf(')', startIndex) + 1) - startIndex); validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} contains placeholders that can not be solved! Please remove invalid placeholder '{1}'.", nameof(SchemeCuesheet), realRegularExpression)); + validationMessages.Add(new ValidationMessage("{0} contains no placeholder!", nameof(SchemeCuesheet))); } } - if (SchemeCuesheet?.Contains(enterRegularExpression) == true) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("Replace '{0}' by a regular expression!", enterRegularExpression)); - } break; case nameof(SchemeTracks): validationStatus = ValidationStatus.Success; - foreach (var availableScheme in AvailableSchemeCuesheet) + if (String.IsNullOrEmpty(SchemeTracks) == false) { - if (SchemeTracks?.Contains(availableScheme.Value.Substring(0, availableScheme.Value.IndexOf(EnterRegularExpressionHere))) == true) + var containsPlaceHolder = false; + var enumerator = AvailableSchemesTrack.GetEnumerator(); + if (enumerator.MoveNext()) + { + do + { + containsPlaceHolder = SchemeTracks?.Contains(enumerator.Current) == true; + } while ((containsPlaceHolder == false) && (enumerator.MoveNext())); + } + if (containsPlaceHolder == false) { - var startIndex = SchemeTracks.IndexOf(availableScheme.Value.Substring(0, availableScheme.Value.IndexOf(EnterRegularExpressionHere))); - var realRegularExpression = SchemeTracks.Substring(startIndex, (SchemeTracks.IndexOf(')', startIndex) + 1) - startIndex); validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} contains placeholders that can not be solved! Please remove invalid placeholder '{1}'.", nameof(SchemeTracks), realRegularExpression)); + validationMessages.Add(new ValidationMessage("{0} contains no placeholder!", nameof(SchemeTracks))); } } - if (SchemeTracks?.Contains(enterRegularExpression) == true) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("Replace '{0}' by a regular expression!", enterRegularExpression)); - } break; } return ValidationResult.Create(validationStatus, validationMessages); diff --git a/AudioCuesheetEditor/Model/IO/Projectfile.cs b/AudioCuesheetEditor/Model/IO/Projectfile.cs index da0b73e2..075cd7ec 100644 --- a/AudioCuesheetEditor/Model/IO/Projectfile.cs +++ b/AudioCuesheetEditor/Model/IO/Projectfile.cs @@ -13,7 +13,9 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. +using AudioCuesheetEditor.Extensions; using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.IO.Audio; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -27,7 +29,12 @@ public class Projectfile(Cuesheet cuesheet) public static readonly JsonSerializerOptions Options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - ReferenceHandler = ReferenceHandler.IgnoreCycles + ReferenceHandler = ReferenceHandler.IgnoreCycles, + Converters = + { + new JsonStringEnumConverter(), + new InterfaceConverter() // Add a custom converter for IAudiofile + } }; public static Cuesheet? ImportFile(byte[] fileContent) diff --git a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs index 7449b09e..39ee60c5 100644 --- a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs +++ b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs @@ -16,47 +16,41 @@ using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO; using AudioCuesheetEditor.Model.IO.Export; +using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Utility; +using AudioCuesheetEditor.Services.UI; using System.Globalization; using System.Text.Json.Serialization; namespace AudioCuesheetEditor.Model.Options { - /// - /// Enum for setting desired GUI mode - /// public enum ViewMode { - ViewModeFull = 0, - ViewModeRecord = 1, - ViewModeImport = 2 + DetailView = 0, + RecordView = 1, + ImportView = 2 } - - public enum TimeSensitivityMode - { - Full = 0, - Seconds = 1, - Minutes = 2 - } - - public class ApplicationOptions : Validateable, IOptions + public class ApplicationOptions : Validateable, IOptions { - public const String DefaultCultureName = "en-US"; - - public static IReadOnlyCollection AvailableCultures + private string? projectFilename = Projectfile.DefaultFilename; + private string? cuesheetFilename = Exportfile.DefaultCuesheetFilename; + public String? CuesheetFilename { - get + get => cuesheetFilename; + set { - var cultures = new List + if (String.IsNullOrEmpty(value) == false) { - new("en-US"), - new("de-DE") - }; - return cultures.AsReadOnly(); + var extension = Path.GetExtension(value); + if (extension?.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) + { + value = $"{value}{FileExtensions.Cuesheet}"; + } + } + cuesheetFilename = value; } } - public String? CuesheetFilename { get; set; } = Exportfile.DefaultCuesheetFilename; - public String? CultureName { get; set; } = DefaultCultureName; + public String? CultureName { get; set; } = LocalizationService.DefaultCulture; [JsonIgnore] public CultureInfo Culture { @@ -73,15 +67,15 @@ public CultureInfo Culture } } [JsonIgnore] - public ViewMode ViewMode { get; set; } - public String? ViewModename + public ViewMode ActiveTab { get; set; } + public String? ActiveTabName { - get { return Enum.GetName(typeof(ViewMode), ViewMode); } - set + get => Enum.GetName(ActiveTab); + set { if (value != null) { - ViewMode = (ViewMode)Enum.Parse(typeof(ViewMode), value); + ActiveTab = Enum.Parse(value); } else { @@ -89,13 +83,31 @@ public String? ViewModename } } } - public Boolean LinkTracksWithPreviousOne { get; set; } = true; - public String? ProjectFilename { get; set; } = Projectfile.DefaultFilename; + public String? ProjectFilename + { + get => projectFilename; + set + { + if (String.IsNullOrEmpty(value) == false) + { + var extension = Path.GetExtension(value); + if (extension?.Equals(FileExtensions.Projectfile, StringComparison.OrdinalIgnoreCase) == false) + { + value = $"{value}{FileExtensions.Projectfile}"; + } + } + projectFilename = value; + } + } public TimeSpanFormat? TimeSpanFormat { get; set; } - public Boolean TracksTableSelectionVisible { get; set; } = false; - public Boolean TracksTableHeaderPinned { get; set; } = false; + public Boolean LinkTracks { get; set; } = true; + public TextImportScheme ImportScheme { get; set; } = TextImportScheme.DefaultTextImportScheme; + public TimeSpanFormat ImportTimeSpanFormat { get; set; } = new(); + public uint RecordCountdownTimer { get; set; } = 5; + public Boolean FixedTracksTableHeader { get; set; } = false; + public String? DisplayTimeSpanFormat { get; set; } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; diff --git a/AudioCuesheetEditor/Model/Options/ExportOptions.cs b/AudioCuesheetEditor/Model/Options/ExportOptions.cs index 276e2cef..eb36939b 100644 --- a/AudioCuesheetEditor/Model/Options/ExportOptions.cs +++ b/AudioCuesheetEditor/Model/Options/ExportOptions.cs @@ -54,7 +54,7 @@ public class ExportOptions : IOptions public ExportOptions() { ExportProfiles = DefaultExportProfiles; - SelectedProfileId = ExportProfiles.First().Id; + SelectedProfileId = DefaultSelectedExportProfile.Id; } [JsonConstructor] public ExportOptions(ICollection exportProfiles, Guid? selectedProfileId = null) diff --git a/AudioCuesheetEditor/Model/Options/RecordOptions.cs b/AudioCuesheetEditor/Model/Options/RecordOptions.cs deleted file mode 100644 index 2a5f365d..00000000 --- a/AudioCuesheetEditor/Model/Options/RecordOptions.cs +++ /dev/null @@ -1,76 +0,0 @@ -//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.Entity; -using AudioCuesheetEditor.Model.IO.Audio; -using System.Text.Json.Serialization; - -namespace AudioCuesheetEditor.Model.Options -{ - public class RecordOptions : Validateable, IOptions - { - public String RecordedAudiofilename { get; set; } = Audiofile.RecordingFileName; - [JsonIgnore] - public TimeSensitivityMode RecordTimeSensitivity { get; set; } - public uint RecordCountdownTimer { get; set; } = 5; - public String? RecordTimeSensitivityname - { - get { return Enum.GetName(typeof(TimeSensitivityMode), RecordTimeSensitivity); } - set - { - if (value != null) - { - RecordTimeSensitivity = (TimeSensitivityMode)Enum.Parse(typeof(TimeSensitivityMode), value); - } - else - { - throw new ArgumentNullException(nameof(value)); - } - } - } - protected override ValidationResult Validate(string property) - { - ValidationStatus validationStatus = ValidationStatus.NoValidation; - List? validationMessages = null; - switch (property) - { - case nameof(RecordedAudiofilename): - validationStatus = ValidationStatus.Success; - if (String.IsNullOrEmpty(RecordedAudiofilename)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(RecordedAudiofilename))); - } - else - { - var extension = Path.GetExtension(RecordedAudiofilename); - if (extension.Equals(Audiofile.AudioCodecWEBM.FileExtension, StringComparison.OrdinalIgnoreCase) == false) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must end with '{1}'!", nameof(RecordedAudiofilename), Audiofile.AudioCodecWEBM.FileExtension)); - } - var filename = Path.GetFileNameWithoutExtension(RecordedAudiofilename); - if (String.IsNullOrEmpty(filename)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must have a filename!", nameof(RecordedAudiofilename))); - } - } - break; - } - return ValidationResult.Create(validationStatus, validationMessages); - } - } -} diff --git a/AudioCuesheetEditor/Model/UI/EditMultipleTracksModalResult.cs b/AudioCuesheetEditor/Model/UI/EditMultipleTracksModalResult.cs new file mode 100644 index 00000000..0f1f4e33 --- /dev/null +++ b/AudioCuesheetEditor/Model/UI/EditMultipleTracksModalResult.cs @@ -0,0 +1,46 @@ +//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.Model.UI +{ + public enum DynamicEditValue + { + EnteredValueEquals = 0, + EnteredValueAdd = 1, + EnteredValueSubstract = 2 + } + public class EditMultipleTracksModalResult(Track editedTrack, Boolean isLinkedToPreviousTrackChanged, Boolean positionChanged, Boolean artistChanged, Boolean titleChanged, Boolean beginChanged, Boolean endChanged, Boolean lengthChanged, Boolean flagsChanged, Boolean pregapChanged, Boolean postgapChanged, DynamicEditValue? positionEditMode = null, DynamicEditValue? beginEditMode = null, DynamicEditValue? endEditMode = null, DynamicEditValue? lengthEditMode = null, DynamicEditValue? pregapEditMode = null, DynamicEditValue? postgapEditMode = null) + { + public Track EditedTrack { get; } = editedTrack; + public Boolean IsLinkedToPreviousTrackChanged { get; } = isLinkedToPreviousTrackChanged; + public Boolean PositionChanged { get; } = positionChanged; + public DynamicEditValue? PositionEditMode { get; } = positionEditMode; + public Boolean ArtistChanged { get; } = artistChanged; + public Boolean TitleChanged { get; } = titleChanged; + public Boolean BeginChanged { get; } = beginChanged; + public DynamicEditValue? BeginEditMode { get; } = beginEditMode; + public Boolean EndChanged { get; } = endChanged; + public DynamicEditValue? EndEditMode { get; } = endEditMode; + public Boolean LengthChanged { get; } = lengthChanged; + public DynamicEditValue? LengthEditMode { get; } = lengthEditMode; + public Boolean FlagsChanged { get; } = flagsChanged; + public Boolean PregapChanged { get; } = pregapChanged; + public DynamicEditValue? PregapEditMode { get; } = pregapEditMode; + public Boolean PostgapChanged { get; } = postgapChanged; + public DynamicEditValue? PostgapEditMode { get; } = postgapEditMode; + } +} diff --git a/AudioCuesheetEditor/Model/UI/ITraceable.cs b/AudioCuesheetEditor/Model/UI/ITraceable.cs index 28be4040..7db690eb 100644 --- a/AudioCuesheetEditor/Model/UI/ITraceable.cs +++ b/AudioCuesheetEditor/Model/UI/ITraceable.cs @@ -13,11 +13,6 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace AudioCuesheetEditor.Model.UI { public class TraceableChange @@ -36,13 +31,9 @@ public TraceableChange(object? previousValue, string propertyName) public string PropertyName { get; } } - public class TraceablePropertiesChangedEventArgs : EventArgs + public class TraceablePropertiesChangedEventArgs(TraceableChange traceableChange) : EventArgs { - public TraceablePropertiesChangedEventArgs(TraceableChange traceableChange) - { - TraceableChange = traceableChange; - } - public TraceableChange TraceableChange { get; } + public TraceableChange TraceableChange { get; } = traceableChange; } /// diff --git a/AudioCuesheetEditor/Model/UI/ValidatorUtility.cs b/AudioCuesheetEditor/Model/UI/ValidatorUtility.cs deleted file mode 100644 index 14b80de0..00000000 --- a/AudioCuesheetEditor/Model/UI/ValidatorUtility.cs +++ /dev/null @@ -1,60 +0,0 @@ -//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.Entity; -using Blazorise; -using Blazorise.Localization; -using System.Linq.Expressions; - -namespace AudioCuesheetEditor.Model.UI -{ - public class ValidatorUtility where T : IValidateable - { - public static Task Validate(ValidatorEventArgs args, T? entity, Expression> expression, ITextLocalizer textLocalizer, CancellationToken cancellationToken) - { - if (!cancellationToken.IsCancellationRequested) - { - if (entity != null) - { - var validationResult = entity.Validate(expression); - if (!cancellationToken.IsCancellationRequested) - { - switch (validationResult.Status) - { - case Entity.ValidationStatus.NoValidation: - args.Status = Blazorise.ValidationStatus.None; - break; - case Entity.ValidationStatus.Success: - args.Status = Blazorise.ValidationStatus.Success; - break; - case Entity.ValidationStatus.Error: - args.Status = Blazorise.ValidationStatus.Error; - if (validationResult.ValidationMessages != null) - { - foreach (var validationMessage in validationResult.ValidationMessages) - { - args.ErrorText += String.Format("{0}{1}", validationMessage.GetMessageLocalized(textLocalizer), Environment.NewLine); - } - } - break; - } - } - } - } - - return Task.CompletedTask; - } - } -} diff --git a/AudioCuesheetEditor/Model/Utility/IOUtility.cs b/AudioCuesheetEditor/Model/Utility/IOUtility.cs deleted file mode 100644 index eade907c..00000000 --- a/AudioCuesheetEditor/Model/Utility/IOUtility.cs +++ /dev/null @@ -1,66 +0,0 @@ -//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 Blazorise; - -namespace AudioCuesheetEditor.Model.Utility -{ - public class IOUtility - { - public static Boolean CheckFileMimeType(IFileEntry file, String mimeType, String fileExtension) - { - Boolean fileMimeTypeMatches = false; - if ((file != null) && (String.IsNullOrEmpty(mimeType) == false) && (String.IsNullOrEmpty(fileExtension) == false)) - { - if (String.IsNullOrEmpty(file.Type) == false) - { - fileMimeTypeMatches = file.Type.Equals(mimeType, StringComparison.CurrentCultureIgnoreCase); - } - else - { - //Try to find by file extension - var extension = Path.GetExtension(file.Name); - fileMimeTypeMatches = extension.Equals(fileExtension, StringComparison.CurrentCultureIgnoreCase); - } - } - return fileMimeTypeMatches; - } - - public static Boolean CheckFileMimeTypeForAudioCodec(IFileEntry file) - { - return GetAudioCodec(file) != null; - } - - public static AudioCodec? GetAudioCodec(IFileEntry fileEntry) - { - AudioCodec? foundAudioCodec = null; - var extension = Path.GetExtension(fileEntry.Name); - // First search with mime type and file extension - var audioCodecsFound = Audiofile.AudioCodecs.Where(x => x.MimeType.Equals(fileEntry.Type, StringComparison.OrdinalIgnoreCase) && x.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)); - if (audioCodecsFound.Count() <= 1) - { - foundAudioCodec = audioCodecsFound.FirstOrDefault(); - } - else - { - // Second search with mime type or file extension - audioCodecsFound = Audiofile.AudioCodecs.Where(x => x.MimeType.Equals(fileEntry.Type, StringComparison.OrdinalIgnoreCase) || x.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)); - foundAudioCodec = audioCodecsFound.FirstOrDefault(); - } - return foundAudioCodec; - } - } -} diff --git a/AudioCuesheetEditor/Model/Utility/TimeSpanFormat.cs b/AudioCuesheetEditor/Model/Utility/TimeSpanFormat.cs index 874e3d23..6661e82c 100644 --- a/AudioCuesheetEditor/Model/Utility/TimeSpanFormat.cs +++ b/AudioCuesheetEditor/Model/Utility/TimeSpanFormat.cs @@ -14,12 +14,11 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Model.Entity; -using Blazorise.Localization; using System.Text.RegularExpressions; namespace AudioCuesheetEditor.Model.Utility { - public class TimeSpanFormat : Validateable + public class TimeSpanFormat : Validateable { public const String Days = "Days"; public const String Hours = "Hours"; @@ -27,27 +26,11 @@ public class TimeSpanFormat : Validateable public const String Seconds = "Seconds"; public const String Milliseconds = "Milliseconds"; - public const String EnterRegularExpressionHere = "ENTER REGULAR EXPRESSION HERE"; - public static readonly IReadOnlyDictionary AvailableTimespanScheme; - - public static ITextLocalizer? TextLocalizer { get; set; } + public static readonly IEnumerable AvailableTimespanScheme; static TimeSpanFormat() { - var schemeDays = String.Format("(?'{0}.{1}'{2})", nameof(TimeSpanFormat), nameof(Days), EnterRegularExpressionHere); - var schemeHours = String.Format("(?'{0}.{1}'{2})", nameof(TimeSpanFormat), nameof(Hours), EnterRegularExpressionHere); - var schemeMinutes = String.Format("(?'{0}.{1}'{2})", nameof(TimeSpanFormat), nameof(Minutes), EnterRegularExpressionHere); - var schemeSeconds = String.Format("(?'{0}.{1}'{2})", nameof(TimeSpanFormat), nameof(Seconds), EnterRegularExpressionHere); - var schemeMilliseconds = String.Format("(?'{0}.{1}'{2})", nameof(TimeSpanFormat), nameof(Milliseconds), EnterRegularExpressionHere); - - AvailableTimespanScheme = new Dictionary - { - { nameof(TimeSpanFormat.Days), schemeDays }, - { nameof(TimeSpanFormat.Hours), schemeHours }, - { nameof(TimeSpanFormat.Minutes), schemeMinutes }, - { nameof(TimeSpanFormat.Seconds), schemeSeconds }, - { nameof(TimeSpanFormat.Milliseconds), schemeMilliseconds } - }; + AvailableTimespanScheme = [Days, Hours, Minutes, Seconds, Milliseconds]; } public event EventHandler? SchemeChanged; @@ -68,40 +51,32 @@ public String? Scheme public TimeSpan? ParseTimeSpan(String input) { TimeSpan? timespan = null; - if (String.IsNullOrEmpty(Scheme) == false) + if ((String.IsNullOrEmpty(Scheme) == false) && (String.IsNullOrEmpty(input) == false)) { - var match = Regex.Match(input, Scheme.Replace(String.Format("{0}.", nameof(TimeSpanFormat)), "")); + var pattern = CreateFlexibleRegexPattern(Scheme); + var match = Regex.Match(input, pattern); if (match.Success) { - int days = 0; - int hours = 0; - int minutes = 0; - int seconds = 0; - int milliseconds = 0; - for (int groupCounter = 1; groupCounter < match.Groups.Count; groupCounter++) + int days = 0, hours = 0, minutes = 0, seconds = 0, milliseconds = 0; + foreach (var groupName in match.Groups.Keys) { - var key = match.Groups.Keys.ElementAt(groupCounter); - var group = match.Groups.GetValueOrDefault(key); - if ((group != null) && group.Success) + switch (groupName) { - switch (key) - { - case Days: - days = Convert.ToInt32(group.Value); - break; - case Hours: - hours = Convert.ToInt32(group.Value); - break; - case Minutes: - minutes = Convert.ToInt32(group.Value); - break; - case Seconds: - seconds = Convert.ToInt32(group.Value); - break; - case Milliseconds: - milliseconds = Convert.ToInt32(group.Value); - break; - } + case nameof(Days): + days = Convert.ToInt32(match.Groups[groupName].Value); + break; + case nameof(Hours): + hours = Convert.ToInt32(match.Groups[groupName].Value); + break; + case nameof(Minutes): + minutes = Convert.ToInt32(match.Groups[groupName].Value); + break; + case nameof(Seconds): + seconds = Convert.ToInt32(match.Groups[groupName].Value); + break; + case nameof(Milliseconds): + milliseconds = Convert.ToInt32(match.Groups[groupName].Value); + break; } } timespan = new TimeSpan(days, hours, minutes, seconds, milliseconds); @@ -110,15 +85,10 @@ public String? Scheme return timespan; } - protected override ValidationResult Validate(string property) + public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; List? validationMessages = null; - var enterRegularExpression = EnterRegularExpressionHere; - if (TextLocalizer != null) - { - enterRegularExpression = TextLocalizer[EnterRegularExpressionHere]; - } switch (property) { case nameof(Scheme): @@ -131,18 +101,27 @@ protected override ValidationResult Validate(string property) && (Scheme.Contains(Seconds) == false) && (Scheme.Contains(Milliseconds) == false)) { - validationMessages ??= new(); + validationMessages ??= []; validationMessages.Add(new ValidationMessage("{0} contains no placeholder!", nameof(Scheme))); } - if (Scheme.Contains(enterRegularExpression)) - { - validationMessages ??= new(); - validationMessages.Add(new ValidationMessage("Replace '{0}' by a regular expression!", enterRegularExpression)); - } } break; } return ValidationResult.Create(validationStatus, validationMessages); } + + private static string CreateFlexibleRegexPattern(string scheme) + { + var regex = Regex.Escape(scheme); + + regex = regex.Replace(Days, $"(?<{nameof(Days)}>\\d+)"); + regex = regex.Replace(Hours, $"(?<{nameof(Hours)}>\\d+)"); + regex = regex.Replace(Minutes, $"(?<{nameof(Minutes)}>\\d+)"); + regex = regex.Replace(Seconds, $"(?<{nameof(Seconds)}>\\d+)"); + regex = regex.Replace(Milliseconds, $"(?<{nameof(Milliseconds)}>\\d+)"); + regex = regex.Replace(@"\\:", @"[\s|:.,-]?"); + + return $"^{regex}$"; + } } } diff --git a/AudioCuesheetEditor/Pages/About.de.resx b/AudioCuesheetEditor/Pages/About.de.resx new file mode 100644 index 00000000..9a1a11c8 --- /dev/null +++ b/AudioCuesheetEditor/Pages/About.de.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Über AudioCuesheetEditor + + + Spenden Sie für dieses Projekt + + + Lizenz + + + Vorschauumgebung + + + Projekt website + + + Version + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/About.razor b/AudioCuesheetEditor/Pages/About.razor index 1f0cd627..1ec14e9c 100644 --- a/AudioCuesheetEditor/Pages/About.razor +++ b/AudioCuesheetEditor/Pages/About.razor @@ -15,46 +15,37 @@ You should have received a copy of the GNU General Public License along with Foobar. If not, see . --> +@inherits BaseLocalizedComponent @page "/about" -@inject HttpClient _httpClient -@inject ITextLocalizer _localizer +@layout MainLayoutWithoutMenu - - - @_localizer["About AudioCuesheetEditor"] - @_localizer["Version"]: @VersionString - @_localizer["Project url"]: https://www.github.com/NeoCoderMatrix86/AudioCuesheetEditor - @_localizer["Preview environment"]: https://preview-audiocuesheeteditor.netlify.app/ - @_localizer["Donate for this project"] -
-
- @_localizer["Licence"] - @((MarkupString)licence) -
-
+@inject HttpClient _httpClient +@inject IStringLocalizer _localizer + +@_localizer["About AudioCuesheetEditor"] +@_localizer["Version"]: @VersionString +@_localizer["Project url"]: https://www.github.com/NeoCoderMatrix86/AudioCuesheetEditor +@_localizer["Preview environment"]: https://preview-audiocuesheeteditor.netlify.app/ +@_localizer["Donate for this project"] +
+
+@_localizer["Licence"] +@if (licence != null) +{ + @((MarkupString)licence) +} @code { - [CascadingParameter] - public MainLayout? mainLayout { get; set; } - - String licence = default!; + String? licence; protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); var licenceContent = await _httpClient.GetStringAsync("Licence.txt"); licence = Markdown.ToHtml(licenceContent); } - protected override async Task OnParametersSetAsync() - { - await base.OnParametersSetAsync(); - if (mainLayout != null) - { - mainLayout.SetDisplayMenuBar(false); - } - } - public String VersionString { get diff --git a/AudioCuesheetEditor/Pages/About.resx b/AudioCuesheetEditor/Pages/About.resx new file mode 100644 index 00000000..6e9142a3 --- /dev/null +++ b/AudioCuesheetEditor/Pages/About.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + About AudioCuesheetEditor + + + Donate for this project + + + Licence + + + Preview environment + + + Project url + + + Version + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/EditSections.razor b/AudioCuesheetEditor/Pages/EditSections.razor deleted file mode 100644 index 24c721cb..00000000 --- a/AudioCuesheetEditor/Pages/EditSections.razor +++ /dev/null @@ -1,321 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject SessionStateContainer _sessionStateContainer -@inject ITextLocalizer _validationMessageLocalizer -@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser -@inject ITextLocalizerService _localizationService -@inject TraceChangeManager _traceChangeManager -@inject IJSRuntime _jsRuntime - - - @if (Cuesheet != null) - { - @if (_sessionStateContainer.CurrentViewMode == ViewMode.ViewModeFull) - { - - - - - -
- } - - - - @_localizer["Controls"] - @_localizer["Begin"] - @_localizer["End"] - @_localizer["CD artist"] - @_localizer["CD title"] - @_localizer["Audiofile name"] - - - - @foreach (var cuesheetSection in Cuesheet.Sections) - { - - - -
- - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @if (String.IsNullOrEmpty(cuesheetSection.AudiofileName)) - { - x.MimeType))" Changed="(args) => OnSectionAudiofileChanged(args, cuesheetSection)" AutoReset="false"> - - - - - } - else - { - - - - - - - - - - - - - } - - -
- } -
-
- } -
- - - -@code { - Validations? validations; - ModalDialog? modalDialog; - Validation? audiofileValidation; - Dictionary fileEditAudiofileIds = new(); - - public Cuesheet? Cuesheet - { - get - { - Cuesheet? cuesheet; - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeImport: - cuesheet = _sessionStateContainer.ImportCuesheet; - break; - default: - cuesheet = _sessionStateContainer.Cuesheet; - break; - } - return cuesheet; - } - } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - if (Cuesheet != null) - { - foreach (var track in Cuesheet.Tracks) - { - track.ValidateablePropertyChanged -= Track_ValidateablePropertyChanged; - } - Cuesheet.TracksAdded -= Cuesheet_TracksAdded; - Cuesheet.TracksRemoved -= Cuesheet_TracksRemoved; - } - _traceChangeManager.UndoDone -= TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone -= TraceChangeManager_RedoDone; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - } - - protected override void OnInitialized() - { - base.OnInitialized(); - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - if (Cuesheet != null) - { - Cuesheet.TracksAdded += Cuesheet_TracksAdded; - Cuesheet.TracksRemoved += Cuesheet_TracksRemoved; - } - _traceChangeManager.UndoDone += TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone += TraceChangeManager_RedoDone; - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll(); - } - - Task AddSectionClicked() - { - if (Cuesheet != null) - { - var section = Cuesheet.AddSection(); - _traceChangeManager.TraceChanges(section); - fileEditAudiofileIds.Add(section, Guid.NewGuid()); - } - return Task.CompletedTask; - } - - Task DeleteSectionClicked(CuesheetSection section) - { - if (Cuesheet != null) - { - Cuesheet.RemoveSection(section); - } - return Task.CompletedTask; - } - - void Cuesheet_TracksAdded(object? sender, TracksAddedRemovedEventArgs args) - { - foreach (var track in args.Tracks) - { - track.ValidateablePropertyChanged += Track_ValidateablePropertyChanged; - } - validations?.ValidateAll().GetAwaiter().GetResult(); - } - - void Cuesheet_TracksRemoved(object? sender, TracksAddedRemovedEventArgs args) - { - foreach (var track in args.Tracks) - { - track.ValidateablePropertyChanged -= Track_ValidateablePropertyChanged; - } - validations?.ValidateAll().GetAwaiter().GetResult(); - } - - void Track_ValidateablePropertyChanged(object? sender, string property) - { - switch (property) - { - case nameof(Track.Begin): - case nameof(Track.End): - validations?.ValidateAll().GetAwaiter().GetResult(); - break; - } - } - - void TraceChangeManager_UndoDone(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll().GetAwaiter().GetResult(); - } - - void TraceChangeManager_RedoDone(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll().GetAwaiter().GetResult(); - } - - void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - async Task OnSectionAudiofileChangedClicked(CuesheetSection section) - { - section.AudiofileName = null; - StateHasChanged(); - await Task.Delay(1); - await _jsRuntime.InvokeVoidAsync("triggerClick", fileEditAudiofileIds[section]); - } - - async Task OnSectionAudiofileChanged(FileChangedEventArgs e, CuesheetSection section) - { - if (e.Files.FirstOrDefault() != null) - { - if (Cuesheet != null) - { - var file = e.Files.First(); - if (IOUtility.CheckFileMimeTypeForAudioCodec(file) == true) - { - section.AudiofileName = file.Name; - } - else - { - if (modalDialog != null) - { - modalDialog.Title = _localizer["Error"]; - modalDialog.Text = String.Format(_localizer["The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!"], file.Name, _localizer["Audiofile"]); - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Alert; - await modalDialog.ShowModal(); - } - } - } - } - if (audiofileValidation != null) - { - await audiofileValidation.ValidateAsync(); - } - } -} diff --git a/AudioCuesheetEditor/Pages/Help.razor b/AudioCuesheetEditor/Pages/Help.razor deleted file mode 100644 index fcfd2b33..00000000 --- a/AudioCuesheetEditor/Pages/Help.razor +++ /dev/null @@ -1,171 +0,0 @@ - - -@page "/Help" - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject ITextLocalizerService _localizationService - - - - @_localizer["Help"] - @_localizer["Quick links"] - @_localizer["Introduction"] -
- @_localizer["What is AudioCuesheetEditor"] -
- @_localizer["Features"] -
- @_localizer["User Interface"] -
- @_localizer["Shortcuts"] -
- @_localizer["Validation"] -
- @_localizer["Track linking"] -
- @_localizer["Create cuesheet"] -
- @_localizer["Import of files"] -
- @_localizer["Import of textfiles"] -
- @_localizer["Import of cuesheetfiles"] -
- @_localizer["Export of data"] -
- @_localizer["Sections"] -
- @_localizer["Recordmode"] -
- @_localizer["Options"] -
- @_localizer["Offline usage (progressive WebApp)"] - - @_localizer["Introduction"] - @_localizer["Scroll to top"] - - - @_localizer["What is AudioCuesheetEditor"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["What is AudioCuesheetEditor helptext"]) - - @_localizer["Features"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Features helptext"]) - - @_localizer["Validation"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Validation helptext"]) - - @_localizer["User Interface"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["User Interface helptext"]) - - @_localizer["Shortcuts"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Shortcuts helptext"]) - - @_localizer["Track linking"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Track linking helptext"]) - - @_localizer["Create cuesheet"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Create cuesheet helptext"]) - - @_localizer["Import of files"] - @_localizer["Scroll to top"] - - - @_localizer["Import of textfiles"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Import of textfiles helptext"]) - - @_localizer["Import of cuesheetfiles"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Import of cuesheetfiles helptext"]) - - @_localizer["Export of data"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Export of data helptext"]) - - @_localizer["Sections"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Sections helptext"]) - - @_localizer["Recordmode"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Recordmode helptext"]) - - @_localizer["Options"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Options helptext"]) - - @_localizer["Offline usage (progressive WebApp)"] - @_localizer["Scroll to top"] - - @((MarkupString)_localizer["Offline usage helptext"]) -
-
- -@code { - [CascadingParameter] - public MainLayout? mainLayout { get; set; } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - } - - protected override Task OnInitializedAsync() - { - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - return base.OnInitializedAsync(); - } - - protected override async Task OnParametersSetAsync() - { - await base.OnParametersSetAsync(); - if (mainLayout != null) - { - mainLayout.SetDisplayMenuBar(false); - } - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/ImportFileView.razor b/AudioCuesheetEditor/Pages/ImportFileView.razor deleted file mode 100644 index f586cccc..00000000 --- a/AudioCuesheetEditor/Pages/ImportFileView.razor +++ /dev/null @@ -1,101 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject SessionStateContainer _sessionStateContainer - - - - - - @_localizer["Preview"] - @_localizer["Edit"] - - - - - - - - @if (FileContentRecognized != null) - { -
-                            @foreach (var line in FileContentRecognized)
-                            {
-                                if (line != null)
-                                {
-                                    @((MarkupString)String.Format("{0}
", line)) - } - } -
- } -
-
- - - -
-
-
- -@code { - String selectedTab = "recognizedFilecontent"; - String? fileContent; - - public IEnumerable? FileContentRecognized => _sessionStateContainer.Importfile?.FileContentRecognized; - - public void Dispose() - { - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - } - - protected override void OnInitialized() - { - base.OnInitialized(); - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - } - - void SelectedTabChanged(string newTabName) - { - selectedTab = newTabName; - fileContent = null; - if (newTabName == "editFilecontent") - { - //Set fileContent just when component is visible in order to autosize the MemoEdit - if (_sessionStateContainer.Importfile?.FileContent != null) - { - fileContent = String.Join(Environment.NewLine, _sessionStateContainer.Importfile.FileContent); - } - } - } - - void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs e) - { - StateHasChanged(); - } - - void FileContent_TextChanged(string text) - { - var fileContentValue = text?.Split(Environment.NewLine); - if ((fileContentValue != null) && (_sessionStateContainer.Importfile != null)) - { - _sessionStateContainer.Importfile.FileContent = fileContentValue; - } - } -} diff --git a/AudioCuesheetEditor/Pages/Index.de.resx b/AudioCuesheetEditor/Pages/Index.de.resx new file mode 100644 index 00000000..f360f3ff --- /dev/null +++ b/AudioCuesheetEditor/Pages/Index.de.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Detailansicht + + + Importansicht + + + Aufnahmeansicht + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/Index.razor b/AudioCuesheetEditor/Pages/Index.razor index 5b897946..669526e2 100644 --- a/AudioCuesheetEditor/Pages/Index.razor +++ b/AudioCuesheetEditor/Pages/Index.razor @@ -15,133 +15,76 @@ You should have received a copy of the GNU General Public License along with Foobar. If not, see . --> -@implements IAsyncDisposable @page "/" -@inject IJSRuntime _jsRuntime -@inject ITextLocalizer _localizer -@inject NavigationManager _navigationManager -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ILogger _logger -@inject HotKeys _hotKeys -@inject HttpClient _httpClient -@inject SessionStateContainer _sessionStateContainer -@inject ITextLocalizerService _localizationService -@inject TraceChangeManager _traceChangeManager - - @switch (_sessionStateContainer.CurrentViewMode) +@inherits BaseLocalizedComponent + +@inject ISessionStateContainer _sessionStateContainer +@inject IStringLocalizer _localizer + + + + + + + + + + + + + + + + + + + + + +@code{ + protected override async Task OnInitializedAsync() { - case ViewMode.ViewModeRecord: - - break; - case ViewMode.ViewModeFull: - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- break; - case ViewMode.ViewModeImport: - - break; + await base.OnInitializedAsync(); + _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; + StateHasChanged(); } -
- - - -@code { - public async ValueTask DisposeAsync() + protected override void Dispose(bool disposing) { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.CurrentViewModeChanged -= CurrentViewModeChanged; - if (hotKeysContext != null) + base.Dispose(disposing); + if (disposing) { - await hotKeysContext.DisposeAsync(); + _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; } } - [CascadingParameter] - public MainLayout? mainLayout { get; set; } - - Boolean cuesheetSplitPointsVisible = false; - Boolean cuesheetDataVisible = true; - Boolean cuesheetTracksVisible = true; - - HotKeysContext? hotKeysContext; - - ModalDialog? modalDialog; - AudioPlayer? audioPlayer; - - protected override void OnInitialized() + async Task ActiveTabIndexChanged(int tabIndex) { - base.OnInitialized(); - - _logger.LogDebug("OnInitializedAsync"); - _logger.LogInformation("CultureInfo.CurrentCulture = {0}", CultureInfo.CurrentCulture); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEnterKeyDown) - .Add(ModKey.Ctrl, Key.z, () => _traceChangeManager.Undo()) - .Add(ModKey.Ctrl, Key.y, () => _traceChangeManager.Redo()); - - _sessionStateContainer.CurrentViewModeChanged += CurrentViewModeChanged; + var activeViewMode = (ViewMode)tabIndex; + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ActiveTab, activeViewMode); } - protected override void OnParametersSet() + void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) { - base.OnParametersSet(); - mainLayout?.SetDisplayMenuBar(true); + StateHasChanged(); } - async ValueTask OnEnterKeyDown() + void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) { - if ((modalDialog != null) && (modalDialog.Visible)) - { - await modalDialog.Confirm(); - } + StateHasChanged(); } - private void CurrentViewModeChanged(object? sender, EventArgs args) + ViewMode GetViewMode() { - if (audioPlayer != null) + ViewMode viewMode = ViewMode.DetailView; + if (ApplicationOptions != null) { - Task.Run(async () => await audioPlayer.Reset()); + viewMode = ApplicationOptions.ActiveTab; } - StateHasChanged(); - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); + return viewMode; } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/Index.resx b/AudioCuesheetEditor/Pages/Index.resx new file mode 100644 index 00000000..8ea0fbbc --- /dev/null +++ b/AudioCuesheetEditor/Pages/Index.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Detail view + + + Import view + + + Record view + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Pages/RecordControl.razor b/AudioCuesheetEditor/Pages/RecordControl.razor deleted file mode 100644 index a167e0fd..00000000 --- a/AudioCuesheetEditor/Pages/RecordControl.razor +++ /dev/null @@ -1,297 +0,0 @@ - -@implements IAsyncDisposable - -@inject SessionStateContainer _sessionStateContainer -@inject ITextLocalizer _localizer -@inject ITextLocalizerService _localizationService -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject HotKeys _hotKeys - -@if (_sessionStateContainer.Cuesheet.IsRecording == true) -{ - var backgroundCSS = _sessionStateContainer.Cuesheet.IsRecording ? "BackgroundBlink rounded" : "rounded"; -
- @_localizer["Record running!"] -
-} -@if ((startRecordTimer != null) && (startRecordTimer.Enabled)) -{ - - -} -
-
- - - - - @_localizer["Start record countdown timer"] - - -
- -
- @if (_sessionStateContainer.Cuesheet.RecordingTime.HasValue == true) - { - @GetTimespanAsString(_sessionStateContainer.Cuesheet.RecordingTime, true) - } - else - { - @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) - } -
-
- -
-
- - - - - - - @_localizer["Start record countdown timer"] - - - - - @_localizer["Seconds till record starts"] - - - - - - - - - - - - -@code { - Timer updateGUITimer = new Timer(300); - Timer? startRecordTimer; - DateTime recordTimerStarted; - HotKeysContext? hotKeysContext; - - RecordOptions? recordOptions; - uint recordCountdownTimer; - - ModalDialog? modalDialog; - Modal? modalInputCountdownTime; - Boolean modalInputCountdownTimeVisible = false; - - [Parameter] - public EventCallback StartRecordClicked { get; set; } - - [Parameter] - public EventCallback StopRecordClicked { get; set; } - - public async ValueTask DisposeAsync() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEnterKeyDown); - - recordOptions = await _localStorageOptionsProvider.GetOptions(); - ReadOutOptions(); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; - - InitializeUIUpdate(); - } - - void InitializeUIUpdate() - { - updateGUITimer.AutoReset = true; - updateGUITimer.Elapsed += delegate - { - StateHasChanged(); - Boolean startRecordTimeEnabled = false; - if (startRecordTimer != null) - { - startRecordTimeEnabled = startRecordTimer.Enabled; - } - if ((startRecordTimeEnabled == false) && (_sessionStateContainer.Cuesheet.IsRecording == false)) - { - updateGUITimer.Stop(); - } - }; - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - String GetTimespanAsString(TimeSpan? timeSpan, Boolean removeMilliseconds = false) - { - String resultString = String.Empty; - if ((timeSpan != null) && (timeSpan.HasValue)) - { - if (removeMilliseconds == true) - { - resultString = timeSpan.Value.Subtract(new TimeSpan(0, 0, 0, 0, timeSpan.Value.Milliseconds)).ToString(); - } - else - { - resultString = timeSpan.Value.ToString(); - } - } - return resultString; - } - - async Task StartRecordingClicked() - { - //Check for empty cuesheet and warn! - if (_sessionStateContainer.Cuesheet.Tracks.Count > 0) - { - if (modalDialog != null) - { - modalDialog.Title = _localizer["Error"]; - modalDialog.Text = _localizer["Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet."]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Alert; - await modalDialog.ShowModal(); - } - } - else - { - _sessionStateContainer.Cuesheet.StartRecording(); - updateGUITimer.Start(); - await StartRecordClicked.InvokeAsync(); - } - } - - async Task OpenCountdownModal() - { - if (modalInputCountdownTime != null) - { - await modalInputCountdownTime.Show(); - } - } - - async Task HideCountdownModal() - { - if (modalInputCountdownTime != null) - { - await modalInputCountdownTime.Hide(); - } - } - - async Task StartRecordCountdownTimer() - { - recordTimerStarted = DateTime.Now; - startRecordTimer = new Timer(recordCountdownTimer * 1000); - startRecordTimer.Elapsed += async delegate - { - await StartRecordingClicked(); - startRecordTimer.Stop(); - }; - startRecordTimer.Start(); - await _localStorageOptionsProvider.SaveOptionsValue(x => x.RecordCountdownTimer, recordCountdownTimer); - updateGUITimer.Start(); - await HideCountdownModal(); - } - - async Task StopRecordingClicked() - { - var options = await _localStorageOptionsProvider.GetOptions(); - _sessionStateContainer.Cuesheet.StopRecording(options); - await StopRecordClicked.InvokeAsync(); - } - - void LocalStorageOptionsProvider_OptionsSaved(object? sender, IOptions options) - { - if (options is RecordOptions recordingOptions) - { - recordOptions = recordingOptions; - ReadOutOptions(); - } - } - - void ReadOutOptions() - { - if (recordOptions != null) - { - recordCountdownTimer = recordOptions.RecordCountdownTimer; - } - } - - async ValueTask OnEnterKeyDown() - { - if (modalInputCountdownTimeVisible) - { - await StartRecordCountdownTimer(); - } - } - - void StopRecordCountdownTimer() - { - startRecordTimer?.Stop(); - startRecordTimer = null; - } -} diff --git a/AudioCuesheetEditor/Pages/ViewModeImport.razor b/AudioCuesheetEditor/Pages/ViewModeImport.razor deleted file mode 100644 index 8f302f3c..00000000 --- a/AudioCuesheetEditor/Pages/ViewModeImport.razor +++ /dev/null @@ -1,338 +0,0 @@ - - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject SessionStateContainer _sessionStateContainer -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ITextLocalizerService _localizationService -@inject HotKeys _hotKeys -@inject IJSRuntime _jsRuntime -@inject HttpClient _httpClient -@inject ImportManager _importManager - - - - - @_localizer["Select files"] - @_localizer["Validate"] - - - - - @_localizer["Select files for import"] - - - - - @((MarkupString)(_localizer["Choose file or drag it here"])) - - @foreach (var invalidFileName in invalidDropFileNames) - { - - @_localizer["Invalid file"] - @String.Format(_localizer["You dropped an invalid file ({0}) that can not be processed."], invalidFileName) - - - } - - - - - - @_localizer["Validate data for import"] - - - - @_localizer["Recognition of import data finished"] - - @_localizer["Please validate the following data recognized by import assistant. Once you have validated all input, you can confirm import of data."] - - - - - - - @if (displayFileContent) - { - - - - - - - - - } - @if (displayEditImportOptions) - { - - - - - - - - - @_localizer["Reset import options"] - - - - - - - - @if (_sessionStateContainer.Importfile?.AnalyseException != null) - { - - - - - - - - @_localizer["Error during textimport"] : @_sessionStateContainer.Importfile.AnalyseException.Message - - } - } - @if (displaySplitPoints) - { - - - - - - - - - } - - - - - - - - - - - - - - - - - - - - - - - - - - -@code { - String selectedStep = "selectFiles"; - String dragNDropUploadFilter = String.Join(',', FileMimeTypes.Text, FileExtensions.Cuesheet, FileExtensions.Projectfile); - Boolean cuesheetDataVisible = true; - Boolean cuesheetTracksVisible = true; - Boolean cuesheetSplitPointsVisible = true; - Boolean importFileContentVisible = true; - Boolean importOptionsVisible = true; - Boolean displaySplitPoints = false; - Boolean displayFileContent = true; - Boolean displayEditImportOptions = true; - Alert? alertInvalidFile; - ModalDialog? modalDialog; - List invalidDropFileNames = new(); - - HotKeysContext? hotKeysContext; - Validations? validations; - - public void Dispose() - { - hotKeysContext?.DisposeAsync(); - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - } - - protected override void OnInitialized() - { - base.OnInitialized(); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEnterKeyDown); - } - - Boolean SelectFilesCompleted => (_sessionStateContainer.ImportCuesheet != null) || (_sessionStateContainer.Importfile != null); - - bool NavigationAllowed(StepNavigationContext context) - { - if (context.CurrentStepName == "selectFiles" && context.NextStepName == "validateData") - { - return SelectFilesCompleted; - } - - return true; - } - - private async Task OnDropFileChanged(FileChangedEventArgs e) - { - invalidDropFileNames.Clear(); - foreach (var file in e.Files) - { - if ((IOUtility.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile) == false) - && (IOUtility.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet) == false) - && (IOUtility.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text) == false) - && (IOUtility.CheckFileMimeTypeForAudioCodec(file) == false)) - { - invalidDropFileNames.Add(file.Name); - } - } - if (invalidDropFileNames.Count == 0) - { - await OnFileChanged(e.Files); - } - StateHasChanged(); - } - - private async Task OnFileChanged(IReadOnlyCollection files) - { - _sessionStateContainer.ResetImport(); - var importedFiles = await _importManager.ImportFilesAsync(files); - // Audio file is handled seperatly - foreach (var file in files) - { - if (IOUtility.CheckFileMimeTypeForAudioCodec(file)) - { - var audioFileObjectURL = await _jsRuntime.InvokeAsync("getObjectURL", "dropFileInput"); - var codec = IOUtility.GetAudioCodec(file); - var audiofile = new Audiofile(file.Name, audioFileObjectURL, codec, _httpClient); - _ = audiofile.LoadContentStream(); - _sessionStateContainer.ImportAudiofile = audiofile; - importedFiles.Add(file, ImportFileType.Audiofile); - } - } - displaySplitPoints = importedFiles.ContainsValue(ImportFileType.ProjectFile); - displayFileContent = importedFiles.ContainsValue(ImportFileType.Textfile); - displayEditImportOptions = importedFiles.ContainsValue(ImportFileType.Textfile); - selectedStep = "validateData"; - StateHasChanged(); - } - - private String? GetLocalizedString(Boolean expressionToCheck, String localizedStringName) - { - if (expressionToCheck == true) - { - return _localizer[localizedStringName]; - } - else - { - return null; - } - } - - async ValueTask OnEnterKeyDown() - { - if ((modalDialog != null) && (modalDialog.Visible)) - { - await modalDialog.Confirm(); - } - } - - private async Task ImportData() - { - await _importManager.ImportCuesheetAsync(); - _sessionStateContainer.CurrentViewMode = ViewMode.ViewModeFull; - StateHasChanged(); - } - - void AbortImport() - { - _sessionStateContainer.ResetImport(); - selectedStep = "selectFiles"; - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll(); - } - - private void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - async Task OnResetImportOptionsClicked() - { - if (modalDialog != null) - { - modalDialog.Title = _localizer["Please confirm"]; - modalDialog.Text = _localizer["Do you really want to reset the import options to default? This can not be undone!"]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Confirm; - modalDialog.Confirmed += OnModalDialogConfirmed; - await modalDialog.ShowModal(); - } - } - - void OnModalDialogConfirmed(object? sender, EventArgs args) - { - ResetImportOptions().ConfigureAwait(false); - if (modalDialog != null) - { - modalDialog.Confirmed -= OnModalDialogConfirmed; - } - } - - async Task ResetImportOptions() - { - var newOptions = new ImportOptions(); - await _localStorageOptionsProvider.SaveOptions(newOptions); - } - - void TextImportScheme_ValidateablePropertyChanged(object? sender, String property) - { - if (validations != null) - { - validations.ValidateAll().GetAwaiter().GetResult(); - } - } - - void Timespanformat_ValidateablePropertyChanged(object? sender, String property) - { - if (validations != null) - { - validations.ValidateAll().GetAwaiter().GetResult(); - } - } - - async Task EditImportOptions_OptionsChanged(ImportOptions importOptions) - { - if ((_sessionStateContainer.Importfile?.FileType == ImportFileType.Textfile) && (_sessionStateContainer.Importfile?.FileContent != null)) - { - await _importManager.ImportTextAsync(_sessionStateContainer.Importfile.FileContent); - } - } -} diff --git a/AudioCuesheetEditor/Pages/ViewModeRecord.razor b/AudioCuesheetEditor/Pages/ViewModeRecord.razor deleted file mode 100644 index 30c402bd..00000000 --- a/AudioCuesheetEditor/Pages/ViewModeRecord.razor +++ /dev/null @@ -1,312 +0,0 @@ - - -@implements IDisposable - -@inject IJSRuntime _jsRuntime -@inject ITextLocalizer _localizer -@inject ILogger _logger -@inject HttpClient _httpClient -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ITextLocalizerService _localizationService -@inject SessionStateContainer _sessionStateContainer -@inject MusicBrainzDataProvider _musicBrainzDataProvider - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @_localizer["Artist"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - - @_localizer["Title"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - - - - - - - - - - - - - - -
- - - @if ((_sessionStateContainer.Cuesheet.Audiofile != null) && (_sessionStateContainer.Cuesheet.Audiofile.IsRecorded)) - { - - @_localizer["Recording"] - @if ((_sessionStateContainer.Cuesheet.Audiofile != null) && (_sessionStateContainer.Cuesheet.Audiofile.IsRecorded)) - { - - - - } - - } -
- -@code { - RecordOptions? recordOptions; - ApplicationOptions? applicationOptions; - - public void Dispose() - { - _jsRuntime.InvokeVoidAsync("closeAudioRecording"); - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; - } - - [JSInvokable()] - public Task AudioRecordingFinished(String url) - { - var fileName = Audiofile.RecordingFileName; - if (recordOptions != null) - { - fileName = recordOptions.RecordedAudiofilename; - } - var audiofile = new Audiofile(fileName, url, Audiofile.AudioCodecWEBM, _httpClient, true); - _ = audiofile.LoadContentStream(); - _sessionStateContainer.Cuesheet.Audiofile = audiofile; - StateHasChanged(); - return Task.CompletedTask; - } - - Autocomplete? autocompleteArtist; - Autocomplete? autocompleteTitle; - - Track currentRecordingTrack = new Track(); - - Boolean cuesheetDataVisible = false; - Boolean recordControlVisible = true; - Boolean enterNewTrackVisible = true; - Boolean cuesheetTracksVisible = true; - Boolean recordOptionsVisible = false; - - IEnumerable? autocompleteArtists; - IEnumerable? autocompleteTitles; - - protected override async Task OnInitializedAsync() - { - var dotNetReference = DotNetObjectReference.Create(this); - await _jsRuntime.InvokeVoidAsync("GLOBAL.SetViewModeRecordReference", dotNetReference); - await _jsRuntime.InvokeVoidAsync("setupAudioRecording"); - - recordOptions = await _localStorageOptionsProvider.GetOptions(); - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; - } - - async Task OnKeyDownRecordArtist(KeyboardEventArgs args) - { - _logger.LogDebug("args = {0}", args); - if ((args.Key == "Enter") && (args.CtrlKey == false) && (args.AltKey == false) && (args.MetaKey == false) && (args.Repeat == false) && (args.ShiftKey == false)) - { - if (autocompleteTitle != null) - { - await autocompleteTitle.Focus(); - } - } - } - - async Task OnKeyDownRecordTitle(KeyboardEventArgs args) - { - _logger.LogDebug("args = {0}", args); - if ((args.Key == "Enter") && (args.CtrlKey == false) && (args.AltKey == false) && (args.MetaKey == false) && (args.Repeat == false) && (args.ShiftKey == false)) - { - await AddTrackRecordingClicked(); - } - } - - async Task OnKeyDownAddRecording(KeyboardEventArgs args) - { - _logger.LogDebug("args = {0}", args); - if ((args.Key == "Enter") && (args.CtrlKey == false) && (args.AltKey == false) && (args.MetaKey == false) && (args.Repeat == false) && (args.ShiftKey == false)) - { - await AddTrackRecordingClicked(); - } - } - - async Task AddTrackRecordingClicked() - { - if (_sessionStateContainer.Cuesheet.IsRecording == true) - { - _sessionStateContainer.Cuesheet.AddTrack(currentRecordingTrack, applicationOptions, recordOptions); - currentRecordingTrack = new Track(); - if (autocompleteTitle != null) - { - await autocompleteTitle.Clear(); - await autocompleteTitle.Focus(); - } - if (autocompleteArtist != null) - { - await autocompleteArtist.Clear(); - await autocompleteArtist.Focus(); - } - } - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - async Task OnReadDataAutocompleteArtist(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var artists = await _musicBrainzDataProvider.SearchArtistAsync(autocompleteReadDataEventArgs.SearchValue); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteArtists = artists; - } - } - } - - async Task OnReadDataAutocompleteTitle(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var titles = await _musicBrainzDataProvider.SearchTitleAsync(autocompleteReadDataEventArgs.SearchValue, currentRecordingTrack.Artist); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteTitles = titles; - } - } - } - - async Task OnSelectedValueChangedTrackTitle(Guid selectedValue) - { - if (String.IsNullOrEmpty(currentRecordingTrack.Artist)) - { - var trackDetails = await _musicBrainzDataProvider.GetDetailsAsync(selectedValue); - if (trackDetails != null) - { - currentRecordingTrack.Artist = trackDetails.Artist; - } - } - } - - async Task RecordingStarted() - { - await _jsRuntime.InvokeVoidAsync("startAudioRecording"); - if ((_sessionStateContainer.Cuesheet.Audiofile != null) && (_sessionStateContainer.Cuesheet.Audiofile.IsRecorded)) - { - await _jsRuntime.InvokeVoidAsync("URL.revokeObjectURL", _sessionStateContainer.Cuesheet.Audiofile.ObjectURL); - } - _sessionStateContainer.Cuesheet.Audiofile = null; - if (autocompleteArtist != null) - { - await autocompleteArtist.Focus(); - } - } - - async Task RecordingStopped() - { - await _jsRuntime.InvokeVoidAsync("stopAudioRecording"); - } - - void LocalStorageOptionsProvider_OptionsSaved(object? sender, IOptions options) - { - if (options is RecordOptions recordingOptions) - { - recordOptions = recordingOptions; - } - if (options is ApplicationOptions applicationOption) - { - applicationOptions = applicationOption; - } - } -} diff --git a/AudioCuesheetEditor/Program.cs b/AudioCuesheetEditor/Program.cs index e03f3db7..a936db92 100644 --- a/AudioCuesheetEditor/Program.cs +++ b/AudioCuesheetEditor/Program.cs @@ -17,16 +17,15 @@ using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Data.Services; using AudioCuesheetEditor.Extensions; -using AudioCuesheetEditor.Model.UI; +using AudioCuesheetEditor.Services.Audio; using AudioCuesheetEditor.Services.IO; using AudioCuesheetEditor.Services.UI; +using AudioCuesheetEditor.Services.Validation; using BlazorDownloadFile; -using Blazorise; -using Blazorise.Bootstrap5; -using Blazorise.Icons.FontAwesome; using Howler.Blazor.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; using Toolbelt.Blazor.Extensions.DependencyInjection; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -34,14 +33,8 @@ builder.RootComponents.Add("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); - -builder.Services.AddBlazorise(options => -{ - options.Debounce = true; - options.DebounceInterval = 300; -}) -.AddBootstrap5Providers() -.AddFontAwesomeIcons(); +builder.Services.AddLocalization(); +builder.Services.AddMudServices(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -51,12 +44,20 @@ 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.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddLogging(); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); @@ -65,6 +66,6 @@ var host = builder.Build(); -await host.SetDefaultCulture(); +await host.SetCultureFromConfigurationAsync(); -await builder.Build().RunAsync(); +await host.RunAsync(); diff --git a/AudioCuesheetEditor/Resources/Localization/About/de.json b/AudioCuesheetEditor/Resources/Localization/About/de.json deleted file mode 100644 index 760c3d3b..00000000 --- a/AudioCuesheetEditor/Resources/Localization/About/de.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "culture": "de", - "translations": { - "About AudioCuesheetEditor": "Über AudioCuesheetEditor", - "Version": "Version", - "Licence": "Lizenz", - "Donate for this project": "Spenden Sie für dieses Projekt", - "Project url": "Projekt website", - "Preview environment": "Vorschauumgebung" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/About/en.json b/AudioCuesheetEditor/Resources/Localization/About/en.json deleted file mode 100644 index 1ed86fc7..00000000 --- a/AudioCuesheetEditor/Resources/Localization/About/en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "culture": "en", - "translations": { - "About AudioCuesheetEditor": "About AudioCuesheetEditor", - "Version": "Version", - "Licence": "Licence", - "Donate for this project": "Donate for this project", - "Project url": "Project url", - "Preview environment": "Preview environment" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/AudioPlayer/de.json b/AudioCuesheetEditor/Resources/Localization/AudioPlayer/de.json deleted file mode 100644 index f78fa531..00000000 --- a/AudioCuesheetEditor/Resources/Localization/AudioPlayer/de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "culture": "de", - "translations": { - "Audioplayer": "Wiedergabe", - "Set playback to begin of previous track": "Wiedergabeposition auf Begin von vorherigen Titel setzen", - "Set playback to begin of next track": "Wiedergabeposition auf Begin von nächstem Titel setzen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/AudioPlayer/en.json b/AudioCuesheetEditor/Resources/Localization/AudioPlayer/en.json deleted file mode 100644 index c5a6f2b0..00000000 --- a/AudioCuesheetEditor/Resources/Localization/AudioPlayer/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "culture": "en", - "translations": { - "Audioplayer": "Audioplayer", - "Set playback to begin of previous track": "Set playback to begin of previous track", - "Set playback to begin of next track": "Set playback to begin of next track" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/de.json b/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/de.json deleted file mode 100644 index a7c25981..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "de", - "translations": { - "Cuesheet": "Cuesheet" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/en.json b/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/en.json deleted file mode 100644 index 039f91f9..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CatalogueNumber/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "en", - "translations": { - "Cuesheet": "Cuesheet" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CuesheetData/de.json b/AudioCuesheetEditor/Resources/Localization/CuesheetData/de.json deleted file mode 100644 index a9cacc25..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CuesheetData/de.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "culture": "de", - "translations": { - "CD artist": "CD Künstler", - "CD title": "CD Titel", - "Audiofile": "Audiodatei", - "Download recorded audio": "Aufnahme herunterladen", - "Change": "Ändern", - "CD textfile": "CD Textdatei", - "Cataloguenumber": "Katalognummer", - "Enter cd artist here": "CD Künstler hier eingeben", - "Enter cd title here": "CD Titel hier eingeben", - "Enter cataloguenumber here": "Geben Sie hier die Katalognummer des Cuesheets an. Die Katalognummer ist eine 13 stellige Zahl.", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "Die Datei {0} kann nicht für folgende Operation genutzt werden: {1}. Die Datei ist ungültig, bitte verwenden Sie eine gültige Datei!" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CuesheetData/en.json b/AudioCuesheetEditor/Resources/Localization/CuesheetData/en.json deleted file mode 100644 index a1277865..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CuesheetData/en.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "culture": "en", - "translations": { - "CD artist": "CD artist", - "CD title": "CD title", - "Audiofile": "Audiofile", - "Download recorded audio": "Download recorded audiofile", - "Change": "Change", - "CD textfile": "CD Textfile", - "Cataloguenumber": "Cataloguenumber", - "Enter cd artist here": "Enter CD artist here", - "Enter cd title here": "Enter CD title here", - "Enter cataloguenumber here": "Enter the catalogue number of the cuesheet. Catalogue number is a 13 decimal digits number.", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CultureSelector/de.json b/AudioCuesheetEditor/Resources/Localization/CultureSelector/de.json deleted file mode 100644 index 53f521f7..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CultureSelector/de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "de", - "translations": { - - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/CultureSelector/en.json b/AudioCuesheetEditor/Resources/Localization/CultureSelector/en.json deleted file mode 100644 index dec04c3a..00000000 --- a/AudioCuesheetEditor/Resources/Localization/CultureSelector/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "en", - "translations": { - - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditImportOptions/de.json b/AudioCuesheetEditor/Resources/Localization/EditImportOptions/de.json deleted file mode 100644 index 1d41f3e2..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditImportOptions/de.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "culture": "de", - "translations": { - "Enter textimportscheme cuesheet tooltip": "Textschema für den Import von Cuesheeteigenschaften hier eingeben. Die Identifikation wird über reguläre Ausdrücke ausgeführt.", - "Enter textimportscheme cuesheet here": "Textschema für den Import von Cuesheeteigenschaften hier eingeben", - "Select placeholder": "Platzhalter auswählen", - "Enter textimportscheme track tooltip": "Textschema für den Import von Titeleigenschaften hier eingeben. Die Identifikation wird über reguläre Ausdrücke ausgeführt.", - "Enter textimportscheme track here": "Textschema für den Import von Titeleigenschaften hier eingeben", - "Textimportscheme cuesheet": "Import Schema Cuesheet", - "Textimportscheme track": "Import Schema Titel", - "Import Options": "Importoptionen", - "ENTER REGULAR EXPRESSION HERE": "Hier regulären Ausdruck eingeben", - "Enter custom timespan format here": "Hier können Sie bei Bedarf ein angepasstes Format für Zeitspannen eingeben. Details können der Hilfe entnommen werden.", - "Customized timespan format import": "Zeitspannenformat für Import", - "CD artist": "CD Künstler", - "CD title": "CD Titel", - "Audiofile": "Audiodatei", - "Change": "Ändern", - "Cataloguenumber": "Katalognummer", - "CD textfile": "CD Textdatei", - "Artist": "Künstler", - "Title": "Titel", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Days": "Tage", - "Hours": "Stunden", - "Minutes": "Minuten", - "Seconds": "Sekunden", - "Milliseconds": "Millisekunden", - "StartDateTime": "Startzeitpunkt" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditImportOptions/en.json b/AudioCuesheetEditor/Resources/Localization/EditImportOptions/en.json deleted file mode 100644 index b453cd98..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditImportOptions/en.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "culture": "en", - "translations": { - "Enter textimportscheme cuesheet tooltip": "Enter textscheme for cuesheet properties import here. Identification will be done using regular expressions.", - "Enter textimportscheme cuesheet here": "Enter textscheme for cuesheet properties import here", - "Select placeholder": "Select placeholder", - "Enter textimportscheme track tooltip": "Enter textscheme for track properties import here. Identification will be done using regular expressions.", - "Enter textimportscheme track here": "Enter textscheme for track properties import here", - "Textimportscheme cuesheet": "Textimport scheme cuesheet", - "Textimportscheme track": "Textimport scheme track", - "Import Options": "Import options", - "ENTER REGULAR EXPRESSION HERE": "Enter regular expression here", - "Enter custom timespan format here": "You can enter a custom format for timespan here. Details can be found in help.", - "Customized timespan format import": "Timespan format for import", - "CD artist": "CD artist", - "CD title": "CD title", - "Audiofile": "Audiofile", - "CD textfile": "CD Textfile", - "Cataloguenumber": "Cataloguenumber", - "Artist": "Artist", - "Title": "Title", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Days": "Days", - "Hours": "Hours", - "Minutes": "Minutes", - "Seconds": "Seconds", - "Milliseconds": "Milliseconds", - "StartDateTime": "Startdatetime" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json b/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json deleted file mode 100644 index 30df6a1e..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "culture": "de", - "translations": { - "Filename for recorded audio": "Dateiname für aufgenommenes Audio", - "Record countdown timer in seconds": "Aufnahmecountdown in Sekunden", - "Record time sensitivity": "Genauigkeit der Aufnahmezeiten", - "TimeSensitivityMode.Full": "Alles (inkl. Millisekunden)", - "TimeSensitivityMode.Seconds": "Nur Sekunden", - "TimeSensitivityMode.Minutes": "Nur Minuten", - "Reset options to defaults": "Optionen auf Standardwerte zurücksetzen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/en.json b/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/en.json deleted file mode 100644 index efe746c3..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "culture": "en", - "translations": { - "Filename for recorded audio": "Filename for recorded audio", - "Record time sensitivity": "Recordtime sensitivity", - "TimeSensitivityMode.Full": "Everything (incl. milliseconds)", - "TimeSensitivityMode.Seconds": "Only seconds", - "TimeSensitivityMode.Minutes": "Only minutes", - "Reset options to defaults": "Reset options to defaults" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditSections/de.json b/AudioCuesheetEditor/Resources/Localization/EditSections/de.json deleted file mode 100644 index 2c343c56..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditSections/de.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "culture": "de", - "translations": { - "Add new section": "Neuen Abschnitt hinzufügen", - "Controls": "Steuerung", - "Begin": "Anfang", - "End": "Ende", - "Delete section tooltip": "Löscht diesen Abschnitt", - "CD artist": "CD Künstler", - "CD title": "CD Titel", - "Audiofile name": "Audiodatei", - "Error": "Fehler", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "Die Datei {0} kann nicht für folgende Operation genutzt werden: {1}. Die Datei ist ungültig, bitte verwenden Sie eine gültige Datei!", - "Audiofile": "Audiofile", - "Change": "Ändern" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditSections/en.json b/AudioCuesheetEditor/Resources/Localization/EditSections/en.json deleted file mode 100644 index f6bb71d4..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditSections/en.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "culture": "en", - "translations": { - "Add new section": "Add new section", - "Controls": "Controls", - "Begin": "Begin", - "End": "End", - "Delete section tooltip": "Deletes this section", - "CD artist": "CD artist", - "CD title": "CD title", - "Audiofile name": "Audiofile", - "Error": "Error", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!", - "Audiofile": "Audiofile", - "Change": "Change" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditTrackModal/de.json b/AudioCuesheetEditor/Resources/Localization/EditTrackModal/de.json deleted file mode 100644 index 98e51337..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditTrackModal/de.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "culture": "de", - "translations": { - "Artist": "Künstler", - "Title": "Titel", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Edit track details": "Titeldetails bearbeiten", - "Position": "Position", - "Flags": "Markierungen", - "Flag4CHTooltip": "Setzen Sie diese Markierung, wenn der Titel 4 Kanal Audio enthält", - "FlagDCPTooltip": "Setzen Sie diese Markierung, wenn digitale Kopien verboten sind", - "FlagPRETooltip": "Setzen Sie diese Markierung, wenn der Titel Höhenanhebung hat", - "FlagSCMSTooltip": "Setzen Sie diese Markierung, wenn der Titel \"Serial Copy Management System\" hat", - "PreGap": "Vorlücke", - "PostGap": "Nachlücke", - "Abort": "Abbrechen", - "Save changes": "Änderungen speichern", - "IsLinkedToPreviousTrack": "Verknüpft", - "IsLinkedToPreviousTrackValue": "Verknüpft mit Vorgänger", - "Change": "Ändern", - "Property": "Eigenschaft", - "Value": "Wert" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/EditTrackModal/en.json b/AudioCuesheetEditor/Resources/Localization/EditTrackModal/en.json deleted file mode 100644 index 8030ce56..00000000 --- a/AudioCuesheetEditor/Resources/Localization/EditTrackModal/en.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "culture": "en", - "translations": { - "Artist": "Artist", - "Title": "Title", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Edit track details": "Edit track details", - "Position": "Position", - "Flags": "Flags", - "Flag4CHTooltip": "Set this if track contains four channel audio", - "FlagDCPTooltip": "Set this if track is digital copy permitted", - "FlagPRETooltip": "Set this if track has Pre-emphasis enabled", - "FlagSCMSTooltip": "Set this if track has \"Serial Copy Management System\"", - "PreGap": "Pregap", - "PostGap": "Postgap", - "Abort": "Abort", - "Save changes": "Save changes", - "IsLinkedToPreviousTrack": "Linked", - "IsLinkedToPreviousTrackValue": "Linked with previous", - "Change": "Change", - "Property": "Property", - "Value": "Value" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/FileEdit/de.json b/AudioCuesheetEditor/Resources/Localization/FileEdit/de.json deleted file mode 100644 index 2d062d98..00000000 --- a/AudioCuesheetEditor/Resources/Localization/FileEdit/de.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "de", - "translations": { - "Choose file": "Datei wählen", - "Choose files": "Wähle Dateien" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/FileEdit/en.json b/AudioCuesheetEditor/Resources/Localization/FileEdit/en.json deleted file mode 100644 index 1a67b8de..00000000 --- a/AudioCuesheetEditor/Resources/Localization/FileEdit/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "en", - "translations": { - "Choose file": "Choose file", - "Choose files": "Choose files" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/Help/de.json b/AudioCuesheetEditor/Resources/Localization/Help/de.json deleted file mode 100644 index a9531a2c..00000000 --- a/AudioCuesheetEditor/Resources/Localization/Help/de.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "culture": "de", - "translations": { - "Help": "Hilfe", - "Quick links": "Schnellzugriff", - "Introduction": "Einleitung", - "What is AudioCuesheetEditor": "Was ist AudioCuesheetEditor?", - "Features": "Funktionen", - "Validation": "Validierung", - "Track linking": "Track Kopplung", - "Create cuesheet": "Cuesheet erstellen", - "Import of files": "Import von Dateien", - "Import of textfiles": "Import von Textdateien", - "Import of cuesheetfiles": "Import von Cuesheetdateien", - "Recordmode": "Aufnahmemodus", - "Options": "Optionen", - "Scroll to top": "Nach oben springen", - "What is AudioCuesheetEditor helptext": "AudioCuesheet ist eine Web Applikation für das Schreiben von Audio Cuesheet Dateien. Audio Cuesheet Dateien sind kleine Textdateien, die Informationen über die Audiodatei bereit stellen, wie etwa z.B. Künstler, Titel, aber auch Anfang und Ende. AudioCuesheetEditor ist eine Applikation die Ihnen dabei helfen soll, gültige und korrekte Cuesheets zu erhalten. Sie können außerdem auch fertige Dateien importieren, um ihre Arbeit zu minimieren. Außerdem können Sie die gesammelten Daten auch in verschiedene, individuelle Exportformate überführen.", - "Features helptext": "
  • Validierung aller Daten
  • Verschiedene Importmöglichkeiten (Text, Cuesheet, Projektdateien)
  • Verschiedene Export Möglichkeiten (XML/CSV,Text, etc)
  • Audio Wiedergabe
  • Aufnahmemodus
  • Rückgängig/Wiederherstellen von Änderungen
  • Massenänderungen
  • Offlinefähigkeit (als Progressive WebApp)
", - "Validation helptext": "AudioCuesheetEditor arbeitet mit einem validiertem Datenmodell und prüft jede Eingabe. In den meisten Fällen werden Validierungsfehler neben den Eingabefeldern angezeigt, um Ihnen eine klare Information zu ermöglichen.", - "Track linking helptext": "Es ist möglich einen Track mit dem vorherigen Track zu koppeln. Dies stellt sicher, dass Änderungen an Beginn, Ende und Position automatisch in den anschließenden Track übertragen werden.
Wenn ein Track mit dem vorherigen Track gekoppelt ist, können Sie beispielsweise das Ende verändern und ihre Änderung wird automatisch in den nachfolgenden Track bei Beginn eingetragen. Wenn Sie die Kopplung aufheben möchten, klicken sie einfach auf den orangfarbenenen \"Abkoppeln\" Button. Wenn Sie anschließend die Kopplung wiederherstellen möchten, klicken Sie einfach auf den grünen \"Koppeln\" Button.", - "Create cuesheet helptext": "Cuesheets können sehr einfach und schnell mit der vorhandenen Eingabemaske erstellt werden. Dafür füllen Sie einfach CD Künstler und CD Titel in den vorhandenen Feldern aus. Wählen Sie bitte außerdem eine Audio Datei aus indem Sie eine Datei aus ihrem Datei System auf die Ablagefläche ziehen oder die Datei manuell mit dem Auswahldialog auswählen.
Anschließend können Sie einen Track hinzufügen indem Sie den \"Neuen Track hinzufügen\" Button klicken. Ein neuer Track wird angezeigt und sie können seine Eigenschaften wie Künstler, Titel, Beginn, Ende oder Länge bearbeiten. Bitte beachten Sie, dass der Beginn jedes Tracks von seinem vorherigen Ende automatisch berechnet wird. Daher ist es ratsam, einen weiteren Track erst dann hinzuzufügen, wenn alle Daten ausgefüllt sind.
Wenn Sie alle Tracks hinzugefügt haben, die in der Audio Datei vorhanden sind und alle notwendigen Felder ausgefüllt haben, können Sie das Cuesheet herunterladen indem Sie auf \"Export\" klicken und dann auf \"Cuesheet herunterladen\". Außerdem können Sie die Daten auch in anderen Formaten herunterladen, indem Sie auf \"Export\" und dann auf \"Export Profile anzeigen\" klicken.
", - "Import of textfiles helptext": "Sie können Textdatein importieren und den Inhalt zeilenweise mit regulären Ausdrücken auswerten. Eine einfache Beispiel Textdatei kann hier gefunden werden.
Jede Zeile repräsentiert einen Track, die erste Zeile jedoch Cuesheet Details und jedes Detail kann aus einer Zeile ausgelesen werden. Die erste Information ist der Track Künstler gefolgt von \" - \" und dem Track Titel. Dann kommen ein paar Tabs und Informationen über das Track Ende. So benötigen wir in diesem Beispiel den Regulären Ausdruck \"(?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Track.Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'Track.End'.{1,})\" für Tracks und \"(?'Cuesheet.Artist'\\A.*) - (?'Cuesheet.Title'\\w{1,})\t{1,}(?'Cuesheet.Audiofile'.{1,})\" für Cuesheet Details. Jedes Feld kann mit \"Entität.Feldname\" erreicht werden. Sie können den Textimport einfach starten, indem Sie in den Anzeigemodus \"Import Assistent\" wechseln und dort eine Datei aus dem Dateisystem auf die Ablagefläche zieht oder sie manuell über den Dateiauswahldialog auswählt.
Der Assistent wechselt nach der Analyse der Daten in die Detailansicht und sie können den Textimport verändern und so anpassen, dass er zu ihrer Textdatei passt und alle Informationen erfasst werden können.
Änderungen am Import Textschema verändern die Vorschau und spiegeln das Ergebnis wieder.
Sie können alle gültigen Platzhalter eines Tracks erreichen, indem Sie \"Platzhalter auswählen\" anklicken und dann den Platzhalter den Sie benötigen.
Wenn Sie ein ungültiges Import Textschema angeben, wird ihnen ein Validierungsfehler angezeigt.
Geben Sie ein Schema an, dass nicht zur Textdatei passt, wird ihnen ein Fehler angezeigt.
Fehler blockieren immer den Import und müssen korrigiert werden.
Wenn Sie nur Tracks importieren möchten, lassen Sie das Schema für Cuesheet leer und geben Sie eine Textdatei an, die nur Trackdetails enthält. Ein Beispiel kann hier gefunden werden.
Der Import kann nach dem gleichem Muster stattfinden.
", - "Import of cuesheetfiles helptext": "Sie können bereits fertige Cuesheets importieren und bearbeiten. Dabei ist es nicht wichtig, woher die Datei stammt (von diesem oder einem anderen Programm), sondern nur das Format. Um ein Cuesheet zu importieren, wechseln sie über \"Anzeigemodus auswählen\" in den \"Import Assisstent\", ziehen Sie es aus ihrem Dateisystem auf die Ablagefläche oder wählen Sie es manuell über den Dateiauswahldialog:
Anschließend wird Ihnen das Ergebnis der Analyse der Datei angezeigt und Sie können die Daten prüfen und verändern.
Durch klicken von \"Angezeigte Daten importieren\" werden die Daten in das eigentliche Cuesheet übernommen und Sie kommen in die ursprüngliche Bearbeitung zurück.
Ein Beispielcuesheet kann hier gefunden werden.", - "Recordmode helptext": "AudioCuesheetEditor hat verschiedene Ansichten für verschiedene Zielgruppen. Der Aufnahmemodus richtet sich an Leute, die in Echtzeit ein Cuesheet aufnehmen möchten und dabei Tracks mitschreiben, sowie anschließend eventuell die Audio Datei herunterladen möchten. Um den Aufnahmemodus zu verwenden, wechseln Sie den Ansichtsmodus auf Aufnahmemodus.
Sie werden gefragt, ob Sie der Applikation Zugriff auf ihre Audio Eingabegeräte erlauben wollen. Wenn Sie eine Aufnahme erzeugen möchten, die Sie später herunterladen möchten, wählen Sie hier bitte \"Zulassen\". Dies ist nicht zwingend erforderlich, jedoch kann kein Audiosignal mitgeschnitten werden, wenn Sie dies nicht erlauben. AudioCuesheetEditor speichert Audiodaten im WebM Format. Sie können anschließend die Aufnahme mit \"Aufnahme starten\" beginnen. Dies sorgt dafür, dass die Aufnahme beginnt und ihnen ein roter Balken mitteilt, dass die Aufnahme aktiv ist. Sie können alternativ den Button \"Aufnahmecountdown starten\" benutzen, um einen konfigurierbaren Countdown zu starten, bis die Aufnahme aktiv ist.
Beginnen Sie mit der Wiedergabe ihrer Audiostücke und sobald ein Track beendet ist, geben Sie die Daten Künstler und Titel ein und klicken Sie auf \"Neuen Track hinzufügen\". Dies fügt den neuen Track hinzu und setzt das Ende des aktuellen Tracks, sowie den Anfang des neuen Tracks. Sie können auch die \"Enter\" Taste auf dem Keyboard benutzen, um die Eingabefelder automatisch zu wechseln, bzw. den Track hinzuzufügen.
Sobald ein neuer Track hinzugefügt wurde, wird er unter Tracks angezeigt und Sie können dort nachträglich Künstler und Artist verändern, sowie ihn wieder löschen.
Wenn Sie mit der Aufnahme fertig sind, beenden Sie diese mit einem Klick auf \"Aufnahme beenden\". Das stoppt die Aufnahme und macht die Aufnahme zum Download verfügbar, sofern der Zugriff auf Audiogeräte erlaubt wurde.
Sie können nun zur vollen Bearbeitung wechseln und alle Änderungen am Cuesheet durchführen, die noch offen sind. Außerdem können Sie die Datei unter den Cuesheet Daten herunterladen.", - "Options helptext": "AudioCuesheetEditor stellt viele Optionen bereit, um sich ihren Bedürfnissen anzupassen.

Sprache

Sie können ihre Sprache auswählen.

Standard Anzeigemodus

Setzt den standardmäßig genutzten Anzeigemodus, mit dem die Applikation startet.

Cuesheet Dateiname

Stellt den standardmäßigen Dateinamen für ein Cuesheet ein, der beim Download genutzt wird.

Projektdateiname

Stellt den standardmäßigen Dateinamen für ein Projekt ein, der beim Download genutzt wird.

Tracks standardmäßig koppeln

Setzt den Standardwert, ob Tracks automatisch gekoppelt werden oder nicht.

Angepasstes Zeitspannenformat

Hier können sie das Standard Zeitspannenformat überschreiben. Das ist immer dann sinnvoll, wenn ihre Eingaben beispielsweise nur Minuten und Sekunden enthalten, statt Stunden, Minuten und Sekunden. Angegeben wird das Format als regulärer Ausdruck. Ein Beispiel für Zeitformate mit Minuten und Sekunden ist \"(?'TimeSpanFormat.Minutes'\\d{1,})[:](?'TimeSpanFormat.Seconds'\\d{1,})\", womit beispielsweise Eingaben von \"63:12\" als Zeitformat erkannt werden.", - "Export of data": "Exportmöglichkeiten", - "Export of data helptext": "Es ist sehr einfach in AudioCuesheetEditor alle Eingaben zu exportieren. Sie können den Export selbst anpassen und selbstständig eigene Exportprofile anlegen. Wenn Sie ein Cuesheet exportieren möchen, klicken sie einfach auf \"Export Profile anzeigen\" im oberen Bereich im Menü \"Export\".
Es öffnet sich der Exportprofil Dialog bei dem Sie jederzeit zwischen den verschiedenen Exportprofilen wechseln können, sowie alte löschen und neue anlegen. Jede Änderung wird automatisch gespeichert. Sie können einen Namen vergeben, einen Dateinamen für den Download, sowie die Schema für Kopf, Fuß und Tracks verändern.
Alle gültigen Platzhalter können eingefügt werden, indem man sie manuell tippt oder den Button \"Platzhalter auswählen\" und den Platzhalter anklickt den man hinzufügen möchte. Sie können außerdem Text zwischen den Platzhaltern angeben, der nicht ersetzt wird.
Sobald sie das Exportprofil benutzen möchten, klicken sie einfach auf den Button \"Exportdatei herunterladen\" und der Export wird vorgenommen.", - "User Interface": "Benutzeroberfläche", - "User Interface helptext": "

Automatische Vervollständigung

AudioCuesheetEditor bietet ihnen die Möglichkeit die Daten von Tracks automatisch vervollständigen zu lassen. Die verwendete Musikdatenbank ist MusicBrainz. Dazu geben Sie einen Track ein und erhalten hier bereits Vorschläge.
Wählen Sie einen gültigen Interpreten aus, so werden Ihnen beim Eingeben des Titels passende Vorschläge gemacht.
Wenn sie einen Titel ausgewählt haben, wird die Länge automatisch übernommen.

Massenänderungen

AudioCuesheetEditor bietet ihnen die Möglichkeit mehrere Tracks gleichzeit zu bearbeiten. Wählen Sie dazu einfach mehrere Tracks in der Übersicht aus (über \"Auswahl für Tracks einblenden\") und klicken auf \"Selektierte Tracks bearbeiten\".
Der Bearbeitungsdialog öffnet sich und sie können wählen, ob die Bearbeitung dynamisch angewendet werden soll (Wert erhöhen oder verringern) oder der eingebene Wert absolut verwendet werden soll (für einige Eigenschaften). Dabei bedeutet \"=\" dass der absolute Wert übernommen werden soll, \"-\" dass der Wert abgezogen werden soll und \"+\" dass der Wert addiert werden soll.
", - "Shortcuts": "Tastenkürzel", - "Shortcuts helptext": "Die folgenden Tastenkürzeln sind verfügbar:
  • Strg + p: Startet oder pausiert die Audiowiedergabe
  • Play: Startet oder pausiert die Audiowiedergabe
  • Strg + Left: Vorherigen Track wiedergeben
  • Previous: Vorherigen Track wiedergeben
  • Strg + Right: Nächsten Track wiedergeben
  • Next: Nächsten Track wiedergeben
  • Stop: Stoppt die Audiowiedergabe
  • Strg + z: Letzte Änderung rückgängig machen
  • Strg + y: Letzte Änderung wiederherstellen
  • Strg + h: Navigiere zu dieser Hilfe
  • Strg + e: Öffne die Exportprofile
  • Strg + s: Speicher das Projekt (Download als Datei)
", - "Sections": "Abschnitte", - "Sections helptext": "
Abschnitte ermöglichen es ein Cuesheet in mehrere Dateien aufzuteilen. Dies ist z.B. beim Import aus einer Textdatei hilfreich, um das Projekt in mehrere Dateien aufzuteilen. Abschnitte können im Anzeigemodus \"Komplette Bearbeitung\" und \"Aufnahmemodus\" angegeben werden.", - "Offline usage (progressive WebApp)": "Offlinefähigkeit (Progressive WebApp)", - "Offline usage helptext": "AudioCuesheetEditor ist eine progressive WebApp und kann auf ihrem Gerät lokal installiert werden, um ohne Internetverbindung genutzt zu werden. Um die Applikation zu installieren folgen Sie bitte den Anweisungen ihres Browsers. Üblicherweise ist in der Adresszeile ein Hinweis zu finden, dass die Applikation lokal installiert werden kann." - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/Help/en.json b/AudioCuesheetEditor/Resources/Localization/Help/en.json deleted file mode 100644 index e2a4ecf5..00000000 --- a/AudioCuesheetEditor/Resources/Localization/Help/en.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "culture": "en", - "translations": { - "Help": "Help", - "Quick links": "Quick links", - "Introduction": "Introduction", - "What is AudioCuesheetEditor": "What is AudioCuesheetEditor?", - "Features": "Features", - "Validation": "Validation", - "Track linking": "Track linking", - "Create cuesheet": "Create a Cuesheet", - "Import of files": "Import of files", - "Import of textfiles": "Import of text files", - "Import of cuesheetfiles": "Import of cuesheet files", - "Recordmode": "Live record mode", - "Options": "Options", - "Scroll to top": "Scroll to top", - "What is AudioCuesheetEditor helptext": "AudioCuesheet is a web application for writting audio cuesheet files. Audio cuesheet files are little text files which provide information about the provided audio file like for example artist, name, but also start and end. AudioCuesheetEditor is an application that helps you writting valid cuesheet files by checking and validation input data. You can also import data from files to minimize your work. Exporting data to individual formats is also possible.", - "Features helptext": "
  • Validation for all data
  • Several import options (Text, Cuesheet, Projectfiles)
  • Several export profiles (XML/CSV,Text, etc)
  • Audio playback
  • Live record mode
  • Undo/Redo changes
  • Bulk edit
  • Offline usage (as Progressive WebApp)
", - "Validation helptext": "AudioCuesheetEditor works with a validation model and evaluates every input. In most cases the validation messages are presented next to the input, giving you a clear instruction what is missing.", - "Track linking helptext": "It is possible to link a track with the previous track. This makes shure, that changes to Begin,End and Position are automatically transfered to the next following track. If a track is linked to the previous track, you can change for example the End and the Begin of the following linked track is automatically replaced with the End you entered. If you wish to disable linking, just click on the orange \"Unlink\" Button. If you afterwards want to link the tracks again, just hit the green \"Link\" Button.", - "Create cuesheet helptext": "Creation of Cuesheet can be done very easily and fast using the standard GUI without import of files. To do so, enter the CD artist and CD title in the appropriate fields. Also select an audio file on your computer by draging the file from file explorer to the drop zone or manually selecting the file via file chooser.
Afterwards you need to insert a track by clickin the \"Add new track\" button. A new track is added and you can edit its properties like Artist, Title or Begin, End or Length. Please keep in mind, that every tracks begin gets calculated from the end of the previous track. Therefore its adviced to edit one track and fill its data before adding a new one.
When you have added all tracks from the audio file and filled all necessary fields, you can export the cuesheet in the top by clickin \"Export\" and then \"Download cuesheet\". You can also export the result in other formats like XML, CSV or text by clicking \"Export\" and then \"Display Export Profiles\".
", - "Import of textfiles helptext": "You can import plain text files and analyse the content of each line by simple regular expressions. A simple sample textfile can be found here.
Each line represents a track, but the first line represents cuesheet details and every detail can be extracted from the line. First information that can be found is the track artist, followed by \" - \" and then track title. Afterwards some tabs come and you can see information about the track end time. So in this example we can work with the regular expression for tracks \"%?'Track.Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Track.Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'Track.End'.{1,})\" and with \"(?'Cuesheet.Artist'\\A.*) - (?'Cuesheet.Title'\\w{1,})\t{1,}(?'Cuesheet.Audiofile'.{1,})\" for cuesheet data. Each field can be accessed via \"Entity.Fieldname\".You can start importing a textfile by selecting viewmode \"Import asssistant\" and drag the file from filesystem to the dropzone or by manually selecting it from file chooser dialog.
Changing the import textscheme will result in changes of the import result and the data to import will be shown to you.
You can select the valid fields by pressing the button \"Select placeholder\" and afterwards selecting the placeholder you want to add.
If you enter an invalid text import scheme, a validation error will be displayed.
If the import scheme doesn't match the text file, an error will be displayed to you.
Errors will always block import of text, so you have to correct them at first before import can take place.
If you want to import a tracks only textfile, just leave the cuesheet scheme empty and add a textfile with track properties only. A sample can be found here.
Import can be done the same way.
", - "Import of cuesheetfiles helptext": "You can import already finished cuesheets and edit them. It doesn't matter if the cuesheet is written by this program or another one. Only the format of file matters. To import a cuesheet, switch via \"Select viewmode\" to \"Import assistant mode\", drag a cuesheet from your file system to the dropzone or manually select it via file system chooser:
Afterwards the result of file analysis will be displayed and you can check and edit data to your needs.
By clicking \"Import the displayed data\" the result will be imported to the main cuesheet and you come back to the main edit form.
A sample cuesheet file can be found here.", - "Recordmode helptext": "AudioCuesheetEditor has different view modes you can use for different scopes. Record mode has the focus on live recording a cuesheet and tracks with a soundfile you can download afterwards. To use the record mode switch the view mode to record mode.
You will be asked if you want to allow using audio input. In case you want to have the audio recorded select \"Allow\". This is optional, but keep in mind, that if you block access to audio, no audio will be recorded by AudioCuesheetEditor. AudioCuesheetEditor records audio data in WebM format. You can afterwards start your recording by pressing the \"Start recording\" Button. This causes the record to start and a display will tell you, that you are currently recording. You can also use the \"Start record countdown timer\" Button in order to have a customizable countdown before recording is started.
Play audio and once the end of the track has been reached, please enter the track artist and title and click the button \"Add new track\". This will set the end of the track to the current time. You can use \"Enter\" on the input fields to fast switch the focus inputs and enter the new track.
Once you have entered a track, it will display in the tracks and you can edit artist and title again and also delete the track afterwards.
Once you have finished your recording, click the button \"Stop recording\" which will stop the recording and make the recording (if audio is not blocked) downloadable to you.
You can now switch back to full edit mode in order to do more editing on the cuesheet. All tracks are present and also the recording is available as download in the cuesheet data part.", - "Options helptext": "AudioCuesheetEditor provides several options you can use to customize the application to your fits.

Language

You can select your language.

Default view mode

Sets the default view mode the application starts with.

Cuesheet filename

Sets the default cuesheet filename that is used, when you download the cuesheet.

Project filename

Set the default project filename that is used, when you download the projectfile.

Automatically link tracks

Sets the default value, if tracks should be automatically linked or not.

Customized timespan format

With this option you can change the default timespan format. This is useful, if your input contains only minutes and seconds and not hours, minutes and seconds. The format needs is entered as regular expression. A sample for a timespan format with minutes and seconds is \"(?'TimeSpanFormat.Minutes'\\d{1,})[:](?'TimeSpanFormat.Seconds'\\d{1,})\" which recognizes input of \"63:12\" as timeformat also.", - "Export of data": "Export of data", - "Export of data helptext": "Exporting all entered data is really easy in AudioCuesheetEditor. You can customize the export yourself and also can add new export profiles by yourself. Once you have a cuesheet you want to export, just click on the \"Display Export Profiles\" Button in the top menu bar inside \"Export\".
The export profiles dialog comes up, where you can easily switch between the current export profiles, delete and add new export profiles. Each change to export profile will get saved automatically. You can enter a name for the profile, a filename for download and the scheme for header, footer and tracks.
All valid placeholders can be added by manually typing them or selecting the \"Select placeholder\" button and click on the placeholder you want to add. You can also add text between placeholders that will not get touched by the export logic.
Once you want to use the export profile, just click on the \"Download export file\" button and the export will be generated for you.", - "User Interface": "User interface", - "User Interface helptext": "

Autocomplete

AudioCuesheetEditor has an build autocomplete feature for tracks. The data used come from MusicBrainz. Adding a Track and enter an artist to receive autocomplete suggestions.
Select a valid artist and you will get suggestions for title also.
If you select a title, the length will automatically be set.

Bulk edit

AudioCuesheetEditor offers you the possibility to edit multiple tracks at once. Simply choose the tracks you want to edit in overview (via \"Display selection of tracks\") and click \"Edit selected tracks\".
Edit dialog will appear and you can choose if you want to edit values dynamically (add or subtract value) or statically (entered value will be applied). \"=\" means that the absolute value is applied, \"-\" means that the value is subtracted and \"+\" means that the value is added.
", - "Shortcuts": "Shortcuts", - "Shortcuts helptext": "The following shortcuts are available:
  • Ctrl + p: Starts or pauses playback of the audio file
  • Play: Starts or pauses playback of the audio file
  • Ctrl + Left: Play previous track
  • Previous: Play previous track
  • Ctrl + Right: Play next track
  • Next: Play next track
  • Stop: Stops the playback of the audio file
  • Ctrl + z: Undo the last edit
  • Ctrl + y: Redo the last edit
  • Ctrl + h: Navigate to this help
  • Ctrl + e: Open the export profiles
  • Ctrl + s: Save the project (download as project file)
", - "Sections": "Sections", - "Sections helptext": "
Sections allow a cuesheet to be split into multiple files. This is useful, for example, when importing from a text file to split the project into several files. Sections can be specified in \"Full edit mode\" and \"Live record mode\" view modes.", - "Offline usage (progressive WebApp)": "Offline usage (progressive WebApp)", - "Offline usage helptext": "AudioCuesheetEditor is a progressive web app and can be installed on your device locally to run without an internet connection. To install the application on your device follow the instructions of your browser. Usually there is an icon in the adress bar indicating that the application can be installed locally." - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ImportFileView/de.json b/AudioCuesheetEditor/Resources/Localization/ImportFileView/de.json deleted file mode 100644 index 1eab9393..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ImportFileView/de.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "de", - "translations": { - "Preview": "Vorschau", - "Edit": "Bearbeiten" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ImportFileView/en.json b/AudioCuesheetEditor/Resources/Localization/ImportFileView/en.json deleted file mode 100644 index 1a2e8cea..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ImportFileView/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "en", - "translations": { - "Preview": "Preview", - "Edit": "Edit" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/Index/de.json b/AudioCuesheetEditor/Resources/Localization/Index/de.json deleted file mode 100644 index 65c4083c..00000000 --- a/AudioCuesheetEditor/Resources/Localization/Index/de.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "culture": "de", - "translations": { - "Cuesheet data": "Cuesheet Daten", - "Tracks": "Titel", - "Unlink this track from previous track": "Diesen Titel vom vorherigen Titel abkoppeln", - "Link this track with previous track": "Diesen Titel an den vorherigen Titel koppeln", - "Current track is played": "Dieser Titel wird aktuell wiedergegeben", - "Edit track tooltip": "Titeldetails in einem Dialog bearbeiten", - "Copy track tooltip": "Kopiere diesen Titel mit allen Werten und füge ihn anschließend zum Cuesheet hinzu.", - "Start playback this track": "Diesen Titel wiedergeben", - "Move track up tooltip": "Diesen Titel eine Position nach oben schieben", - "Delete track tooltip": "Diesen Titel löschen", - "Move track down tooltip": "Diesen Titel eine Position nach unten schieben", - "Add new track": "Neuen Titel hinzufügen", - "Abort": "Abbrechen", - "Name": "Name", - "Filename": "Dateiname", - "Close": "Schließen", - "Edit track details": "Titeldetails bearbeiten", - "Position": "Position", - "Flag4CHTooltip": "Setzen Sie diese Markierung, wenn der Titel 4 Kanal Audio enthält", - "FlagDCPTooltip": "Setzen Sie diese Markierung, wenn digitale Kopien verboten sind", - "FlagPRETooltip": "Setzen Sie diese Markierung, wenn der Titel Höhenanhebung hat", - "FlagSCMSTooltip": "Setzen Sie diese Markierung, wenn der Titel \"Serial Copy Management System\" hat", - "PreGap": "Vorlücke", - "PostGap": "Nachlücke", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "Die Datei \"{0}\" kann nicht für die angeforderte Operation verwendet werden: \"{1}\". Bitte nutzen Sie eine gültige, passende Datei!", - "Import textfile": "Import Textdatei", - "Error": "Fehler", - "Confirmation required": "Bestätigung erforderlich", - "Do you really want to import this file? This can not be undone and unsaved changes are lost!": "Möchten Sie wirklich diese Datei importieren? Dies kann nicht rückgängig gemacht werden und ungespeicherte Änderungen gehen verloren!", - "Import cuesheet": "Import Cuesheet", - "Enter cd artist here": "CD Künstler hier eingeben", - "Enter cd title here": "CD Titel hier eingeben", - "Enter cataloguenumber here": "Geben Sie hier die Katalognummer des Cuesheets an. Die Katalognummer ist eine 13 stellige Zahl.", - "Here all validation messages are displayed. Each message contains a reference to the corresponding field and can clicked to enter the field.": "Hier werden alle Validierungsnachrichten angezeigt. Jede Nachricht enthält eine Referenz auf das ursprüngliche Feld und kann daher angeklickt werden, um zu dem entsprechenden Feld zu gelangen.", - "Display selection of tracks": "Auswahl für Titel einblenden", - "Selection": "Auswahl", - "Select this track for multiple track operations": "Diesen Titel in Mehrfachauswahl übernehmen", - "Delete selected tracks": "Ausgewählte Titel löschen", - "Hide selection of tracks": "Auswahl für Titel ausblenden", - "Date": "Datum", - "DateTime": "Datum und Uhrzeit", - "Time": "Uhrzeit", - "Save changes": "Änderungen speichern", - "Cuesheet sections": "Cuesheet Abschnitte" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/Index/en.json b/AudioCuesheetEditor/Resources/Localization/Index/en.json deleted file mode 100644 index 2e8bd0f6..00000000 --- a/AudioCuesheetEditor/Resources/Localization/Index/en.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "culture": "en", - "translations": { - "Cuesheet data": "Cuesheet Data", - "Tracks": "Tracks", - "Unlink this track from previous track": "Unlink this track from previous track", - "Link this track with previous track": "Link this track with previous track", - "Current track is played": "Current track is played", - "Edit track tooltip": "Edit track details in a dialog", - "Copy track tooltip": "Copy this track with all values and attach it to the cuesheet.", - "Start playback this track": "Start playing this track", - "Move track up tooltip": "Move this track one position up", - "Delete track tooltip": "Delete this track", - "Move track down tooltip": "Move this track one position down", - "Add new track": "Add new track", - "Abort": "Abort", - "Name": "Name", - "Filename": "Filename", - "Close": "Close", - "Edit track details": "Edit track details", - "Position": "Position", - "Flag4CHTooltip": "Set this if track contains four channel audio", - "FlagDCPTooltip": "Set this if track is digital copy permitted", - "FlagPRETooltip": "Set this if track has Pre-emphasis enabled", - "FlagSCMSTooltip": "Set this if track has \"Serial Copy Management System\"", - "PreGap": "Pregap", - "PostGap": "Postgap", - "The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!": "The file \"{0}\" you supplied does not match the requested operation: \"{1}\". Please provide a valid file!", - "Import textfile": "Import textfile", - "Error": "Error", - "Confirmation required": "Confirmation required", - "Do you really want to import this file? This can not be undone and unsaved changes are lost!": "Do you really want to import this file? This can not be reversed and unsaved changes are lost!", - "Import cuesheet": "Import Cuesheet", - "Enter cd artist here": "Enter CD artist here", - "Enter cd title here": "Enter CD title here", - "Enter cataloguenumber here": "Enter the catalogue number of the cuesheet. Catalogue number is a 13 decimal digits number.", - "Here all validation messages are displayed. Each message contains a reference to the corresponding field and can clicked to enter the field.": "Here all validation messages are displayed. Each message contains a reference to the corresponding field and can clicked to enter the field.", - "Display selection of tracks": "Display selection of tracks", - "Selection": "Selection", - "Select this track for multiple track operations": "Select this track for multiple track operations", - "Delete selected tracks": "Delete selected tracks", - "Hide selection of tracks": "Hide selection of tracks", - "Date": "Date", - "DateTime": "Date and Time", - "Time": "Time", - "Save changes": "Save changes", - "Cuesheet sections": "Cuesheet sections" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/MainLayout/de.json b/AudioCuesheetEditor/Resources/Localization/MainLayout/de.json deleted file mode 100644 index 8370e1c7..00000000 --- a/AudioCuesheetEditor/Resources/Localization/MainLayout/de.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "culture": "de", - "translations": { - "About": "Über", - "An error occured": "Ein Fehler ist aufgetreten!", - "An error has occured in this application. Please report this error with as much details as possible here: https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?assignees=&labels=unreviewed+bug&template=bug_report.md&title=.": "Es ist ein Fehler in der Anwendung aufgetreten. Bitte melden Sie diesen Fehler unter Angabe von möglichst vielen Details hier: https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?assignees=&labels=unreviewed+bug&template=bug_report.md&title=.", - "Help": "Hilfe", - "Undo last change": "Letzte Änderung Rückgängig machen", - "Redo last change": "Letzte Änderung wiederherstellen", - "Export": "Export", - "Please check processinghints for errors, otherwise the file is not exportable: {0}": "Bitte prüfen sie die Bearbeitungshinweise auf Fehler. Andernfalls ist Datei nicht exportierbar: {0}", - "Download cuesheet": "Cuesheet herunterladen", - "Save project": "Projekt speichern", - "Open exportprofiles": "Exportprofile anzeigen", - "ViewMode": "Anzeigemodus", - "Reset": "Zurücksetzen", - "Delete all tracks": "Alle Titel löschen", - "Reset cuesheet": "Cuesheet zurücksetzen", - "Restart complete application": "Komplette Anwendung neustarten", - "Reset complete application": "Komplette Anwendung zurücksetzen", - "Filename": "Dateiname", - "Download projectfile": "Projektdatei herunterladen", - "Abort": "Abbrechen", - "Confirmation required": "Bestätigung erforderlich", - "Do you really want to reset the cuesheet? This can not be reversed.": "Möchten Sie wirklich das Cuesheet zurücksetzen? Dies kann nicht rückgangig gemacht werden.", - "Do you really want to delete all tracks? This can not be reversed.": "Möchten Sie wirklich alle Titel löschen? Dies kann nicht rückgängig gemacht werden.", - "Confirm restart of application. All unsaved changes are lost!": "Bitte Bestätigen Sie, dass die komplette Anwendung neugestartet werden soll. Alle ungespeicherten Änderungen werden dadurch verloren!", - "Confirm reset of application. All unsaved changes are lost and the application is reloaded!": "Bitte Bestätigen Sie, dass die komplette Anwendung auf Werkseinstellungen zurückgesetzt werden soll. Alle Daten werden gelöscht und die Anwedung neu geladen. Alle nicht gespeicherten Daten sind gelöscht!", - "ViewModeFull": "Detailbearbeitung", - "ViewModeRecord": "Aufnahmemodus", - "ViewModeImport": "Import Assistent", - "Undo": "Rückgängig", - "Redo": "Wiederherstellen", - "Tools": "Werkzeuge", - "Split points": "Aufteilungspunkte", - "Exportprofiles": "Export Profile", - "Select exportprofile": "Exportprofil auswählen", - "Add new exportprofile": "Neues Exportprofil hinzufügen", - "Delete selected exportprofile": "Aktuelles Exportprofil löschen", - "Name": "Name", - "Enter exportprofile name here": "Geben Sie hier den Namen des Exportprofils an", - "Enter exportprofile filename here": "Geben Sie hier den Dateinamen für das Exportprofil an", - "Enter exportschemehead here tooltip": "Bitte geben Sie hier das Kopf Schema für den Export an. Jeder Platzhalter wird durch seinen zugehörigen Wert ersetzt. Platzhalter beginnen und enden mit %.", - "Exportprofilescheme head": "Kopfschema", - "Enter exportscheme head here": "Bitte geben Sie hier das Kopf Schema für den Export an", - "Exportprofilescheme head validationerrors": "ExportProfil Kopf Schema Validierungsfehler", - "Enter exportscheme track here tooltip": "Bitte geben Sie hier das Titel Schema für den Export an. Jeder Platzhalter wird durch seinen zugehörigen Wert ersetzt. Platzhalter beginnen und enden mit %.", - "Exportprofilescheme track": "Titelschema", - "Enter exportscheme track here": "Bitte geben Sie hier das Titel Schema für den Export an", - "Exportprofilescheme track validationerrors": "ExportProfil Titel Schema Validierungsfehler", - "Enter exportscheme footer here tooltip": "Bitte geben Sie hier das Fuß Schema für den Export an. Jeder Platzhalter wird durch seinen zugehörigen Wert ersetzt. Platzhalter beginnen und enden mit %.", - "Exportprofilescheme footer": "Fußschema", - "Enter exportscheme footer here": "Bitte geben Sie hier das Fuß Schema für den Export an", - "Exportprofilescheme footer validationerrors": "ExportProfil Fuß Schema Validierungsfehler", - "Exportprofile is not exportable. Please check validationerrors and solve errors in order to download export.": "Export Profil ist nicht exportierbar. Bitte prüfen Sie die Validierungsfehler und beheben Sie diese, um die Exportdatei herunterzuladen.", - "Download export": "Exportdatei herunterladen", - "Select placeholder": "Platzhalter auswählen", - "Artist": "Künstler", - "Title": "Titel", - "Audiofile": "Audiodatei", - "CDTextfile": "CD Textdatei", - "Cataloguenumber": "Katalognummer", - "Date": "Datum", - "DateTime": "Datum und Uhrzeit", - "Time": "Uhrzeit", - "Position": "Position", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Flags": "Markierungen", - "PreGap": "Vorlücke", - "PostGap": "Nachlücke", - "Error details": "Fehlerdetails", - "Reload application": "Applikation neu laden", - "Options": "Optionen", - "Preview environment": "Vorschauumgebung" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/MainLayout/en.json b/AudioCuesheetEditor/Resources/Localization/MainLayout/en.json deleted file mode 100644 index aa66eaad..00000000 --- a/AudioCuesheetEditor/Resources/Localization/MainLayout/en.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "culture": "en", - "translations": { - "About": "About", - "An error occured": "An error has occured!", - "An error has occured in this application. Please report this error with as much details as possible here: https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?assignees=&labels=unreviewed+bug&template=bug_report.md&title=.": "An error has occured in this application. Please report this error with as much details as possible here: https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?assignees=&labels=unreviewed+bug&template=bug_report.md&title=.", - "Help": "Help", - "Undo last change": "Undo last change", - "Redo last change": "Redo last change", - "Export": "Export", - "Please check processinghints for errors, otherwise the file is not exportable: {0}": "Please check processinghints for errors, otherwise the file is not exportable: {0}", - "Download cuesheet": "Download cuesheet", - "Save project": "Save project", - "Open exportprofiles": "Display Exportprofiles", - "ViewMode": "Viewmode", - "Reset": "Reset", - "Delete all tracks": "Delete all tracks", - "Reset cuesheet": "Reset cuesheet", - "Restart complete application": "Restart complete application", - "Reset complete application": "Reset complete application", - "Filename": "Filename", - "Download projectfile": "Download projectfile", - "Abort": "Abort", - "Confirmation required": "Confirmation required", - "Do you really want to reset the cuesheet? This can not be reversed.": "Do you really want to reset the cuesheet? This can not be reversed.", - "Do you really want to delete all tracks? This can not be reversed.": "Do you really want to delete all tracks? This can not be reversed.", - "Confirm restart of application. All unsaved changes are lost!": "Please confirm, that you want to restart the whole application. All data will be lost if not saved before!", - "Confirm reset of application. All unsaved changes are lost and the application is reloaded!": "Please confirm, that you want to reset the whole application. All data will be reset and the application will be reloaded. Every unchanged data will be lost!", - "ViewModeFull": "Full edit mode", - "ViewModeRecord": "Live record mode", - "ViewModeImport": "Import assistant", - "Undo": "Undo", - "Redo": "Redo", - "Tools": "Tools", - "Split points": "Split points", - "Exportprofiles": "Export Profiles", - "Select exportprofile": "Select export profile", - "Add new exportprofile": "Add new exportprofile", - "Delete selected exportprofile": "Delete selected exportprofile", - "Name": "Name", - "Enter exportprofile name here": "Enter name of export profile here", - "Enter exportprofile filename here": "Enter name of file for export profile here", - "Enter exportschemehead here tooltip": "Enter scheme for export of header here. Each placeholder will be replaced by the placeholder value. Placeholder start and end with %.", - "Exportprofilescheme head": "Headerscheme", - "Enter exportscheme head here": "Enter scheme for export of header here", - "Exportprofilescheme head validationerrors": "Export profile header scheme validation errors", - "Enter exportscheme track here tooltip": "Enter scheme for export of tracks here. Each placeholder will be replaced by the placeholder value. Placeholder start and end with %.", - "Exportprofilescheme track": "Trackscheme", - "Enter exportscheme track here": "Enter scheme for export of tracks here", - "Exportprofilescheme track validationerrors": "Export profile track scheme validation errors", - "Enter exportscheme footer here tooltip": "Enter scheme for export of footer here. Each placeholder will be replaced by the placeholder value. Placeholder start and end with %.", - "Exportprofilescheme footer": "Footerscheme", - "Enter exportscheme footer here": "Enter scheme for export of footer here", - "Exportprofilescheme footer validationerrors": "Export profile footer scheme validation errors", - "Exportprofile is not exportable. Please check validationerrors and solve errors in order to download export.": "Export profile is not exportable. Please check the validation errors and fix them in order to download the export.", - "Download export": "Download export file", - "Select placeholder": "Select placeholder", - "Artist": "Artist", - "Title": "Title", - "Audiofile": "Audiofile", - "CDTextfile": "CD Textfile", - "Cataloguenumber": "Cataloguenumber", - "Date": "Date", - "DateTime": "Date and Time", - "Time": "Time", - "Position": "Position", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Flags": "Flags", - "PreGap": "Pregap", - "PostGap": "Postgap", - "Error details": "Error details", - "Reload application": "Reload application", - "Options": "Options", - "Preview environment": "Preview environment" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ModalDialog/de.json b/AudioCuesheetEditor/Resources/Localization/ModalDialog/de.json deleted file mode 100644 index d45c6f6f..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ModalDialog/de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "culture": "de", - "translations": { - "Confirm": "Bestätigen", - "Decline": "Abbrechen", - "Ok": "Okay" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ModalDialog/en.json b/AudioCuesheetEditor/Resources/Localization/ModalDialog/en.json deleted file mode 100644 index 52a63c07..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ModalDialog/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "culture": "en", - "translations": { - "Confirm": "Confirm", - "Decline": "Decline", - "Ok": "Okay" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/de.json b/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/de.json deleted file mode 100644 index 2709c8c3..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "culture": "de", - "translations": { - "Prepare export": "Export vorbereiten", - "Result": "Ergebnis", - "Name": "Name", - "Begin": "Anfang", - "End": "Ende", - "Content": "Inhalt", - "Download this file": "Diese Datei herunterladen", - "Generate export files": "Exportdateien generieren", - "Abort": "Abbrechen", - "Export files can not be generated. Please check validationerrors and solve errors in order to download export: {0}": "Exportdateien können nicht generiert werden. Bitte prüfen Sie die Validierungsfehler und beheben Sie diese, um die Exportdateien herunterladen zu können: {0}", - "Close": "Schließen", - "Download split audio file": "Verarbeitete Audiodatei herunterladen", - "Generation in progress, please stand by ...": "Export wird durchgeführt, bitte warten Sie ...." - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/en.json b/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/en.json deleted file mode 100644 index 973e4b8a..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ModalExportdialog/en.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "culture": "en", - "translations": { - "Prepare export": "Prepare export", - "Result": "Result", - "Name": "Name", - "Begin": "Begin", - "End": "End", - "Content": "Content", - "Download this file": "Download this file", - "Generate export files": "Generate export files", - "Abort": "Abort", - "Export files can not be generated. Please check validationerrors and solve errors in order to download export: {0}": "Export files can not be generated. Please check validationerrors and solve errors in order to download export: {0}", - "Close": "Close", - "Download split audio file": "Download split audio file", - "Generation in progress, please stand by ...": "Generation in progress, please stand by ..." - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/OptionsDialog/de.json b/AudioCuesheetEditor/Resources/Localization/OptionsDialog/de.json deleted file mode 100644 index af0e6203..00000000 --- a/AudioCuesheetEditor/Resources/Localization/OptionsDialog/de.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "culture": "de", - "translations": { - "Options": "Optionen", - "Common settings": "Allgemeine Einstellungen", - "Record settings": "Aufnahme Einstellungen", - "Culture setting": "Sprache wählen", - "Default viewmode": "Standard Anzeigemodus", - "ViewModeFull": "Detailbearbeitung", - "ViewModeRecord": "Aufnahmemodus", - "ViewModeImport": "Import Assistent", - "Cuesheet filename": "Cuesheet Dateiname", - "Project filename": "Projektdateiname", - "Automatically link tracks": "Titel standardmäßig koppeln", - "Automatically link tracks with previous": "Titel standardmäßig an den vorherigen Titel koppeln", - "Textimportscheme cuesheet": "Textschema für den Import von Cuesheeteigenschaften", - "Textimportscheme track": "Textschema für den Import von Titeleigenschaften", - "Filename for recorded audio": "Dateiname für aufgenommenes Audio", - "Record countdown timer in seconds": "Aufnahmecountdown in Sekunden", - "Record time sensitivity": "Genauigkeit der Aufnahmezeiten", - "TimeSensitivityMode.Full": "Alles (inkl. Millisekunden)", - "TimeSensitivityMode.Seconds": "Nur Sekunden", - "TimeSensitivityMode.Minutes": "Nur Minuten", - "Save": "Speichern", - "Reload options": "Optionen neu laden", - "Reset": "Zurücksetzen", - "Close": "Schließen", - "Enter custom timespan format here": "Hier können Sie bei Bedarf ein angepasstes Format für Zeitspannen eingeben. Details können der Hilfe entnommen werden.", - "Customized timespan format": "Angepasstes Zeitspannenformat", - "Select placeholder": "Platzhalter auswählen", - "Days": "Tage", - "Hours": "Stunden", - "Minutes": "Minuten", - "Seconds": "Sekunden", - "Milliseconds": "Millisekunden", - "ENTER REGULAR EXPRESSION HERE": "Hier regulären Ausdruck eingeben" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/OptionsDialog/en.json b/AudioCuesheetEditor/Resources/Localization/OptionsDialog/en.json deleted file mode 100644 index d4da8ab8..00000000 --- a/AudioCuesheetEditor/Resources/Localization/OptionsDialog/en.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "culture": "en", - "translations": { - "Options": "Options", - "Common settings": "Common settings", - "Record settings": "Record settings", - "Culture setting": "Select language", - "Default viewmode": "Default viewmode", - "ViewModeFull": "Full edit mode", - "ViewModeRecord": "Live record mode", - "ViewModeImport": "Import assistant", - "Cuesheet filename": "Cuesheet filename", - "Project filename": "Project filename", - "Automatically link tracks": "Automatically link tracks", - "Automatically link tracks with previous": "Automatically link tracks with previous track", - "Textimportscheme cuesheet": "Textimport regular expression for cuesheet properties", - "Textimportscheme track": "Textimport regular expression for track properties", - "Filename for recorded audio": "Filename for recorded audio", - "Record countdown timer in seconds": "Record countdown timer in seconds", - "Record time sensitivity": "Recordtime sensitivity", - "TimeSensitivityMode.Full": "Everything (incl. milliseconds)", - "TimeSensitivityMode.Seconds": "Only seconds", - "TimeSensitivityMode.Minutes": "Only minutes", - "Save": "Save", - "Reload options": "Reload options", - "Reset": "Reset", - "Close": "Close", - "Enter custom timespan format here": "You can enter a custom format for timespan here. Details can be found in help.", - "Customized timespan format": "Customized timespan format", - "Select placeholder": "Select placeholder", - "Days": "Days", - "Hours": "Hours", - "Minutes": "Minutes", - "Seconds": "Seconds", - "Milliseconds": "Milliseconds", - "ENTER REGULAR EXPRESSION HERE": "Enter regular expression here" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json b/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json deleted file mode 100644 index 4de5cadf..00000000 --- a/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "culture": "de", - "translations": { - "Record running!": "Aufnahme aktiv!", - "Start recording": "Aufnahme starten", - "Stop recording": "Aufnahme beenden", - "Start record countdown timer": "Aufnahmecountdown starten", - "Record will start in {0} seconds!": "Aufnahme wird in {0} Sekunden starten!", - "Error": "Fehler", - "Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet.": "Das Cuesheet enthält bereits Titel. Die Aufnahme ist nicht möglich, wenn bereits Titel vorhanden sind. Bitte speichern Sie ihre Arbeit und starten Sie anschließend mit einem leeren Cuesheet.", - "Start countdown": "Aufnahmecountdown starten", - "Abort": "Abbrechen", - "Seconds till record starts": "Sekunden bis die Aufnahme startet", - "Abort countdown": "Aufnahmecountdown abbrechen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/RecordControl/en.json b/AudioCuesheetEditor/Resources/Localization/RecordControl/en.json deleted file mode 100644 index 20daa93e..00000000 --- a/AudioCuesheetEditor/Resources/Localization/RecordControl/en.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "culture": "en", - "translations": { - "Record running!": "Record is running!", - "Start recording": "Start recording", - "Stop recording": "Stop recording", - "Start record countdown timer": "Start record countdown timer", - "Record will start in {0} seconds!": "Record will start in {0} seconds!", - "Error": "Error", - "Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet.": "Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet.", - "Start countdown": "Start countdown", - "Abort": "Abort", - "Seconds till record starts": "Seconds till record starts", - "Abort countdown": "Abort countdown" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/de.json b/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/de.json deleted file mode 100644 index 5c21be15..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/de.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "de", - "translations": { - "Unlink this track from previous track": "Diesen Titel vom vorherigen Titel abkoppeln", - "Link this track with previous track": "Diesen Titel an den vorherigen Titel koppeln" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/en.json b/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/en.json deleted file mode 100644 index 5effbf15..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackLinkControl/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "en", - "translations": { - "Unlink this track from previous track": "Unlink this track from previous track", - "Link this track with previous track": "Link this track with previous track" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackList/de.json b/AudioCuesheetEditor/Resources/Localization/TrackList/de.json deleted file mode 100644 index 2e75cffa..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackList/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "culture": "de", - "translations": { - "Selection": "Auswahl", - "Select all": "Wählen Sie alle Spuren für Mehrfachspuroperationen aus", - "Controls": "Steuerung", - "Artist": "Künstler", - "Title": "Titel", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Position": "Position", - "Abort": "Abbrechen", - "Save changes": "Änderungen speichern", - "Confirmation required": "Bestätigung erforderlich", - "Do you really want to delete all tracks?": "Möchten Sie wirklich alle Titel löschen?", - "Validation errors": "Validierungsfehler" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackList/en.json b/AudioCuesheetEditor/Resources/Localization/TrackList/en.json deleted file mode 100644 index c5a255f1..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackList/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "culture": "en", - "translations": { - "Selection": "Selection", - "Select all": "Select all track for multiple track operations", - "Delete all tracks": "Delete all tracks", - "Controls": "Controls", - "Artist": "Artist", - "Title": "Title", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Position": "Position", - "Abort": "Abort", - "Save changes": "Save changes", - "Confirmation required": "Confirmation required", - "Do you really want to delete all tracks?": "Do you really want to delete all tracks?", - "Validation errors": "Validation errors" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/de.json b/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/de.json deleted file mode 100644 index 4830f203..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/de.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "culture": "de", - "translations": { - "Add new track": "Neuen Titel hinzufügen", - "Display selection of tracks": "Auswahl einblenden", - "Delete selected tracks": "Ausgewählte löschen", - "Hide selection of tracks": "Auswahl ausblenden", - "Delete all tracks": "Alle Titel löschen", - "Edit selected tracks": "Ausgewählte Titel bearbeiten", - "Pin table header": "Tabellenkopf anheften", - "Pin the table header to top, displaying all controls above the table while scrolling through the records": "Tabellenkopf anheften, um während des scrollens alle Buttons oberhalb anzuordnen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/en.json b/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/en.json deleted file mode 100644 index a6a5a5a6..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListControlButtons/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "culture": "en", - "translations": { - "Add new track": "Add new track", - "Display selection of tracks": "Display selection", - "Delete selected tracks": "Delete selected", - "Hide selection of tracks": "Hide selection", - "Delete all tracks": "Delete all tracks", - "Edit selected tracks": "Edit selected tracks", - "Pin table header": "Pin table header", - "Pin the table header to top, displaying all controls above the table while scrolling through the records": "Pin the table header to top, displaying all controls above the table while scrolling through the records" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListItem/de.json b/AudioCuesheetEditor/Resources/Localization/TrackListItem/de.json deleted file mode 100644 index f7bdaaf0..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListItem/de.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "de", - "translations": { - "Current track is played": "Dieser Track wird aktuell wiedergegeben", - "A split point is currently set at the time of this track": "Ein Aufteilungspunkt ist aktuell in dem Zeitfenster dieses Titels gesetzt" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListItem/en.json b/AudioCuesheetEditor/Resources/Localization/TrackListItem/en.json deleted file mode 100644 index b8f6bf35..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListItem/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "culture": "en", - "translations": { - "Current track is played": "Current track is played", - "A split point is currently set at the time of this track": "A split point is currently set at the time of this track" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/de.json b/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/de.json deleted file mode 100644 index 04281f60..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/de.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "culture": "de", - "translations": { - "Edit track tooltip": "Titeldetails in einem Dialog bearbeiten", - "Copy track tooltip": "Kopiere diesen Titel mit allen Werten und füge ihn anschließend zum Cuesheet hinzu.", - "Start playback this track": "Diesen Titel wiedergeben", - "Move track up tooltip": "Diesen Titel eine Position nach oben schieben", - "Delete track tooltip": "Diesen Titel löschen", - "Move track down tooltip": "Diesen Titel eine Position nach unten schieben" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/en.json b/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/en.json deleted file mode 100644 index 144c6602..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackListItemControlColumn/en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "culture": "en", - "translations": { - "Edit track tooltip": "Edit track details in a dialog", - "Copy track tooltip": "Copy this track with all values and attach it to the cuesheet.", - "Start playback this track": "Start playing this track", - "Move track up tooltip": "Move this track one position up", - "Delete track tooltip": "Delete this track", - "Move track down tooltip": "Move this track one position down" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackSelection/de.json b/AudioCuesheetEditor/Resources/Localization/TrackSelection/de.json deleted file mode 100644 index 7d261e5e..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackSelection/de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "de", - "translations": { - "Select this track for multiple track operations": "Diesen Titel in Mehrfachauswahl übernehmen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/TrackSelection/en.json b/AudioCuesheetEditor/Resources/Localization/TrackSelection/en.json deleted file mode 100644 index a61c2a09..00000000 --- a/AudioCuesheetEditor/Resources/Localization/TrackSelection/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "culture": "en", - "translations": { - "Select this track for multiple track operations": "Select this track for multiple track operations" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ValidationMessage/de.json b/AudioCuesheetEditor/Resources/Localization/ValidationMessage/de.json deleted file mode 100644 index 5d513fa6..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ValidationMessage/de.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "culture": "de", - "translations": { - "{0} has no value!": "{0} hat keinen Wert!", - "{0} may not be 0!": "{0} darf nicht 0 sein!", - "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!": "Track({0},{1},{2},{3},{4}) hat nicht die korrekte Position '{5}'!", - "{0} must be equal or greater zero!": "{0} muss größer oder gleich 0 sein!", - "{0} must only contain numbers!": "{0} darf nur Ziffern enthalten!", - "{0} has an invalid length. Allowed length is {1}!": "{0} hat eine ungültige Länge. Erlaubte Länge ist {1}!", - "{0} has invalid Count ({1})!": "{0} hat eine ungültige Anzahl ({1})!", - "{0} {1} '{2}' is used also by {3}({4},{5},{6},{7},{8}). Positions must be unique!": "{0} {1} '{2}' wird auch von {3}({4},{5},{6},{7},{8}) benutzt. Positionen müssen eindeutig sein!", - "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!": "{0}({1},{2},{3},{4},{5}) überschneidet sich mit {0}({6},{7},{8},{9},{10}). Bitte stellen sie sicher, dass das Zeitinterval nur einmal genutzt wird!", - "{0} has invalid value!": "{0} hat einen ungültigen Wert!", - "{0} contains no placeholder!": "{0} enthält keinen Platzhalter!", - "Replace '{0}' by a regular expression!": "Ersetzen sie '{0}' durch einen regulären Ausdruck!", - "{0} contains placeholder '{1}' that can not be resolved!": "{0} enthält Platzhalter '{1}' die nicht aufgelöst werden können!", - "{0} contains placeholders that can not be solved! Please remove invalid placeholder '{1}'.": "{0} enhält Platzhalter die nicht aufgelöst werden können! Bitte entfernen Sie den ungültigen Platzhalter '{1}'.", - "Position": "Position", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Value": "Wert", - "Tracks": "Titel", - "Audiofile": "Audiodatei", - "SchemeType": "Schematyp", - "Scheme": "Schema", - "ENTER REGULAR EXPRESSION HERE": "Hier regulären Ausdruck eingeben", - "SchemeHead": "Kopfschema", - "SchemeTracks": "Titelschema", - "SchemeFooter": "Fußschema", - "Moment": "Zeitpunkt", - "{0} must end with '{1}'!": "{0} musss mit '{1}' enden!", - "{0} must have a filename!": "{0} muss einen Dateinamen haben!", - "CuesheetFilename": "Cuesheet Dateiname", - "RecordedAudiofilename": "Dateiname für aufgenommenes Audio", - "ProjectFilename": "Projektdateiname", - "Filename": "Dateiname", - "Artist": "Künstler", - "Title": "Titel", - "Exportprofile": "Exportprofil", - "ApplicationOptions": "Applikationsoptionen", - "AudiofileName": "Audiodatei", - "{0} should be greater than or equal '{1}'!": "{0} sollte größer oder gleich '{1}' sein!", - "{0} should be less than or equal '{1}'!": "{0} sollte kleiner oder gleich '{1}' sein!" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ValidationMessage/en.json b/AudioCuesheetEditor/Resources/Localization/ValidationMessage/en.json deleted file mode 100644 index 588a2cc6..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ValidationMessage/en.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "culture": "en", - "translations": { - "{0} has no value!": "{0} has no value!", - "{0} may not be 0!": "{0} may not be 0!", - "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!": "Track({0},{1},{2},{3},{4}) does not have the correct position '{5}'!", - "{0} must be equal or greater zero!": "{0} must be equal or greater zero!", - "{0} must only contain numbers!": "{0} must only contain numbers!", - "{0} has an invalid length. Allowed length is {1}!": "{0} has an invalid length. Allowed length is {1}!", - "{0} has invalid Count ({1})!": "{0} has invalid Count ({1})!", - "{0} {1} '{2}' is used also by {3}({4},{5},{6},{7},{8}). Positions must be unique!": "{0} {1} '{2}' is used also by {3}({4},{5},{6},{7},{8}). Positions must be unique!", - "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!": "{0}({1},{2},{3},{4},{5}) is overlapping with {0}({6},{7},{8},{9},{10}). Please make shure the timeinterval is only used once!", - "{0} has invalid value!": "{0} has invalid value!", - "{0} contains no placeholder!": "{0} contains no placeholder!", - "Replace '{0}' by a regular expression!": "Replace '{0}' by a regular expression!", - "{0} contains placeholder '{1}' that can not be resolved!": "{0} contains placeholder '{1}' that can not be resolved!", - "{0} contains placeholders that can not be solved! Please remove invalid placeholder '{1}'.": "{0} contains placeholders that can not be solved! Please remove invalid placeholder '{1}'.", - "Position": "Position", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Value": "Value", - "Tracks": "Tracks", - "Audiofile": "Audiofile", - "SchemeType": "Schemetype", - "Scheme": "Scheme", - "ENTER REGULAR EXPRESSION HERE": "Enter regular expression here", - "SchemeHead": "Headerscheme", - "SchemeTracks": "Trackscheme", - "SchemeFooter": "Footerscheme", - "Moment": "Moment", - "{0} must end with '{1}'!": "{0} must end with '{1}'!", - "{0} must have a filename!": "{0} must have a filename!", - "CuesheetFilename": "Cuesheet filename", - "RecordedAudiofilename": "Filename for recorded audio", - "ProjectFilename": "Project filename", - "Filename": "Filename", - "Artist": "Artist", - "Title": "Title", - "Exportprofile": "Exportprofile", - "ApplicationOptions": "Application options", - "AudiofileName": "Audiofile", - "{0} should be greater than or equal '{1}'!": "{0} should be greater than or equal '{1}'!", - "{0} should be less than or equal '{1}'!": "{0} should be less than or equal '{1}'!" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ViewModeImport/de.json b/AudioCuesheetEditor/Resources/Localization/ViewModeImport/de.json deleted file mode 100644 index 6fadba0f..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeImport/de.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "culture": "de", - "translations": { - "Select files": "Datei wählen", - "Validate": "Validieren", - "Select files for import": "Dateien für den Import auswählen", - "Choose file or drag it here": "Wählen Sie die Dateien oder ziehen Sie diese hier her", - "Invalid file": "Ungültige Datei", - "You dropped an invalid file ({0}) that can not be processed.": "Sie haben eine ungültige Datei ({0}) selektiert, die nicht verarbeitet werden kann.", - "Validate data for import": "Daten für den Import validieren", - "Recognition of import data finished": "Analyse der Importdateien abgeschlossen", - "Please validate the following data recognized by import assistant. Once you have validated all input, you can confirm import of data.": "Bitte validieren sie die analysierten und angezeigten Dateien. Anschließend können Sie die Daten bestätigen und den Import durchführen.", - "Import the displayed data": "Angezeigte Daten importieren", - "Cuesheet data": "Cuesheet Daten", - "Tracks": "Titel", - "Textimport assistant": "Importassistent für Textdateien", - "Filecontent": "Dateiinhalt", - "Enter textimportscheme cuesheet tooltip": "Textschema für den Import von Cuesheeteigenschaften hier eingeben. Die Identifikation wird über reguläre Ausdrücke ausgeführt.", - "Enter textimportscheme cuesheet here": "Textschema für den Import von Cuesheeteigenschaften hier eingeben", - "Select placeholder": "Platzhalter auswählen", - "Enter textimportscheme track tooltip": "Textschema für den Import von Titeleigenschaften hier eingeben. Die Identifikation wird über reguläre Ausdrücke ausgeführt.", - "Enter textimportscheme track here": "Textschema für den Import von Titeleigenschaften hier eingeben", - "Error during textimport": "Ein Fehler ist während des Textimport aufgetreten", - "Flags": "Markierungen", - "Import not possible due to textimport errors. Please check errors!": "Import ist nicht möglich, weil der Textimport Fehler aufweist. Bitte prüfen Sie die Fehler!", - "Start textimport": "Textimport starten", - "Abort": "Abbrechen", - "CD artist": "CD Künstler", - "CD title": "CD Titel", - "Audiofile": "Audiodatei", - "Change": "Ändern", - "Cataloguenumber": "Katalognummer", - "CD textfile": "CD Textdatei", - "Artist": "Künstler", - "Title": "Titel", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Textimportscheme cuesheet": "Import Schema Cuesheet", - "Textimportscheme track": "Import Schema Titel", - "Import Options": "Importoptionen", - "ENTER REGULAR EXPRESSION HERE": "Hier regulären Ausdruck eingeben", - "Enter custom timespan format here": "Hier können Sie bei Bedarf ein angepasstes Format für Zeitspannen eingeben. Details können der Hilfe entnommen werden.", - "Customized timespan format import": "Zeitspannenformat für Import", - "Days": "Tage", - "Hours": "Stunden", - "Minutes": "Minuten", - "Seconds": "Sekunden", - "Milliseconds": "Millisekunden", - "Reset import options": "Optionen zurücksetzen", - "Reload import options": "Optionen erneut laden", - "Reset import options to defaults": "Optionen auf Standardeinstellung zurücksetzen", - "Please confirm": "Bestätigung erforderlich", - "Do you really want to reset the import options to default? This can not be undone!": "Möchten Sie wirklich die Import Optionen zurücksetzen? Dies kann nicht rückgängig gemacht werden!", - "Abort import of displayed data": "Import der angezeigten Daten abbrechen", - "Cuesheet sections": "Cuesheet Abschnitte" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ViewModeImport/en.json b/AudioCuesheetEditor/Resources/Localization/ViewModeImport/en.json deleted file mode 100644 index be324ec3..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeImport/en.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "culture": "en", - "translations": { - "Select files": "Select files", - "Validate": "Validate", - "Select files for import": "Select files for import", - "Choose file or drag it here": "Choose files or drag them here", - "Invalid file": "Invalid file", - "You dropped an invalid file ({0}) that can not be processed.": "You dropped an invalid file ({0}) that can not be processed.", - "Validate data for import": "Validate data for import", - "Recognition of import data finished": "Recognition of import data finished", - "Please validate the following data recognized by import assistant. Once you have validated all input, you can confirm import of data.": "Please validate the following data recognized by import assistant. Once you have validated all input, you can confirm import of data.", - "Import the displayed data": "Import the displayed data", - "Cuesheet data": "Cuesheet Data", - "Tracks": "Tracks", - "Textimport assistant": "Import textfile assistant", - "Filecontent": "Filecontent", - "Enter textimportscheme cuesheet tooltip": "Enter textscheme for cuesheet properties import here. Identification will be done using regular expressions.", - "Enter textimportscheme cuesheet here": "Enter textscheme for cuesheet properties import here", - "Select placeholder": "Select placeholder", - "Enter textimportscheme track tooltip": "Enter textscheme for track properties import here. Identification will be done using regular expressions.", - "Enter textimportscheme track here": "Enter textscheme for track properties import here", - "Error during textimport": "An error occured during text import", - "Flags": "Flags", - "Import not possible due to textimport errors. Please check errors!": "Import is not possible due to errors. Please check import errors!", - "Start textimport": "Start textimport", - "Abort": "Abort", - "CD artist": "CD artist", - "CD title": "CD title", - "Audiofile": "Audiofile", - "CD textfile": "CD Textfile", - "Cataloguenumber": "Cataloguenumber", - "Artist": "Artist", - "Title": "Title", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Textimportscheme cuesheet": "Textimport scheme cuesheet", - "Textimportscheme track": "Textimport scheme track", - "Import Options": "Import options", - "ENTER REGULAR EXPRESSION HERE": "Enter regular expression here", - "Enter custom timespan format here": "You can enter a custom format for timespan here. Details can be found in help.", - "Customized timespan format import": "Timespan format for import", - "Days": "Days", - "Hours": "Hours", - "Minutes": "Minutes", - "Seconds": "Seconds", - "Milliseconds": "Milliseconds", - "Reset import options": "Reset import options", - "Reload import options": "Reload import options", - "Reset import options to defaults": "Reset import options to defaults", - "Please confirm": "Please confirm", - "Do you really want to reset the import options to default? This can not be undone!": "Do you really want to reset the import options to default? This can not be undone!", - "Abort import of displayed data": "Abort import of displayed data", - "Cuesheet sections": "Cuesheet sections" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json b/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json deleted file mode 100644 index 9fd846dc..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "culture": "de", - "translations": { - "Recordcontrol": "Aufnahmesteuerung", - "Record running!": "Aufnahme aktiv!", - "Start recording": "Aufnahme starten", - "Stop recording": "Aufnahme beenden", - "Cuesheet data": "Cuesheet Daten", - "CD artist": "CD Künstler", - "CD title": "CD Titel", - "Enter cd artist here": "CD Künstler hier eingeben", - "Enter cd title here": "CD Titel hier eingeben", - "Audiofile": "Audiodatei", - "Artist": "Künstler", - "Title": "Titel", - "Begin": "Beginn", - "End": "Ende", - "Length": "Länge", - "Add new track": "Neuen Titel hinzufügen", - "Enter new track": "Neuen Titel eingeben", - "Tracks": "Titel", - "Controls": "Steuerung", - "Hints": "Hinweise", - "Recording": "Aufnahme", - "Download recorded audio": "Aufnahme herunterladen", - "Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet.": "Das Cuesheet enthält bereits Titel. Die Aufnahme ist nicht möglich, wenn bereits Titel vorhanden sind. Bitte speichern Sie ihre Arbeit und starten Sie anschließend mit einem leeren Cuesheet.", - "Error": "Fehler", - "Here all validation messages will be displayed. For details switch to full edit mode.": "Hier werden alle Validierungsnachrichten angezeigt. Für Details wechseln Sie bitte in den Bearbeitungsmodus \"Komplette Bearbeitung\".", - "Start record timer": "Aufnahmecountdown starten", - "Record will start in {0} seconds!": "Aufnahme wird in {0} Sekunden starten!", - "Record options": "Aufnahmeoptionen" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/en.json b/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/en.json deleted file mode 100644 index c84a3a9f..00000000 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/en.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "culture": "en", - "translations": { - "Recordcontrol": "Control recording", - "Record running!": "Record is running!", - "Start recording": "Start recording", - "Stop recording": "Stop recording", - "Cuesheet data": "Cuesheet data", - "CD artist": "CD artist", - "CD title": "CD title", - "Enter cd artist here": "Enter CD artist here", - "Enter cd title here": "Enter CD title here", - "Audiofile": "Audiofile", - "Artist": "Artist", - "Title": "Title", - "Begin": "Begin", - "End": "End", - "Length": "Length", - "Add new track": "Add new track", - "Enter new track": "Enter new track", - "Tracks": "Tracks", - "Controls": "Controls", - "Hints": "Hints", - "Recording": "Recording", - "Download recorded audio": "Download recorded audio", - "Cuesheet already contains tracks. Recording is not possible, if tracks are present. Please save your work and start with a clean cuesheet.": "Cuesheet already contains tracks. Recording is not possible when there are tracks present in the cuesheet. Please save your work and start with a clean cuesheet.", - "Error": "Error", - "Here all validation messages will be displayed. For details switch to full edit mode.": "All validation messages will be displayed here. For details please switch to \"Full edit mode\".", - "Start record timer": "Start record countdown timer", - "Record will start in {0} seconds!": "Record will start in {0} seconds!", - "Record options": "Record options" - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Services/Audio/PlaybackService.cs b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs new file mode 100644 index 00000000..89f90592 --- /dev/null +++ b/AudioCuesheetEditor/Services/Audio/PlaybackService.cs @@ -0,0 +1,259 @@ +//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.UI; +using Howler.Blazor.Components; + +namespace AudioCuesheetEditor.Services.Audio +{ + public class PlaybackService : IDisposable + { + private readonly ISessionStateContainer _sessionStateContainer; + private readonly IHowl _howl; + + private int? soundId; + private IAudiofile? currentlyPlayingAudiofile; + private Timer? updateTimer; + private bool disposedValue; + private readonly Lock timerLock = new(); + private TimeSpan? currentPosition; + private Cuesheet cuesheet; + + public event Action? CurrentPositionChanged; + + public TimeSpan? CurrentPosition + { + get => currentPosition; + private set + { + if (currentPosition != value) + { + currentPosition = value; + CurrentPositionChanged?.Invoke(); + } + } + } + public Track? CurrentlyPlayingTrack => cuesheet.Tracks.SingleOrDefault(x => x.Begin.HasValue == true && x.End.HasValue == true && x.Begin <= CurrentPosition && x.End > CurrentPosition); + public TimeSpan? TotalTime => cuesheet.Audiofile?.Duration; + public Boolean IsPlaying { get; private set; } = false; + //Refer to Cuesheet (not ImportCuesheet) since playback will always be done on the imported cuesheet + public Boolean PlaybackPossible => cuesheet.Audiofile != null && cuesheet.Audiofile.PlaybackPossible; + public Boolean PreviousPossible => (CurrentlyPlayingTrack != null) && cuesheet.Tracks.ToList().IndexOf(CurrentlyPlayingTrack) >= 1; + public Boolean NextPossible => (CurrentlyPlayingTrack != null) && cuesheet.Tracks.ToList().IndexOf(CurrentlyPlayingTrack) < cuesheet.Tracks.Count - 1; + + public PlaybackService(ISessionStateContainer sessionStateContainer, IHowl howl) + { + _sessionStateContainer = sessionStateContainer; + cuesheet = _sessionStateContainer.Cuesheet; + cuesheet.AudiofileChanged += Cuesheet_AudiofileChanged; + _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; + _howl = howl; + _howl.OnPlay += Howl_OnPlay; + _howl.OnPause += Howl_OnPause; + _howl.OnEnd += Howl_OnEnd; + _howl.OnStop += Howl_OnStop; + } + + public async Task PlayOrPauseAsync() + { + //Reset if the last played audiofile is not the current one + if (currentlyPlayingAudiofile !=cuesheet.Audiofile) + { + soundId = null; + } + //If the current audiofile already started, we just pause + if (soundId != null) + { + await _howl.Pause(soundId.Value); + } + else + { + if (PlaybackPossible) + { + string[]? sources = null; + string[]? formats = null; + if (cuesheet.Audiofile?.ObjectURL != null) + { + sources = [cuesheet.Audiofile.ObjectURL]; + } + if (cuesheet.Audiofile?.AudioFileType != null) + { + formats = [cuesheet.Audiofile.AudioFileType.ToLower()]; + } + var options = new HowlOptions + { + Sources = sources, + Formats = formats, + Html5 = true + }; + soundId = await _howl.Play(options); + currentlyPlayingAudiofile = cuesheet.Audiofile; + } + } + } + + public async Task PlayAsync(Track trackToPlay) + { + if (trackToPlay?.Begin.HasValue == true) + { + if (IsPlaying == false) + { + await PlayOrPauseAsync(); + } + if (soundId.HasValue) + { + await _howl.Seek(soundId.Value, trackToPlay.Begin.Value); + } + } + } + + public async Task StopAsync() + { + if (soundId != null) + { + await _howl.Stop(soundId.Value); + } + } + + public async Task PlayNextTrackAsync() + { + if (CurrentlyPlayingTrack != null) + { + var index = cuesheet.Tracks.ToList().IndexOf(CurrentlyPlayingTrack); + var trackToPlay = cuesheet.Tracks.ElementAtOrDefault(index + 1); + if (trackToPlay != null) + { + await PlayAsync(trackToPlay); + } + } + } + + public async Task PlayPreviousTrackAsync() + { + if (CurrentlyPlayingTrack != null) + { + var index = cuesheet.Tracks.ToList().IndexOf(CurrentlyPlayingTrack); + var trackToPlay = cuesheet.Tracks.ElementAtOrDefault(index - 1); + if (trackToPlay != null) + { + await PlayAsync(trackToPlay); + } + } + } + + public async Task SeekAsync(TimeSpan time) + { + if (soundId.HasValue == false) + { + await PlayOrPauseAsync(); + } + if (soundId.HasValue) + { + if (IsPlaying == false) + { + await PlayOrPauseAsync(); + } + await _howl.Seek(soundId.Value, time); + } + } + + public void Dispose() + { + // Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in der Methode "Dispose(bool disposing)" ein. + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + cuesheet.AudiofileChanged -= Cuesheet_AudiofileChanged; + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; + _howl.OnPlay -= Howl_OnPlay; + _howl.OnPause -= Howl_OnPause; + _howl.OnEnd -= Howl_OnEnd; + _howl.OnStop -= Howl_OnStop; + } + disposedValue = true; + } + } + + private void Howl_OnStop(Howler.Blazor.Components.Events.HowlEventArgs obj) + { + IsPlaying = false; + soundId = null; + StopTimer(); + CurrentPosition = null; + } + + private void Howl_OnEnd(Howler.Blazor.Components.Events.HowlEventArgs obj) + { + IsPlaying = false; + StopTimer(); + CurrentPosition = null; + } + + private void Howl_OnPause(Howler.Blazor.Components.Events.HowlEventArgs obj) + { + IsPlaying = false; + StopTimer(); + } + + private void Howl_OnPlay(Howler.Blazor.Components.Events.HowlPlayEventArgs obj) + { + IsPlaying = true; + StartTimer(); + } + + private void StartTimer() + { + updateTimer ??= new Timer(UpdateCurrentPosition, null, 0, 500); + } + + private void StopTimer() + { + updateTimer?.Dispose(); + updateTimer = null; + } + + private async void UpdateCurrentPosition(object? state) + { + // Thread-safe access + lock (timerLock) + { + if (soundId == null || !IsPlaying) return; + } + CurrentPosition = await _howl.GetCurrentTime(soundId.Value); + } + + private void SessionStateContainer_CuesheetChanged(object? sender, EventArgs e) + { + _ = StopAsync(); + cuesheet.AudiofileChanged -= Cuesheet_AudiofileChanged; + cuesheet = _sessionStateContainer.Cuesheet; + cuesheet.AudiofileChanged += Cuesheet_AudiofileChanged; + } + + private void Cuesheet_AudiofileChanged(object? sender, EventArgs e) + { + _ = StopAsync(); + } + } +} diff --git a/AudioCuesheetEditor/Services/IO/CuesheetExportService.cs b/AudioCuesheetEditor/Services/IO/CuesheetExportService.cs new file mode 100644 index 00000000..ec095fd8 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/CuesheetExportService.cs @@ -0,0 +1,155 @@ +//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.Entity; +using AudioCuesheetEditor.Model.IO; +using AudioCuesheetEditor.Model.IO.Export; +using AudioCuesheetEditor.Services.UI; +using System.Text; + +namespace AudioCuesheetEditor.Services.IO +{ + public class CuesheetExportService(ISessionStateContainer sessionStateContainer) + { + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; + + public IEnumerable CanGenerateExportfiles(string? filename) + { + List validationMessages = []; + var extension = Path.GetExtension(filename); + if (extension?.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("File extension is not '{0}'", FileExtensions.Cuesheet)); + } + validationMessages.AddRange(_sessionStateContainer.Cuesheet.Validate().ValidationMessages); + return validationMessages; + } + + public IReadOnlyCollection GenerateExportfiles(string? filename) + { + List exportfiles = []; + if (!CanGenerateExportfiles(filename).Any()) + { + if (_sessionStateContainer.Cuesheet.Sections.Count != 0) + { + var counter = 1; + string? content = null; + string? audioFileName = null; + foreach (var section in _sessionStateContainer.Cuesheet.Sections.OrderBy(x => x.Begin)) + { + audioFileName = section.AudiofileName; + if (section.Validate().Status == ValidationStatus.Success) + { + content = WriteCuesheet(audioFileName, section); + var name = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(filename), counter, FileExtensions.Cuesheet); + if (content != null) + { + exportfiles.Add(new Exportfile() { Name = name, Content = Encoding.UTF8.GetBytes(content), Begin = section.Begin, End = section.End }); + } + counter++; + } + } + } + else + { + string? content = null; + var extension = Path.GetExtension(filename); + if (extension?.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) + { + filename = $"{filename}{FileExtensions.Cuesheet}"; + } + if (_sessionStateContainer.Cuesheet.Audiofile != null) + { + content = WriteCuesheet(_sessionStateContainer.Cuesheet.Audiofile.Name); + } + if (content != null) + { + var begin = _sessionStateContainer.Cuesheet.Tracks.Min(x => x.Begin); + var end = _sessionStateContainer.Cuesheet.Tracks.Max(x => x.End); + exportfiles.Add(new Exportfile() { Name = filename!, Content = Encoding.UTF8.GetBytes(content), Begin = begin, End = end }); + } + } + } + return exportfiles; + } + + private string WriteCuesheet(string? audiofileName, CuesheetSection? section = null) + { + var builder = new StringBuilder(); + if (string.IsNullOrEmpty(_sessionStateContainer.Cuesheet.Cataloguenumber) == false) + { + builder.AppendLine(string.Format("{0} {1}", CuesheetConstants.CuesheetCatalogueNumber, _sessionStateContainer.Cuesheet.Cataloguenumber)); + } + if (_sessionStateContainer.Cuesheet.CDTextfile != null) + { + builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetCDTextfile, _sessionStateContainer.Cuesheet.CDTextfile.Name)); + } + builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetTitle, section != null ? section.Title : _sessionStateContainer.Cuesheet.Title)); + builder.AppendLine(string.Format("{0} \"{1}\"", CuesheetConstants.CuesheetArtist, section != null ? section.Artist : _sessionStateContainer.Cuesheet.Artist)); + builder.AppendLine(string.Format("{0} \"{1}\" {2}", CuesheetConstants.CuesheetFileName, audiofileName, _sessionStateContainer.Cuesheet.Audiofile?.AudioFileType)); + IEnumerable tracks = _sessionStateContainer.Cuesheet.Tracks.OrderBy(x => x.Position); + if (section != null) + { + tracks = _sessionStateContainer.Cuesheet.Tracks.Where(x => x.Begin <= section.End && x.End >= section.Begin).OrderBy(x => x.Position); + } + if (tracks.Any()) + { + //Position and begin should always start from 0 even with splitpoints + int positionDifference = 1 - Convert.ToInt32(tracks.First().Position); + foreach (var track in tracks) + { + builder.AppendLine(string.Format("{0}{1} {2:00} {3}", CuesheetConstants.Tab, CuesheetConstants.CuesheetTrack, track.Position + positionDifference, CuesheetConstants.CuesheetTrackAudio)); + builder.AppendLine(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackTitle, track.Title)); + builder.AppendLine(string.Format("{0}{1}{2} \"{3}\"", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackArtist, track.Artist)); + if (track.Flags.Any()) + { + builder.AppendLine(string.Format("{0}{1}{2} {3}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackFlags, string.Join(" ", track.Flags.Select(x => x.CuesheetLabel)))); + } + if (track.PreGap.HasValue) + { + builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPreGap, Math.Floor(track.PreGap.Value.TotalMinutes), track.PreGap.Value.Seconds, track.PreGap.Value.Milliseconds * 75 / 1000)); + } + if (track.Begin.HasValue) + { + var begin = track.Begin.Value; + if (section != null && section.Begin.HasValue) + { + if (section.Begin >= track.Begin) + { + begin = TimeSpan.Zero; + } + else + { + begin = track.Begin.Value - section.Begin.Value; + } + } + builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackIndex01, Math.Floor(begin.TotalMinutes), begin.Seconds, begin.Milliseconds * 75 / 1000)); + } + else + { + throw new NullReferenceException(string.Format("{0} may not be null!", nameof(Track.Begin))); + } + if (track.PostGap.HasValue) + { + builder.AppendLine(string.Format("{0}{1}{2} {3:00}:{4:00}:{5:00}", CuesheetConstants.Tab, CuesheetConstants.Tab, CuesheetConstants.TrackPostGap, Math.Floor(track.PostGap.Value.TotalMinutes), track.PostGap.Value.Seconds, track.PostGap.Value.Milliseconds * 75 / 1000)); + } + } + } + return builder.ToString(); + } + } +} diff --git a/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs b/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs index 648109dc..47ad4494 100644 --- a/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs +++ b/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs @@ -184,7 +184,7 @@ public static IImportfile Analyse(IEnumerable fileContent) var flagList = Flag.AvailableFlags.Where(x => flags.Contains(x.CuesheetLabel)); if (track != null) { - track.SetFlags(flagList); + track.Flags = flagList; } else { diff --git a/AudioCuesheetEditor/Services/IO/ExportfileGenerator.cs b/AudioCuesheetEditor/Services/IO/ExportfileGenerator.cs new file mode 100644 index 00000000..2601f387 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/ExportfileGenerator.cs @@ -0,0 +1,166 @@ +//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.Entity; +using AudioCuesheetEditor.Model.IO.Export; +using AudioCuesheetEditor.Services.UI; +using System.Data; +using System.Text; + +namespace AudioCuesheetEditor.Services.IO +{ + public class ExportfileGenerator(ISessionStateContainer sessionStateContainer) + { + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; + + public IEnumerable CanGenerateExportfiles(Exportprofile? exportprofile) + { + List validationMessages = []; + if (exportprofile != null) + { + validationMessages.AddRange(exportprofile.Validate().ValidationMessages); + } + else + { + validationMessages.Add(new ValidationMessage("No exportprofile selected!")); + } + validationMessages.AddRange(_sessionStateContainer.Cuesheet.Validate().ValidationMessages); + return validationMessages; + } + + public IReadOnlyCollection GenerateExportfiles(Exportprofile? exportprofile) + { + List exportfiles = []; + if ((!CanGenerateExportfiles(exportprofile).Any()) && (exportprofile != null)) + { + if (_sessionStateContainer.Cuesheet.Sections.Count != 0) + { + var counter = 1; + string? content = null; + string filename = string.Empty; + string? audioFileName = null; + foreach (var section in _sessionStateContainer.Cuesheet.Sections.OrderBy(x => x.Begin)) + { + audioFileName = section.AudiofileName; + if (section.Validate().Status == ValidationStatus.Success) + { + content = WriteExport(exportprofile, audioFileName, section); + filename = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(exportprofile.Filename), counter, Path.GetExtension(exportprofile.Filename)); + if (content != null) + { + exportfiles.Add(new Exportfile() { Name = filename, Content = Encoding.UTF8.GetBytes(content), Begin = section.Begin, End = section.End }); + } + counter++; + } + } + } + else + { + string? content = null; + if (_sessionStateContainer.Cuesheet.Audiofile != null) + { + content = WriteExport(exportprofile, _sessionStateContainer.Cuesheet.Audiofile.Name); + } + if (content != null) + { + var begin = _sessionStateContainer.Cuesheet.Tracks.Min(x => x.Begin); + var end = _sessionStateContainer.Cuesheet.Tracks.Max(x => x.End); + exportfiles.Add(new Exportfile() { Name = exportprofile.Filename, Content = Encoding.UTF8.GetBytes(content), Begin = begin, End = end }); + } + } + } + return exportfiles; + } + + private string WriteExport(Exportprofile exportprofile, string? audiofileName, CuesheetSection? section = null) + { + var builder = new StringBuilder(); + if (exportprofile != null) + { + var header = exportprofile.SchemeHead + .Replace(Exportprofile.SchemeCuesheetArtist, section != null ? section.Artist : _sessionStateContainer.Cuesheet.Artist) + .Replace(Exportprofile.SchemeCuesheetTitle, section != null ? section.Title : _sessionStateContainer.Cuesheet.Title) + .Replace(Exportprofile.SchemeCuesheetAudiofile, audiofileName) + .Replace(Exportprofile.SchemeCuesheetCDTextfile, _sessionStateContainer.Cuesheet.CDTextfile?.Name) + .Replace(Exportprofile.SchemeCuesheetCatalogueNumber, _sessionStateContainer.Cuesheet.Cataloguenumber) + .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) + .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) + .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); + builder.AppendLine(header); + IEnumerable tracks = _sessionStateContainer.Cuesheet.Tracks.OrderBy(x => x.Position); + if (section != null) + { + tracks = _sessionStateContainer.Cuesheet.Tracks.Where(x => x.Begin <= section.End && x.End >= section.Begin).OrderBy(x => x.Position); + } + if (tracks.Any()) + { + //Position, Begin and End should always start from 0 even with splitpoints + int positionDifference = 1 - Convert.ToInt32(tracks.First().Position); + foreach (var track in tracks) + { + TimeSpan begin; + var end = track.End; + if (track.Begin.HasValue) + { + begin = track.Begin.Value; + if (section?.Begin != null) + { + if (section.Begin >= track.Begin) + { + begin = TimeSpan.Zero; + } + else + { + begin = track.Begin.Value - section.Begin.Value; + } + end = track.End - section.Begin.Value; + } + } + else + { + throw new NullReferenceException(string.Format("{0} may not be null!", nameof(Track.Begin))); + } + var trackLine = exportprofile.SchemeTracks + .Replace(Exportprofile.SchemeTrackArtist, track.Artist) + .Replace(Exportprofile.SchemeTrackTitle, track.Title) + .Replace(Exportprofile.SchemeTrackPosition, (track.Position + positionDifference).ToString()) + .Replace(Exportprofile.SchemeTrackBegin, begin.ToString()) + .Replace(Exportprofile.SchemeTrackEnd, end.ToString()) + .Replace(Exportprofile.SchemeTrackLength, (end - begin).ToString()) + .Replace(Exportprofile.SchemeTrackFlags, string.Join(" ", track.Flags.Select(x => x.CuesheetLabel))) + .Replace(Exportprofile.SchemeTrackPreGap, track.PreGap != null ? track.PreGap.Value.ToString() : string.Empty) + .Replace(Exportprofile.SchemeTrackPostGap, track.PostGap != null ? track.PostGap.Value.ToString() : string.Empty) + .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) + .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) + .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); + builder.AppendLine(trackLine); + } + } + var footer = exportprofile.SchemeFooter + .Replace(Exportprofile.SchemeCuesheetArtist, section != null ? section.Artist : _sessionStateContainer.Cuesheet.Artist) + .Replace(Exportprofile.SchemeCuesheetTitle, section != null ? section.Title : _sessionStateContainer.Cuesheet.Title) + .Replace(Exportprofile.SchemeCuesheetAudiofile, audiofileName) + .Replace(Exportprofile.SchemeCuesheetCDTextfile, _sessionStateContainer.Cuesheet.CDTextfile?.Name) + .Replace(Exportprofile.SchemeCuesheetCatalogueNumber, _sessionStateContainer.Cuesheet.Cataloguenumber) + .Replace(Exportprofile.SchemeDate, DateTime.Now.ToShortDateString()) + .Replace(Exportprofile.SchemeDateTime, DateTime.Now.ToString()) + .Replace(Exportprofile.SchemeTime, DateTime.Now.ToLongTimeString()); + builder.AppendLine(footer); + } + return builder.ToString(); + } + } +} diff --git a/AudioCuesheetEditor/Services/IO/FileInputManager.cs b/AudioCuesheetEditor/Services/IO/FileInputManager.cs new file mode 100644 index 00000000..9d5e7dc1 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/FileInputManager.cs @@ -0,0 +1,132 @@ +//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; +using AudioCuesheetEditor.Model.IO.Audio; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.JSInterop; + +namespace AudioCuesheetEditor.Services.IO +{ + public class FileInputManager(IJSRuntime jsRuntime, HttpClient httpClient) + { + private readonly IJSRuntime _jsRuntime = jsRuntime; + private readonly HttpClient _httpClient = httpClient; + + public static AudioCodec? GetAudioCodec(IBrowserFile browserFile) + { + AudioCodec? foundAudioCodec = null; + var extension = Path.GetExtension(browserFile.Name); + // First search with mime type and file extension + var audioCodecsFound = Audiofile.AudioCodecs.Where(x => x.MimeType.Equals(browserFile.ContentType, StringComparison.OrdinalIgnoreCase) && x.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)); + if (audioCodecsFound.Count() <= 1) + { + foundAudioCodec = audioCodecsFound.FirstOrDefault(); + } + else + { + // Second search with mime type or file extension + audioCodecsFound = Audiofile.AudioCodecs.Where(x => x.MimeType.Equals(browserFile.ContentType, StringComparison.OrdinalIgnoreCase) || x.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)); + foundAudioCodec = audioCodecsFound.FirstOrDefault(); + } + return foundAudioCodec; + } + + public static Boolean CheckFileMimeType(IBrowserFile file, String mimeType, String fileExtension) + { + Boolean fileMimeTypeMatches = false; + if ((file != null) && (String.IsNullOrEmpty(mimeType) == false) && (String.IsNullOrEmpty(fileExtension) == false)) + { + if (String.IsNullOrEmpty(file.ContentType) == false) + { + fileMimeTypeMatches = file.ContentType.Equals(mimeType, StringComparison.CurrentCultureIgnoreCase); + } + else + { + //Try to find by file extension + var extension = Path.GetExtension(file.Name); + fileMimeTypeMatches = extension.Equals(fileExtension, StringComparison.CurrentCultureIgnoreCase); + } + } + return fileMimeTypeMatches; + } + + public async Task CreateAudiofileAsync(String? fileInputId, IBrowserFile? browserFile, Action>? afterContentStreamLoaded = null) + { + Audiofile? audiofile = null; + if ((String.IsNullOrEmpty(fileInputId) == false) && (browserFile != null)) + { + // Check file mime type + var codec = GetAudioCodec(browserFile); + if (codec != null) + { + var audioFileObjectURL = await _jsRuntime.InvokeAsync("getObjectURLFromMudFileUpload", fileInputId); + audiofile = new Audiofile(browserFile.Name, audioFileObjectURL, codec); + if (String.IsNullOrEmpty(audioFileObjectURL) == false) + { + var loadContentStreamTask = _httpClient.GetStreamAsync(audioFileObjectURL) + .ContinueWith(x => audiofile.ContentStream = x.Result); + if (afterContentStreamLoaded != null) + { + _ = loadContentStreamTask + .ContinueWith(afterContentStreamLoaded); + } + } + } + else + { + throw new ArgumentException("The audiofile provided is not of a valid type."); + } + } + return audiofile; + } + + public Audiofile? CreateRecordedAudiofile(String objectUrl, Action>? afterContentStreamLoaded = null) + { + Audiofile? audiofile = null; + if (String.IsNullOrEmpty(objectUrl) == false) + { + audiofile = new Audiofile(Audiofile.RecordingFileName, objectUrl, Audiofile.AudioCodecWEBM, true); + var loadContentStreamTask = _httpClient.GetStreamAsync(objectUrl) + .ContinueWith(x => audiofile.ContentStream = x.Result); + if (afterContentStreamLoaded != null) + { + _ = loadContentStreamTask + .ContinueWith(afterContentStreamLoaded); + } + } + return audiofile; + } + + public static CDTextfile? CreateCDTextfile(IBrowserFile? browserFile) + { + CDTextfile? cdTextfile = null; + if (browserFile != null) + { + if (CheckFileMimeType(browserFile, FileMimeTypes.CDTextfile, FileExtensions.CDTextfile)) + { + cdTextfile = new CDTextfile(browserFile.Name); + } + else + { + throw new ArgumentException("The cdtextfile provided is not of a valid type."); + } + } + return cdTextfile; + } + } +} diff --git a/AudioCuesheetEditor/Services/IO/ImportManager.cs b/AudioCuesheetEditor/Services/IO/ImportManager.cs index 8eca0d48..4df51410 100644 --- a/AudioCuesheetEditor/Services/IO/ImportManager.cs +++ b/AudioCuesheetEditor/Services/IO/ImportManager.cs @@ -14,15 +14,15 @@ //along with Foobar. If not, see //. using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Extensions; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO; using AudioCuesheetEditor.Model.IO.Audio; +using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.UI; using AudioCuesheetEditor.Model.Utility; -using Blazorise; +using AudioCuesheetEditor.Services.UI; +using Microsoft.AspNetCore.Components.Forms; namespace AudioCuesheetEditor.Services.IO { @@ -34,29 +34,28 @@ public enum ImportFileType Textfile, Audiofile } - public class ImportManager(SessionStateContainer sessionStateContainer, ILocalStorageOptionsProvider localStorageOptionsProvider, TextImportService textImportService, TraceChangeManager traceChangeManager) + public class ImportManager(ISessionStateContainer sessionStateContainer, ILocalStorageOptionsProvider localStorageOptionsProvider, ITraceChangeManager traceChangeManager) { - private readonly SessionStateContainer _sessionStateContainer = sessionStateContainer; + private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; private readonly ILocalStorageOptionsProvider _localStorageOptionsProvider = localStorageOptionsProvider; - private readonly TextImportService _textImportService = textImportService; - private readonly TraceChangeManager _traceChangeManager = traceChangeManager; + private readonly ITraceChangeManager _traceChangeManager = traceChangeManager; - public async Task> ImportFilesAsync(IEnumerable files) + public async Task> ImportFilesAsync(IEnumerable files) { - Dictionary importFileTypes = []; + Dictionary importFileTypes = []; foreach (var file in files) { - if (IOUtility.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile)) + if (FileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile)) { var fileContent = await ReadFileContentAsync(file); var cuesheet = Projectfile.ImportFile(fileContent.ToArray()); if (cuesheet != null) { - _sessionStateContainer.ImportCuesheet = cuesheet; + _sessionStateContainer.Cuesheet = cuesheet; } importFileTypes.Add(file, ImportFileType.ProjectFile); } - if (IOUtility.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet)) + if (FileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet)) { var fileContent = await ReadFileContentAsync(file); fileContent.Position = 0; @@ -66,10 +65,10 @@ public async Task> ImportFilesAsync(IEnum { lines.Add(reader.ReadLine()); } - await ImportCuesheetAsync(lines); + ImportCuesheet(lines); importFileTypes.Add(file, ImportFileType.Cuesheet); } - if (IOUtility.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text)) + if (FileInputManager.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text)) { var fileContent = await ReadFileContentAsync(file); fileContent.Position = 0; @@ -79,48 +78,45 @@ public async Task> ImportFilesAsync(IEnum { lines.Add(reader.ReadLine()); } - await ImportTextAsync([.. lines]); + var options = await _localStorageOptionsProvider.GetOptions(); + ImportText([.. lines], options.ImportScheme, options.ImportTimeSpanFormat); importFileTypes.Add(file, ImportFileType.Textfile); } } return importFileTypes; } - public async Task ImportTextAsync(IEnumerable fileContent) + public void ImportText(IEnumerable fileContent, TextImportScheme textImportScheme, TimeSpanFormat timeSpanFormat) { - var options = await _localStorageOptionsProvider.GetOptions(); - _sessionStateContainer.Importfile = _textImportService.Analyse(options, fileContent); + _sessionStateContainer.Importfile = TextImportService.Analyse(textImportScheme, fileContent, timeSpanFormat); if (_sessionStateContainer.Importfile.AnalysedCuesheet != null) { var importCuesheet = new Cuesheet(); - await CopyCuesheetAsync(importCuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); + CopyCuesheet(importCuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); _sessionStateContainer.ImportCuesheet = importCuesheet; } } - public async Task ImportCuesheetAsync() + public void ImportCuesheet() { if (_sessionStateContainer.ImportCuesheet != null) { _traceChangeManager.BulkEdit = true; - await CopyCuesheetAsync(_sessionStateContainer.Cuesheet, _sessionStateContainer.ImportCuesheet); + CopyCuesheet(_sessionStateContainer.Cuesheet, _sessionStateContainer.ImportCuesheet); _traceChangeManager.BulkEdit = false; } _sessionStateContainer.ResetImport(); } - private async Task ImportCuesheetAsync(IEnumerable fileContent) + private void ImportCuesheet(IEnumerable fileContent) { _sessionStateContainer.Importfile = CuesheetImportService.Analyse(fileContent); if (_sessionStateContainer.Importfile.AnalysedCuesheet != null) { - var importCuesheet = new Cuesheet(); - await CopyCuesheetAsync(importCuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); - _sessionStateContainer.ImportCuesheet = importCuesheet; + CopyCuesheet(_sessionStateContainer.Cuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); } } - - private static async Task ReadFileContentAsync(IFileEntry file) + private static async Task ReadFileContentAsync(IBrowserFile file) { var fileContent = new MemoryStream(); var stream = file.OpenReadStream(); @@ -129,11 +125,12 @@ private static async Task ReadFileContentAsync(IFileEntry file) return fileContent; } - private async Task CopyCuesheetAsync(Cuesheet target, ICuesheet cuesheetToCopy) + private static void CopyCuesheet(Cuesheet target, ICuesheet cuesheetToCopy) { target.IsImporting = true; target.Artist = cuesheetToCopy.Artist; target.Title = cuesheetToCopy.Title; + target.Cataloguenumber = cuesheetToCopy.Cataloguenumber; IEnumerable? tracks = null; if (cuesheetToCopy is Cuesheet originCuesheet) { @@ -159,14 +156,9 @@ private async Task CopyCuesheetAsync(Cuesheet target, ICuesheet cuesheetToCopy) { target.CDTextfile = new CDTextfile(importCuesheet.CDTextfile); } - target.Cataloguenumber = new Cataloguenumber() - { - Value = importCuesheet.Cataloguenumber - }; } if (tracks != null) { - var applicationOptions = await _localStorageOptionsProvider.GetOptions(); var begin = TimeSpan.Zero; for (int i = 0; i < tracks.Count(); i++) { @@ -190,7 +182,7 @@ private async Task CopyCuesheetAsync(Cuesheet target, ICuesheet cuesheetToCopy) } } } - target.AddTrack(track, applicationOptions); + target.AddTrack(track); } } else @@ -198,7 +190,6 @@ private async Task CopyCuesheetAsync(Cuesheet target, ICuesheet cuesheetToCopy) throw new NullReferenceException(); } target.IsImporting = false; - _sessionStateContainer.FireCuesheetImported(); } } } diff --git a/AudioCuesheetEditor/Services/IO/TextImportService.cs b/AudioCuesheetEditor/Services/IO/TextImportService.cs index c90ce366..5d46e3c4 100644 --- a/AudioCuesheetEditor/Services/IO/TextImportService.cs +++ b/AudioCuesheetEditor/Services/IO/TextImportService.cs @@ -18,7 +18,6 @@ using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.Utility; using System.Reflection; using System.Text.RegularExpressions; @@ -27,8 +26,7 @@ namespace AudioCuesheetEditor.Services.IO { public class TextImportService { - public ImportOptions? ImportOptions { get; private set; } - public IImportfile Analyse(ImportOptions importOptions, IEnumerable fileContent) + public static IImportfile Analyse(TextImportScheme textImportScheme, IEnumerable fileContent, TimeSpanFormat? timeSpanFormat = null) { Importfile importfile = new() { @@ -37,32 +35,34 @@ public IImportfile Analyse(ImportOptions importOptions, IEnumerable fil try { importfile.FileContent = fileContent; - ImportOptions = importOptions; importfile.AnalysedCuesheet = new ImportCuesheet(); Boolean cuesheetRecognized = false; List recognizedFileContent = []; + Regex? regExCuesheet = null, regExTracks = null; + if (String.IsNullOrEmpty(textImportScheme.SchemeCuesheet) == false) + { + regExCuesheet = CreateCuesheetRegexPattern(textImportScheme.SchemeCuesheet); + } + if (String.IsNullOrEmpty(textImportScheme.SchemeTracks) == false) + { + regExTracks = CreateTrackRegexPattern(textImportScheme.SchemeTracks); + } foreach (var line in fileContent) { var recognizedLine = line; if (String.IsNullOrEmpty(line) == false) { Boolean recognized = false; - if ((recognized == false) && (cuesheetRecognized == false) && (String.IsNullOrEmpty(ImportOptions.TextImportScheme.SchemeCuesheet) == false)) + if ((recognized == false) && (cuesheetRecognized == false) && (regExCuesheet != null)) { - //Remove entity names - var expression = ImportOptions.TextImportScheme.SchemeCuesheet.Replace(String.Format("{0}.", nameof(Cuesheet)), String.Empty).Replace(String.Format("{0}.", nameof(Track)), String.Empty); - var regExCuesheet = new Regex(expression); - recognizedLine = AnalyseLine(line, importfile.AnalysedCuesheet, regExCuesheet); + recognizedLine = AnalyseLine(line, importfile.AnalysedCuesheet, regExCuesheet, timeSpanFormat); recognized = recognizedLine != null; cuesheetRecognized = recognizedLine != null; } - if ((recognized == false) && (String.IsNullOrEmpty(ImportOptions.TextImportScheme.SchemeTracks) == false)) + if ((recognized == false) && (regExTracks != null)) { - //Remove entity names - var expression = ImportOptions.TextImportScheme.SchemeTracks.Replace(String.Format("{0}.", nameof(Cuesheet)), String.Empty).Replace(String.Format("{0}.", nameof(Track)), String.Empty); - var regExTracks = new Regex(expression); var track = new ImportTrack(); - recognizedLine = AnalyseLine(line, track, regExTracks); + recognizedLine = AnalyseLine(line, track, regExTracks, timeSpanFormat); recognized = recognizedLine != null; importfile.AnalysedCuesheet.Tracks.Add(track); } @@ -76,6 +76,7 @@ public IImportfile Analyse(ImportOptions importOptions, IEnumerable fil } catch (Exception ex) { + importfile.FileContent = fileContent; importfile.FileContentRecognized = fileContent; importfile.AnalyseException = ex; importfile.AnalysedCuesheet = null; @@ -83,7 +84,7 @@ public IImportfile Analyse(ImportOptions importOptions, IEnumerable fil return importfile; } - private String? AnalyseLine(String line, object entity, Regex regex) + private static String? AnalyseLine(String line, object entity, Regex regex, TimeSpanFormat? timeSpanFormat) { String? recognized = null; string? recognizedLine = line; @@ -101,7 +102,7 @@ public IImportfile Analyse(ImportOptions importOptions, IEnumerable fil var property = entity.GetType().GetProperty(key, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (property != null) { - SetValue(entity, property, group.Value); + SetValue(entity, property, group.Value, timeSpanFormat); recognizedLine = string.Concat(recognizedLine.AsSpan(0, group.Index + (13 * (groupCounter - 1))) , String.Format(CuesheetConstants.RecognizedMarkHTML, group.Value) , recognizedLine.AsSpan(group.Index + (13 * (groupCounter - 1)) + group.Length)); @@ -129,11 +130,11 @@ public IImportfile Analyse(ImportOptions importOptions, IEnumerable fil return recognized; } - private void SetValue(object entity, PropertyInfo property, string value) + private static void SetValue(object entity, PropertyInfo property, string value, TimeSpanFormat? timeSpanFormat) { if (property.PropertyType == typeof(TimeSpan?)) { - property.SetValue(entity, TimeSpanUtility.ParseTimeSpan(value, ImportOptions?.TimeSpanFormat)); + property.SetValue(entity, TimeSpanUtility.ParseTimeSpan(value, timeSpanFormat)); } if (property.PropertyType == typeof(uint?)) { @@ -143,19 +144,14 @@ private void SetValue(object entity, PropertyInfo property, string value) { property.SetValue(entity, value); } - if (property.PropertyType == typeof(IReadOnlyCollection)) + if (property.PropertyType == typeof(IEnumerable)) { - var list = Flag.AvailableFlags.Where(x => value.Contains(x.CuesheetLabel)); - ((ITrack)entity).SetFlags(list); + ((ITrack)entity).Flags = Flag.AvailableFlags.Where(x => value.Contains(x.CuesheetLabel)); } if (property.PropertyType == typeof(Audiofile)) { property.SetValue(entity, new Audiofile(value)); } - if (property.PropertyType == typeof(Cataloguenumber)) - { - ((Cuesheet)entity).Cataloguenumber.Value = value; - } if (property.PropertyType == typeof(DateTime?)) { if (DateTime.TryParse(value, out var date)) @@ -164,5 +160,60 @@ private void SetValue(object entity, PropertyInfo property, string value) } } } + + private static Regex CreateCuesheetRegexPattern(string scheme) + { + var regex = new Regex(scheme); + var groupNames = regex.GetGroupNames(); + //GroupNames always has a group "0", so we count for more than one group + if (groupNames.Any(x => x != "0")) + { + return regex; + } + else + { + var regexString = Regex.Escape(scheme); + + regexString = regexString.Replace(nameof(Cuesheet.Artist), $"(?<{nameof(Cuesheet.Artist)}>.+)"); + regexString = regexString.Replace(nameof(Cuesheet.Title), $"(?<{nameof(Cuesheet.Title)}>.+)"); + regexString = regexString.Replace(nameof(Cuesheet.Audiofile), $"(?<{nameof(Cuesheet.Audiofile)}>.+)"); + regexString = regexString.Replace(nameof(Cuesheet.CDTextfile), $"(?<{nameof(Cuesheet.CDTextfile)}>.+)"); + regexString = regexString.Replace(nameof(Cuesheet.Cataloguenumber), $"(?<{nameof(Cuesheet.Cataloguenumber)}>.+)"); + //Replace tab with non matching group + regexString = regexString.Replace("\\t", "(?:...\\t)"); + + return new Regex(regexString); + } + } + + private static Regex CreateTrackRegexPattern(string scheme) + { + var regex = new Regex(scheme); + var groupNames = regex.GetGroupNames(); + //GroupNames always has a group "0", so we count for more than one group + if (groupNames.Any(x => x != "0")) + { + return regex; + } + else + { + var regexString = Regex.Escape(scheme); + + regexString = regexString.Replace(nameof(ImportTrack.Artist), $"(?<{nameof(ImportTrack.Artist)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.Title), $"(?<{nameof(ImportTrack.Title)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.Begin), $"(?<{nameof(ImportTrack.Begin)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.End), $"(?<{nameof(ImportTrack.End)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.Length), $"(?<{nameof(ImportTrack.Length)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.Position), $"(?<{nameof(ImportTrack.Position)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.Flags), $"(?<{nameof(ImportTrack.Flags)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.PreGap), $"(?<{nameof(ImportTrack.PreGap)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.PostGap), $"(?<{nameof(ImportTrack.PostGap)}>.+)"); + regexString = regexString.Replace(nameof(ImportTrack.StartDateTime), $"(?<{nameof(ImportTrack.StartDateTime)}>.+)"); + //Replace tab with non matching group + regexString = regexString.Replace("\\t", "(?:...\\t)"); + + return new Regex(regexString); + } + } } } diff --git a/AudioCuesheetEditor/Services/UI/ApplicationOptionsTimeSpanParser.cs b/AudioCuesheetEditor/Services/UI/ApplicationOptionsTimeSpanParser.cs index f6b16cd9..75f14604 100644 --- a/AudioCuesheetEditor/Services/UI/ApplicationOptionsTimeSpanParser.cs +++ b/AudioCuesheetEditor/Services/UI/ApplicationOptionsTimeSpanParser.cs @@ -56,6 +56,23 @@ public async Task TimespanTextChanged(T entity, Expression. +using AudioCuesheetEditor.Data.Services; + +namespace AudioCuesheetEditor.Services.UI +{ + public class AutocompleteManager(MusicBrainzDataProvider musicBrainzDataProvider) + { + private readonly MusicBrainzDataProvider _musicBrainzDataProvider = musicBrainzDataProvider; + + public async Task> SearchArtistsAsync(String? searchString, CancellationToken cancellationToken) + { + try + { + var artists = await _musicBrainzDataProvider.SearchArtistAsync(searchString, cancellationToken); + return artists; + } + catch (TaskCanceledException) + { + return []; + } + catch (OperationCanceledException) + { + return []; + } + } + + public async Task> SearchTitlesAsync(String? searchString, String? artist, CancellationToken cancellationToken) + { + try + { + var tracks = await _musicBrainzDataProvider.SearchTitleAsync(searchString, artist, cancellationToken); + return tracks; + } + catch (TaskCanceledException) + { + return []; + } + catch (OperationCanceledException) + { + return []; + } + } + } +} diff --git a/AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs b/AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs new file mode 100644 index 00000000..3320eeb1 --- /dev/null +++ b/AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs @@ -0,0 +1,181 @@ +//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.UI; +using AudioCuesheetEditor.Shared.Dialogs; +using MudBlazor; + +namespace AudioCuesheetEditor.Services.UI +{ + public class EditTrackModalManager(IDialogService dialogService, ITraceChangeManager traceChangeManager) + { + private readonly IDialogService _dialogService = dialogService; + private readonly ITraceChangeManager _traceChangeManager = traceChangeManager; + + public async Task ShowAndHandleModalEditDialogAsync(IEnumerable tracks) + { + if (tracks.Count() == 1) + { + var parameters = new DialogParameters { { x => x.EditedTrack, tracks.First().Clone() } }; + var options = new DialogOptions() { CloseOnEscapeKey = true, BackdropClick = false, FullWidth = true }; + var dialog = await _dialogService.ShowAsync(null, parameters, options); + var result = await dialog.Result; + if ((result?.Canceled == false) && (result.Data is Track editedTrack)) + { + tracks.First().CopyValues(editedTrack, setCuesheet: false); + } + } + if (tracks.Count() > 1) + { + var parameters = new DialogParameters { { x => x.EditedTrack, new() { AutomaticallyCalculateLength = false } } }; + var options = new DialogOptions() { CloseOnEscapeKey = true, BackdropClick = false, FullWidth = true }; + var dialog = await _dialogService.ShowAsync(null, parameters, options); + var result = await dialog.Result; + if ((result?.Canceled == false) && (result.Data is EditMultipleTracksModalResult editMultipleTracksModalResult)) + { + _traceChangeManager.BulkEdit = true; + foreach (var track in tracks) + { + var position = editMultipleTracksModalResult.EditedTrack.Position; + var begin = editMultipleTracksModalResult.EditedTrack.Begin; + var end = editMultipleTracksModalResult.EditedTrack.End; + var length = editMultipleTracksModalResult.EditedTrack.Length; + var preGap = editMultipleTracksModalResult.EditedTrack.PreGap; + var postGap = editMultipleTracksModalResult.EditedTrack.PostGap; + Boolean copyTrackPosition = editMultipleTracksModalResult.PositionChanged; + Boolean copyTrackBegin = editMultipleTracksModalResult.BeginChanged; + Boolean copyTrackEnd = editMultipleTracksModalResult.EndChanged; + Boolean copyTrackLength = editMultipleTracksModalResult.LengthChanged; + Boolean copyTrackPreGap = editMultipleTracksModalResult.PregapChanged; + Boolean copyTrackPostGap = editMultipleTracksModalResult.PostgapChanged; + //First process dynamic edit, because we need to increase each value seperately + switch (editMultipleTracksModalResult.PositionEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + editMultipleTracksModalResult.EditedTrack.Position += track.Position; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: copyTrackPosition, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackPosition = false; + editMultipleTracksModalResult.EditedTrack.Position = position; + break; + case DynamicEditValue.EnteredValueSubstract: + var newValue = track.Position - editMultipleTracksModalResult.EditedTrack.Position; + editMultipleTracksModalResult.EditedTrack.Position = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: copyTrackPosition, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackPosition = false; + break; + } + switch (editMultipleTracksModalResult.BeginEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + var newValue = editMultipleTracksModalResult.EditedTrack.Begin + track.Begin; + editMultipleTracksModalResult.EditedTrack.Begin = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: copyTrackBegin, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackBegin = false; + break; + case DynamicEditValue.EnteredValueSubstract: + newValue = track.Begin - editMultipleTracksModalResult.EditedTrack.Begin; + editMultipleTracksModalResult.EditedTrack.Begin = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: copyTrackBegin, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackBegin = false; + break; + } + switch (editMultipleTracksModalResult.EndEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + var newValue = editMultipleTracksModalResult.EditedTrack.End + track.End; + editMultipleTracksModalResult.EditedTrack.End = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: copyTrackEnd, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackEnd = false; + break; + case DynamicEditValue.EnteredValueSubstract: + newValue = track.End - editMultipleTracksModalResult.EditedTrack.End; + editMultipleTracksModalResult.EditedTrack.End = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: copyTrackEnd, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackEnd = false; + break; + } + switch (editMultipleTracksModalResult.LengthEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + var newValue = editMultipleTracksModalResult.EditedTrack.Length + track.Length; + editMultipleTracksModalResult.EditedTrack.Length = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: copyTrackLength, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackLength = false; + break; + case DynamicEditValue.EnteredValueSubstract: + newValue = track.Length - editMultipleTracksModalResult.EditedTrack.Length; + editMultipleTracksModalResult.EditedTrack.Length = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: copyTrackLength, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackLength = false; + break; + } + switch (editMultipleTracksModalResult.PregapEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + var newValue = editMultipleTracksModalResult.EditedTrack.PreGap + track.PreGap; + editMultipleTracksModalResult.EditedTrack.PreGap = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: copyTrackPreGap, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackPreGap = false; + break; + case DynamicEditValue.EnteredValueSubstract: + newValue = track.PreGap - editMultipleTracksModalResult.EditedTrack.PreGap; + editMultipleTracksModalResult.EditedTrack.PreGap = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: copyTrackPreGap, setPostGap: false, useInternalSetters: Track.AllPropertyNames); + copyTrackPreGap = false; + break; + } + switch (editMultipleTracksModalResult.PostgapEditMode) + { + case DynamicEditValue.EnteredValueEquals: + break; + case DynamicEditValue.EnteredValueAdd: + var newValue = editMultipleTracksModalResult.EditedTrack.PostGap + track.PostGap; + editMultipleTracksModalResult.EditedTrack.PostGap = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: copyTrackPostGap, useInternalSetters: Track.AllPropertyNames); + copyTrackPostGap = false; + break; + case DynamicEditValue.EnteredValueSubstract: + newValue = track.PostGap - editMultipleTracksModalResult.EditedTrack.PostGap; + editMultipleTracksModalResult.EditedTrack.PostGap = newValue; + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: copyTrackPostGap, useInternalSetters: Track.AllPropertyNames); + copyTrackPostGap = false; + break; + } + editMultipleTracksModalResult.EditedTrack.Position = position; + editMultipleTracksModalResult.EditedTrack.Begin = begin; + editMultipleTracksModalResult.EditedTrack.End = end; + editMultipleTracksModalResult.EditedTrack.Length = length; + editMultipleTracksModalResult.EditedTrack.PreGap = preGap; + editMultipleTracksModalResult.EditedTrack.PostGap = postGap; + //Now copy all values + track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: editMultipleTracksModalResult.IsLinkedToPreviousTrackChanged, setPosition: copyTrackPosition, setArtist: editMultipleTracksModalResult.ArtistChanged, setTitle: editMultipleTracksModalResult.TitleChanged, setBegin: copyTrackBegin, setEnd: copyTrackEnd, setLength: copyTrackLength, setFlags: editMultipleTracksModalResult.FlagsChanged, setPreGap: copyTrackPreGap, setPostGap: copyTrackPostGap); + } + _traceChangeManager.BulkEdit = false; + } + } + } + } +} diff --git a/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs new file mode 100644 index 00000000..10796b37 --- /dev/null +++ b/AudioCuesheetEditor/Services/UI/ISessionStateContainer.cs @@ -0,0 +1,32 @@ +//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.Model.IO.Import; + +namespace AudioCuesheetEditor.Services.UI +{ + public interface ISessionStateContainer + { + public event EventHandler? CuesheetChanged; + public event EventHandler? ImportCuesheetChanged; + public Cuesheet Cuesheet { get; set; } + public Cuesheet? ImportCuesheet { get; set; } + public Audiofile? ImportAudiofile { get; set; } + public IImportfile? Importfile { get; set; } + public void ResetImport(); + } +} diff --git a/AudioCuesheetEditor/Services/UI/ITraceChangeManager.cs b/AudioCuesheetEditor/Services/UI/ITraceChangeManager.cs new file mode 100644 index 00000000..09852d03 --- /dev/null +++ b/AudioCuesheetEditor/Services/UI/ITraceChangeManager.cs @@ -0,0 +1,28 @@ +using AudioCuesheetEditor.Model.UI; + +namespace AudioCuesheetEditor.Services.UI +{ + /// + /// Manager for Undo and Redo operations on objects. + /// + public interface ITraceChangeManager + { + public event EventHandler? TracedObjectHistoryChanged; + public event EventHandler? UndoDone; + public event EventHandler? RedoDone; + /// + /// Is Undo() currently possible (are there any changes)? + /// + public bool CanUndo { get; } + /// + /// Is Redo() currently possible (are there any changes)? + /// + public bool CanRedo { get; } + public bool BulkEdit { get; set; } + public void TraceChanges(ITraceable traceable); + public void Reset(); + public void Undo(); + public void Redo(); + public void MergeLastEditWithEdit(Func targetEdit); + } +} diff --git a/AudioCuesheetEditor/Services/UI/LocalizationService.cs b/AudioCuesheetEditor/Services/UI/LocalizationService.cs new file mode 100644 index 00000000..3fa80cfc --- /dev/null +++ b/AudioCuesheetEditor/Services/UI/LocalizationService.cs @@ -0,0 +1,73 @@ +//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.Data.Options; +using AudioCuesheetEditor.Model.Options; +using System.Globalization; + +namespace AudioCuesheetEditor.Services.UI +{ + public class LocalizationService(ILocalStorageOptionsProvider localStorageOptionsProvider) + { + private readonly ILocalStorageOptionsProvider _localStorageOptionsProvider = localStorageOptionsProvider; + + public const String DefaultCulture = "en-US"; + + public static IReadOnlyCollection AvailableCultures + { + get + { + var cultures = new List + { + new(DefaultCulture), + new("de-DE") + }; + return cultures.AsReadOnly(); + } + } + + public event EventHandler? LocalizationChanged; + + public CultureInfo SelectedCulture { get; private set; } = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture; + + public async Task SetCultureFromConfigurationAsync() + { + var options = await _localStorageOptionsProvider.GetOptions(); + + ChangeLanguage(options.Culture.Name); + } + + public async Task ChangeLanguageAsync(string name) + { + if (ChangeLanguage(name)) + { + await _localStorageOptionsProvider.SaveOptionsValue(x => x.CultureName!, name); + LocalizationChanged?.Invoke(this, new EventArgs()); + } + } + + private Boolean ChangeLanguage(string name) + { + var newCulture = AvailableCultures.SingleOrDefault(c => c.Name == name); + if (newCulture != null) + { + SelectedCulture = newCulture; + CultureInfo.DefaultThreadCurrentUICulture = SelectedCulture; + CultureInfo.CurrentUICulture = SelectedCulture; + } + return newCulture != null; + } + } +} diff --git a/AudioCuesheetEditor/Extensions/SessionStateContainer.cs b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs similarity index 71% rename from AudioCuesheetEditor/Extensions/SessionStateContainer.cs rename to AudioCuesheetEditor/Services/UI/SessionStateContainer.cs index 1a988975..848cfa50 100644 --- a/AudioCuesheetEditor/Extensions/SessionStateContainer.cs +++ b/AudioCuesheetEditor/Services/UI/SessionStateContainer.cs @@ -16,27 +16,23 @@ using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.UI; -namespace AudioCuesheetEditor.Extensions +namespace AudioCuesheetEditor.Services.UI { - public class SessionStateContainer + public class SessionStateContainer : ISessionStateContainer { - public event EventHandler? CurrentViewModeChanged; public event EventHandler? CuesheetChanged; public event EventHandler? ImportCuesheetChanged; - private readonly TraceChangeManager _traceChangeManager; - private ViewMode currentViewMode; + private readonly ITraceChangeManager _traceChangeManager; private Cuesheet cuesheet; private Cuesheet? importCuesheet; private Audiofile? importAudiofile; - public SessionStateContainer(TraceChangeManager traceChangeManager) + public SessionStateContainer(ITraceChangeManager traceChangeManager) { _traceChangeManager = traceChangeManager; - cuesheet = new Cuesheet(_traceChangeManager); + cuesheet = new Cuesheet(); SetCuesheetReference(cuesheet); } public Cuesheet Cuesheet @@ -52,11 +48,11 @@ public Cuesheet? ImportCuesheet var previousValue = importCuesheet; importCuesheet = value; //When there is an audiofile from import, we use this file because it has an object url and gets duration, etc. - if ((importCuesheet != null) && (ImportAudiofile != null)) + if (importCuesheet != null && ImportAudiofile != null) { importCuesheet.Audiofile = ImportAudiofile; } - if (Object.Equals(previousValue, importCuesheet) == false) + if (Equals(previousValue, importCuesheet) == false) { ImportCuesheetChanged?.Invoke(this, EventArgs.Empty); } @@ -69,7 +65,7 @@ public Audiofile? ImportAudiofile set { importAudiofile = value; - if ((ImportCuesheet != null) && (ImportAudiofile != null)) + if (ImportCuesheet != null && ImportAudiofile != null) { ImportCuesheet.Audiofile = ImportAudiofile; ImportCuesheetChanged?.Invoke(this, EventArgs.Empty); @@ -77,16 +73,6 @@ public Audiofile? ImportAudiofile } } - public ViewMode CurrentViewMode - { - get { return currentViewMode; } - set - { - currentViewMode = value; - CurrentViewModeChanged?.Invoke(this, EventArgs.Empty); - } - } - public IImportfile? Importfile{ get; set; } public void ResetImport() @@ -103,10 +89,5 @@ private void SetCuesheetReference(Cuesheet value) _traceChangeManager.TraceChanges(Cuesheet); CuesheetChanged?.Invoke(this, EventArgs.Empty); } - - public void FireCuesheetImported() - { - CuesheetChanged?.Invoke(this, EventArgs.Empty); - } } } diff --git a/AudioCuesheetEditor/Model/UI/TraceChangeManager.cs b/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs similarity index 87% rename from AudioCuesheetEditor/Model/UI/TraceChangeManager.cs rename to AudioCuesheetEditor/Services/UI/TraceChangeManager.cs index a4321ad1..ed1198e6 100644 --- a/AudioCuesheetEditor/Model/UI/TraceChangeManager.cs +++ b/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs @@ -13,7 +13,9 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. -namespace AudioCuesheetEditor.Model.UI +using AudioCuesheetEditor.Model.UI; + +namespace AudioCuesheetEditor.Services.UI { /// /// Class for tracing changes on an object @@ -39,14 +41,12 @@ public ITraceable? TraceableObject public class TracedChanges(IEnumerable changes) { - public List Changes { get; } = new(changes); - public Boolean HasTraceableObject { get { return Changes.Any(x => x.TraceableObject != null); } } + public List Changes { get; } = [.. changes]; + public bool HasTraceableObject { get { return Changes.Any(x => x.TraceableObject != null); } } } - /// - /// Manager for Undo and Redo operations on objects. - /// - public class TraceChangeManager(ILogger logger) + /// + public class TraceChangeManager(ILogger logger) : ITraceChangeManager { private readonly ILogger _logger = logger; @@ -59,22 +59,17 @@ public class TraceChangeManager(ILogger logger) public event EventHandler? UndoDone; public event EventHandler? RedoDone; - public Boolean CurrentlyHandlingRedoOrUndoChanges { get; private set; } = false; - /// - /// Is Undo() currently possible (are there any changes)? - /// - public Boolean CanUndo + public bool CurrentlyHandlingRedoOrUndoChanges { get; private set; } = false; + /// + public bool CanUndo { get { return undoStack.Count > 0; } } - - /// - /// Is Redo() currently possible (are there any changes)? - /// - public Boolean CanRedo + /// + public bool CanRedo { get { @@ -100,7 +95,7 @@ public void Undo() { CurrentlyHandlingRedoOrUndoChanges = true; TracedChanges? changes = null; - while ((undoStack.Count > 0) && (changes == null)) + while (undoStack.Count > 0 && changes == null) { changes = undoStack.Pop(); if (changes.HasTraceableObject == false) @@ -108,7 +103,7 @@ public void Undo() changes = null; } } - if ((changes != null) && changes.HasTraceableObject) + if (changes != null && changes.HasTraceableObject) { var redoChanges = new List(); for (int i = changes.Changes.Count - 1; i >= 0; i--) @@ -117,7 +112,7 @@ public void Undo() var tracedObject = change?.TraceableObject; var traceAbleChange = change?.TraceableChange; _logger.LogDebug("tracedObject = {tracedObject}, traceAbleChange = {traceAbleChange}", tracedObject, traceAbleChange); - if ((tracedObject != null) && (traceAbleChange != null)) + if (tracedObject != null && traceAbleChange != null) { var propertyInfo = tracedObject.GetType().GetProperty(traceAbleChange.PropertyName); if (propertyInfo != null) @@ -129,7 +124,7 @@ public void Undo() } else { - throw new NullReferenceException(String.Format("Property {0} could not be found!", traceAbleChange.PropertyName)); + throw new NullReferenceException(string.Format("Property {0} could not be found!", traceAbleChange.PropertyName)); } } if (change != null) @@ -152,7 +147,7 @@ public void Redo() { CurrentlyHandlingRedoOrUndoChanges = true; TracedChanges? changes = null; - while ((redoStack.Count > 0) && (changes == null)) + while (redoStack.Count > 0 && changes == null) { changes = redoStack.Pop(); if (changes.HasTraceableObject == false) @@ -160,7 +155,7 @@ public void Redo() changes = null; } } - if ((changes != null) && changes.HasTraceableObject) + if (changes != null && changes.HasTraceableObject) { var undoChanges = new List(); for (int i = changes.Changes.Count - 1;i >= 0; i--) @@ -169,7 +164,7 @@ public void Redo() var tracedObject = change?.TraceableObject; var traceAbleChange = change?.TraceableChange; _logger.LogDebug("tracedObject = {tracedObject}, traceAbleChange = {traceAbleChange}", tracedObject, traceAbleChange); - if ((tracedObject != null) && (traceAbleChange != null)) + if (tracedObject != null && traceAbleChange != null) { var propertyInfo = tracedObject.GetType().GetProperty(traceAbleChange.PropertyName); if (propertyInfo != null) @@ -181,7 +176,7 @@ public void Redo() } else { - throw new NullReferenceException(String.Format("Property {0} could not be found!", traceAbleChange.PropertyName)); + throw new NullReferenceException(string.Format("Property {0} could not be found!", traceAbleChange.PropertyName)); } } if (change != null) @@ -197,7 +192,7 @@ public void Redo() } } - public Boolean BulkEdit + public bool BulkEdit { get => bulkEditTracedChanges != null; set @@ -219,14 +214,6 @@ public Boolean BulkEdit } } - public TracedChanges? LastEdit - { - get - { - return undoStack.Peek(); - } - } - public void MergeLastEditWithEdit(Func targetEdit) { var edit = undoStack.FirstOrDefault(targetEdit); diff --git a/AudioCuesheetEditor/Services/Validation/ValidationService.cs b/AudioCuesheetEditor/Services/Validation/ValidationService.cs new file mode 100644 index 00000000..5c005ed0 --- /dev/null +++ b/AudioCuesheetEditor/Services/Validation/ValidationService.cs @@ -0,0 +1,54 @@ +//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.Entity; +using Microsoft.Extensions.Localization; + +namespace AudioCuesheetEditor.Services.Validation +{ + public class ValidationService(IStringLocalizer localizer) + { + private readonly IStringLocalizer _localizer = localizer; + public Func>> ValidateProperty => (model, propertyName) => Task.FromResult(Validate(model, propertyName)); + + public IEnumerable Validate(object model, string propertyName) + { + List errors = []; + if (model is IValidateable validateable) + { + var validationResult = validateable.Validate(propertyName); + switch (validationResult?.Status) + { + case ValidationStatus.NoValidation: + case ValidationStatus.Success: + // Nothing to do + break; + + case ValidationStatus.Error: + errors = validationResult.ValidationMessages.Select(x => x.GetMessageLocalized(_localizer)).ToList(); + break; + + default: + throw new InvalidOperationException("Unknown validation status."); + } + } + else + { + throw new NotSupportedException(string.Format("Model was not of supposed type '{0}'", nameof(IValidateable))); + } + return errors.AsEnumerable(); + } + } +} diff --git a/AudioCuesheetEditor/Shared/AppBar.de.resx b/AudioCuesheetEditor/Shared/AppBar.de.resx new file mode 100644 index 00000000..9b1a4bcd --- /dev/null +++ b/AudioCuesheetEditor/Shared/AppBar.de.resx @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Über + + + Möchten Sie die Anwendung wirklich zurücksetzen? Alle Änderungen die nicht gespeichert wurden sind verloren! + + + Möchten Sie wirklich das Cuesheet zurücksetzen? Alle Änderungen die nicht gespeichert wurden sind verloren! + + + Sprache wechseln + + + Bestätigen + + + Cuesheet + + + Cuesheet runterladen + + + Export + + + Exportprofile + + + Datei + + + Hilfe + + + Tastenkürzel + + + Öffnen + + + Vorschauumgebung + + + Projektdatei + + + Letzte Änderung wiederherstellen + + + Zurücksetzen + + + Anwendung zurücksetzen + + + Cuesheet zurücksetzen + + + Projekt speichern + + + Datei auswählen + + + Einstellungen + + + Textdatei + + + Letzte Änderung rückgänging machen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/AppBar.razor b/AudioCuesheetEditor/Shared/AppBar.razor new file mode 100644 index 00000000..149c0a87 --- /dev/null +++ b/AudioCuesheetEditor/Shared/AppBar.razor @@ -0,0 +1,195 @@ + + +@inherits BaseLocalizedComponent + +@inject NavigationManager _navigationManager +@inject IStringLocalizer _localizer +@inject IDialogService _dialogService +@inject ISessionStateContainer _sessionStateContainer +@inject IJSRuntime _jsRuntime +@inject HotKeys _hotKeys + + + + + AudioCuesheetEditor + + + @if (DisplayUndoRedoButtonGroup) + { + + + + + + + + + } + + @if (DisplayFileMenu) + { + + @_localizer["Open"] + + @_localizer["Cuesheet"] + @_localizer["Projectfile"] + @_localizer["Textfile"] + + + } + + @foreach (var culture in LocalizationService.AvailableCultures) + { + @culture.DisplayName + } + + + @if (DisplaySettings) + { + @_localizer["Settings"] + } + @_localizer["Help"] + @_localizer["About"] + @_localizer["Hotkeys"] + @_localizer["Preview environment"] + @if (DisplayReset) + { + + @_localizer["Reset cuesheet"] + @_localizer["Reset application"] + + } + + + +@code { + HotKeysContext? hotKeysContext; + + protected override void OnInitialized() + { + base.OnInitialized(); + hotKeysContext = _hotKeys.CreateContext() + .Add(ModKey.Ctrl, Key.z, () => TraceChangeManager.Undo()) + .Add(ModKey.Ctrl, Key.y, () => TraceChangeManager.Redo()) + .Add(ModKey.Ctrl, Key.s, DownloadProjectfileClicked) + .Add(ModKey.Ctrl, Key.e, DownloadExportClicked) + .Add(ModKey.Ctrl, Key.u, DownloadCuesheetClicked) + .Add(ModKey.Ctrl, Key.r, ResetCuesheetClicked); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + hotKeysContext?.DisposeAsync(); + } + + [Parameter] + public Boolean DisplayUndoRedoButtonGroup { get; set; } + + [Parameter] + public Boolean DisplayFileMenu { get; set; } + + [Parameter] + public Boolean DisplaySettings { get; set; } + + [Parameter] + public Boolean DisplayReset { get; set; } + + async Task SelectedCultureChanged(string name) + { + await base.LocalizationService.ChangeLanguageAsync(name); + } + + String GetStyle(CultureInfo cultureInfo) + { + String style = "white-space: nowrap;"; + if (cultureInfo.Name == base.LocalizationService.SelectedCulture.Name) + { + style += "background-color: var(--mud-palette-info-lighten);"; + } + return style; + } + + async Task DownloadProjectfileClicked() + { + var options = new DialogOptions() { BackdropClick = false, CloseButton = true }; + await _dialogService.ShowAsync(_localizer["Save project"], options); + } + + async Task DownloadCuesheetClicked() + { + var options = new DialogOptions() { BackdropClick = false, CloseButton = true, FullWidth = true }; + await _dialogService.ShowAsync(_localizer["Download cuesheet"], options); + } + + async Task DownloadExportClicked() + { + var options = new DialogOptions() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Large }; + await _dialogService.ShowAsync(_localizer["Export profiles"], options); + } + + async Task SettingsClicked() + { + var options = new DialogOptions() { CloseButton = true, CloseOnEscapeKey = true, BackdropClick = false, FullWidth = true }; + await _dialogService.ShowAsync(_localizer["Settings"], options); + } + + async Task ResetCuesheetClicked() + { + var parameters = new DialogParameters + { + { x => x.ConfirmText, _localizer["Are you sure you want reset the cuesheet? All information you have not saved are lost!"] }, + }; + var dialog = await _dialogService.ShowAsync(_localizer["Confirm"], parameters); + var result = await dialog.Result; + if (result?.Canceled == false) + { + _sessionStateContainer.Cuesheet = new(); + } + } + + async Task ResetApplicationClicked() + { + var parameters = new DialogParameters + { + { x => x.ConfirmText, _localizer["Are you sure you want reset the application? All information you have not saved are lost!"] }, + }; + var dialog = await _dialogService.ShowAsync(_localizer["Confirm"], parameters); + var result = await dialog.Result; + if (result?.Canceled == false) + { + await _jsRuntime.InvokeVoidAsync("resetLocalStorage"); + await _jsRuntime.InvokeVoidAsync("removeBeforeunload"); + _navigationManager.NavigateTo(_navigationManager.Uri, true); + } + } + + async Task OpenFileClicked() + { + var options = new DialogOptions() { BackdropClick = false, CloseButton = true }; + await _dialogService.ShowAsync(_localizer["Select file"], options); + } + + async Task DisplayHotkeys() + { + var options = new DialogOptions() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Large }; + await _dialogService.ShowAsync(_localizer["Hotkeys"], options); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/AppBar.resx b/AudioCuesheetEditor/Shared/AppBar.resx new file mode 100644 index 00000000..adabed2e --- /dev/null +++ b/AudioCuesheetEditor/Shared/AppBar.resx @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + About + + + Are you sure you want reset the application? All information you have not saved are lost! + + + Are you sure you want reset the cuesheet? All information you have not saved are lost! + + + Change language + + + Confirm + + + Cuesheet + + + Download cuesheet + + + Export + + + Export profiles + + + File + + + Help + + + Hotkeys + + + Open + + + Preview environment + + + Projectfile + + + Redo last edit + + + Reset + + + Reset application + + + Reset cuesheet + + + Save project + + + Select file + + + Settings + + + Textfile + + + Undo last edit + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Audio/AudioPlayer.de.resx b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.de.resx new file mode 100644 index 00000000..7fbc949c --- /dev/null +++ b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.de.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Zum nächsten Titel springen + + + Zum vorherigen Titel springen + + + Wiedergabe oder Pause des aktuellen Audios + + + Wiedergabe + + + Die aktuelle Wiedergabe stoppen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor new file mode 100644 index 00000000..11224223 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.razor @@ -0,0 +1,153 @@ + + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject PlaybackService _playbackService +@inject HotKeys _hotKeys + + + + + @_localizer["Playback"] + + + + + @if (_playbackService.CurrentPosition.HasValue) + { + @_playbackService.CurrentPosition.Value.ToString("hh\\:mm\\:ss") + } + else + { + @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) + } + + + @GetSliderTimeValue() + + + @if (_playbackService.TotalTime.HasValue) + { + @_playbackService.TotalTime.Value.ToString("hh\\:mm\\:ss") + } + else + { + @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) + } + + + + + + + + + + + + + + + + + + + + +@code { + double sliderValue; + HotKeysContext? hotKeysContext; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _playbackService.CurrentPositionChanged -= PlaybackService_CurrentPositionChanged; + hotKeysContext?.DisposeAsync(); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _playbackService.CurrentPositionChanged += PlaybackService_CurrentPositionChanged; + hotKeysContext = _hotKeys.CreateContext() + .Add(ModKey.Ctrl, Key.p, OnPlayOrPauseClicked) + .Add(ModKey.Ctrl, Key.ArrowRight, PlayNextTrackAsync) + .Add(ModKey.Ctrl, Key.ArrowLeft, PlayPreviousTrackAsync) + .Add(ModKey.Ctrl, Key.b, StopAsync) + .Add(Key.MediaPlayPause, OnPlayOrPauseClicked) + .Add(Key.MediaTrackNext, PlayNextTrackAsync) + .Add(Key.MediaTrackPrevious, PlayPreviousTrackAsync) + .Add(Key.MediaStop, StopAsync); + } + + async Task OnPlayOrPauseClicked() + { + await _playbackService.PlayOrPauseAsync(); + } + + async Task PlayPreviousTrackAsync() + { + await _playbackService.PlayPreviousTrackAsync(); + } + + async Task PlayNextTrackAsync() + { + await _playbackService.PlayNextTrackAsync(); + } + async Task StopAsync() + { + await _playbackService.StopAsync(); + } + + async Task OnSliderValueChanged(double newValue) + { + if (_playbackService.TotalTime.HasValue) + { + var newPosition = newValue * _playbackService.TotalTime.Value / 100.0; + await _playbackService.SeekAsync(newPosition); + } + } + + void PlaybackService_CurrentPositionChanged() + { + if ((_playbackService.CurrentPosition.HasValue) && (_playbackService.TotalTime.HasValue)) + { + sliderValue = (_playbackService.CurrentPosition.Value / _playbackService.TotalTime.Value) * 100.0; + } + else + { + sliderValue = 0.0; + } + InvokeAsync(StateHasChanged); + } + + string GetSliderTimeValue() + { + var time = sliderValue * _playbackService.TotalTime / 100.0; + if (time.HasValue) + { + return time.Value.ToString(@"hh\:mm\:ss"); + } + else + { + return string.Empty; + } + } +} diff --git a/AudioCuesheetEditor/Shared/Audio/AudioPlayer.resx b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.resx new file mode 100644 index 00000000..f1c5187e --- /dev/null +++ b/AudioCuesheetEditor/Shared/Audio/AudioPlayer.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Jump to next track + + + Jump to previous track + + + Play or pause the current audio + + + Playback + + + Stop the current playback + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/AudioPlayer.razor b/AudioCuesheetEditor/Shared/AudioPlayer.razor deleted file mode 100644 index ee987d0e..00000000 --- a/AudioCuesheetEditor/Shared/AudioPlayer.razor +++ /dev/null @@ -1,429 +0,0 @@ - -@implements IAsyncDisposable - -@inject ITextLocalizer _localizer -@inject IHowl _howl -@inject HotKeys _hotKeys -@inject ITextLocalizerService _localizationService -@inject SessionStateContainer _sessionStateContainer - - - @_localizer["Audioplayer"] - - - - @if (CurrentPlaybackPosition.HasValue) - { - @CurrentPlaybackPosition.Value.ToString("hh\\:mm\\:ss") - } - else - { - @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) - } - - - - - - @if (TotalTime.HasValue) - { - @TotalTime.Value.ToString("hh\\:mm\\:ss") - } - else - { - @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) - } - - - - - @if (AudioIsPlaying == true) - { - - - } - - - - - - - } - break; - case ViewMode.ViewModeFull: - case ViewMode.ViewModeImport: - - - @_localizer["Audiofile"] - - @if (Cuesheet.Audiofile == null) - { - x.MimeType))" Changed="OnAudioFileChanged" AutoReset="false"> - - - - - } - else - { - - @if (Cuesheet.Audiofile.IsRecorded) - { - - - - } - - - - - - - - - - - - } - - - - - - @_localizer["CD textfile"] - - @if (Cuesheet.CDTextfile == null) - { - - - - - - } - else - { - - - - - - - - - } - - - - - - @_localizer["Cataloguenumber"] - - - - - - - - - - break; - } - -} - - -@code { - ModalDialog? modalDialog; - Guid fileEditCDTextFileId = Guid.NewGuid(); - Guid fileEditAudiofileId = Guid.NewGuid(); - Validation? audiofileValidation; - Validations? validations; - - public Cuesheet? Cuesheet - { - get - { - Cuesheet? cuesheet; - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeImport: - cuesheet = _sessionStateContainer.ImportCuesheet; - break; - default: - cuesheet = _sessionStateContainer.Cuesheet; - break; - } - return cuesheet; - } - } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - _traceChangeManager.UndoDone -= TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone -= TraceChangeManager_RedoDone; - } - - protected override Task OnInitializedAsync() - { - _logger.LogDebug("OnInitializedAsync"); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - _traceChangeManager.UndoDone += TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone += TraceChangeManager_RedoDone; - - return base.OnInitializedAsync(); - } - - private async Task OnChangeAudioFileClicked() - { - _logger.LogInformation("OnChangeAudioFileClicked"); - if (Cuesheet != null) - { - Cuesheet.Audiofile = null; - StateHasChanged(); - await Task.Delay(1); - await _jsRuntime.InvokeVoidAsync("triggerClick", fileEditAudiofileId); - } - } - - async Task OnAudioFileChanged(FileChangedEventArgs e) - { - _logger.LogInformation("OnAudioFileChanged with {0}", e); - if (e.Files.FirstOrDefault() != null) - { - if (Cuesheet != null) - { - var file = e.Files.First(); - if (IOUtility.CheckFileMimeTypeForAudioCodec(file) == true) - { - await SetAudioFile(file); - } - else - { - if (modalDialog != null) - { - modalDialog.Title = _localizer["Error"]; - modalDialog.Text = String.Format(_localizer["The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!"], file.Name, _localizer["Audiofile"]); - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Alert; - await modalDialog.ShowModal(); - } - } - } - } - if (audiofileValidation != null) - { - await audiofileValidation.ValidateAsync(); - } - } - - private async Task SetAudioFile(IFileEntry file) - { - _logger.LogInformation("SetAudioFile with {0}", file); - if (Cuesheet != null) - { - if ((Cuesheet.Audiofile != null) && (Cuesheet.Audiofile.IsRecorded)) - { - await _jsRuntime.InvokeVoidAsync("URL.revokeObjectURL", Cuesheet.Audiofile.ObjectURL); - } - if (file != null) - { - var audioFileObjectURL = await _jsRuntime.InvokeAsync("getObjectURL", fileEditAudiofileId); - var codec = IOUtility.GetAudioCodec(file); - var audiofile = new Audiofile(file.Name, audioFileObjectURL, codec, _httpClient); - _ = audiofile.LoadContentStream(); - Cuesheet.Audiofile = audiofile; - } - else - { - Cuesheet.Audiofile = null; - } - } - } - - private async Task OnChangeCDTextfileClicked() - { - _logger.LogInformation("OnChangeCDTextfileClicked"); - if (Cuesheet != null) - { - Cuesheet.CDTextfile = null; - StateHasChanged(); - await Task.Delay(1); - await _jsRuntime.InvokeVoidAsync("triggerClick", fileEditCDTextFileId); - } - } - - async Task OnCDTextfileChanged(FileChangedEventArgs e) - { - _logger.LogInformation("OnCDTextfileChanged with {0}", e); - if (e.Files.FirstOrDefault() != null) - { - if (Cuesheet != null) - { - var file = e.Files.First(); - if (IOUtility.CheckFileMimeType(file, CDTextfile.MimeType, CDTextfile.FileExtension) == true) - { - Cuesheet.CDTextfile = new CDTextfile(file.Name); - } - else - { - if (modalDialog != null) - { - modalDialog.Title = _localizer["Error"]; - modalDialog.Text = String.Format(_localizer["The file {0} can not be used for operation: {1}. The file is invalid, please use a valid file!"], file.Name, _localizer["CD textfile"]); - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Alert; - await modalDialog.ShowModal(); - } - } - } - } - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll(); - } - - private void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void TraceChangeManager_UndoDone(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll().GetAwaiter().GetResult(); - } - - private void TraceChangeManager_RedoDone(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll().GetAwaiter().GetResult(); - } -} diff --git a/AudioCuesheetEditor/Shared/CultureSelector.razor b/AudioCuesheetEditor/Shared/CultureSelector.razor deleted file mode 100644 index 7d6566d2..00000000 --- a/AudioCuesheetEditor/Shared/CultureSelector.razor +++ /dev/null @@ -1,70 +0,0 @@ - - -@implements IDisposable - -@inject ITextLocalizerService _localizationService -@inject ITextLocalizer _localizer -@inject ILogger _logger -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider - - - - - - - - - - - - -@code{ - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - } - - protected override async Task OnInitializedAsync() - { - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - _logger.LogDebug("CultureName = {0}", _localizationService.SelectedCulture.Name); - - await base.OnInitializedAsync(); - } - - private async Task OnCultureSelectionChanged(String value) - { - var applicationOptions = await _localStorageOptionsProvider.GetOptions(); - applicationOptions.CultureName = value; - await _localStorageOptionsProvider.SaveOptions(applicationOptions); - _localizationService.ChangeLanguage(applicationOptions.CultureName); - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.de.resx new file mode 100644 index 00000000..9e1aaed5 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.de.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Nein + + + Ja + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.razor new file mode 100644 index 00000000..ac4a9859 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.razor @@ -0,0 +1,38 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + @ConfirmText + + + @_localizer["Yes"] + @_localizer["No"] + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + public string? ConfirmText { get; set; } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.resx new file mode 100644 index 00000000..fa503090 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/ConfirmDialog.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + No + + + Yes + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.de.resx new file mode 100644 index 00000000..0f541391 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.de.resx @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allgemein + + + Letzte Bearbeitung rückgängig machen + + + Strg + z + + + Letzte Bearbeitung wiederholen + + + Strg + y + + + Cuesheet zurücksetzen + + + Strg + r + + + Speichern + + + Projektdatei herunterladen + + + Strg + s + + + Textexport herunterladen + + + Strg + e + + + Cuesheet herunterladen + + + Strg + u + + + Audio + + + Wiedergabe starten/pausieren + + + Strg + p + + + MediaPlayPause + + + Zum vorherigen Titel springen + + + Strg + Links + + + MediaTrackPrevious + + + Zum nächsten Titel springen + + + Strg + Rechts + + + MediaTrackNext + + + Wiedergabe stoppen + + + Strg + b + + + MediaStop + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.razor new file mode 100644 index 00000000..40fb6131 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.razor @@ -0,0 +1,106 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + @_localizer["Common"] + + + + @_localizer["Undo the last edit"] + @_localizer["Ctrl + z"] + + + + + @_localizer["Redo the last edit"] + @_localizer["Ctrl + y"] + + + + + @_localizer["Reset cuesheet"] + @_localizer["Ctrl + r"] + + + + @_localizer["Save"] + + + + @_localizer["Download project file"] + @_localizer["Ctrl + s"] + + + + + @_localizer["Download text export"] + @_localizer["Ctrl + e"] + + + + + @_localizer["Download cuesheet"] + @_localizer["Ctrl + u"] + + + + @_localizer["Audio"] + + + + @_localizer["Start/pause playback"] + + @_localizer["Ctrl + p"] + @_localizer["MediaPlayPause"] + + + + + + @_localizer["Jump to previous track"] + + @_localizer["Ctrl + Left"] + @_localizer["MediaTrackPrevious"] + + + + + + @_localizer["Jump to next track"] + + @_localizer["Ctrl + Right"] + @_localizer["MediaTrackNext"] + + + + + + @_localizer["Stop playback"] + + @_localizer["Ctrl + b"] + @_localizer["MediaStop"] + + + + + + diff --git a/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.resx new file mode 100644 index 00000000..223f62d5 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DisplayHotkeysDialog.resx @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Common + + + Undo the last edit + + + Ctrl + z + + + Redo the last edit + + + Ctrl + y + + + Reset cuesheet + + + Ctrl + r + + + Save + + + Download project file + + + Ctrl + s + + + Download text export + + + Ctrl + e + + + Download cuesheet + + + Ctrl + u + + + Audio + + + Start/pause playback + + + Ctrl + p + + + MediaPlayPause + + + Jump to previous track + + + Ctrl + Left + + + MediaTrackPrevious + + + Jump to next track + + + Ctrl + Right + + + MediaTrackNext + + + Stop playback + + + Ctrl + b + + + MediaStop + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.de.resx new file mode 100644 index 00000000..d7c88ffe --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.de.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Projekt herunterladen + + + Dateiname + + + Projekt speichern + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor new file mode 100644 index 00000000..ae9b2fd9 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor @@ -0,0 +1,66 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ISessionStateContainer _sessionStateContainer +@inject IBlazorDownloadFileService _blazorDownloadFileService +@inject ValidationService _validationService + + + + + + + @_localizer["Download project"] + @_localizer["Cancel"] + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + async Task DownloadProjectClick() + { + MudDialog?.Close(); + var projectFile = new Projectfile(_sessionStateContainer.Cuesheet); + var fileData = projectFile.GenerateFile(); + await _blazorDownloadFileService.DownloadFile(ApplicationOptions?.ProjectFilename, fileData, "text/plain"); + } + + async Task FilenameChanged(string newFilename) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ProjectFilename, newFilename); + } + + String? GetValidationErrorMessage() + { + String? validationErrorMessage = null; + if (ApplicationOptions != null) + { + var validationMessages = _validationService.Validate(ApplicationOptions, nameof(ApplicationOptions.ProjectFilename)); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.resx new file mode 100644 index 00000000..60fa0360 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Cancel + + + Download project + + + Filename + + + Save project + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.de.resx b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.de.resx new file mode 100644 index 00000000..ab6650e3 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.de.resx @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Künstler + + + Start + + + Berechnen + + + Ändern + + + Spuren bearbeiten + + + Ende + + + Markierungen + + + Länge + + + Mit vorherigem Titel verknüpfen + + + Position + + + Nachlücke + + + Vorlücke + + + Änderungen speichern + + + Titel + + + Wert + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.razor b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.razor new file mode 100644 index 00000000..eabf09f1 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.razor @@ -0,0 +1,225 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser +@inject AutocompleteManager _autocompleteManager +@inject ValidationService _validationService + + + + @_localizer["Edit tracks"] + + + + + @_localizer["Change"] + @_localizer["Value"] + @_localizer["Calculate"] + + + + + + + + + + + = + + + - + + + + + + @{ + MusicBrainzArtist? autocompleteArtist = new() + { + Name = EditedTrack.Artist + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Name, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Name + } + + + + + + + @{ + MusicBrainzTrack? autocompleteTrack = new() + { + Artist = EditedTrack.Artist, + Title = EditedTrack.Title + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Title, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Title + } + + + + + + + + + + + = + + + - + + + + + + + + + + = + + + - + + + + + + + + + + = + + + - + + + + + + + + @foreach (var flag in Flag.AvailableFlags) + { + @flag.Name + } + + + + + + + + + + + = + + + - + + + + + + + + + + = + + + - + + + + + + + @_localizer["Save changes"] + @_localizer["Abort"] + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + [EditorRequired] + public Track EditedTrack { get; set; } = null!; + + Boolean editTrackIsLinkedToPreviousTrack, editTrackPosition, editTrackArtist, editTrackTitle, editTrackBegin, editTrackEnd, editTrackLength, editTrackFlags, editTrackPregap, editTrackPostgap; + DynamicEditValue editModeTrackPosition, editModeTrackBegin, editModeTrackEnd, editModeTrackLength, editModeTrackPregap, editModeTrackPostgap; + + void SaveClick() + { + var result = new EditMultipleTracksModalResult(EditedTrack, editTrackIsLinkedToPreviousTrack, editTrackPosition, editTrackArtist, editTrackTitle, editTrackBegin, + editTrackEnd, editTrackLength, editTrackFlags, editTrackPregap, editTrackPostgap, editModeTrackPosition, editModeTrackBegin, editModeTrackEnd, editModeTrackLength, + editModeTrackPregap, editModeTrackPostgap); + MudDialog?.Close(DialogResult.Ok(result)); + } + + String? GetValidationErrorMessage(Boolean changeActive, object model, string propertyName) + { + String? validationErrorMessage = null; + if (changeActive) + { + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.resx b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.resx new file mode 100644 index 00000000..be5b3c71 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditMultipleTracksModal.resx @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Artist + + + Begin + + + Calculate + + + Change + + + Edit tracks + + + End + + + Flags + + + Length + + + Link to previous track + + + Position + + + PostGap + + + PreGap + + + Save changes + + + Title + + + Value + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.de.resx b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.de.resx new file mode 100644 index 00000000..cced431a --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.de.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Titeldetails bearbeiten + + + Mit vorherigem Titel verbinden + + + Position + + + Künstler + + + Titel + + + Beginn + + + Ende + + + Länge + + + Markierungen + + + Vorlücke + + + Nachlücke + + + Änderungen speichern + + + Abbrechen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.razor b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.razor new file mode 100644 index 00000000..ddd0639e --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.razor @@ -0,0 +1,132 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser +@inject AutocompleteManager _autocompleteManager +@inject ValidationService _validationService + + + + @_localizer["Edit track details"] + + + + + + @{ + MusicBrainzArtist? autocompleteArtist = new() + { + Name = EditedTrack.Artist + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Name, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Name + } + + + @{ + MusicBrainzTrack? autocompleteTrack = new() + { + Artist = EditedTrack.Artist, + Title = EditedTrack.Title + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Title, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Title + } + + + + + + + + @foreach(var flag in Flag.AvailableFlags) + { + @flag.Name + } + + + + + + + + @_localizer["Save changes"] + @_localizer["Abort"] + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + [EditorRequired] + public Track EditedTrack { get; set; } = null!; + + void TitleSelected(MusicBrainzTrack? musicBrainzTrack) + { + if ((String.IsNullOrEmpty(EditedTrack.Artist)) && (String.IsNullOrEmpty(musicBrainzTrack?.Artist) == false)) + { + EditedTrack.Artist = musicBrainzTrack.Artist; + } + if ((EditedTrack.Length.HasValue == false) && (musicBrainzTrack?.Length.HasValue == true)) + { + EditedTrack.Length = musicBrainzTrack?.Length; + } + } + + String? GetValidationErrorMessage(object model, string propertyName) + { + String? validationErrorMessage = null; + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + return validationErrorMessage; + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.resx b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.resx new file mode 100644 index 00000000..97863bc0 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/EditTrackModal.resx @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Edit track details + + + Link to previous track + + + Position + + + Artist + + + Title + + + Begin + + + End + + + Length + + + Flags + + + PreGap + + + PostGap + + + Save changes + + + Abort + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.de.resx new file mode 100644 index 00000000..be1422e4 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.de.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Export ist derzeit nicht möglich: + + + Dateiname + + + Name + + + Beginn + + + Ende + + + Inhalt + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor new file mode 100644 index 00000000..338f21a2 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor @@ -0,0 +1,95 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IStringLocalizer _validationMessageLocalizer +@inject ValidationService _validationService +@inject IBlazorDownloadFileService _blazorDownloadFileService +@inject CuesheetExportService _cuesheetExportService + + + + @if (String.IsNullOrEmpty(GetGenerationValidationMessages()) == false) + { + + @_localizer["Export is currently not possible:"] + @((MarkupString)GetGenerationValidationMessages()!) + + } + +
+ + + @_localizer["Name"] + @_localizer["Begin"] + @_localizer["End"] + @_localizer["Content"] + + + @context.Name + @context.Begin + @context.End + + + +
+
+
+ +@code { + IEnumerable exportfiles = []; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + exportfiles = _cuesheetExportService.GenerateExportfiles(ApplicationOptions?.CuesheetFilename); + } + + String? GetValidationErrorMessage() + { + String? validationErrorMessage = null; + if (ApplicationOptions != null) + { + var validationMessages = _validationService.Validate(ApplicationOptions, nameof(ApplicationOptions.CuesheetFilename)); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } + + String? GetGenerationValidationMessages() + { + String? validationErrorMessage = null; + var messages = _cuesheetExportService.CanGenerateExportfiles(ApplicationOptions?.CuesheetFilename); + if (messages.Count() > 0) + { + validationErrorMessage = String.Join("
", messages.Select(x => x.GetMessageLocalized(_validationMessageLocalizer))); + } + return validationErrorMessage; + } + + async Task CuesheetFilenameChanged(string newFilename) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.CuesheetFilename, newFilename); + exportfiles = _cuesheetExportService.GenerateExportfiles(ApplicationOptions?.CuesheetFilename); + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.resx new file mode 100644 index 00000000..fd4c9fa2 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Export is currently not possible: + + + Filename + + + Name + + + Begin + + + End + + + Content + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.de.resx new file mode 100644 index 00000000..cd9263b0 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.de.resx @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Export konfigurieren + + + Export ist derzeit nicht möglich: + + + Exportprofil auswählen + + + Neues Exportprofil hinzufügen + + + Ausgewähltes Exportprofil löschen + + + Name + + + Geben Sie hier den Namen für dieses Profil ein + + + Dateiname + + + Geben Sie hier den Dateinamen für dieses Profil ein + + + Schema Kopf + + + Geben Sie hier das Kopf-Schema für dieses Profil ein + + + Platzhalter hinzufügen + + + Schema Titel + + + Geben Sie hier das Spuren-Schema für dieses Profil ein + + + Schema Fuß + + + Geben Sie hier das Fuß-Schema für dieses Profil ein + + + Export herunterladen + + + Beginn + + + Ende + + + Inhalt + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor new file mode 100644 index 00000000..57089a98 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor @@ -0,0 +1,216 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IStringLocalizer _validationMessageLocalizer +@inject ValidationService _validationService +@inject ExportfileGenerator _exportfileGenerator +@inject IBlazorDownloadFileService _blazorDownloadFileService + +@if (exportOptions != null) +{ + + + + + @if (String.IsNullOrEmpty(GetGenerationValidationMessages()) == false) + { + + @_localizer["Export is currently not possible:"] + @((MarkupString)GetGenerationValidationMessages()!) + + } + + @foreach (var profile in exportOptions.ExportProfiles) + { + @profile.Name + } + + + @_localizer["Add new export profile"] + @_localizer["Delete selected export profile"] + +
+ + + + + + + @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + { + + @_localizer[placeholder.Key] + + } + + + + + + @foreach (var placeholder in Exportprofile.AvailableTrackSchemes) + { + + @_localizer[placeholder.Key] + + } + + + + + + @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + { + + @_localizer[placeholder.Key] + + } + + +
+ + + + @_localizer["Name"] + @_localizer["Begin"] + @_localizer["End"] + @_localizer["Content"] + + + @context.Name + @context.Begin + @context.End + + + + +
+
+
+} + +@code { + ExportOptions? exportOptions; + IEnumerable exportFiles = []; + Boolean configureExportCompleted = false; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + LocalStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + LocalStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + exportOptions = await LocalStorageOptionsProvider.GetOptions(); + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is ExportOptions exportOption) + { + exportOptions = exportOption; + StateHasChanged(); + } + } + + async Task AddClick() + { + if (exportOptions != null) + { + var newProfile = new Exportprofile(); + exportOptions.ExportProfiles.Add(newProfile); + exportOptions.SelectedExportProfile = newProfile; + await LocalStorageOptionsProvider.SaveOptions(exportOptions); + } + } + + async Task DeleteClick() + { + if (exportOptions?.SelectedExportProfile != null) + { + exportOptions.ExportProfiles.Remove(exportOptions.SelectedExportProfile); + exportOptions.SelectedExportProfile = exportOptions.ExportProfiles.LastOrDefault(); + await LocalStorageOptionsProvider.SaveOptions(exportOptions); + } + } + + String? GetValidationErrorMessage(object? model, string propertyName) + { + String? validationErrorMessage = null; + if (model != null) + { + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } + + String? GetGenerationValidationMessages() + { + String? validationErrorMessage = null; + var messages = _exportfileGenerator.CanGenerateExportfiles(exportOptions?.SelectedExportProfile); + if (messages.Count() > 0) + { + validationErrorMessage = String.Join("
", messages.Select(x => x.GetMessageLocalized(_validationMessageLocalizer))); + } + return validationErrorMessage; + } + + Task PreviewInteraction(StepperInteractionEventArgs arg) + { + if (arg.StepIndex == 0) + { + arg.Cancel = String.IsNullOrEmpty(GetGenerationValidationMessages()) == false; + } + return Task.CompletedTask; + } + + void ActiveIndexChanged(int newIndex) + { + if (newIndex == 1) + { + exportFiles = _exportfileGenerator.GenerateExportfiles(exportOptions?.SelectedExportProfile); + configureExportCompleted = exportFiles.Any(); + } + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.resx new file mode 100644 index 00000000..efd6621f --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.resx @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Configure export + + + Export is currently not possible: + + + Select export profile + + + Add new export profile + + + Delete selected export profile + + + Name + + + Enter the name for this profile here + + + Filename + + + Enter the filename for this profile here + + + Scheme head + + + Enter the header scheme for this profile here + + + Add placeholder + + + Scheme tracks + + + Enter the tracks scheme for this profile here + + + Scheme footer + + + Enter the footer scheme for this profile here + + + Download export + + + Begin + + + End + + + Content + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.de.resx new file mode 100644 index 00000000..f7dbba2c --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.de.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Ok + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.razor new file mode 100644 index 00000000..86ab4547 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.razor @@ -0,0 +1,51 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + + + + @_localizer["Ok"] + + + +@code { + private string? inputValue; + + protected override void OnParametersSet() + { + base.OnParametersSet(); + inputValue = InitialValue; + } + + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + public string? Label { get; set; } + + [Parameter] + public string? Placeholder { get; set; } + + [Parameter] + public String? InitialValue { get; set; } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.resx new file mode 100644 index 00000000..f7dbba2c --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/InputTextDialog.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Ok + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.de.resx new file mode 100644 index 00000000..24c65e85 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.de.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Sie haben eine ungültige Datei ({0}) hinzugefügt die nicht verarbeitet werden kann. + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor new file mode 100644 index 00000000..184fb8ee --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor @@ -0,0 +1,65 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ImportManager _importManager + + + +
+ + + + @foreach (var invalidFileName in invalidDropFileNames) + { + + @String.Format(_localizer["You dropped an invalid file ({0}) that can not be processed."], invalidFileName) + + } + + +
+
+
+ +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + string dropFileInputId = "dropFileInputId_SelectFileDialog"; + List invalidDropFileNames = new(); + + async Task InputFilesChanged(IReadOnlyCollection files) + { + invalidDropFileNames.Clear(); + foreach (var file in files) + { + if ((FileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile) == false) + && (FileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet) == false)) + { + invalidDropFileNames.Add(file.Name); + } + } + if (invalidDropFileNames.Count == 0) + { + await _importManager.ImportFilesAsync(files); + MudDialog?.Close(); + } + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.resx new file mode 100644 index 00000000..a87aefc4 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + You dropped an invalid file ({0}) that can not be processed. + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.de.resx new file mode 100644 index 00000000..1b166b0f --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.de.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Platzhalter einfügen + + + Titel automatisch miteinander verknüpfen + + + Tage + + + Anzeige + + + Stunden + + + Eingabe + + + Millisekunden + + + Minuten + + + Sekunden + + + Anzeigeformat Zeit + + + Eingabeformat Zeit + + + Benutzt .NET format, bitte prüfen sie die Hilfe für mehr Informationen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor new file mode 100644 index 00000000..c7026b28 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor @@ -0,0 +1,89 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ValidationService _validationService + + + + @_localizer["Input"] + + + + + @foreach(var scheme in TimeSpanFormat.AvailableTimespanScheme) + { + @_localizer[scheme] + } + + + @_localizer["Display"] + + + + +@code { + MudTextField? timeInputFormatTextField; + + async Task TimeInputFormatChangedAsync(string newValue) + { + TimeSpanFormat? timeSpanFormat = ApplicationOptions?.TimeSpanFormat; + if (string.IsNullOrEmpty(newValue)) + { + timeSpanFormat = null; + } + else + { + if (timeSpanFormat == null) + { + timeSpanFormat = new(); + } + timeSpanFormat.Scheme = newValue; + } + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.TimeSpanFormat!, timeSpanFormat); + } + + async Task DisplayTimeSpanFormatChangedAsync(string newValue) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.DisplayTimeSpanFormat, newValue); + } + + void AppendPlaceholderToTimeInputFormatTextField(string placeholder) + { + timeInputFormatTextField?.SetText($"{timeInputFormatTextField.Text}{placeholder}"); + } + + String? GetValidationErrorMessage(object? model, string propertyName) + { + String? validationErrorMessage = null; + if (model != null) + { + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.resx new file mode 100644 index 00000000..3efb8245 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Add placeholder + + + Automatically link tracks + + + Days + + + Display + + + Hours + + + Input + + + Milliseconds + + + Minutes + + + Seconds + + + Time display format + + + Time input format + + + Uses .NET format, check help for more information + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.de.resx new file mode 100644 index 00000000..bf14fe6b --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.de.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Aufnahmecountdown in Sekunden + + + Sekunden bis die Aufnahme startet + + + Countdown starten + + + Aufnahmecountdown starten + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor new file mode 100644 index 00000000..b903d559 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor @@ -0,0 +1,41 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + +@if (ApplicationOptions != null) +{ + + + @_localizer["Start record countdown timer"] + + + + + + @_localizer["Start countdown"] + @_localizer["Abort"] + + +} + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } +} diff --git a/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.resx new file mode 100644 index 00000000..beb8b8e3 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Record countdown in sconds + + + Seconds till record starts + + + Start countdown + + + Start record countdown timer + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/EditImportOptions.razor b/AudioCuesheetEditor/Shared/EditImportOptions.razor deleted file mode 100644 index 961e45be..00000000 --- a/AudioCuesheetEditor/Shared/EditImportOptions.razor +++ /dev/null @@ -1,179 +0,0 @@ - - -@implements IDisposable - -@inject ITextLocalizerService _localizationService -@inject ITextLocalizer _localizer -@inject ITextLocalizer _validationMessageLocalizer -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider - - - - - - - @_localizer["Textimportscheme cuesheet"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableSchemeTrack in TextImportScheme.AvailableSchemeCuesheet) - { - @_localizer[availableSchemeTrack.Key] - } - - - - - - - - - - - - @_localizer["Textimportscheme track"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableSchemeTrack in TextImportScheme.AvailableSchemesTrack) - { - @_localizer[availableSchemeTrack.Key] - } - - - - - - - - - - - - @_localizer["Customized timespan format import"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableFormat in TimeSpanFormat.AvailableTimespanScheme) - { - @_localizer[availableFormat.Key] - } - - - - - - - - - -@code{ - [Parameter] - public EventCallback OptionsChanged { get; set; } - - public ImportOptions? ImportOptions { get; private set; } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; - } - - protected override async Task OnInitializedAsync() - { - TimeSpanFormat.TextLocalizer = _localizer; - TextImportScheme.TextLocalizer = _localizer; - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; - - ImportOptions = await _localStorageOptionsProvider.GetOptions(); - - await base.OnInitializedAsync(); - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - TimeSpanFormat.TextLocalizer = _localizer; - TextImportScheme.TextLocalizer = _localizer; - StateHasChanged(); - } - - void LocalStorageOptionsProvider_OptionsSaved(object? sender, IOptions options) - { - if (options is ImportOptions importOptions) - { - ImportOptions = importOptions; - StateHasChanged(); - } - } - - async Task TextChangedAsync(Action setter, string text) - { - if (ImportOptions == null) - { - throw new NullReferenceException(); - } - setter(ImportOptions, text); - await _localStorageOptionsProvider.SaveOptions(ImportOptions); - await OptionsChanged.InvokeAsync(ImportOptions); - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/EditRecordOptions.razor b/AudioCuesheetEditor/Shared/EditRecordOptions.razor deleted file mode 100644 index e96e7037..00000000 --- a/AudioCuesheetEditor/Shared/EditRecordOptions.razor +++ /dev/null @@ -1,111 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ITextLocalizerService _localizationService -@inject ITextLocalizer _validationMessageLocalizer - - - - - @_localizer["Filename for recorded audio"] - - - - - - - - - - - @_localizer["Record time sensitivity"] - - - - - - - - - - - - -@code { - RecordOptions? recordOptions; - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; - } - - protected override async Task OnInitializedAsync() - { - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; - - recordOptions = await _localStorageOptionsProvider.GetOptions(); - await base.OnInitializedAsync(); - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void LocalStorageOptionsProvider_OptionsSaved(object? sender, IOptions options) - { - if (options is RecordOptions recordingOptions) - { - recordOptions = recordingOptions; - } - } - - async Task RecordedAudiofilenameChanged(string newValue) - { - if (recordOptions != null) - { - recordOptions.RecordedAudiofilename = newValue; - } - await _localStorageOptionsProvider.SaveOptionsValue(x => x.RecordedAudiofilename, newValue); - } - - async Task RecordTimeSensitivityChanged(string newValue) - { - if (recordOptions != null) - { - recordOptions.RecordTimeSensitivityname = newValue; - } - await _localStorageOptionsProvider.SaveOptionsValue(x => x.RecordTimeSensitivityname!, newValue); - } - - async Task ResetOptions() - { - var newOptions = new RecordOptions(); - await _localStorageOptionsProvider.SaveOptions(newOptions); - } -} diff --git a/AudioCuesheetEditor/Shared/EditTrackModal.razor b/AudioCuesheetEditor/Shared/EditTrackModal.razor deleted file mode 100644 index 84f5e846..00000000 --- a/AudioCuesheetEditor/Shared/EditTrackModal.razor +++ /dev/null @@ -1,864 +0,0 @@ - -@implements IAsyncDisposable - -@inject ITextLocalizer _localizer -@inject MusicBrainzDataProvider _musicBrainzDataProvider -@inject SessionStateContainer _sessionStateContainer -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject TraceChangeManager _traceChangeManager -@inject HotKeys _hotKeys -@inject ITextLocalizer _validationMessageLocalizer -@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser - - - - - @_localizer["Edit track details"] - - - - - @if (IsMultipleEdit == false) - { - - - @_localizer["IsLinkedToPreviousTrack"] - - - - @_localizer["IsLinkedToPreviousTrackValue"] - - - - - - - - - - - @_localizer["Position"] - - - - - - - - - - - @_localizer["Artist"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - @_localizer["Title"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - @_localizer["Begin"] - - - - - - - - - - - - @_localizer["End"] - - - - - - - - - - - - @_localizer["Length"] - - - - - - - - - - - @_localizer["Flags"] - - @foreach (var flag in Flag.AvailableFlags) - { - - @flag.Name - - } - - - - - @_localizer["PreGap"] - - - - - - - - - - - - @_localizer["PostGap"] - - - - - - - - - - } - else - { - - - - @_localizer["Change"] - @_localizer["Property"] - @_localizer["Value"] - - - - - - @_localizer["IsLinkedToPreviousTrack"] - @_localizer["IsLinkedToPreviousTrackValue"] - - - - @_localizer["Position"] - - - - - - - - - - - - - - @_localizer["Artist"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - @_localizer["Title"] - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - @_localizer["Begin"] - - - - - - - - - - - - - - @_localizer["End"] - - - - - - - - - - - - - - @_localizer["Length"] - - - - - - - - - - - - - - @_localizer["Flags"] - - @foreach (var flag in Flag.AvailableFlags) - { - - @flag.Name - - } - - - - - @_localizer["PreGap"] - - - - - - - - - - - - - - @_localizer["PostGap"] - - - - - - - - - - - - -
- } -
-
- - - - -
-
- -@code { - enum DynamicEditValue - { - EnteredValueEquals = 0, - EnteredValueAdd = 1, - EnteredValueSubstract = 2 - } - - public IReadOnlyCollection? TracksToEdit - { - get => _tracksToEdit; - set - { - editedTrack.ValidateablePropertyChanged -= editedTrack_ValidateablePropertyChanged; - _tracksToEdit = value; - if (IsMultipleEdit) - { - editedTrack = new() { AutomaticallyCalculateLength = false }; - } - else - { - if (_tracksToEdit != null) - { - editedTrack = _tracksToEdit.First().Clone(); - } - } - editedTrack.ValidateablePropertyChanged += editedTrack_ValidateablePropertyChanged; - if (validations != null) - { - validations.ValidateAll(); - } - } - } - - [Parameter] public EventCallback SaveClicked { get; set; } - - public Boolean IsMultipleEdit { get => TracksToEdit?.Count > 1; } - - IEnumerable? autocompleteTrackArtistsEditDialog; - IEnumerable? autocompleteTrackTitlesEditDialog; - - IReadOnlyCollection? _tracksToEdit; - Modal? modalTrackEdit; - Track editedTrack = new() { AutomaticallyCalculateLength = false }; - - Boolean _editTrackIsLinkedToPreviousTrack; - Boolean _editTrackPosition; - Boolean _editTrackArtist; - Boolean _editTrackTitle; - Boolean _editTrackBegin; - Boolean _editTrackEnd; - Boolean _editTrackLength; - Boolean _editTrackFlags; - Boolean _editTrackPreGap; - Boolean _editTrackPostGap; - DynamicEditValue _editModeTrackPosition; - DynamicEditValue _editModeTrackBegin; - DynamicEditValue _editModeTrackEnd; - DynamicEditValue _editModeTrackLength; - DynamicEditValue _editModeTrackPreGap; - DynamicEditValue _editModeTrackPostGap; - - HotKeysContext? hotKeysContext; - Validations? validations; - ApplicationOptions? applicationOptions; - - public async ValueTask DisposeAsync() - { - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - protected override async Task OnInitializedAsync() - { - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEditTrackModalSaveKeyDown) - .Add(Key.Escape, OnHideModalKeyDown); - - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; - - } - - public Boolean EditTrackIsLinkedToPreviousTrack - { - get => _editTrackIsLinkedToPreviousTrack; - private set - { - _editTrackIsLinkedToPreviousTrack = value; - if (_editTrackIsLinkedToPreviousTrack == false) - { - editedTrack.IsLinkedToPreviousTrack = false; - } - } - } - - public Boolean EditTrackPosition - { - get => _editTrackPosition; - private set - { - _editTrackPosition = value; - if (_editTrackPosition == false) - { - editedTrack.Position = null; - } - } - } - - public Boolean EditTrackArtist - { - get => _editTrackArtist; - private set - { - _editTrackArtist = value; - if (_editTrackArtist == false) - { - editedTrack.Artist = null; - } - } - } - - public Boolean EditTrackTitle - { - get => _editTrackTitle; - private set - { - _editTrackTitle = value; - if (_editTrackTitle == false) - { - editedTrack.Title = null; - } - } - } - - public Boolean EditTrackBegin - { - get => _editTrackBegin; - private set - { - _editTrackBegin = value; - if (_editTrackBegin == false) - { - editedTrack.Begin = null; - } - } - } - - public Boolean EditTrackEnd - { - get => _editTrackEnd; - private set - { - _editTrackEnd = value; - if (_editTrackEnd == false) - { - editedTrack.End = null; - } - } - } - - public Boolean EditTrackLength - { - get => _editTrackLength; - private set - { - _editTrackLength = value; - if (_editTrackLength == false) - { - editedTrack.Length = null; - } - } - } - - public Boolean EditTrackFlags - { - get => _editTrackFlags; - private set - { - _editTrackFlags = value; - if (_editTrackFlags == false) - { - editedTrack.SetFlags(new List()); - } - } - } - - public Boolean EditTrackPreGap - { - get => _editTrackPreGap; - private set - { - _editTrackPreGap = value; - if (_editTrackPreGap == false) - { - editedTrack.PreGap = null; - } - } - } - - public Boolean EditTrackPostGap - { - get => _editTrackPostGap; - private set - { - _editTrackPostGap = value; - if (_editTrackPostGap == false) - { - editedTrack.PostGap = null; - } - } - } - - public async Task Show() - { - SetInitialState(); - if (modalTrackEdit != null) - { - await modalTrackEdit.Show(); - } - } - - void SetInitialState() - { - if (IsMultipleEdit) - { - EditTrackIsLinkedToPreviousTrack = false; - EditTrackPosition = false; - EditTrackArtist = false; - EditTrackTitle = false; - EditTrackLength = false; - EditTrackBegin = false; - EditTrackEnd = false; - EditTrackFlags = false; - EditTrackPreGap = false; - EditTrackPostGap = false; - _editModeTrackPosition = DynamicEditValue.EnteredValueEquals; - _editModeTrackBegin = DynamicEditValue.EnteredValueEquals; - _editModeTrackEnd = DynamicEditValue.EnteredValueEquals; - _editModeTrackLength = DynamicEditValue.EnteredValueEquals; - _editModeTrackPreGap = DynamicEditValue.EnteredValueEquals; - _editModeTrackPostGap = DynamicEditValue.EnteredValueEquals; - } - } - - async Task EditTrackModalSaveClicked() - { - if (TracksToEdit != null) - { - if (IsMultipleEdit) - { - _traceChangeManager.BulkEdit = true; - } - foreach (var track in TracksToEdit) - { - if (IsMultipleEdit) - { - var position = editedTrack.Position; - var begin = editedTrack.Begin; - var end = editedTrack.End; - var length = editedTrack.Length; - var preGap = editedTrack.PreGap; - var postGap = editedTrack.PostGap; - Boolean copyTrackPosition = EditTrackPosition; - Boolean copyTrackBegin = EditTrackBegin; - Boolean copyTrackEnd = EditTrackEnd; - Boolean copyTrackLength = EditTrackLength; - Boolean copyTrackPreGap = EditTrackPreGap; - Boolean copyTrackPostGap = EditTrackPostGap; - //First process dynamic edit, because we need to increase each value seperately - switch (_editModeTrackPosition) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - editedTrack.Position += track.Position; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: copyTrackPosition, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackPosition = false; - editedTrack.Position = position; - break; - case DynamicEditValue.EnteredValueSubstract: - var newValue = track.Position - editedTrack.Position; - editedTrack.Position = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: copyTrackPosition, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackPosition = false; - break; - } - switch (_editModeTrackBegin) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - var newValue = editedTrack.Begin + track.Begin; - editedTrack.Begin = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: copyTrackBegin, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackBegin = false; - break; - case DynamicEditValue.EnteredValueSubstract: - newValue = track.Begin - editedTrack.Begin; - editedTrack.Begin = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: copyTrackBegin, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackBegin = false; - break; - } - switch (_editModeTrackEnd) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - var newValue = editedTrack.End + track.End; - editedTrack.End = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: copyTrackEnd, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackEnd = false; - break; - case DynamicEditValue.EnteredValueSubstract: - newValue = track.End - editedTrack.End; - editedTrack.End = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: copyTrackEnd, setLength: false, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackEnd = false; - break; - } - switch (_editModeTrackLength) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - var newValue = editedTrack.Length + track.Length; - editedTrack.Length = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: copyTrackLength, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackLength = false; - break; - case DynamicEditValue.EnteredValueSubstract: - newValue = track.Length - editedTrack.Length; - editedTrack.Length = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: copyTrackLength, setFlags: false, setPreGap: false, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackLength = false; - break; - } - switch (_editModeTrackPreGap) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - var newValue = editedTrack.PreGap + track.PreGap; - editedTrack.PreGap = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: copyTrackPreGap, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackPreGap = false; - break; - case DynamicEditValue.EnteredValueSubstract: - newValue = track.PreGap - editedTrack.PreGap; - editedTrack.PreGap = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: copyTrackPreGap, setPostGap: false, useInternalSetters: Track.AllPropertyNames); - copyTrackPreGap = false; - break; - } - switch (_editModeTrackPostGap) - { - case DynamicEditValue.EnteredValueEquals: - break; - case DynamicEditValue.EnteredValueAdd: - var newValue = editedTrack.PostGap + track.PostGap; - editedTrack.PostGap = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: copyTrackPostGap, useInternalSetters: Track.AllPropertyNames); - copyTrackPostGap = false; - break; - case DynamicEditValue.EnteredValueSubstract: - newValue = track.PostGap - editedTrack.PostGap; - editedTrack.PostGap = newValue; - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: false, setPosition: false, setArtist: false, setTitle: false, setBegin: false, setEnd: false, setLength: false, setFlags: false, setPreGap: false, setPostGap: copyTrackPostGap, useInternalSetters: Track.AllPropertyNames); - copyTrackPostGap = false; - break; - } - editedTrack.Position = position; - editedTrack.Begin = begin; - editedTrack.End = end; - editedTrack.Length = length; - editedTrack.PreGap = preGap; - editedTrack.PostGap = postGap; - //Now set all values! - track.CopyValues(editedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: EditTrackIsLinkedToPreviousTrack, setPosition: copyTrackPosition, setArtist: EditTrackArtist, setTitle: EditTrackTitle, setBegin: copyTrackBegin, setEnd: copyTrackEnd, setLength: copyTrackLength, setFlags: EditTrackFlags, setPreGap: copyTrackPreGap, setPostGap: copyTrackPostGap); - } - else - { - track.CopyValues(editedTrack, setCuesheet: false); - } - } - // We need to fire events for IsLinkedToPreviousTrack to work, if we have done a multiple edit. - if (IsMultipleEdit) - { - _traceChangeManager.BulkEdit = false; - } - } - await ControlModalDialog(modalTrackEdit, false); - await SaveClicked.InvokeAsync(); - } - - async ValueTask OnEditTrackModalSaveKeyDown() - { - await EditTrackModalSaveClicked(); - } - - async Task ControlModalDialog(Modal? dialog, Boolean show) - { - if (dialog != null) - { - if (show) - { - await dialog.Show(); - } - else - { - await dialog.Hide(); - } - } - } - - async ValueTask OnHideModalKeyDown() - { - await ControlModalDialog(modalTrackEdit, false); - } - - async Task OnReadDataAutocompleteTrackArtistEditDialog(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var artists = await _musicBrainzDataProvider.SearchArtistAsync(autocompleteReadDataEventArgs.SearchValue); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteTrackArtistsEditDialog = artists; - } - } - } - - async Task OnReadDataAutocompleteTrackTitleEditDialog(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs, Track track) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var titles = await _musicBrainzDataProvider.SearchTitleAsync(autocompleteReadDataEventArgs.SearchValue, track.Artist); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteTrackTitlesEditDialog = titles; - } - } - } - - async Task OnSelectedValueChangedTrackTitle(Guid selectedValue, Track track) - { - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeFull: - case ViewMode.ViewModeImport: - var trackDetails = await _musicBrainzDataProvider.GetDetailsAsync(selectedValue); - if (trackDetails != null) - { - if (track.Length.HasValue == false) - { - if ((IsMultipleEdit == false) || (EditTrackLength)) - { - track.Length = trackDetails.Length; - } - } - if (String.IsNullOrEmpty(track.Artist)) - { - if ((IsMultipleEdit == false) || (EditTrackArtist)) - { - track.Artist = trackDetails.Artist; - } - } - } - break; - } - } - - String? GetTimespanAsString(TimeSpan? timeSpan, Boolean removeMilliseconds = false) - { - String? resultString = null; - if ((timeSpan != null) && (timeSpan.HasValue)) - { - if (removeMilliseconds == true) - { - resultString = timeSpan.Value.Subtract(new TimeSpan(0, 0, 0, 0, timeSpan.Value.Milliseconds)).ToString(); - } - else - { - resultString = timeSpan.Value.ToString(); - } - } - return resultString; - } - - void editedTrack_ValidateablePropertyChanged(object? sender, string property) - { - if (validations != null) - { - validations.ValidateAll().GetAwaiter().GetResult(); - } - } - - void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions options) - { - if (options is ApplicationOptions) - { - applicationOptions = (ApplicationOptions)options; - } - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/ImportFileContent.de.resx b/AudioCuesheetEditor/Shared/Import/ImportFileContent.de.resx new file mode 100644 index 00000000..cb591bb2 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportFileContent.de.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Analysierter Dateiinhalt + + + Bearbeiten + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor b/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor new file mode 100644 index 00000000..d90881ca --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor @@ -0,0 +1,63 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ISessionStateContainer _sessionStateContainer + + + @if (FileContentRecognized != null) + { + + +
                
+                    @foreach (var line in FileContentRecognized)
+                    {
+                        @((MarkupString)String.Format("{0}
", line)) + } +
+
+
+ + + + } +
+ +@code { + [Parameter] + public EventCallback FileContentChanged { get; set; } + public IEnumerable? FileContentRecognized => _sessionStateContainer.Importfile?.FileContentRecognized; + public String FileContent + { + get + { + String fileContent = String.Empty; + if (_sessionStateContainer.Importfile?.FileContent != null) + { + fileContent = String.Join(Environment.NewLine, _sessionStateContainer.Importfile.FileContent); + } + return fileContent; + } + } + + async Task FileContent_TextChangedAsync(string newFileContent) + { + await FileContentChanged.InvokeAsync(newFileContent); + } +} diff --git a/AudioCuesheetEditor/Shared/Import/ImportFileContent.resx b/AudioCuesheetEditor/Shared/Import/ImportFileContent.resx new file mode 100644 index 00000000..f861a443 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportFileContent.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Analyzed file content + + + Edit + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx b/AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx new file mode 100644 index 00000000..1262de70 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Platzhalter hinzufügen + + + Möchten Sie wirklich die Importschemata auf Werkseinstellungen zurücksetzen? + + + Künstler + + + Audiodatei + + + Start + + + Katalognummer + + + CD-Textdatei + + + Bestätigen + + + Tage + + + Ende + + + Markierungen + + + Stunden + + + Importschemata + + + Länge + + + Millisekunden + + + Minuten + + + Position + + + Nachlücke + + + Vorlücke + + + Sekunden + + + Startzeitpunkt + + + Textimportschema Cuesheet + + + Textimportschema Titel + + + Zeitformat für den Import + + + Titel + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor b/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor new file mode 100644 index 00000000..3f3d1108 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor @@ -0,0 +1,133 @@ +@using System.Linq.Expressions + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ValidationService _validationService +@inject IDialogService _dialogService + + + @_localizer["Import schemes"] + + + + + + @foreach (var scheme in TextImportScheme.AvailableSchemeCuesheet) + { + @_localizer[scheme] + } + + + + + + @foreach (var scheme in TextImportScheme.AvailableSchemesTrack) + { + @_localizer[scheme] + } + + + + + + @foreach (var scheme in TimeSpanFormat.AvailableTimespanScheme) + { + @_localizer[scheme] + } + + + +@code { + [Parameter] + public EventCallback ImportSchemeCuesheetChanged { get; set; } + + [Parameter] + public EventCallback ImportSchemeTracksChanged { get; set; } + + [Parameter] + public EventCallback ImportTimeInputFormatChanged { get; set; } + + MudTextField? importSchemeCuesheetTextField, importSchemeTracksTextField, importTimeInputFormatTextField; + + void AppendPlaceholderToTextField(MudTextField? mudTextField, string placeholder) + { + mudTextField?.SetText($"{mudTextField.Text}{placeholder}"); + } + + async Task ImportSchemeCuesheetTextChangedAsync(string newScheme) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ImportScheme.SchemeCuesheet, newScheme); + await ImportSchemeCuesheetChanged.InvokeAsync(newScheme); + } + + async Task ImportSchemeTracksTextChangedAsync(string newScheme) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ImportScheme.SchemeTracks, newScheme); + await ImportSchemeTracksChanged.InvokeAsync(newScheme); + } + + async Task ImportTimeInputFormatChangedAsync(string newScheme) + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ImportTimeSpanFormat.Scheme, newScheme); + await ImportTimeInputFormatChanged.InvokeAsync(newScheme); + } + + String? GetValidationErrorMessage(object? model, string propertyName) + { + String? validationErrorMessage = null; + if (model != null) + { + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } + + async Task ResetSchemes() + { + if (ApplicationOptions != null) + { + var parameters = new DialogParameters + { + { x => x.ConfirmText, _localizer["Are you sure you want to reset the import schemes to factory default?"] }, + }; + var dialog = await _dialogService.ShowAsync(_localizer["Confirm"], parameters); + var result = await dialog.Result; + if (result?.Canceled == false) + { + ApplicationOptions.ImportScheme = TextImportScheme.DefaultTextImportScheme; + ApplicationOptions.ImportTimeSpanFormat = new(); + await LocalStorageOptionsProvider.SaveOptions(ApplicationOptions); + await ImportSchemeCuesheetChanged.InvokeAsync(ApplicationOptions.ImportScheme.SchemeCuesheet); + await ImportSchemeTracksChanged.InvokeAsync(ApplicationOptions.ImportScheme.SchemeTracks); + await ImportTimeInputFormatChanged.InvokeAsync(ApplicationOptions.ImportTimeSpanFormat.Scheme); + } + } + } +} diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.resx b/AudioCuesheetEditor/Shared/Import/ImportSchemes.resx new file mode 100644 index 00000000..e6597bc9 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/ImportSchemes.resx @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Add placeholder + + + Are you sure you want to reset the import schemes to factory default? + + + Artist + + + Audiofile + + + Begin + + + Cataloguenumber + + + CDTextfile + + + Confirm + + + Days + + + End + + + Flags + + + Hours + + + Import schemes + + + Length + + + Milliseconds + + + Minutes + + + Position + + + PostGap + + + PreGap + + + Seconds + + + StartDateTime + + + Textimport scheme cuesheet + + + Textimport scheme tracks + + + Time input format for import + + + Title + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/SelectImportFiles.de.resx b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.de.resx new file mode 100644 index 00000000..4fbd4abb --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.de.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Datei für Import auswählen + + + Sie haben eine ungültige Datei ({0}) hinzugefügt die nicht verarbeitet werden kann. + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor new file mode 100644 index 00000000..abb560ff --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor @@ -0,0 +1,96 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ISessionStateContainer _sessionStateContainer +@inject ImportManager _importManager +@inject FileInputManager _fileInputManager + + + + + @_localizer["Select files for import"] + + + + + @foreach (var invalidFileName in invalidDropFileNames) + { + + @String.Format(_localizer["You dropped an invalid file ({0}) that can not be processed."], invalidFileName) + + } + + + +@code { + string dropFileInputId = "dropFileInputId"; + List invalidDropFileNames = new(); + + [Parameter] + public EventCallback> FilesImported { get; set; } + + [Parameter] + public EventCallback> InvalidFilesChanged { get; set; } + + async Task InputFilesChanged(IReadOnlyCollection files) + { + invalidDropFileNames.Clear(); + foreach (var file in files) + { + if ((FileInputManager.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text) == false) + && (FileInputManager.GetAudioCodec(file) == null)) + { + invalidDropFileNames.Add(file.Name); + } + } + if (invalidDropFileNames.Count == 0) + { + await ImportFiles(files); + } + else + { + await InvalidFilesChanged.InvokeAsync(invalidDropFileNames); + } + } + + async Task ImportFiles(IReadOnlyCollection files) + { + _sessionStateContainer.ResetImport(); + var importedFiles = await _importManager.ImportFilesAsync(files); + // Audio file is handled seperatly + foreach (var file in files) + { + var codec = FileInputManager.GetAudioCodec(file); + if (codec != null) + { + var audiofile = await _fileInputManager.CreateAudiofileAsync(dropFileInputId, file); + _sessionStateContainer.ImportAudiofile = audiofile; + importedFiles.Add(file, ImportFileType.Audiofile); + } + } + await FilesImported.InvokeAsync(importedFiles); + } + + async Task CloseInvalidFileClicked(string invalidFile) + { + invalidDropFileNames.Remove(invalidFile); + await InvalidFilesChanged.InvokeAsync(invalidDropFileNames); + } +} diff --git a/AudioCuesheetEditor/Shared/Import/SelectImportFiles.resx b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.resx new file mode 100644 index 00000000..062757f8 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Select files for import + + + You dropped an invalid file ({0}) that can not be processed. + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Inputs/DropFileInput.de.resx b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.de.resx new file mode 100644 index 00000000..7c955337 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.de.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Dateien hier ablegen oder klicken um Dateien auszuwählen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Inputs/DropFileInput.razor b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.razor new file mode 100644 index 00000000..8dc091a9 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.razor @@ -0,0 +1,57 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + +@code { + [Parameter] + public String? Filter { get; set; } + + const string DefaultDragClass = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full"; + string dragClass = DefaultDragClass; + + void SetDragClass() => dragClass = $"{DefaultDragClass} mud-border-primary"; + + void ClearDragClass() => dragClass = DefaultDragClass; + + [Parameter] + public EventCallback> OnFilesSelected { get; set; } + + void InputFilesChanged(InputFileChangeEventArgs e) + { + ClearDragClass(); + var files = e.GetMultipleFiles(); + OnFilesSelected.InvokeAsync(files); + } +} diff --git a/AudioCuesheetEditor/Shared/Inputs/DropFileInput.resx b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.resx new file mode 100644 index 00000000..dd3c9c82 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/DropFileInput.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Drag and drop files here or click to choose files + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Inputs/FileInput.de.resx b/AudioCuesheetEditor/Shared/Inputs/FileInput.de.resx new file mode 100644 index 00000000..b05832b9 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/FileInput.de.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Datei herunterladen + + + Keine Datei ausgewählt + + + Datei umbenennen + + + Durchsuchen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Inputs/FileInput.razor b/AudioCuesheetEditor/Shared/Inputs/FileInput.razor new file mode 100644 index 00000000..4376ad40 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/FileInput.razor @@ -0,0 +1,109 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IBlazorDownloadFileService _blazorDownloadFileService + + + + + @_localizer["Search"] + + + @if (DisplayMenu) + { + + @if (DisplayDownloadFile) + { + @_localizer["Download file"] + } + @_localizer["Rename file"] + + } + +@if(String.IsNullOrEmpty(Error) == false) +{ + @Error +} + +@code { + [Parameter] + [EditorRequired] + public String? Label { get; set; } + + [Parameter] + public EventCallback OnFileSelected { get; set; } + + [Parameter] + public EventCallback OnDownloadFileClicked { get; set; } + + [Parameter] + public EventCallback OnFileRenameClicked { get; set; } + + [Parameter] + public String Id { get; set; } = $"FileInput_{Guid.NewGuid()}"; + + [Parameter] + public String? Error { get; set; } + + [Parameter] + public String? Filter { get; set; } + + [Parameter] + public String? FileName { get; set; } + + [Parameter] + public Boolean DisplayDownloadFile { get; set; } = false; + + [Parameter] + public Boolean DisplayMenu { get; set; } = true; + + IBrowserFile? selectedFile; + MudFileUpload? fileUpload; + string? fieldClass; + String TextFieldValue => String.IsNullOrEmpty(FileName) ? _localizer["No file selected"] : FileName; + + void SetDragClass() => fieldClass = "relative rounded-lg border-2 border-dashed mud-border-primary"; + + void ClearDragClass() => fieldClass = null; + + async Task PickFilesClickedAsync() + { + await (fileUpload?.OpenFilePickerAsync() ?? Task.CompletedTask); + } + + async Task ClearClickedAsync() + { + await (fileUpload?.ClearAsync() ?? Task.CompletedTask); + await SetSelectedFileAsync(null); + ClearDragClass(); + } + + async Task SetSelectedFileAsync(IBrowserFile? browserFile) + { + selectedFile = browserFile; + FileName = selectedFile?.Name; + await OnFileSelected.InvokeAsync(selectedFile); + } +} diff --git a/AudioCuesheetEditor/Shared/Inputs/FileInput.resx b/AudioCuesheetEditor/Shared/Inputs/FileInput.resx new file mode 100644 index 00000000..dea080b3 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Inputs/FileInput.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Download file + + + No file selected + + + Rename file + + + Search + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/BaseLocalizedLayoutComponentBase.cs b/AudioCuesheetEditor/Shared/Layouts/BaseLocalizedLayoutComponentBase.cs new file mode 100644 index 00000000..63470c21 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/BaseLocalizedLayoutComponentBase.cs @@ -0,0 +1,59 @@ +//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.Services.UI; +using Microsoft.AspNetCore.Components; + +namespace AudioCuesheetEditor.Shared.Layouts +{ + public class BaseLocalizedLayoutComponentBase: LayoutComponentBase, IDisposable + { + private bool disposedValue; + + [Inject] + protected LocalizationService LocalizationService { get; set; } = default!; + + protected override void OnInitialized() + { + base.OnInitialized(); + LocalizationService.LocalizationChanged += LocalizationService_LocalizationChanged; + } + + void LocalizationService_LocalizationChanged(object? sender, EventArgs args) + { + StateHasChanged(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + LocalizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in der Methode "Dispose(bool disposing)" ein. + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayout.de.resx b/AudioCuesheetEditor/Shared/Layouts/MainLayout.de.resx new file mode 100644 index 00000000..fc62d010 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayout.de.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Es ist ein Fehler in der Anwendung aufgetreten. Bitte melden Sie diesen Fehler unter Angabe von möglichst vielen Details hier: + + + Fehlerdetails + + + Applikation neu laden + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayout.razor b/AudioCuesheetEditor/Shared/Layouts/MainLayout.razor new file mode 100644 index 00000000..41971df2 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayout.razor @@ -0,0 +1,65 @@ + + +@inherits BaseLocalizedLayoutComponentBase + +@inject NavigationManager _navigationManager +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsRuntime + +@* Required *@ + + + +@* Needed for dialogs *@ + + +@* Needed for snackbars *@ + + + + + + + + + @Body + + + + + + + @_localizer["An error has occured in this application. Please report this error with as much details as possible here:"]https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?&labels=bug&template=bug_report.md +
+ @_localizer["Error details"] +
+ @context +
+ @_localizer["Reload application"] +
+
+
+ +@code { + async Task ReloadApplication() + { + await _jsRuntime.InvokeVoidAsync("removeBeforeunload"); + _navigationManager.NavigateTo(_navigationManager.Uri, true); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayout.resx b/AudioCuesheetEditor/Shared/Layouts/MainLayout.resx new file mode 100644 index 00000000..5348a143 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayout.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + An error has occured in this application. Please report this error with as much details as possible here: + + + Error details + + + Reload application + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.de.resx b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.de.resx new file mode 100644 index 00000000..fc62d010 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.de.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Es ist ein Fehler in der Anwendung aufgetreten. Bitte melden Sie diesen Fehler unter Angabe von möglichst vielen Details hier: + + + Fehlerdetails + + + Applikation neu laden + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.razor b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.razor new file mode 100644 index 00000000..c3af3813 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.razor @@ -0,0 +1,65 @@ + + +@inherits BaseLocalizedLayoutComponentBase + +@inject NavigationManager _navigationManager +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsRuntime + +@* Required *@ + + + +@* Needed for dialogs *@ + + +@* Needed for snackbars *@ + + + + + + + + + @Body + + + + + + + @_localizer["An error has occured in this application. Please report this error with as much details as possible here:"]https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?&labels=bug&template=bug_report.md +
+ @_localizer["Error details"] +
+ @context +
+ @_localizer["Reload application"] +
+
+
+ +@code { + async Task ReloadApplication() + { + await _jsRuntime.InvokeVoidAsync("removeBeforeunload"); + _navigationManager.NavigateTo(_navigationManager.Uri, true); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.resx b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.resx new file mode 100644 index 00000000..5348a143 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Layouts/MainLayoutWithoutMenu.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + An error has occured in this application. Please report this error with as much details as possible here: + + + Error details + + + Reload application + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/MainLayout.razor b/AudioCuesheetEditor/Shared/MainLayout.razor deleted file mode 100644 index b1b270f1..00000000 --- a/AudioCuesheetEditor/Shared/MainLayout.razor +++ /dev/null @@ -1,914 +0,0 @@ - - -@inherits LayoutComponentBase - -@implements IAsyncDisposable - -@inject NavigationManager _navigationManager -@inject ITextLocalizer _localizer -@inject ITextLocalizerService _localizationService -@inject TraceChangeManager _traceChangeManager -@inject ILogger _logger -@inject IJSRuntime _jsRuntime -@inject HotKeys _hotKeys -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject SessionStateContainer _sessionStateContainer -@inject IBlazorDownloadFileService _blazorDownloadFileService -@inject ITextLocalizer _validationMessageLocalizer - - - - - - - - @if ((displayMenuBar) && (sidebar != null)) - { - //Just a little a hack for displaying a toggler for the sidebar always and independent from the breakpoint of the top bar - - - - } - - - - - - - AudioCuesheetEditor - - - @if (displayMenuBar) - { - - - - - - - - - - - - - - } - - - - - - - - - - - - - - @_localizer["Help"] - @_localizer["About"] - @_localizer["Preview environment"] - - - - - - - - @if (displayMenuBar) - { - - - - - - - - - - - - - @_localizer["ViewMode"] - - - @foreach (var name in Enum.GetNames(typeof(ViewMode))) - { - - - - } - - - - - - - - - - - @_localizer["Export"] - - - - - - - - - - - - - - - - - - - - - - - - - - @_localizer["Reset"] - - - - - - - - - - - - - - - - - - - - - - - - - - - } - - - @Body - - - - - - - - @_localizer["An error occured"] - @((MarkupString)_localizer["An error has occured in this application. Please report this error with as much details as possible here: https://github.com/NeoCoderMatrix86/AudioCuesheetEditor/issues/new?assignees=&labels=unreviewed+bug&template=bug_report.md&title=."]) - @_localizer["Error details"] - @context - - - - - - - -@if (applicationOptions != null) -{ - - - - - @_localizer["Save project"] - - - - - - - - @_localizer["Filename"] - - - - - - - - - - - - - - - - - -} - - - - @if (applicationOptions != null) - { - - - @_localizer["Filename"] - - - - - - - - - - } - - - - - - @if (exportOptions != null) - { - - @_localizer["Select exportprofile"] - - - - - - - - - - @_localizer["Name"] - - - - - - - - - - - - @_localizer["Filename"] - - - - - - - - - - - - - - @_localizer["Exportprofilescheme head"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableScheme in Exportprofile.AvailableCuesheetSchemes) - { - @_localizer[availableScheme.Key] - } - - - - - - - - - - - - @_localizer["Exportprofilescheme track"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableScheme in Exportprofile.AvailableTrackSchemes) - { - @_localizer[availableScheme.Key] - } - - - - - - - - - - - - @_localizer["Exportprofilescheme footer"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableScheme in Exportprofile.AvailableCuesheetSchemes) - { - @_localizer[availableScheme.Key] - } - - - - - - - - } - - - -@code { - Bar? sidebar; - Boolean displayMenuBar = false; - HotKeysContext? hotKeysContext; - ApplicationOptions? applicationOptions; - Boolean modalDownloadProjectfileVisible = false; - Boolean sidebarVisible = false; - - OptionsDialog? optionsDialog; - ModalDialog? modalDialog; - Modal? modalDownloadProjectfile; - ModalExportdialog? modalExportdialogCuesheet; - ModalExportdialog? modalExportdialogExportprofile; - ExportOptions? exportOptions; - - String? IsCuesheetExportableTooltip - { - get - { - var generator = new ExportfileGenerator(ExportType.Cuesheet, _sessionStateContainer.Cuesheet, applicationOptions: applicationOptions); - var validationResult = generator.Validate(); - if (validationResult.Status == Model.Entity.ValidationStatus.Error) - { - string? detailText = null; - if (validationResult.ValidationMessages != null) - { - foreach (var validationMessage in validationResult.ValidationMessages) - { - detailText += String.Format("{0}{1}", validationMessage.GetMessageLocalized(_validationMessageLocalizer), Environment.NewLine); - } - } - return _localizer["Please check processinghints for errors, otherwise the file is not exportable: {0}", detailText]; - } - return null; - } - } - - Boolean IsCuesheetExportable => IsCuesheetExportableTooltip == null; - - protected override async Task OnInitializedAsync() - { - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _traceChangeManager.TracedObjectHistoryChanged += TraceChangeManager_TracedObjectHistoryChanged; - _traceChangeManager.UndoDone += TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone += TraceChangeManager_RedoDone; - hotKeysContext = _hotKeys.CreateContext() - .Add(ModKey.Ctrl, Key.h, OnCtrlHKeyDown) - .Add(ModKey.Ctrl, Key.e, OnCtrlEKeyDown) - .Add(ModKey.Ctrl, Key.s, OnCtrlSKeyDown) - .Add(Key.Enter, OnEnterKeyDown); - - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - exportOptions = await _localStorageOptionsProvider.GetOptions(); - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; - - if (modalExportdialogExportprofile != null) - { - modalExportdialogExportprofile.GenerateExportfilesClicked += ModalExportdialogExportprofile_GenerateExportfilesClicked; - } - if (modalExportdialogCuesheet != null) - { - modalExportdialogCuesheet.GenerateExportfilesClicked += ModalExportdialogCuesheet_GenerateExportfilesClicked; - } - - _sessionStateContainer.CurrentViewMode = applicationOptions.ViewMode; - - await base.OnInitializedAsync(); - } - - public async ValueTask DisposeAsync() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; - _traceChangeManager.TracedObjectHistoryChanged -= TraceChangeManager_TracedObjectHistoryChanged; - _traceChangeManager.UndoDone -= TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone -= TraceChangeManager_RedoDone; - if (modalExportdialogExportprofile != null) - { - modalExportdialogExportprofile.GenerateExportfilesClicked -= ModalExportdialogExportprofile_GenerateExportfilesClicked; - } - if (modalExportdialogCuesheet != null) - { - modalExportdialogCuesheet.GenerateExportfilesClicked -= ModalExportdialogCuesheet_GenerateExportfilesClicked; - } - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - public void SetDisplayMenuBar(Boolean display) - { - if (displayMenuBar != display) - { - displayMenuBar = display; - StateHasChanged(); - } - } - - async Task OnDeleteAllTracksClicked() - { - _logger.LogInformation("OnDeleteAllTracksClicked"); - //Display a confirm warning - if (modalDialog != null) - { - modalDialog.Title = _localizer["Confirmation required"]; - modalDialog.Text = _localizer["Do you really want to delete all tracks? This can not be reversed."]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Confirm; - void deleteTracksDelegate(object? sender, EventArgs args) - { - _logger.LogInformation("deleteTracksDelegate"); - _sessionStateContainer.Cuesheet.RemoveTracks(_sessionStateContainer.Cuesheet.Tracks); - modalDialog.Confirmed -= deleteTracksDelegate; - StateHasChanged(); - }; - modalDialog.Confirmed += deleteTracksDelegate; - await modalDialog.ShowModal(); - } - } - - async Task RestartCuesheetClicked() - { - _logger.LogInformation("RestartCuesheetClicked clicked"); - //Display a confirm warning - if (modalDialog != null) - { - modalDialog.Title = _localizer["Confirmation required"]; - modalDialog.Text = _localizer["Do you really want to reset the cuesheet? This can not be reversed."]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Confirm; - void newCuesheetDelegate(object? sender, EventArgs args) - { - _logger.LogInformation("newCuesheetDelegate"); - _sessionStateContainer.Cuesheet = new Cuesheet(_traceChangeManager); - modalDialog.Confirmed -= newCuesheetDelegate; - StateHasChanged(); - }; - modalDialog.Confirmed += newCuesheetDelegate; - await modalDialog.ShowModal(); - } - } - - async Task OnRestartCompleteApplicationClicked() - { - _logger.LogInformation("OnRestartCompleteApplicationClicked"); - //Display a confirm warning - if (modalDialog != null) - { - modalDialog.Title = _localizer["Confirmation required"]; - modalDialog.Text = _localizer["Confirm restart of application. All unsaved changes are lost!"]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Confirm; - async void restartApplicationDelegate(object? sender, EventArgs args) - { - _logger.LogInformation("restartApplicationDelegate"); - modalDialog.Confirmed -= restartApplicationDelegate; - await ReloadApplication(); - }; - modalDialog.Confirmed += restartApplicationDelegate; - await modalDialog.ShowModal(); - } - } - - async Task OnResetCompleteApplicationClicked() - { - _logger.LogInformation("OnResetCompleteApplicationClicked"); - //Display a confirm warning - if (modalDialog != null) - { - modalDialog.Title = _localizer["Confirmation required"]; - modalDialog.Text = _localizer["Confirm reset of application. All unsaved changes are lost and the application is reloaded!"]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Confirmed += OnRestartApplicationConfirmed; - await modalDialog.ShowModal(); - } - } - - private async void OnRestartApplicationConfirmed(object? sender, EventArgs args) - { - _logger.LogInformation("OnRestartApplicationConfirmed"); - await _jsRuntime.InvokeVoidAsync("resetLocalStorage"); - await _jsRuntime.InvokeVoidAsync("removeBeforeunload"); - if (modalDialog != null) - { - modalDialog.Confirmed -= OnRestartApplicationConfirmed; - } - _navigationManager.NavigateTo(_navigationManager.Uri, true); - } - - async Task OnDisplayExportDialogClicked() - { - if (modalExportdialogCuesheet != null) - { - await modalExportdialogCuesheet.Show(); - } - } - - async Task OnDisplayExportProfilesClicked() - { - if (modalExportdialogExportprofile != null) - { - await modalExportdialogExportprofile.Show(); - } - } - - private async Task DownloadProjectfileClicked() - { - if (applicationOptions == null) - { - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - } - //Save ApplicationOptions with information from modal popup! - await _localStorageOptionsProvider.SaveOptions(applicationOptions); - var projectFile = new Projectfile(_sessionStateContainer.Cuesheet); - var fileData = projectFile.GenerateFile(); - await _blazorDownloadFileService.DownloadFile(applicationOptions.ProjectFilename, fileData, "text/plain"); - if (modalDownloadProjectfile != null) - { - await modalDownloadProjectfile.Hide(); - } - } - - async ValueTask OnEnterKeyDown() - { - if (modalDownloadProjectfileVisible) - { - await DownloadProjectfileClicked(); - } - } - - ValueTask OnCtrlHKeyDown() - { - if (ShortCutsEnabled) - { - _navigationManager.NavigateTo("Help"); - } - return ValueTask.CompletedTask; - } - - async ValueTask OnCtrlEKeyDown() - { - if (ShortCutsEnabled) - { - await OnDisplayExportProfilesClicked(); - } - } - - async ValueTask OnCtrlSKeyDown() - { - if ((ShortCutsEnabled) && (modalDownloadProjectfile != null)) - { - await modalDownloadProjectfile.Show(); - } - } - - private Task OnViewModeSelected(ViewMode selectedViewMode) - { - _sessionStateContainer.CurrentViewMode = selectedViewMode; - return Task.CompletedTask; - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void TraceChangeManager_TracedObjectHistoryChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void TraceChangeManager_UndoDone(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void TraceChangeManager_RedoDone(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void ModalDownloadProjectfile_VisibleChanged(Boolean visible) - { - modalDownloadProjectfileVisible = visible; - } - - async Task ControlModalDialog(Modal? dialog, Boolean show) - { - if (dialog != null) - { - if (show) - { - await dialog.Show(); - } - else - { - await dialog.Hide(); - } - } - } - - async Task OpenOptionsDialog(OptionsDialog? dialog) - { - if (dialog != null) - { - await dialog.Show(); - } - } - - void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions options) - { - if (options is ApplicationOptions) - { - applicationOptions = (ApplicationOptions)options; - } - if (options is ExportOptions) - { - exportOptions = (ExportOptions)options; - } - } - - void OnAddNewExportProfileClicked() - { - _logger.LogInformation("OnAddNewExportProfileClicked"); - if (exportOptions != null) - { - var newProfile = new Exportprofile(); - exportOptions.ExportProfiles.Add(newProfile); - exportOptions.SelectedExportProfile = newProfile; - } - Task.Run(SaveExportOptions); - } - - void OnDeleteExportProfileClicked() - { - _logger.LogInformation("OnDeleteExportProfileClicked"); - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.ExportProfiles.Remove(exportOptions.SelectedExportProfile); - exportOptions.SelectedExportProfile = exportOptions.ExportProfiles.LastOrDefault(); - } - Task.Run(SaveExportOptions); - } - - void SelectedExportProfileChanged(Guid? newValue) - { - _logger.LogInformation("SelectedExportProfileChanged with {0}", newValue); - if (exportOptions != null) - { - var selectedProfile = exportOptions.ExportProfiles.First(x => x.Id == newValue); - exportOptions.SelectedExportProfile = selectedProfile; - } - modalExportdialogExportprofile?.Validations?.ValidateAll().GetAwaiter().GetResult(); - modalExportdialogExportprofile?.Reset(); - Task.Run(SaveExportOptions); - StateHasChanged(); - } - - async Task SaveExportOptions() - { - _logger.LogDebug("SaveExportOptions"); - if (exportOptions != null) - { - await _localStorageOptionsProvider.SaveOptions(exportOptions); - } - } - - async Task SaveApplicationOptions() - { - _logger.LogDebug("SaveApplicationOptions"); - if (applicationOptions != null) - { - await _localStorageOptionsProvider.SaveOptions(applicationOptions); - } - } - - void ModalExportdialogExportprofile_GenerateExportfilesClicked(object? sender, EventArgs args) - { - Task.Run(SaveExportOptions); - } - - void ModalExportdialogCuesheet_GenerateExportfilesClicked(object? sender, EventArgs args) - { - Task.Run(SaveApplicationOptions); - } - - Task OnApplicationOptionsCuesheetFilenameChanged(string value) - { - if (applicationOptions != null) - { - applicationOptions.CuesheetFilename = value; - } - if (modalExportdialogCuesheet != null) - { - modalExportdialogCuesheet.Reset(); - } - return Task.CompletedTask; - } - - async Task OnSelectedProfileNameChanged(string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.Name = value; - } - modalExportdialogExportprofile?.Reset(); - await SaveExportOptions(); - } - - async Task OnSelectedExportProfileFilenameChanged(string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.Filename = value; - } - modalExportdialogExportprofile?.Reset(); - await SaveExportOptions(); - } - - async Task OnSelectedExportProfileSchemeHeadChanged(string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeHead = value; - } - modalExportdialogExportprofile?.Reset(); - await SaveExportOptions(); - } - - async Task OnSelectedExportProfileSchemeTracksChanged(string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeTracks = value; - } - modalExportdialogExportprofile?.Reset(); - await SaveExportOptions(); - } - - async Task OnSelectedExportProfileSchemeFooterChanged(string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeFooter = value; - } - modalExportdialogExportprofile?.Reset(); - await SaveExportOptions(); - } - - async Task DropDownItemSelected(string schemeName, string value) - { - if (exportOptions?.SelectedExportProfile != null) - { - switch (schemeName) - { - case nameof(Exportprofile.SchemeHead): - await OnSelectedExportProfileSchemeHeadChanged(exportOptions.SelectedExportProfile.SchemeHead += value); - break; - case nameof(Exportprofile.SchemeTracks): - await OnSelectedExportProfileSchemeTracksChanged(exportOptions.SelectedExportProfile.SchemeTracks += value); - break; - case nameof(Exportprofile.SchemeFooter): - await OnSelectedExportProfileSchemeFooterChanged(exportOptions.SelectedExportProfile.SchemeFooter += value); - break; - } - - } - } - - async Task ReloadApplication() - { - await _jsRuntime.InvokeVoidAsync("removeBeforeunload"); - _navigationManager.NavigateTo(_navigationManager.Uri, true); - } - - Task SideBarToogleClicked() - { - sidebarVisible = !sidebarVisible; - if (sidebar != null) - { - sidebar.OnBreakpoint(sidebarVisible); - } - return Task.CompletedTask; - } - - Boolean ShortCutsEnabled - { - get - { - return !(_navigationManager.Uri.EndsWith("/help", StringComparison.InvariantCultureIgnoreCase)) && !(_navigationManager.Uri.EndsWith("/about", StringComparison.InvariantCultureIgnoreCase)); - } - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ModalDialog.razor b/AudioCuesheetEditor/Shared/ModalDialog.razor deleted file mode 100644 index 8355b413..00000000 --- a/AudioCuesheetEditor/Shared/ModalDialog.razor +++ /dev/null @@ -1,189 +0,0 @@ - -@implements IAsyncDisposable - -@inject ITextLocalizer _localizer -@inject HotKeys _hotKeys -@inject ITextLocalizerService _localizationService - - - - - - @switch (Mode) - { - case DialogMode.Confirm: - - - - - break; - case DialogMode.Alert: - - - - - break; - } - @Title - - - - - @Text - - - @switch (Mode) - { - case DialogMode.Confirm: - - - break; - case DialogMode.Alert: - - break; - } - - - - -@code { - - public async ValueTask DisposeAsync() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - protected override Task OnInitializedAsync() - { - hotKeysContext = _hotKeys.CreateContext() - .Add(ModKey.Alt, Key.O, ConfirmedKeyDown) - .Add(Key.Escape, HideKeyDown); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - return base.OnInitializedAsync(); - } - - public enum DialogMode - { - Confirm, - Alert - } - - Modal? modalRef; - ModalSize modalSize = ModalSize.Default; - DialogMode mode = DialogMode.Confirm; - HotKeysContext? hotKeysContext; - Boolean modalRefVisible = false; - - public String Title = default!; - public String Text = default!; - - public event EventHandler Confirmed = default!; - - public async Task ShowModal() - { - if (modalRef != null) - { - await modalRef.Show(); - } - } - - public async Task HideModal() - { - if (modalRef != null) - { - await modalRef.Hide(); - } - //Reset all confirmed handlers - Confirmed = default!; - } - - public ModalSize ModalSize - { - get { return modalSize; } - set { modalSize = value; } - } - - public DialogMode Mode - { - get { return mode; } - set { mode = value; } - } - - public Boolean Visible - { - get => modalRefVisible; - } - - public async Task Confirm() - { - OnConfirmed(new EventArgs()); - await HideModal(); - } - - protected virtual void OnConfirmed(EventArgs args) - { - if (Confirmed != null) - { - Confirmed(this, args); - } - } - - async ValueTask ConfirmedKeyDown() - { - await Confirm(); - } - - async ValueTask HideKeyDown() - { - await HideModal(); - } - - private String? ModalHeaderStyle - { - get - { - String? headerStyle = null; - switch (Mode) - { - case DialogMode.Alert: - headerStyle = "alert-danger"; - break; - case DialogMode.Confirm: - break; - } - return headerStyle; - } - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - private void ModalRef_VisibleChanged(Boolean visible) - { - modalRefVisible = visible; - } -} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ModalExportdialog.razor b/AudioCuesheetEditor/Shared/ModalExportdialog.razor deleted file mode 100644 index fd83b9df..00000000 --- a/AudioCuesheetEditor/Shared/ModalExportdialog.razor +++ /dev/null @@ -1,284 +0,0 @@ - -@implements IAsyncDisposable - -@inject ITextLocalizer _localizer -@inject ILogger _logger -@inject ITextLocalizer _validationMessageLocalizer -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject SessionStateContainer _sessionStateContainer -@inject IBlazorDownloadFileService _blazorDownloadFileService -@inject HotKeys _hotKeys - - - - - - @Title - - - - - - - @_localizer["Prepare export"] - @_localizer["Result"] - - - - - @PrepareExportStepContent - - - - - - - @_localizer[nameof(Exportfile.Name)] - @_localizer[nameof(Exportfile.Begin)] - @_localizer[nameof(Exportfile.End)] - @_localizer[nameof(Exportfile.Content)] - - - - @if (exportfiles != null) - { - foreach (var exportfile in exportfiles) - { - - @exportfile.Name - @exportfile.Begin - @exportfile.End - - - - - - - } - } - -
-
-
-
-
- - @if (selectedStep != "displayExportResult") - { - - - - - } - else - { - - } - -
-
- -@code { - public event EventHandler? GenerateExportfilesClicked; - - [Parameter] - [EditorRequired] - public String? Title { get; set; } - - [Parameter] - [EditorRequired] - public RenderFragment? PrepareExportStepContent { get; set; } - - [Parameter] - [EditorRequired] - public ExportOptions? ExportOptions { get; set; } - - [Parameter] - [EditorRequired] - public ApplicationOptions? ApplicationOptions { get; set; } - - [Parameter] - [EditorRequired] - public ExportType ExportType { get; set; } - - public Boolean IsVisible { get; private set; } - public Validations? Validations { get; private set; } - - Modal? modalExportdialog; - String selectedStep = "prepareExport"; - IReadOnlyCollection? exportfiles; - Boolean prepareExportCompleted = false; - HotKeysContext? hotKeysContext; - Steps? stepsRef; - - Boolean StepNavigationAllowed - { - get - { - Boolean navigationAllowed = true; - if (Validations != null) - { - navigationAllowed = Validations.ValidateAll().GetAwaiter().GetResult(); - if (navigationAllowed) - { - navigationAllowed = ExportPossible; - } - if (navigationAllowed) - { - navigationAllowed = exportfiles != null; - } - } - return navigationAllowed; - } - } - - String? ExportPossibleTooltip - { - get - { - var generator = new ExportfileGenerator(ExportType, _sessionStateContainer.Cuesheet, ExportOptions?.SelectedExportProfile, ApplicationOptions); - var validationResult = generator.Validate(); - if (validationResult?.Status == Model.Entity.ValidationStatus.Error) - { - string? detailText = null; - if (validationResult.ValidationMessages != null) - { - foreach (var validationMessage in validationResult.ValidationMessages) - { - detailText += String.Format("{0}{1}", validationMessage.GetMessageLocalized(_validationMessageLocalizer), Environment.NewLine); - } - } - return _localizer["Export files can not be generated. Please check validationerrors and solve errors in order to download export: {0}", detailText]; - } - return null; - } - } - - Boolean ExportPossible - { - get - { - Boolean exportPossible = false; - if (Validations != null) - { - exportPossible = Validations.ValidateAll().GetAwaiter().GetResult(); - } - return exportPossible && ExportPossibleTooltip == null; - } - } - - protected override async Task OnInitializedAsync() - { - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEnterKeyDown); - - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; - - await base.OnInitializedAsync(); - } - - public async ValueTask DisposeAsync() - { - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - public async Task Show() - { - Reset(); - await ControlModalDialog(modalExportdialog, true); - } - - public void Reset() - { - prepareExportCompleted = false; - if (stepsRef != null) - { - stepsRef.SelectStep("prepareExport"); - } - exportfiles = null; - } - - async Task ControlModalDialog(Modal? dialog, Boolean show) - { - if (dialog != null) - { - if (show) - { - await dialog.Show(); - } - else - { - await dialog.Hide(); - } - } - } - - Task GenerateExportfiles_Clicked() - { - _logger.LogDebug("GenerateExportfiles_Clicked called"); - if (ExportPossible) - { - GenerateExportfilesClicked?.Invoke(this, EventArgs.Empty); - var generator = new ExportfileGenerator(ExportType, _sessionStateContainer.Cuesheet, ExportOptions?.SelectedExportProfile, ApplicationOptions); - exportfiles = generator.GenerateExportfiles(); - selectedStep = "displayExportResult"; - prepareExportCompleted = true; - } - return Task.CompletedTask; - } - - bool NavigationAllowed(StepNavigationContext context) - { - return StepNavigationAllowed; - } - - void ModalExportdialog_VisibleChanged(Boolean visible) - { - IsVisible = visible; - } - - async ValueTask OnEnterKeyDown() - { - if (IsVisible) - { - await GenerateExportfiles_Clicked(); - } - } - - void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions options) - { - if (options is ApplicationOptions) - { - ApplicationOptions = (ApplicationOptions)options; - } - if (options is ExportOptions) - { - ExportOptions = (ExportOptions)options; - } - } -} diff --git a/AudioCuesheetEditor/Shared/OptionsDialog.razor b/AudioCuesheetEditor/Shared/OptionsDialog.razor deleted file mode 100644 index a0491b87..00000000 --- a/AudioCuesheetEditor/Shared/OptionsDialog.razor +++ /dev/null @@ -1,339 +0,0 @@ - -@implements IAsyncDisposable - -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ITextLocalizer _localizer -@inject ILogger _logger -@inject IJSRuntime _jsRuntime -@inject NavigationManager _navigationManager -@inject HotKeys _hotKeys -@inject ITextLocalizerService _localizationService -@inject ITextLocalizer _validationMessageLocalizer - -@if (applicationOptions != null) -{ - - - - @_localizer["Options"] - - - - - - - - - @_localizer["Common settings"] - - - - - - - - @_localizer["Culture setting"] - - - - - - @_localizer["Default viewmode"] - - - - - - - @_localizer["Cuesheet filename"] - - - - - - - - - - - - @_localizer["Project filename"] - - - - - - - - - - - @_localizer["Automatically link tracks"] - - @_localizer["Automatically link tracks with previous"] - - - - - - - @_localizer["Customized timespan format"] - - - - - - - - - - - - - - - @_localizer["Select placeholder"] - - - @foreach (var availableFormat in TimeSpanFormat.AvailableTimespanScheme) - { - @_localizer[availableFormat.Key] - } - - - - - - - - - - - - - - - - - - - - - -} - -@code { - String selectedOptionsTab = "common"; - ApplicationOptions? applicationOptions; - Boolean saveOptions; - Boolean modalOptionsVisible = false; - - Modal? modalOptions; - HotKeysContext? hotKeysContext; - Validation? timespanformatValidation; - - public async ValueTask DisposeAsync() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; - if (hotKeysContext != null) - { - await hotKeysContext.DisposeAsync(); - } - } - - public async Task Show() - { - if (modalOptions != null) - { - await modalOptions.Show(); - } - saveOptions = false; - } - - protected override async Task OnInitializedAsync() - { - hotKeysContext = _hotKeys.CreateContext() - .Add(Key.Enter, OnEnterKeyDown); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; - - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - TimeSpanFormat.TextLocalizer = _localizer; - - await base.OnInitializedAsync(); - } - - async ValueTask OnEnterKeyDown() - { - if (modalOptionsVisible) - { - await OnSaveOptionsClicked(); - } - } - - private void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions options) - { - if (options is ApplicationOptions) - { - applicationOptions = (ApplicationOptions)options; - } - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - TimeSpanFormat.TextLocalizer = _localizer; - } - - private Task OnCultureSelectionChanged(String value) - { - if (applicationOptions != null) - { - applicationOptions.CultureName = value; - _localizationService.ChangeLanguage(applicationOptions.CultureName); - } - return Task.CompletedTask; - } - - async Task OnSaveOptionsClicked() - { - _logger.LogInformation("OnSaveOptionsClicked"); - saveOptions = true; - if (applicationOptions != null) - { - await _localStorageOptionsProvider.SaveOptions(applicationOptions); - if (modalOptions != null) - { - await modalOptions.Hide(); - } - } - } - - private async Task OnReloadOptionsClicked() - { - _logger.LogInformation("OnReloadOptionsClicked"); - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _localizationService.ChangeLanguage(applicationOptions.CultureName); - } - - private async Task OnResetOptionsClicked() - { - _logger.LogInformation("OnResetOptionsClicked"); - await _jsRuntime.InvokeVoidAsync("resetLocalStorage"); - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _navigationManager.NavigateTo(_navigationManager.Uri, true); - } - - async Task OnCloseClicked() - { - _logger.LogInformation("OnCloseClicked"); - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _localizationService.ChangeLanguage(applicationOptions.CultureName); - if (modalOptions != null) - { - await modalOptions.Hide(); - } - } - - private async Task OnModalClosed() - { - _logger.LogInformation("OnModalClosed"); - if (saveOptions == false) - { - //Reset the language if needed - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _localizationService.ChangeLanguage(applicationOptions.CultureName); - } - } - - private void ModalOptions_VisibleChanged(Boolean visible) - { - modalOptionsVisible = visible; - } - - Task OnTimespanformatChanged(string value) - { - if ((applicationOptions != null) && (String.IsNullOrEmpty(value) == false)) - { - if (applicationOptions.TimeSpanFormat == null) - { - applicationOptions.TimeSpanFormat = new TimeSpanFormat(); - applicationOptions.TimeSpanFormat.ValidateablePropertyChanged += Timespanformat_ValidateablePropertyChanged; - } - applicationOptions.TimeSpanFormat.Scheme = value; - } - else - { - if (applicationOptions != null) - { - if (applicationOptions.TimeSpanFormat != null) - { - applicationOptions.TimeSpanFormat.ValidateablePropertyChanged -= Timespanformat_ValidateablePropertyChanged; - } - applicationOptions.TimeSpanFormat = null; - } - } - return Task.CompletedTask; - } - - Task OnCustomizedTimeSpanFormatDropdownClicked(object value) - { - if ((applicationOptions != null) && (value != null)) - { - if (applicationOptions.TimeSpanFormat == null) - { - applicationOptions.TimeSpanFormat = new TimeSpanFormat(); - applicationOptions.TimeSpanFormat.ValidateablePropertyChanged += Timespanformat_ValidateablePropertyChanged; - } - applicationOptions.TimeSpanFormat.Scheme += value.ToString()?.Replace(TimeSpanFormat.EnterRegularExpressionHere, _localizer[TimeSpanFormat.EnterRegularExpressionHere]); - } - else - { - if (applicationOptions != null) - { - if (applicationOptions.TimeSpanFormat != null) - { - applicationOptions.TimeSpanFormat.ValidateablePropertyChanged -= Timespanformat_ValidateablePropertyChanged; - } - applicationOptions.TimeSpanFormat = null; - } - } - return Task.CompletedTask; - } - - void Timespanformat_ValidateablePropertyChanged(object? sender, String property) - { - if (timespanformatValidation != null) - { - timespanformatValidation.ValidateAsync().GetAwaiter().GetResult(); - } - } -} diff --git a/AudioCuesheetEditor/Shared/Record/AddTrack.de.resx b/AudioCuesheetEditor/Shared/Record/AddTrack.de.resx new file mode 100644 index 00000000..79b50652 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/AddTrack.de.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Titel hinzufügen + + + Künstler + + + Titel + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Record/AddTrack.razor b/AudioCuesheetEditor/Shared/Record/AddTrack.razor new file mode 100644 index 00000000..0b914962 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/AddTrack.razor @@ -0,0 +1,140 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject AutocompleteManager _autocompleteManager +@inject ValidationService _validationService + + + + + @{ + MusicBrainzArtist? autocompleteArtist = new() + { + Name = currentRecordingTrack.Artist + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Name, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Name + } + + + @{ + MusicBrainzTrack? autocompleteTrack = new() + { + Artist = currentRecordingTrack.Artist, + Title = currentRecordingTrack.Title + }; + } + + + @if (autocompleteContext.Disambiguation != null) + { + @String.Format("{0} ({1})", autocompleteContext.Title, autocompleteContext.Disambiguation) + } + else + { + @autocompleteContext.Title + } + + + + @_localizer["Add track"] + + + + + + + + +@code { + [CascadingParameter] + public Cuesheet? Cuesheet { get; set; } + + protected override void OnParametersSet() + { + if (Cuesheet != null) + { + Cuesheet.IsRecordingChanged -= Cuesheet_IsRecordingChanged; + } + base.OnParametersSet(); + if (Cuesheet != null) + { + Cuesheet.IsRecordingChanged += Cuesheet_IsRecordingChanged; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (Cuesheet != null) + { + Cuesheet.IsRecordingChanged -= Cuesheet_IsRecordingChanged; + } + } + + Track currentRecordingTrack = new Track(); + MudAutocomplete? artistInput; + MudAutocomplete? titleInput; + + String? GetValidationErrorMessage(object model, string propertyName) + { + String? validationErrorMessage = null; + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + return validationErrorMessage; + } + + async Task AddTrackAsync() + { + if (Cuesheet?.IsRecording == true) + { + Cuesheet?.AddTrack(currentRecordingTrack); + currentRecordingTrack = new(); + if (artistInput != null) + { + await artistInput.FocusAsync(); + } + } + } + + void Cuesheet_IsRecordingChanged(object? sender, EventArgs args) + { + StateHasChanged(); + artistInput?.ClearAsync(); + titleInput?.ClearAsync(); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Record/AddTrack.resx b/AudioCuesheetEditor/Shared/Record/AddTrack.resx new file mode 100644 index 00000000..0868c3e6 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/AddTrack.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Add track + + + Artist + + + Title + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Record/ControlRecording.de.resx b/AudioCuesheetEditor/Shared/Record/ControlRecording.de.resx new file mode 100644 index 00000000..a3a03be8 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/ControlRecording.de.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Eine Aufnahme ist bereits verfügbar! + + + Das Cuesheet enthält bereits Titel! + + + Aufgezeichnete Audiodatei herunterladen + + + Countdowntimer angeben + + + Die Aufnahme läuft bereits! + + + Aufnahme nicht möglich + + + Die Aufnahme startet in {0} Sekunden! + + + Aufnahme aktiv! + + + Aufnahme starten + + + Aufnahme stoppen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Record/ControlRecording.razor b/AudioCuesheetEditor/Shared/Record/ControlRecording.razor new file mode 100644 index 00000000..626c409e --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/ControlRecording.razor @@ -0,0 +1,163 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsRuntime +@inject IDialogService _dialogService +@inject IBlazorDownloadFileService _blazorDownloadFileService + +@if (Cuesheet?.IsRecording == false) +{ + var recordingPossibleMessages = Cuesheet?.IsRecordingPossible; + if (recordingPossibleMessages?.Any() == true) + { + + @_localizer["Record not possible"] + @foreach (var recordingPossibleMessage in recordingPossibleMessages) + { + @_localizer[recordingPossibleMessage] + } + + } +} + + + @if (Cuesheet?.IsRecording == true) + { + @_localizer["Recording!"] +
+ } + @if (startRecordTimer?.Enabled == true) + { + + @_localizer["Record will start in {0} seconds!", ((startRecordTimer.Interval / 1000) - (DateTime.Now - recordTimerStarted).Seconds)] + +
+ } + + + + @_localizer["Start recording"] + + + + + + @_localizer["Enter countdown timer"] + + + @if (Cuesheet?.RecordingStart.HasValue == true) + { + var recordingTime = DateTime.UtcNow - Cuesheet.RecordingStart; + recordingTime = recordingTime.Value.Subtract(new TimeSpan(0, 0, 0, 0, recordingTime.Value.Milliseconds)); + @recordingTime + } + else + { + @String.Format("--{0}--{1}--", CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator, CultureInfo.CurrentCulture.DateTimeFormat.TimeSeparator) + } + + @_localizer["Stop recording"] + + + + + + @if (Cuesheet?.Audiofile?.IsRecorded == true) + { +
+ + @_localizer["Download recorded audio file"] + + } +
+
+ +@code { + Timer updateGUITimer = new(500); + Timer? startRecordTimer; + DateTime recordTimerStarted; + + [CascadingParameter] + public Cuesheet? Cuesheet { get; set; } + + public Boolean StartRecordingDisabled => startRecordTimer?.Enabled == true || Cuesheet?.IsRecordingPossible.Any() == true; + + protected override void OnInitialized() + { + base.OnInitialized(); + updateGUITimer.AutoReset = true; + updateGUITimer.Elapsed += delegate + { + StateHasChanged(); + Boolean startRecordTimeEnabled = false; + if (startRecordTimer != null) + { + startRecordTimeEnabled = startRecordTimer.Enabled; + } + if ((startRecordTimeEnabled == false) && (Cuesheet?.IsRecording == false)) + { + updateGUITimer.Stop(); + } + }; + } + + async Task StartRecordingAsync() + { + Cuesheet?.StartRecording(); + updateGUITimer.Start(); + await _jsRuntime.InvokeVoidAsync("startAudioRecording"); + Cuesheet!.Audiofile = null; + } + + async Task StopRecordingAsync() + { + Cuesheet?.StopRecording(); + await _jsRuntime.InvokeVoidAsync("stopAudioRecording"); + } + + async Task DisplayStartCountdownDialog() + { + var options = new DialogOptions() { CloseOnEscapeKey = true, BackdropClick = false }; + var dialog = await _dialogService.ShowAsync(null, options); + var result = await dialog.Result; + if (result?.Canceled == false) + { + startRecordTimer = new Timer(ApplicationOptions!.RecordCountdownTimer * 1000); + startRecordTimer.Elapsed += async delegate + { + await StartRecordingAsync(); + startRecordTimer.Stop(); + startRecordTimer.Dispose(); + }; + startRecordTimer.Start(); + updateGUITimer.Start(); + recordTimerStarted = DateTime.Now; + } + } + + void DownloadAudio() + { + var audioFile = Cuesheet?.Audiofile; + if (audioFile != null) + { + _blazorDownloadFileService.DownloadFile(audioFile.Name, audioFile.ContentStream, audioFile.AudioCodec?.MimeType); + } + } +} diff --git a/AudioCuesheetEditor/Shared/Record/ControlRecording.resx b/AudioCuesheetEditor/Shared/Record/ControlRecording.resx new file mode 100644 index 00000000..00e67770 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Record/ControlRecording.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + A recording is already available! + + + Cuesheet already contains tracks! + + + Download recorded audio file + + + Enter countdown timer + + + Record is already running! + + + Record not possible + + + Record will start in {0} seconds! + + + Recording! + + + Start recording + + + Stop recording + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackLinkControl.razor b/AudioCuesheetEditor/Shared/TrackList/TrackLinkControl.razor deleted file mode 100644 index 40260a05..00000000 --- a/AudioCuesheetEditor/Shared/TrackList/TrackLinkControl.razor +++ /dev/null @@ -1,98 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject ITextLocalizerService _localizationService -@inject SessionStateContainer _sessionStateContainer - -@if ((TrackReference != Cuesheet?.Tracks.FirstOrDefault()) && (_sessionStateContainer.CurrentViewMode == ViewMode.ViewModeFull)) -{ - - - @if (TrackReference?.IsLinkedToPreviousTrack == true) - { - - } - else - { - - } - - -} - -@code { - [Parameter, EditorRequired] - public Boolean TrackSelectionVisible { get; set; } - - [Parameter, EditorRequired] - public Track? TrackReference { get; set; } - - public Cuesheet? Cuesheet - { - get - { - Cuesheet? cuesheet; - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeImport: - cuesheet = _sessionStateContainer.ImportCuesheet; - break; - default: - cuesheet = _sessionStateContainer.Cuesheet; - break; - } - return cuesheet; - } - } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - } - - protected override void OnInitialized() - { - base.OnInitialized(); - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void SetLink(Boolean linkState) - { - if (TrackReference != null) - { - TrackReference.IsLinkedToPreviousTrack = linkState; - } - } -} diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx new file mode 100644 index 00000000..0dcee2ab --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Validierungsfehler + + + Steuerelemente + + + Künstler + + + Titel + + + Beginn + + + Ende + + + Länge + + + Bemerkungen + + + Dieser Titel wird gerade abgespielt + + + Ein Abschnitt beginnt innerhalb dieses Titels + + + Wiedergabe dieses Titel starten + + + Diesen Titel duplizieren + + + Sind Sie sicher, dass Sie alle Titel entfernen möchten? + + + Bestätigen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index 2112939c..e4ac53c1 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -15,426 +15,292 @@ You should have received a copy of the GNU General Public License along with Foobar. If not, see . --> - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject SessionStateContainer _sessionStateContainer +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IStringLocalizer _validationMessageLocalizer +@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser +@inject AutocompleteManager _autocompleteManager +@inject EditTrackModalManager _editTrackModalManager +@inject ValidationService _validationService @inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject TraceChangeManager _traceChangeManager -@inject ILogger _logger -@inject ITextLocalizerService _localizationService -@inject ITextLocalizer _validationMessageLocalizer +@inject PlaybackService _playbackService +@inject IDialogService _dialogService - - @if (_sessionStateContainer.CurrentViewMode == ViewMode.ViewModeFull) +@if (CurrentViewMode == ViewMode.DetailView) +{ + var validationResult = Cuesheet?.Validate(nameof(Cuesheet.Tracks)); + if (validationResult?.Status == ValidationStatus.Error) { - var validationResult = Cuesheet?.Validate(x => x.Tracks); - - - @_localizer["Validation errors"] - - @if (validationResult?.ValidationMessages != null) + + @_localizer["Validation errors"] + @foreach (var message in validationResult.ValidationMessages) { - @foreach(var message in validationResult.ValidationMessages) - { - @message.GetMessageLocalized(_validationMessageLocalizer) - } + @message.GetMessageLocalized(_validationMessageLocalizer) } - - + } + +} - - - - @if ((_sessionStateContainer.CurrentViewMode == ViewMode.ViewModeFull) && (TrackSelectionVisible)) + + + - - - - -
-
- - - - -@code { - ModalDialog? modalDialog; - EditTrackModal? modalTrackEdit; - List selectedTracks = new(); - Validations? validations; - Boolean revalidate = false; - List TracksAttachedToValidateablePropertyChanged = new(); - ApplicationOptions? applicationOptions; - - [Parameter] - public AudioPlayer? AudioPlayer { get; set; } + @if (Cuesheet?.Tracks.FirstOrDefault() != context.Item) + { + + } + + + + - public Cuesheet? Cuesheet + + @if (contextMenuTrack != null) { - get - { - Cuesheet? cuesheet; - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeImport: - cuesheet = _sessionStateContainer.ImportCuesheet; - break; - default: - cuesheet = _sessionStateContainer.Cuesheet; - break; - } - return cuesheet; - } + @_localizer["Start playback of this track"] + @_localizer["Duplicate this track"] } + - public Boolean TrackSelectionVisible - { - get - { - if (applicationOptions != null) - { - return applicationOptions.TracksTableSelectionVisible; - } - return false; - } - set - { - _localStorageOptionsProvider.SaveOptionsValue(x => x.TracksTableSelectionVisible, value); - selectedTracks = new(); - } - } +@code { + HashSet selectedTracks = new(); + MudMenu? trackContextMenu; + Track? contextMenuTrack; - public Boolean PinnedTableHeader - { - get - { - if (applicationOptions != null) - { - return applicationOptions.TracksTableHeaderPinned; - } - return false; - } - set - { - _localStorageOptionsProvider.SaveOptionsValue(x => x.TracksTableHeaderPinned, value); - } - } + [CascadingParameter] + public ViewMode CurrentViewMode { get; set; } - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - _sessionStateContainer.Cuesheet.TracksRemoved -= Cuesheet_TracksRemoved; - _sessionStateContainer.Cuesheet.TracksAdded -= Cuesheet_TracksAdded; - _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; - DetachTrackFromValidateablePropertyChanged(); - DetachCuesheetFromSplitPointsAddedRemoved(); - } + [CascadingParameter] + public Cuesheet? Cuesheet { get; set; } - protected override async Task OnInitializedAsync() + protected override void Dispose(bool disposing) { - await base.OnInitializedAsync(); - - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - - _sessionStateContainer.Cuesheet.TracksAdded += Cuesheet_TracksAdded; - _sessionStateContainer.Cuesheet.TracksRemoved += Cuesheet_TracksRemoved; - - AttachTracksToValidateablePropertyChanged(); - AttachCuesheetToSplitPointsAddedRemoved(); - - applicationOptions = await _localStorageOptionsProvider.GetOptions(); - _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; + base.Dispose(disposing); + _playbackService.CurrentPositionChanged -= PlaybackService_CurrentPositionChanged; } - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override void OnInitialized() { - await base.OnAfterRenderAsync(firstRender); - _logger.LogDebug("OnAfterRenderAsync({firstRender})", firstRender); - if ((revalidate) && (validations != null)) - { - await validations.ValidateAll(); - revalidate = false; - } + base.OnInitialized(); + _playbackService.CurrentPositionChanged += PlaybackService_CurrentPositionChanged; } - void OnAddTrackClicked() + void AddTrackClicked() { - var newTrack = new Track(); - Cuesheet?.AddTrack(newTrack, applicationOptions); - _traceChangeManager.TraceChanges(newTrack); + var newTrack = new Track() + { + IsLinkedToPreviousTrack = ApplicationOptions!.LinkTracks + }; + Cuesheet?.AddTrack(newTrack); + TraceChangeManager.TraceChanges(newTrack); } async Task EditSelectedTracksClicked() { - if (modalTrackEdit != null) - { - modalTrackEdit.TracksToEdit = selectedTracks; - await modalTrackEdit.Show(); - } + await _editTrackModalManager.ShowAndHandleModalEditDialogAsync(selectedTracks); } - private void DeleteSelectedTracksClicked() + void DeleteSelectedTracksClicked() { - Cuesheet?.RemoveTracks(selectedTracks.AsReadOnly()); + Cuesheet?.RemoveTracks(selectedTracks.ToList()); selectedTracks.Clear(); } async Task OnDeleteAllTracksClicked() { - _logger.LogInformation("OnDeleteAllTracksClicked"); - //Display a confirm warning - if (modalDialog != null) + var parameters = new DialogParameters { - modalDialog.Title = _localizer["Confirmation required"]; - modalDialog.Text = _localizer["Do you really want to delete all tracks?"]; - modalDialog.ModalSize = ModalSize.Small; - modalDialog.Mode = ModalDialog.DialogMode.Confirm; - void deleteTracksDelegate(object? sender, EventArgs args) - { - _logger.LogInformation("deleteTracksDelegate"); - Cuesheet?.RemoveTracks(Cuesheet.Tracks); - selectedTracks.Clear(); - modalDialog.Confirmed -= deleteTracksDelegate; - StateHasChanged(); - }; - modalDialog.Confirmed += deleteTracksDelegate; - await modalDialog.ShowModal(); - } - } - - private String? GetLocalizedString(Boolean expressionToCheck, String localizedStringName) - { - if (expressionToCheck == true) - { - return _localizer[localizedStringName]; - } - else + { x => x.ConfirmText, _localizer["Are you sure you want to remove all tracks?"] }, + }; + var dialog = await _dialogService.ShowAsync(_localizer["Confirm"], parameters); + var result = await dialog.Result; + if (result?.Canceled == false) { - return null; + Cuesheet?.RemoveTracks(Cuesheet.Tracks); + selectedTracks.Clear(); } } - bool SelectAllIndeterminate - { - get => selectedTracks.Count > 0 && selectedTracks.Count < Cuesheet?.Tracks.Count; - } - - private void OnSelectAllTracks(bool select) + void TitleSelected(Track track, MusicBrainzTrack? musicBrainzTrack) { - if (select) + track.Title = musicBrainzTrack?.Title; + switch (CurrentViewMode) { - if (Cuesheet != null) - { - foreach (var track in Cuesheet.Tracks) + case ViewMode.DetailView: + case ViewMode.ImportView: + if ((String.IsNullOrEmpty(track.Artist)) && (String.IsNullOrEmpty(musicBrainzTrack?.Artist) == false)) { - if (!selectedTracks.Contains(track)) - { - selectedTracks.Add(track); - } + track.Artist = musicBrainzTrack.Artist; } - } - } - else - { - selectedTracks.Clear(); - } - } - - bool AllTracksSelected - { - get => selectedTracks.Count > 0 && selectedTracks.Count == Cuesheet?.Tracks.Count; - } - - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll(); - } - - private MarkupString GetMarkupString(String? stringValue) - { - MarkupString result = new MarkupString(String.Empty); - if (stringValue != null) - { - result = new MarkupString(stringValue); - } - return result; - } - - private void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - DetachTrackFromValidateablePropertyChanged(); - DetachCuesheetFromSplitPointsAddedRemoved(); - selectedTracks.Clear(); - StateHasChanged(); - AttachTracksToValidateablePropertyChanged(); - AttachCuesheetToSplitPointsAddedRemoved(); - } - - private void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) - { - // Unsubscribe to previous attached events - DetachCuesheetFromSplitPointsAddedRemoved(); - var tracks = TracksAttachedToValidateablePropertyChanged.Except(_sessionStateContainer.Cuesheet.Tracks); - for (int i = tracks.Count() - 1; i >= 0; i--) - { - var track = tracks.ElementAt(i); - DetachTrackFromValidateablePropertyChanged(track); - TracksAttachedToValidateablePropertyChanged.Remove(track); - } - // Reattach if needed - AttachTracksToValidateablePropertyChanged(); - AttachCuesheetToSplitPointsAddedRemoved(); - revalidate = true; - StateHasChanged(); - } - - void Cuesheet_TracksAdded(object? sender, TracksAddedRemovedEventArgs args) - { - StateHasChanged(); - AttachTracksToValidateablePropertyChanged(); - revalidate = true; - } - - void Cuesheet_TracksRemoved(object? sender, TracksAddedRemovedEventArgs args) - { - foreach(var track in args.Tracks) - { - DetachTrackFromValidateablePropertyChanged(track); + if ((track.Length.HasValue == false) && (musicBrainzTrack?.Length.HasValue == true)) + { + track.Length = musicBrainzTrack?.Length; + } + break; } - revalidate = true; } - void AttachTracksToValidateablePropertyChanged() + String? GetValidationErrorMessage(object model, string propertyName) { - if (Cuesheet != null) + String? validationErrorMessage = null; + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) { - foreach (var track in Cuesheet.Tracks) - { - if (TracksAttachedToValidateablePropertyChanged.Contains(track) == false) - { - track.ValidateablePropertyChanged += Track_ValidateablePropertyChanged; - TracksAttachedToValidateablePropertyChanged.Add(track); - } - } + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); } + return validationErrorMessage; } - void DetachTrackFromValidateablePropertyChanged(Track? track = null) + async Task CopyTrackClicked(Track? trackToCopy = null) { - if (track == null) + var trackThatWillBeCopied = trackToCopy; + if (trackThatWillBeCopied == null) { - foreach (var trackCurrentlyAttached in TracksAttachedToValidateablePropertyChanged) - { - trackCurrentlyAttached.ValidateablePropertyChanged -= Track_ValidateablePropertyChanged; - } + trackThatWillBeCopied = selectedTracks.FirstOrDefault(); } - else + if (trackThatWillBeCopied != null) { - track.ValidateablePropertyChanged -= Track_ValidateablePropertyChanged; + var copy = new Track(trackThatWillBeCopied); + Cuesheet?.AddTrack(copy); + TraceChangeManager.TraceChanges(copy); + await EditTrackModalClicked(copy); } } - void Track_ValidateablePropertyChanged(object? sender, string property) + async Task EditTrackModalClicked(Track trackToEdit) { - if (validations != null) - { - validations.ValidateAll().GetAwaiter().GetResult(); - } - StateHasChanged(); + await _editTrackModalManager.ShowAndHandleModalEditDialogAsync([trackToEdit]); } - void AttachCuesheetToSplitPointsAddedRemoved() + async Task OpenMenuContent(DataGridRowClickEventArgs args) { - if (Cuesheet != null) + contextMenuTrack = args.Item; + if (trackContextMenu != null) { - Cuesheet.SectionAdded += Cuesheet_SectionAdded; - Cuesheet.SectionRemoved += Cuesheet_SectionRemoved; + await trackContextMenu.OpenMenuAsync(args.MouseEventArgs); } } - void DetachCuesheetFromSplitPointsAddedRemoved() + String GetLinkedTrackIcon(Track track) { - if (Cuesheet != null) + if (track.IsLinkedToPreviousTrack) { - Cuesheet.SectionAdded -= Cuesheet_SectionAdded; - Cuesheet.SectionRemoved -= Cuesheet_SectionRemoved; + return @Icons.Material.Outlined.Height; } - } - - void Cuesheet_SectionAdded(object? sender, CuesheetSectionAddRemoveEventArgs args) - { - args.Section.ValidateablePropertyChanged += Section_ValidateablePropertyChanged; - } - - void Cuesheet_SectionRemoved(object? sender, CuesheetSectionAddRemoveEventArgs args) - { - args.Section.ValidateablePropertyChanged -= Section_ValidateablePropertyChanged; - } - - void Section_ValidateablePropertyChanged(object? sender, string property) - { - switch (property) + else { - case nameof(CuesheetSection.Begin): - case nameof(CuesheetSection.End): - StateHasChanged(); - break; + return @Icons.Material.Outlined.VerticalAlignCenter; } } - void LocalStorageOptionsProvider_OptionsSaved(object? sender, IOptions options) + void PlaybackService_CurrentPositionChanged() { - if (options is ApplicationOptions applicationOption) - { - applicationOptions = applicationOption; - StateHasChanged(); - } + StateHasChanged(); } -} +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx new file mode 100644 index 00000000..7c24b95a --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Validation errors + + + Controls + + + Artist + + + Title + + + Begin + + + End + + + Length + + + Remarks + + + Currently playing this track + + + A section is beginning inside this track + + + Start playback of this track + + + Duplicate this track + + + Are you sure you want to remove all tracks? + + + Confirm + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx new file mode 100644 index 00000000..3b992c4d --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Neuen Titel hinzufügen + + + Ausgewählten Titel kopieren + + + Alle Titel löschen + + + Ausgewählte Titel löschen + + + Auswahl für Titel einblenden + + + Ausgewählte Titel bearbeiten + + + Fester Tabellenkopf + + + Auswahl für Titel ausblenden + + + Ausgewählte Titel nach unten bewegen + + + Ausgewählte Titel nach oben bewegen + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor index f776f4a4..c2db613a 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor @@ -15,106 +15,57 @@ You should have received a copy of the GNU General Public License along with Foobar. If not, see . --> -@inject ITextLocalizer _localizer - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + + + + @_localizer["Edit selected tracks"] + + + + + + + + @_localizer["Copy selected track"] + + + + + + + + @_localizer["Delete selected tracks"] + + + + + + + + @_localizer["Delete all tracks"] + + + + + + + + + + + + + + + + + @code { [Parameter] @@ -123,39 +74,42 @@ along with Foobar. If not, see [Parameter] public EventCallback EditSelectedTracksClicked { get; set; } + [Parameter] + public Boolean EditSelectedTracksDisabled { get; set; } + + [Parameter] + public EventCallback CopySelectedTracksClicked { get; set; } + + [Parameter] + public Boolean CopySelectedTracksDisabled { get; set; } + [Parameter] public EventCallback DeleteSelectedTracksClicked { get; set; } + [Parameter] + public Boolean DeleteSelectedTracksDisabled { get; set; } + [Parameter] public EventCallback DeleteAllTracksClicked { get; set; } [Parameter] - public Boolean FixedTableHeader { get; set; } + public Boolean DeleteAllTracksDisabled { get; set; } + + [Parameter] + public EventCallback MoveTracksUpClicked { get; set; } + + [Parameter] + public Boolean MoveTracksUpDisabled { get; set; } [Parameter] - public EventCallback FixedTableHeaderChanged { get; set; } + public EventCallback MoveTracksDownClicked { get; set; } [Parameter] - public Boolean TrackSelectionVisible { get; set; } + public Boolean MoveTracksDownDisabled { get; set; } [Parameter] - public EventCallback TrackSelectionVisibleChanged { get; set; } + public Boolean FixedHeader { get; set; } [Parameter] - public IReadOnlyCollection? SelectedTracks { get; set; } - - String TooltipTextTrackSelectionVisible - { - get - { - if (TrackSelectionVisible) - { - return _localizer["Hide selection of tracks"]; - } - else - { - return _localizer["Display selection of tracks"]; - } - } - } + public EventCallback FixedHeaderClicked { get; set; } } diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx new file mode 100644 index 00000000..d2db673d --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Add new track + + + Copy selected tracks + + + Delete all tracks + + + Delete selected tracks + + + Display selection of tracks + + + Edit selected tracks + + + Fixed table header + + + Hide selection of tracks + + + Move selected tracks down + + + Move selected tracks up + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListItem.razor b/AudioCuesheetEditor/Shared/TrackList/TrackListItem.razor deleted file mode 100644 index b111092f..00000000 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListItem.razor +++ /dev/null @@ -1,301 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject SessionStateContainer _sessionStateContainer -@inject ITextLocalizer _validationMessageLocalizer -@inject TraceChangeManager _traceChangeManager -@inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser -@inject MusicBrainzDataProvider _musicBrainzDataProvider -@inject ITextLocalizerService _localizationService -@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@inject ILogger _logger - - - @if (Cuesheet != null) - { - - - - - - - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - - - - - @if (context.Item.Disambiguation != null) - { - @String.Format("{0} ({1})", context.Text, context.Item.Disambiguation) - } - else - { - @context.Text - } - - - - - @switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeRecord: - @track.Begin - @track.End - @track.Length - break; - case ViewMode.ViewModeFull: - case ViewMode.ViewModeImport: - -
- @if (Cuesheet?.GetSectionAtTrack(track) != null) - { - - - - - - - - } - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - break; - } -
-
- } -
- - - -@code { - EditTrackModal? modalTrackEdit; - Validations? validations; - IEnumerable? autocompleteTrackArtists; - IEnumerable? autocompleteTrackTitles; - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - _traceChangeManager.UndoDone -= TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone -= TraceChangeManager_RedoDone; - } - - [Parameter, EditorRequired] - public Boolean TrackSelectionVisible { get; set; } - - [Parameter, EditorRequired] - public IReadOnlyCollection? SelectedTracks { get; set; } - - [Parameter] - public EventCallback> SelectedTracksChanged { get; set; } - - [Parameter, EditorRequired] - public AudioPlayer? AudioPlayer { get; set; } - - public Cuesheet? Cuesheet - { - get - { - Cuesheet? cuesheet; - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeImport: - cuesheet = _sessionStateContainer.ImportCuesheet; - break; - default: - cuesheet = _sessionStateContainer.Cuesheet; - break; - } - return cuesheet; - } - } - - protected override void OnInitialized() - { - base.OnInitialized(); - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - _traceChangeManager.UndoDone += TraceChangeManager_UndoDone; - _traceChangeManager.RedoDone += TraceChangeManager_RedoDone; - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - validations?.ValidateAll(); - } - - String? GetLocalizedString(Boolean expressionToCheck, String localizedStringName) - { - if (expressionToCheck == true) - { - return _localizer[localizedStringName]; - } - else - { - return null; - } - } - - async Task EditTrackModal(Track trackToEdit) - { - if (modalTrackEdit != null) - { - modalTrackEdit.TracksToEdit = new List() { trackToEdit }; - await modalTrackEdit.Show(); - } - } - - async Task CopyTrackClicked(Track trackToCopy) - { - var copy = new Track(trackToCopy); - var applicationOptions = await _localStorageOptionsProvider.GetOptions(); - Cuesheet?.AddTrack(copy, applicationOptions); - _traceChangeManager.TraceChanges(copy); - await EditTrackModal(copy); - } - - void SelectedTrackChanged(Track track, bool selected) - { - var selectedTracks = new List(); - if (SelectedTracks != null) - { - selectedTracks.AddRange(SelectedTracks); - } - if (selected) - { - selectedTracks.Add(track); - } - else - { - selectedTracks.Remove(track); - } - SelectedTracksChanged.InvokeAsync(selectedTracks); - } - - async Task OnReadDataAutocompleteTrackArtist(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var artists = await _musicBrainzDataProvider.SearchArtistAsync(autocompleteReadDataEventArgs.SearchValue); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteTrackArtists = artists; - } - } - } - - async Task OnReadDataAutocompleteTrackTitle(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs, Track track) - { - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - var titles = await _musicBrainzDataProvider.SearchTitleAsync(autocompleteReadDataEventArgs.SearchValue, track.Artist); - if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested) - { - autocompleteTrackTitles = titles; - } - } - } - - async Task OnSelectedValueChangedTrackTitle(Guid selectedValue, Track track) - { - switch (_sessionStateContainer.CurrentViewMode) - { - case ViewMode.ViewModeFull: - case ViewMode.ViewModeImport: - var trackDetails = await _musicBrainzDataProvider.GetDetailsAsync(selectedValue); - if (trackDetails != null) - { - track.Length = trackDetails.Length; - if (String.IsNullOrEmpty(track.Artist)) - { - track.Artist = trackDetails.Artist; - } - } - break; - } - } - - void TrackDeleted(Track track) - { - var selectedTracks = new List(); - if (SelectedTracks != null) - { - selectedTracks.AddRange(SelectedTracks); - } - selectedTracks.Remove(track); - SelectedTracksChanged.InvokeAsync(selectedTracks); - } - - void TraceChangeManager_UndoDone(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void TraceChangeManager_RedoDone(object? sender, EventArgs args) - { - StateHasChanged(); - } -} diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListItemControlColumn.razor b/AudioCuesheetEditor/Shared/TrackList/TrackListItemControlColumn.razor deleted file mode 100644 index 59c0418d..00000000 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListItemControlColumn.razor +++ /dev/null @@ -1,210 +0,0 @@ - -@implements IDisposable - -@inject ITextLocalizer _localizer -@inject ITextLocalizerService _localizationService - -@if (Visible) -{ - - - - - -} - -@code { - [Parameter, EditorRequired] - public Boolean Visible { get; set; } - - [Parameter, EditorRequired] - public bool Selected { get; set; } - - [Parameter, EditorRequired] - public EventCallback SelectedChanged { get; set; } - - public void Dispose() - { - _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; - } - - protected override void OnInitialized() - { - base.OnInitialized(); - _localizationService.LocalizationChanged += LocalizationService_LocalizationChanged; - } - - void LocalizationService_LocalizationChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } -} diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.de.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.de.resx new file mode 100644 index 00000000..75010786 --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.de.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allgemeine Informationen + + + Abschnitte + + + Titel + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.razor b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.razor new file mode 100644 index 00000000..79780edb --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.razor @@ -0,0 +1,55 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer + + + + + @_localizer["Sections"] + + + + + + + + @_localizer["Common data"] + + + + + + + + @_localizer["Tracks"] + + + + + + + + + +@code { + Boolean cuesheetSectionsExpanded = false; + Boolean cuesheetDataExpanded = true; + Boolean cuesheetTracksExpanded = true; +} diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.resx new file mode 100644 index 00000000..e9f5681f --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeFull.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Common data + + + Sections + + + Tracks + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.de.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.de.resx new file mode 100644 index 00000000..9b5df825 --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.de.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allgemeine Informationen + + + Fehler während des Textimports + + + Dateiinhalt + + + Eingabedateien auswählen + + + Titel + + + Validieren + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor new file mode 100644 index 00000000..3c97e6b6 --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor @@ -0,0 +1,114 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject ISessionStateContainer _sessionStateContainer +@inject ImportManager _importManager + + + + + + + + + + @_localizer["Filecontent"] + + + + + @if (_sessionStateContainer.Importfile?.AnalyseException != null) + { + + @_localizer["Error during textimport"] : @_sessionStateContainer.Importfile.AnalyseException.Message + + } + + + + + @_localizer["Common data"] + + + + + + + + @_localizer["Tracks"] + + + + + + + + + +@code { + int activeStepIndex; + Boolean fileContentExpanded = false, cuesheetDataExpanded = false, cuesheetTracksExpanded = false; + Boolean selectFilesStepCompleted = false, selectFilesStepError = false; + + void ImportFileContent_FileContentChanged(string newFileContent) + { + var textToAnalyse = newFileContent.Split(Environment.NewLine); + _importManager.ImportText(textToAnalyse, ApplicationOptions!.ImportScheme, ApplicationOptions!.ImportTimeSpanFormat); + } + + void ReanalyseImportfile() + { + var fileContent = _sessionStateContainer.Importfile?.FileContent; + if (fileContent != null) + { + _importManager.ImportText(fileContent, ApplicationOptions!.ImportScheme, ApplicationOptions.ImportTimeSpanFormat); + } + } + + void FilesImported(Dictionary files) + { + fileContentExpanded = true; + cuesheetDataExpanded = true; + cuesheetTracksExpanded = true; + activeStepIndex = 1; + selectFilesStepCompleted = true; + selectFilesStepError = false; + } + + async Task CompleteImportAsync() + { + await LocalStorageOptionsProvider.SaveOptionsValue(x => x.ActiveTab, ViewMode.DetailView); + _importManager.ImportCuesheet(); + } + + Task PreviewInteraction(StepperInteractionEventArgs arg) + { + if (arg.StepIndex == 0) + { + arg.Cancel = (selectFilesStepCompleted == false) || (selectFilesStepError == true); + } + return Task.CompletedTask; + } + + void InvalidFilesDropped(List files) + { + selectFilesStepError = files.Any(); + } +} diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.resx new file mode 100644 index 00000000..5561d972 --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Common data + + + Error during textimport + + + Filecontent + + + Select inputfiles + + + Tracks + + + Validate + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.de.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.de.resx new file mode 100644 index 00000000..c482082c --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.de.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allgemeine Informationen + + + Titel + + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor new file mode 100644 index 00000000..13c728ad --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor @@ -0,0 +1,79 @@ + +@inherits BaseLocalizedComponent + +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsRuntime +@inject ISessionStateContainer _sessionStateContainer +@inject FileInputManager _fileInputManager + + +
+ +
+ + + + @_localizer["Tracks"] + + + + + + + + @_localizer["Common data"] + + + + + + + +@code { + Boolean cuesheetDataExpanded = false, tracksExpanded = true; + + [CascadingParameter] + public Cuesheet? Cuesheet { get; set; } + + [JSInvokable()] + public Task AudioRecordingFinished(String objectUrl) + { + _sessionStateContainer.Cuesheet.Audiofile = _fileInputManager.CreateRecordedAudiofile(objectUrl, x => + { + Cuesheet?.RecalculateLastTrackEnd(); + TraceChangeManager.MergeLastEditWithEdit(x => x.Changes.All(y => y.TraceableObject == Cuesheet && y.TraceableChange.PropertyName == nameof(Audiofile))); + }); + StateHasChanged(); + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _jsRuntime.InvokeVoidAsync("closeAudioRecording"); + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + var dotNetReference = DotNetObjectReference.Create(this); + await _jsRuntime.InvokeVoidAsync("GLOBAL.SetViewModeRecordReference", dotNetReference); + await _jsRuntime.InvokeVoidAsync("setupAudioRecording"); + } +} diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.resx b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.resx new file mode 100644 index 00000000..6c53c579 --- /dev/null +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Common data + + + Tracks + + \ No newline at end of file diff --git a/AudioCuesheetEditor/_Imports.razor b/AudioCuesheetEditor/_Imports.razor index 61538055..4cd9df9e 100644 --- a/AudioCuesheetEditor/_Imports.razor +++ b/AudioCuesheetEditor/_Imports.razor @@ -12,7 +12,15 @@ @using AudioCuesheetEditor @using AudioCuesheetEditor.Pages @using AudioCuesheetEditor.Shared +@using AudioCuesheetEditor.Shared.Dialogs +@using AudioCuesheetEditor.Shared.ViewModes +@using AudioCuesheetEditor.Shared.Layouts @using AudioCuesheetEditor.Shared.TrackList +@using AudioCuesheetEditor.Shared.Cuesheet +@using AudioCuesheetEditor.Shared.Inputs +@using AudioCuesheetEditor.Shared.Import +@using AudioCuesheetEditor.Shared.Audio +@using AudioCuesheetEditor.Shared.Record @using AudioCuesheetEditor.Model.AudioCuesheet @using AudioCuesheetEditor.Model.IO @using AudioCuesheetEditor.Model.IO.Export @@ -27,11 +35,12 @@ @using AudioCuesheetEditor.Data.Services @using AudioCuesheetEditor.Services.IO @using AudioCuesheetEditor.Services.UI +@using AudioCuesheetEditor.Services.Validation +@using AudioCuesheetEditor.Services.Audio @using Microsoft.Extensions.Logging -@using Blazorise -@using Blazorise.Components -@using Blazorise.Localization +@using Microsoft.Extensions.Localization @using BlazorDownloadFile @using Howler.Blazor.Components @using Markdig -@using Toolbelt.Blazor.HotKeys2 \ No newline at end of file +@using Toolbelt.Blazor.HotKeys2 +@using MudBlazor \ No newline at end of file diff --git a/AudioCuesheetEditor/wwwroot/css/app.css b/AudioCuesheetEditor/wwwroot/css/app.css index afc7f2b5..33d6bb99 100644 --- a/AudioCuesheetEditor/wwwroot/css/app.css +++ b/AudioCuesheetEditor/wwwroot/css/app.css @@ -12,12 +12,6 @@ a, .btn-link { color: #0071c1; } -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - .content { padding-top: 1.1rem; } @@ -30,10 +24,6 @@ a, .btn-link { outline: 1px solid red; } -.validation-message { - color: red; -} - #blazor-error-ui { background: lightyellow; bottom: 0; @@ -46,12 +36,12 @@ a, .btn-link { z-index: 1000; } - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; @@ -59,70 +49,6 @@ a, .btn-link { color: white; } - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.dragNDropFile { - outline: 2px dashed #92b0b3; - outline-offset: -10px; - -webkit-transition: outline-offset .15s ease-in-out, background-color .15s linear; - transition: outline-offset .15s ease-in-out, background-color .15s linear; - font-size: 1.25rem; - background-color: #f7f7f7; - position: relative; - padding: 50px 20px; - text-align: center; -} - - .dragNDropFile.is-dragover { - background-color: lightskyblue; - cursor: move; - } - -.dragNDropFile > .input-group { - width: 0.1px; - height: 0.1px; - opacity: 0; - overflow: hidden; - position: absolute; - z-index: -1; -} - -.dragNDropFile > .form-label { - max-width: 80%; - text-overflow: ellipsis; - white-space: nowrap; - cursor: pointer; - display: inline-block; - overflow: hidden; - } - -.dragNDropFile .dragNDropFileIcon { - width: 100%; - height: 80px; - fill: #92b0b3; - display: block; - margin-bottom: 40px; -} - -.BackgroundBlink { - -webkit-animation: BackgroundBlinkAnimation 2s infinite; - -moz-animation: BackgroundBlinkAnimation 2s infinite; - animation: BackgroundBlinkAnimation 2s infinite; -} - -@keyframes BackgroundBlinkAnimation { - 0% { - background-color: red; - } - - 100% { - background-color: palevioletred; - } -} - -.accordion-button { - color:inherit!important; - background-color: rgba(0,0,0,.03)!important; +.blazor-error-boundary::after { + content: "An error has occurred." } \ No newline at end of file diff --git a/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css b/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css deleted file mode 100644 index 27ce22b8..00000000 --- a/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.1.1 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css.map b/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css.map deleted file mode 100644 index 52ed1968..00000000 --- a/AudioCuesheetEditor/wwwroot/css/bootstrap/bootstrap.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"iBAAA;;;;;ACAA,MAQI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EACA,oBAAA,EAAA,CAAA,EAAA,CAAA,GACA,iBAAA,GAAA,CAAA,GAAA,CAAA,IAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAQA,sBAAA,0BACA,oBAAA,KACA,sBAAA,IACA,sBAAA,IACA,gBAAA,QAIA,aAAA,KCnCF,ECgDA,QADA,SD5CE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BEmPI,UAAA,yBFjPJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YAUF,GACE,OAAA,KAAA,EACA,MAAA,QACA,iBAAA,aACA,OAAA,EACA,QAAA,IAGF,eACE,OAAA,IAUF,IAAA,IAAA,IAAA,IAAA,IAAA,IAAA,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,IAAA,GEwMQ,UAAA,uBAlKJ,0BFtCJ,IAAA,GE+MQ,UAAA,QF1MR,IAAA,GEmMQ,UAAA,sBAlKJ,0BFjCJ,IAAA,GE0MQ,UAAA,MFrMR,IAAA,GE8LQ,UAAA,oBAlKJ,0BF5BJ,IAAA,GEqMQ,UAAA,SFhMR,IAAA,GEyLQ,UAAA,sBAlKJ,0BFvBJ,IAAA,GEgMQ,UAAA,QF3LR,IAAA,GEgLM,UAAA,QF3KN,IAAA,GE2KM,UAAA,KFhKN,EACE,WAAA,EACA,cAAA,KCoBF,6BDTA,YAEE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GCKA,GDHE,aAAA,KCSF,GDNA,GCKA,GDFE,WAAA,EACA,cAAA,KAGF,MCMA,MACA,MAFA,MDDE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECLA,ODOE,YAAA,OAQF,OAAA,ME4EM,UAAA,OFrEN,MAAA,KACE,QAAA,KACA,iBAAA,QASF,ICnBA,IDqBE,SAAA,SEwDI,UAAA,MFtDJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,QACA,gBAAA,UAEA,QACE,MAAA,QAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KCvBJ,KACA,ID6BA,IC5BA,KDgCE,YAAA,yBEcI,UAAA,IFZJ,UAAA,IACA,aAAA,cAOF,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KEAI,UAAA,OFKJ,SELI,UAAA,QFOF,MAAA,QACA,WAAA,OAIJ,KEZM,UAAA,OFcJ,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,MAAA,MExBI,UAAA,OF0BJ,MAAA,KACA,iBAAA,QG7SE,cAAA,MHgTF,QACE,QAAA,EE/BE,UAAA,IFiCF,YAAA,IASJ,OACE,OAAA,EAAA,EAAA,KAMF,IChDA,IDkDE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBCvDF,MAGA,GAFA,MAGA,GDsDA,MCxDA,GD8DE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECrEF,OD0EA,MCxEA,SADA,OAEA,SD4EE,OAAA,EACA,YAAA,QE9HI,UAAA,QFgIJ,YAAA,QAIF,OC3EA,OD6EE,eAAA,KAKF,cACE,OAAA,QAGF,OAGE,UAAA,OAGA,gBACE,QAAA,EAOJ,0CACE,QAAA,KCjFF,cACA,aACA,cDuFA,OAIE,mBAAA,OCvFF,6BACA,4BACA,6BDwFI,sBACE,OAAA,QAON,mBACE,QAAA,EACA,aAAA,KAKF,SACE,OAAA,SAUF,SACE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAQF,OACE,MAAA,KACA,MAAA,KACA,QAAA,EACA,cAAA,MEnNM,UAAA,sBFsNN,YAAA,QExXE,0BFiXJ,OExMQ,UAAA,QFiNN,SACE,MAAA,KC/FJ,kCDsGA,uCCvGA,mCADA,+BAGA,oCAJA,6BAKA,mCD2GE,QAAA,EAGF,4BACE,OAAA,KASF,cACE,eAAA,KACA,mBAAA,UAmBF,4BACE,mBAAA,KAKF,+BACE,QAAA,EAMF,uBACE,KAAA,QAMF,6BACE,KAAA,QACA,mBAAA,OAKF,OACE,QAAA,aAKF,OACE,OAAA,EAOF,QACE,QAAA,UACA,OAAA,QAQF,SACE,eAAA,SAQF,SACE,QAAA,eInlBF,MFyQM,UAAA,QEvQJ,YAAA,IAKA,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,ME7QN,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,QE7QN,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,ME7QN,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,QE7QN,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,ME7QN,WFsQM,UAAA,uBEpQJ,YAAA,IACA,YAAA,IFiGA,0BEpGF,WF6QM,UAAA,QEvPR,eCrDE,aAAA,EACA,WAAA,KDyDF,aC1DE,aAAA,EACA,WAAA,KD4DF,kBACE,QAAA,aAEA,mCACE,aAAA,MAUJ,YFsNM,UAAA,OEpNJ,eAAA,UAIF,YACE,cAAA,KF+MI,UAAA,QE5MJ,wBACE,cAAA,EAIJ,mBACE,WAAA,MACA,cAAA,KFqMI,UAAA,OEnMJ,MAAA,QAEA,2BACE,QAAA,KE9FJ,WCIE,UAAA,KAGA,OAAA,KDDF,eACE,QAAA,OACA,iBAAA,KACA,OAAA,IAAA,MAAA,QHGE,cAAA,OIRF,UAAA,KAGA,OAAA,KDcF,QAEE,QAAA,aAGF,YACE,cAAA,MACA,YAAA,EAGF,gBJ+PM,UAAA,OI7PJ,MAAA,QElCA,WPsmBF,iBAGA,cACA,cACA,cAHA,cADA,eQ1mBE,MAAA,KACA,cAAA,0BACA,aAAA,0BACA,aAAA,KACA,YAAA,KCwDE,yBF5CE,WAAA,cACE,UAAA,OE2CJ,yBF5CE,WAAA,cAAA,cACE,UAAA,OE2CJ,yBF5CE,WAAA,cAAA,cAAA,cACE,UAAA,OE2CJ,0BF5CE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QE2CJ,0BF5CE,WAAA,cAAA,cAAA,cAAA,cAAA,eACE,UAAA,QGfN,KCAA,cAAA,OACA,cAAA,EACA,QAAA,KACA,UAAA,KACA,WAAA,8BACA,aAAA,+BACA,YAAA,+BDHE,OCYF,YAAA,EACA,MAAA,KACA,UAAA,KACA,cAAA,8BACA,aAAA,8BACA,WAAA,mBA+CI,KACE,KAAA,EAAA,EAAA,GAGF,iBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,cACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,UAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,UAxDV,YAAA,YAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,WAxDV,YAAA,aAwDU,WAxDV,YAAA,aAmEM,KXwsBR,MWtsBU,cAAA,EAGF,KXwsBR,MWtsBU,cAAA,EAPF,KXktBR,MWhtBU,cAAA,QAGF,KXktBR,MWhtBU,cAAA,QAPF,KX4tBR,MW1tBU,cAAA,OAGF,KX4tBR,MW1tBU,cAAA,OAPF,KXsuBR,MWpuBU,cAAA,KAGF,KXsuBR,MWpuBU,cAAA,KAPF,KXgvBR,MW9uBU,cAAA,OAGF,KXgvBR,MW9uBU,cAAA,OAPF,KX0vBR,MWxvBU,cAAA,KAGF,KX0vBR,MWxvBU,cAAA,KFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QX65BR,SW35BU,cAAA,EAGF,QX65BR,SW35BU,cAAA,EAPF,QXu6BR,SWr6BU,cAAA,QAGF,QXu6BR,SWr6BU,cAAA,QAPF,QXi7BR,SW/6BU,cAAA,OAGF,QXi7BR,SW/6BU,cAAA,OAPF,QX27BR,SWz7BU,cAAA,KAGF,QX27BR,SWz7BU,cAAA,KAPF,QXq8BR,SWn8BU,cAAA,OAGF,QXq8BR,SWn8BU,cAAA,OAPF,QX+8BR,SW78BU,cAAA,KAGF,QX+8BR,SW78BU,cAAA,MFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QXknCR,SWhnCU,cAAA,EAGF,QXknCR,SWhnCU,cAAA,EAPF,QX4nCR,SW1nCU,cAAA,QAGF,QX4nCR,SW1nCU,cAAA,QAPF,QXsoCR,SWpoCU,cAAA,OAGF,QXsoCR,SWpoCU,cAAA,OAPF,QXgpCR,SW9oCU,cAAA,KAGF,QXgpCR,SW9oCU,cAAA,KAPF,QX0pCR,SWxpCU,cAAA,OAGF,QX0pCR,SWxpCU,cAAA,OAPF,QXoqCR,SWlqCU,cAAA,KAGF,QXoqCR,SWlqCU,cAAA,MFzDN,yBESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QXu0CR,SWr0CU,cAAA,EAGF,QXu0CR,SWr0CU,cAAA,EAPF,QXi1CR,SW/0CU,cAAA,QAGF,QXi1CR,SW/0CU,cAAA,QAPF,QX21CR,SWz1CU,cAAA,OAGF,QX21CR,SWz1CU,cAAA,OAPF,QXq2CR,SWn2CU,cAAA,KAGF,QXq2CR,SWn2CU,cAAA,KAPF,QX+2CR,SW72CU,cAAA,OAGF,QX+2CR,SW72CU,cAAA,OAPF,QXy3CR,SWv3CU,cAAA,KAGF,QXy3CR,SWv3CU,cAAA,MFzDN,0BESE,QACE,KAAA,EAAA,EAAA,GAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QX4hDR,SW1hDU,cAAA,EAGF,QX4hDR,SW1hDU,cAAA,EAPF,QXsiDR,SWpiDU,cAAA,QAGF,QXsiDR,SWpiDU,cAAA,QAPF,QXgjDR,SW9iDU,cAAA,OAGF,QXgjDR,SW9iDU,cAAA,OAPF,QX0jDR,SWxjDU,cAAA,KAGF,QX0jDR,SWxjDU,cAAA,KAPF,QXokDR,SWlkDU,cAAA,OAGF,QXokDR,SWlkDU,cAAA,OAPF,QX8kDR,SW5kDU,cAAA,KAGF,QX8kDR,SW5kDU,cAAA,MFzDN,0BESE,SACE,KAAA,EAAA,EAAA,GAGF,qBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,eAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,eA+BE,cAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,cAxDV,YAAA,EAwDU,cAxDV,YAAA,YAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,eAxDV,YAAA,aAwDU,eAxDV,YAAA,aAmEM,SXivDR,UW/uDU,cAAA,EAGF,SXivDR,UW/uDU,cAAA,EAPF,SX2vDR,UWzvDU,cAAA,QAGF,SX2vDR,UWzvDU,cAAA,QAPF,SXqwDR,UWnwDU,cAAA,OAGF,SXqwDR,UWnwDU,cAAA,OAPF,SX+wDR,UW7wDU,cAAA,KAGF,SX+wDR,UW7wDU,cAAA,KAPF,SXyxDR,UWvxDU,cAAA,OAGF,SXyxDR,UWvxDU,cAAA,OAPF,SXmyDR,UWjyDU,cAAA,KAGF,SXmyDR,UWjyDU,cAAA,MCpHV,OACE,cAAA,YACA,qBAAA,YACA,yBAAA,QACA,sBAAA,oBACA,wBAAA,QACA,qBAAA,mBACA,uBAAA,QACA,oBAAA,qBAEA,MAAA,KACA,cAAA,KACA,MAAA,QACA,eAAA,IACA,aAAA,QAOA,yBACE,QAAA,MAAA,MACA,iBAAA,mBACA,oBAAA,IACA,WAAA,MAAA,EAAA,EAAA,EAAA,OAAA,0BAGF,aACE,eAAA,QAGF,aACE,eAAA,OAIF,uCACE,oBAAA,aASJ,aACE,aAAA,IAUA,4BACE,QAAA,OAAA,OAeF,gCACE,aAAA,IAAA,EAGA,kCACE,aAAA,EAAA,IAOJ,oCACE,oBAAA,EASF,yCACE,qBAAA,2BACA,MAAA,8BAQJ,cACE,qBAAA,0BACA,MAAA,6BAQA,4BACE,qBAAA,yBACA,MAAA,4BCxHF,eAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,iBAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,eAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,YAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,eAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,cAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,aAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QAfF,YAME,cAAA,QACA,sBAAA,QACA,yBAAA,KACA,qBAAA,QACA,wBAAA,KACA,oBAAA,QACA,uBAAA,KAEA,MAAA,KACA,aAAA,QDgIA,kBACE,WAAA,KACA,2BAAA,MHvEF,4BGqEA,qBACE,WAAA,KACA,2BAAA,OHvEF,4BGqEA,qBACE,WAAA,KACA,2BAAA,OHvEF,4BGqEA,qBACE,WAAA,KACA,2BAAA,OHvEF,6BGqEA,qBACE,WAAA,KACA,2BAAA,OHvEF,6BGqEA,sBACE,WAAA,KACA,2BAAA,OE/IN,YACE,cAAA,MASF,gBACE,YAAA,oBACA,eAAA,oBACA,cAAA,EboRI,UAAA,QahRJ,YAAA,IAIF,mBACE,YAAA,kBACA,eAAA,kBb0QI,UAAA,QatQN,mBACE,YAAA,mBACA,eAAA,mBboQI,UAAA,QcjSN,WACE,WAAA,OdgSI,UAAA,Oc5RJ,MAAA,QCLF,cACE,QAAA,MACA,MAAA,KACA,QAAA,QAAA,Of8RI,UAAA,Ke3RJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,QACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KdGE,cAAA,OeHE,WAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCDhBN,cCiBQ,WAAA,MDGN,yBACE,SAAA,OAEA,wDACE,OAAA,QAKJ,oBACE,MAAA,QACA,iBAAA,KACA,aAAA,QACA,QAAA,EAKE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAOJ,2CAEE,OAAA,MAIF,gCACE,MAAA,QAEA,QAAA,EAHF,2BACE,MAAA,QAEA,QAAA,EAQF,uBAAA,wBAEE,iBAAA,QAGA,QAAA,EAIF,oCACE,QAAA,QAAA,OACA,OAAA,SAAA,QACA,mBAAA,OAAA,kBAAA,OACA,MAAA,QE3EF,iBAAA,QF6EE,eAAA,KACA,aAAA,QACA,aAAA,MACA,aAAA,EACA,wBAAA,IACA,cAAA,ECtEE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCDuDJ,oCCtDM,WAAA,MDqEN,yEACE,iBAAA,QAGF,0CACE,QAAA,QAAA,OACA,OAAA,SAAA,QACA,mBAAA,OAAA,kBAAA,OACA,MAAA,QE9FF,iBAAA,QFgGE,eAAA,KACA,aAAA,QACA,aAAA,MACA,aAAA,EACA,wBAAA,IACA,cAAA,ECzFE,mBAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCD0EJ,0CCzEM,mBAAA,KAAA,WAAA,MDwFN,+EACE,iBAAA,QASJ,wBACE,QAAA,MACA,MAAA,KACA,QAAA,QAAA,EACA,cAAA,EACA,YAAA,IACA,MAAA,QACA,iBAAA,YACA,OAAA,MAAA,YACA,aAAA,IAAA,EAEA,wCAAA,wCAEE,cAAA,EACA,aAAA,EAWJ,iBACE,WAAA,0BACA,QAAA,OAAA,MfmJI,UAAA,QClRF,cAAA,McmIF,uCACE,QAAA,OAAA,MACA,OAAA,QAAA,OACA,mBAAA,MAAA,kBAAA,MAGF,6CACE,QAAA,OAAA,MACA,OAAA,QAAA,OACA,mBAAA,MAAA,kBAAA,MAIJ,iBACE,WAAA,yBACA,QAAA,MAAA,KfgII,UAAA,QClRF,cAAA,McsJF,uCACE,QAAA,MAAA,KACA,OAAA,OAAA,MACA,mBAAA,KAAA,kBAAA,KAGF,6CACE,QAAA,MAAA,KACA,OAAA,OAAA,MACA,mBAAA,KAAA,kBAAA,KAQF,sBACE,WAAA,2BAGF,yBACE,WAAA,0BAGF,yBACE,WAAA,yBAKJ,oBACE,MAAA,KACA,OAAA,KACA,QAAA,QAEA,mDACE,OAAA,QAGF,uCACE,OAAA,Md/LA,cAAA,OcmMF,0CACE,OAAA,MdpMA,cAAA,OiBdJ,aACE,QAAA,MACA,MAAA,KACA,QAAA,QAAA,QAAA,QAAA,OAEA,mBAAA,oBlB2RI,UAAA,KkBxRJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,iBAAA,gOACA,kBAAA,UACA,oBAAA,MAAA,OAAA,OACA,gBAAA,KAAA,KACA,OAAA,IAAA,MAAA,QjBFE,cAAA,OeHE,WAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YESJ,mBAAA,KAAA,gBAAA,KAAA,WAAA,KFLI,uCEfN,aFgBQ,WAAA,MEMN,mBACE,aAAA,QACA,QAAA,EAKE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAIJ,uBAAA,mCAEE,cAAA,OACA,iBAAA,KAGF,sBAEE,iBAAA,QAKF,4BACE,MAAA,YACA,YAAA,EAAA,EAAA,EAAA,QAIJ,gBACE,YAAA,OACA,eAAA,OACA,aAAA,MlByOI,UAAA,QkBrON,gBACE,YAAA,MACA,eAAA,MACA,aAAA,KlBkOI,UAAA,QmBjSN,YACE,QAAA,MACA,WAAA,OACA,aAAA,MACA,cAAA,QAEA,8BACE,MAAA,KACA,YAAA,OAIJ,kBACE,MAAA,IACA,OAAA,IACA,WAAA,MACA,eAAA,IACA,iBAAA,KACA,kBAAA,UACA,oBAAA,OACA,gBAAA,QACA,OAAA,IAAA,MAAA,gBACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KACA,2BAAA,MAAA,aAAA,MAGA,iClBXE,cAAA,MkBeF,8BAEE,cAAA,IAGF,yBACE,OAAA,gBAGF,wBACE,aAAA,QACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAAA,qBAGF,0BACE,iBAAA,QACA,aAAA,QAEA,yCAII,iBAAA,8NAIJ,sCAII,iBAAA,sIAKN,+CACE,iBAAA,QACA,aAAA,QAKE,iBAAA,wNAIJ,2BACE,eAAA,KACA,OAAA,KACA,QAAA,GAOA,6CAAA,8CACE,QAAA,GAcN,aACE,aAAA,MAEA,+BACE,MAAA,IACA,YAAA,OACA,iBAAA,uJACA,oBAAA,KAAA,OlB9FA,cAAA,IeHE,WAAA,oBAAA,KAAA,YAIA,uCGyFJ,+BHxFM,WAAA,MGgGJ,qCACE,iBAAA,yIAGF,uCACE,oBAAA,MAAA,OAKE,iBAAA,sIAMR,mBACE,QAAA,aACA,aAAA,KAGF,WACE,SAAA,SACA,KAAA,cACA,eAAA,KAIE,yBAAA,0BACE,eAAA,KACA,OAAA,KACA,QAAA,IC9IN,YACE,MAAA,KACA,OAAA,OACA,QAAA,EACA,iBAAA,YACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KAEA,kBACE,QAAA,EAIA,wCAA0B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,OAAA,qBAC1B,oCAA0B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,OAAA,qBAG5B,8BACE,OAAA,EAGF,kCACE,MAAA,KACA,OAAA,KACA,WAAA,QHzBF,iBAAA,QG2BE,OAAA,EnBZA,cAAA,KeHE,mBAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YImBF,mBAAA,KAAA,WAAA,KJfE,uCIMJ,kCJLM,mBAAA,KAAA,WAAA,MIgBJ,yCHjCF,iBAAA,QGsCA,2CACE,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YnB7BA,cAAA,KmBkCF,8BACE,MAAA,KACA,OAAA,KHnDF,iBAAA,QGqDE,OAAA,EnBtCA,cAAA,KeHE,gBAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YI6CF,gBAAA,KAAA,WAAA,KJzCE,uCIiCJ,8BJhCM,gBAAA,KAAA,WAAA,MI0CJ,qCH3DF,iBAAA,QGgEA,8BACE,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YnBvDA,cAAA,KmB4DF,qBACE,eAAA,KAEA,2CACE,iBAAA,QAGF,uCACE,iBAAA,QCvFN,eACE,SAAA,SAEA,6BtBgjFF,4BsB9iFI,OAAA,mBACA,YAAA,KAGF,qBACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,OAAA,KACA,QAAA,KAAA,OACA,eAAA,KACA,OAAA,IAAA,MAAA,YACA,iBAAA,EAAA,ELDE,WAAA,QAAA,IAAA,WAAA,CAAA,UAAA,IAAA,YAIA,uCKXJ,qBLYM,WAAA,MKCN,6BACE,QAAA,KAAA,OAEA,+CACE,MAAA,YADF,0CACE,MAAA,YAGF,0DAEE,YAAA,SACA,eAAA,QAHF,mCAAA,qDAEE,YAAA,SACA,eAAA,QAGF,8CACE,YAAA,SACA,eAAA,QAIJ,4BACE,YAAA,SACA,eAAA,QAMA,gEACE,QAAA,IACA,UAAA,WAAA,mBAAA,mBAFF,yCtBojFJ,2DACA,kCsBpjFM,QAAA,IACA,UAAA,WAAA,mBAAA,mBAKF,oDACE,QAAA,IACA,UAAA,WAAA,mBAAA,mBCtDN,aACE,SAAA,SACA,QAAA,KACA,UAAA,KACA,YAAA,QACA,MAAA,KAEA,2BvB4mFF,0BuB1mFI,SAAA,SACA,KAAA,EAAA,EAAA,KACA,MAAA,GACA,UAAA,EAIF,iCvB0mFF,gCuBxmFI,QAAA,EAMF,kBACE,SAAA,SACA,QAAA,EAEA,wBACE,QAAA,EAWN,kBACE,QAAA,KACA,YAAA,OACA,QAAA,QAAA,OtBsPI,UAAA,KsBpPJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,OACA,YAAA,OACA,iBAAA,QACA,OAAA,IAAA,MAAA,QrBpCE,cAAA,OFwoFJ,qBuB1lFA,8BvBwlFA,6BACA,kCuBrlFE,QAAA,MAAA,KtBgOI,UAAA,QClRF,cAAA,MFipFJ,qBuB1lFA,8BvBwlFA,6BACA,kCuBrlFE,QAAA,OAAA,MtBuNI,UAAA,QClRF,cAAA,MqBgEJ,6BvBwlFA,6BuBtlFE,cAAA,KvB2lFF,uEuB9kFI,8FrB/DA,wBAAA,EACA,2BAAA,EFipFJ,iEuB5kFI,2FrBtEA,wBAAA,EACA,2BAAA,EqBgFF,0IACE,YAAA,KrBpEA,uBAAA,EACA,0BAAA,EsBzBF,gBACE,QAAA,KACA,MAAA,KACA,WAAA,OvByQE,UAAA,OuBtQF,MAAA,QAGF,eACE,SAAA,SACA,IAAA,KACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MvB4PE,UAAA,QuBzPF,MAAA,KACA,iBAAA,mBtB1BA,cAAA,OFosFJ,0BACA,yBwBtqFI,sCxBoqFJ,qCwBlqFM,QAAA,MA9CF,uBAAA,mCAoDE,aAAA,QAGE,cAAA,qBACA,iBAAA,2OACA,kBAAA,UACA,oBAAA,MAAA,wBAAA,OACA,gBAAA,sBAAA,sBAGF,6BAAA,yCACE,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,OAAA,oBAhEJ,2CAAA,+BAyEI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBA1EJ,sBAAA,kCAiFE,aAAA,QAGE,kDAAA,gDAAA,8DAAA,4DAEE,cAAA,SACA,iBAAA,+NAAA,CAAA,2OACA,oBAAA,MAAA,OAAA,MAAA,CAAA,OAAA,MAAA,QACA,gBAAA,KAAA,IAAA,CAAA,sBAAA,sBAIJ,4BAAA,wCACE,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,OAAA,oBA/FJ,2BAAA,uCAsGE,aAAA,QAEA,mCAAA,+CACE,iBAAA,QAGF,iCAAA,6CACE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAGF,6CAAA,yDACE,MAAA,QAKJ,qDACE,YAAA,KAvHF,oCxBywFJ,mCwBzwFI,gDxBwwFJ,+CwBzoFQ,QAAA,EAIF,0CxB2oFN,yCwB3oFM,sDxB0oFN,qDwBzoFQ,QAAA,EAjHN,kBACE,QAAA,KACA,MAAA,KACA,WAAA,OvByQE,UAAA,OuBtQF,MAAA,QAGF,iBACE,SAAA,SACA,IAAA,KACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MvB4PE,UAAA,QuBzPF,MAAA,KACA,iBAAA,mBtB1BA,cAAA,OF6xFJ,8BACA,6BwB/vFI,0CxB6vFJ,yCwB3vFM,QAAA,MA9CF,yBAAA,qCAoDE,aAAA,QAGE,cAAA,qBACA,iBAAA,2TACA,kBAAA,UACA,oBAAA,MAAA,wBAAA,OACA,gBAAA,sBAAA,sBAGF,+BAAA,2CACE,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,OAAA,oBAhEJ,6CAAA,iCAyEI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBA1EJ,wBAAA,oCAiFE,aAAA,QAGE,oDAAA,kDAAA,gEAAA,8DAEE,cAAA,SACA,iBAAA,+NAAA,CAAA,2TACA,oBAAA,MAAA,OAAA,MAAA,CAAA,OAAA,MAAA,QACA,gBAAA,KAAA,IAAA,CAAA,sBAAA,sBAIJ,8BAAA,0CACE,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,OAAA,oBA/FJ,6BAAA,yCAsGE,aAAA,QAEA,qCAAA,iDACE,iBAAA,QAGF,mCAAA,+CACE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAGF,+CAAA,2DACE,MAAA,QAKJ,uDACE,YAAA,KAvHF,sCxBk2FJ,qCwBl2FI,kDxBi2FJ,iDwBhuFQ,QAAA,EAEF,4CxBouFN,2CwBpuFM,wDxBmuFN,uDwBluFQ,QAAA,ECtIR,KACE,QAAA,aAEA,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,OACA,gBAAA,KAEA,eAAA,OACA,OAAA,QACA,oBAAA,KAAA,iBAAA,KAAA,YAAA,KACA,iBAAA,YACA,OAAA,IAAA,MAAA,YC8GA,QAAA,QAAA,OzBsKI,UAAA,KClRF,cAAA,OeHE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCQhBN,KRiBQ,WAAA,MQAN,WACE,MAAA,QAIF,sBAAA,WAEE,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAAA,qBAcF,cAAA,cAAA,uBAGE,eAAA,KACA,QAAA,IAYF,aCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,mBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,8BAAA,mBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAIJ,+BAAA,gCAAA,oBAAA,oBAAA,mCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,qCAAA,sCAAA,0BAAA,0BAAA,yCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,oBAKN,sBAAA,sBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,eCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,qBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,gCAAA,qBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAIJ,iCAAA,kCAAA,sBAAA,sBAAA,qCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,uCAAA,wCAAA,4BAAA,4BAAA,2CAKI,WAAA,EAAA,EAAA,EAAA,OAAA,qBAKN,wBAAA,wBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,aCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,mBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,8BAAA,mBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAIJ,+BAAA,gCAAA,oBAAA,oBAAA,mCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,qCAAA,sCAAA,0BAAA,0BAAA,yCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,oBAKN,sBAAA,sBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,UCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,gBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,2BAAA,gBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAIJ,4BAAA,6BAAA,iBAAA,iBAAA,gCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,kCAAA,mCAAA,uBAAA,uBAAA,sCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,oBAKN,mBAAA,mBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,aCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,mBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,8BAAA,mBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,mBAIJ,+BAAA,gCAAA,oBAAA,oBAAA,mCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,qCAAA,sCAAA,0BAAA,0BAAA,yCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,mBAKN,sBAAA,sBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,YCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,kBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,6BAAA,kBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,mBAIJ,8BAAA,+BAAA,mBAAA,mBAAA,kCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,oCAAA,qCAAA,yBAAA,yBAAA,wCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,mBAKN,qBAAA,qBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,WCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,iBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,4BAAA,iBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAIJ,6BAAA,8BAAA,kBAAA,kBAAA,iCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,mCAAA,oCAAA,wBAAA,wBAAA,uCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,qBAKN,oBAAA,oBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDZF,UCvCA,MAAA,KRhBA,iBAAA,QQkBA,aAAA,QAGA,gBACE,MAAA,KRtBF,iBAAA,QQwBE,aAAA,QAGF,2BAAA,gBAEE,MAAA,KR7BF,iBAAA,QQ+BE,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,OAAA,kBAIJ,4BAAA,6BAAA,iBAAA,iBAAA,gCAKE,MAAA,KACA,iBAAA,QAGA,aAAA,QAEA,kCAAA,mCAAA,uBAAA,uBAAA,sCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,kBAKN,mBAAA,mBAEE,MAAA,KACA,iBAAA,QAGA,aAAA,QDNF,qBCmBA,MAAA,QACA,aAAA,QAEA,2BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,sCAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAGF,uCAAA,wCAAA,4BAAA,0CAAA,4BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6CAAA,8CAAA,kCAAA,gDAAA,kCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,oBAKN,8BAAA,8BAEE,MAAA,QACA,iBAAA,YDvDF,uBCmBA,MAAA,QACA,aAAA,QAEA,6BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,wCAAA,6BAEE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAGF,yCAAA,0CAAA,8BAAA,4CAAA,8BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,+CAAA,gDAAA,oCAAA,kDAAA,oCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,qBAKN,gCAAA,gCAEE,MAAA,QACA,iBAAA,YDvDF,qBCmBA,MAAA,QACA,aAAA,QAEA,2BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,sCAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,OAAA,mBAGF,uCAAA,wCAAA,4BAAA,0CAAA,4BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6CAAA,8CAAA,kCAAA,gDAAA,kCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,mBAKN,8BAAA,8BAEE,MAAA,QACA,iBAAA,YDvDF,kBCmBA,MAAA,QACA,aAAA,QAEA,wBACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,mCAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,OAAA,oBAGF,oCAAA,qCAAA,yBAAA,uCAAA,yBAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,0CAAA,2CAAA,+BAAA,6CAAA,+BAKI,WAAA,EAAA,EAAA,EAAA,OAAA,oBAKN,2BAAA,2BAEE,MAAA,QACA,iBAAA,YDvDF,qBCmBA,MAAA,QACA,aAAA,QAEA,2BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,sCAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,OAAA,mBAGF,uCAAA,wCAAA,4BAAA,0CAAA,4BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6CAAA,8CAAA,kCAAA,gDAAA,kCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,mBAKN,8BAAA,8BAEE,MAAA,QACA,iBAAA,YDvDF,oBCmBA,MAAA,QACA,aAAA,QAEA,0BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,qCAAA,0BAEE,WAAA,EAAA,EAAA,EAAA,OAAA,mBAGF,sCAAA,uCAAA,2BAAA,yCAAA,2BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,4CAAA,6CAAA,iCAAA,+CAAA,iCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,mBAKN,6BAAA,6BAEE,MAAA,QACA,iBAAA,YDvDF,mBCmBA,MAAA,QACA,aAAA,QAEA,yBACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,oCAAA,yBAEE,WAAA,EAAA,EAAA,EAAA,OAAA,qBAGF,qCAAA,sCAAA,0BAAA,wCAAA,0BAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,2CAAA,4CAAA,gCAAA,8CAAA,gCAKI,WAAA,EAAA,EAAA,EAAA,OAAA,qBAKN,4BAAA,4BAEE,MAAA,QACA,iBAAA,YDvDF,kBCmBA,MAAA,QACA,aAAA,QAEA,wBACE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,mCAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,OAAA,kBAGF,oCAAA,qCAAA,yBAAA,uCAAA,yBAKE,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,0CAAA,2CAAA,+BAAA,6CAAA,+BAKI,WAAA,EAAA,EAAA,EAAA,OAAA,kBAKN,2BAAA,2BAEE,MAAA,QACA,iBAAA,YD3CJ,UACE,YAAA,IACA,MAAA,QACA,gBAAA,UAEA,gBACE,MAAA,QAQF,mBAAA,mBAEE,MAAA,QAWJ,mBAAA,QCuBE,QAAA,MAAA,KzBsKI,UAAA,QClRF,cAAA,MuByFJ,mBAAA,QCmBE,QAAA,OAAA,MzBsKI,UAAA,QClRF,cAAA,MyBnBJ,MVgBM,WAAA,QAAA,KAAA,OAIA,uCUpBN,MVqBQ,WAAA,MUlBN,iBACE,QAAA,EAMF,qBACE,QAAA,KAIJ,YACE,OAAA,EACA,SAAA,OVDI,WAAA,OAAA,KAAA,KAIA,uCULN,YVMQ,WAAA,MUDN,gCACE,MAAA,EACA,OAAA,KVNE,WAAA,MAAA,KAAA,KAIA,uCUAJ,gCVCM,WAAA,MjBu3GR,UADA,SAEA,W4B54GA,QAIE,SAAA,SAGF,iBACE,YAAA,OCqBE,wBACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAhCJ,WAAA,KAAA,MACA,aAAA,KAAA,MAAA,YACA,cAAA,EACA,YAAA,KAAA,MAAA,YAqDE,8BACE,YAAA,ED3CN,eACE,SAAA,SACA,QAAA,KACA,QAAA,KACA,UAAA,MACA,QAAA,MAAA,EACA,OAAA,E3B+QI,UAAA,K2B7QJ,MAAA,QACA,WAAA,KACA,WAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,gB1BVE,cAAA,O0BcF,+BACE,IAAA,KACA,KAAA,EACA,WAAA,QAYA,qBACE,cAAA,MAEA,qCACE,MAAA,KACA,KAAA,EAIJ,mBACE,cAAA,IAEA,mCACE,MAAA,EACA,KAAA,KnBCJ,yBmBfA,wBACE,cAAA,MAEA,wCACE,MAAA,KACA,KAAA,EAIJ,sBACE,cAAA,IAEA,sCACE,MAAA,EACA,KAAA,MnBCJ,yBmBfA,wBACE,cAAA,MAEA,wCACE,MAAA,KACA,KAAA,EAIJ,sBACE,cAAA,IAEA,sCACE,MAAA,EACA,KAAA,MnBCJ,yBmBfA,wBACE,cAAA,MAEA,wCACE,MAAA,KACA,KAAA,EAIJ,sBACE,cAAA,IAEA,sCACE,MAAA,EACA,KAAA,MnBCJ,0BmBfA,wBACE,cAAA,MAEA,wCACE,MAAA,KACA,KAAA,EAIJ,sBACE,cAAA,IAEA,sCACE,MAAA,EACA,KAAA,MnBCJ,0BmBfA,yBACE,cAAA,MAEA,yCACE,MAAA,KACA,KAAA,EAIJ,uBACE,cAAA,IAEA,uCACE,MAAA,EACA,KAAA,MAUN,uCACE,IAAA,KACA,OAAA,KACA,WAAA,EACA,cAAA,QC9CA,gCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAzBJ,WAAA,EACA,aAAA,KAAA,MAAA,YACA,cAAA,KAAA,MACA,YAAA,KAAA,MAAA,YA8CE,sCACE,YAAA,ED0BJ,wCACE,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,YAAA,QC5DA,iCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAlBJ,WAAA,KAAA,MAAA,YACA,aAAA,EACA,cAAA,KAAA,MAAA,YACA,YAAA,KAAA,MAuCE,uCACE,YAAA,EDoCF,iCACE,eAAA,EAMJ,0CACE,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,aAAA,QC7EA,mCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAWA,mCACE,QAAA,KAGF,oCACE,QAAA,aACA,aAAA,OACA,eAAA,OACA,QAAA,GA9BN,WAAA,KAAA,MAAA,YACA,aAAA,KAAA,MACA,cAAA,KAAA,MAAA,YAiCE,yCACE,YAAA,EDqDF,oCACE,eAAA,EAON,kBACE,OAAA,EACA,OAAA,MAAA,EACA,SAAA,OACA,WAAA,IAAA,MAAA,gBAMF,eACE,QAAA,MACA,MAAA,KACA,QAAA,OAAA,KACA,MAAA,KACA,YAAA,IACA,MAAA,QACA,WAAA,QACA,gBAAA,KACA,YAAA,OACA,iBAAA,YACA,OAAA,EAcA,qBAAA,qBAEE,MAAA,QVzJF,iBAAA,QU8JA,sBAAA,sBAEE,MAAA,KACA,gBAAA,KVjKF,iBAAA,QUqKA,wBAAA,wBAEE,MAAA,QACA,eAAA,KACA,iBAAA,YAMJ,oBACE,QAAA,MAIF,iBACE,QAAA,MACA,QAAA,MAAA,KACA,cAAA,E3B0GI,UAAA,Q2BxGJ,MAAA,QACA,YAAA,OAIF,oBACE,QAAA,MACA,QAAA,OAAA,KACA,MAAA,QAIF,oBACE,MAAA,QACA,iBAAA,QACA,aAAA,gBAGA,mCACE,MAAA,QAEA,yCAAA,yCAEE,MAAA,KVhNJ,iBAAA,sBUoNE,0CAAA,0CAEE,MAAA,KVtNJ,iBAAA,QU0NE,4CAAA,4CAEE,MAAA,QAIJ,sCACE,aAAA,gBAGF,wCACE,MAAA,QAGF,qCACE,MAAA,QE5OJ,W9B4rHA,oB8B1rHE,SAAA,SACA,QAAA,YACA,eAAA,O9B8rHF,yB8B5rHE,gBACE,SAAA,SACA,KAAA,EAAA,EAAA,K9BosHJ,4CACA,0CAIA,gCADA,gCADA,+BADA,+B8BjsHE,mC9B0rHF,iCAIA,uBADA,uBADA,sBADA,sB8BrrHI,QAAA,EAKJ,aACE,QAAA,KACA,UAAA,KACA,gBAAA,WAEA,0BACE,MAAA,K9BisHJ,wC8B3rHE,kCAEE,YAAA,K9B6rHJ,4C8BzrHE,uD5BRE,wBAAA,EACA,2BAAA,EFssHJ,6C8BtrHE,+B9BqrHF,iCExrHI,uBAAA,EACA,0BAAA,E4BqBJ,uBACE,cAAA,SACA,aAAA,SAEA,8BAAA,uCAAA,sCAGE,YAAA,EAGF,0CACE,aAAA,EAIJ,0CAAA,+BACE,cAAA,QACA,aAAA,QAGF,0CAAA,+BACE,cAAA,OACA,aAAA,OAoBF,oBACE,eAAA,OACA,YAAA,WACA,gBAAA,OAEA,yB9BopHF,+B8BlpHI,MAAA,K9BspHJ,iD8BnpHE,2CAEE,WAAA,K9BqpHJ,qD8BjpHE,gE5BvFE,2BAAA,EACA,0BAAA,EF4uHJ,sD8BjpHE,8B5B1GE,uBAAA,EACA,wBAAA,E6BxBJ,KACE,QAAA,KACA,UAAA,KACA,aAAA,EACA,cAAA,EACA,WAAA,KAGF,UACE,QAAA,MACA,QAAA,MAAA,KAGA,MAAA,QACA,gBAAA,KdHI,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,YAIA,uCcPN,UdQQ,WAAA,McCN,gBAAA,gBAEE,MAAA,QAKF,mBACE,MAAA,QACA,eAAA,KACA,OAAA,QAQJ,UACE,cAAA,IAAA,MAAA,QAEA,oBACE,cAAA,KACA,WAAA,IACA,OAAA,IAAA,MAAA,Y7BlBA,uBAAA,OACA,wBAAA,O6BoBA,0BAAA,0BAEE,aAAA,QAAA,QAAA,QAEA,UAAA,QAGF,6BACE,MAAA,QACA,iBAAA,YACA,aAAA,Y/BkxHN,mC+B9wHE,2BAEE,MAAA,QACA,iBAAA,KACA,aAAA,QAAA,QAAA,KAGF,yBAEE,WAAA,K7B5CA,uBAAA,EACA,wBAAA,E6BuDF,qBACE,WAAA,IACA,OAAA,E7BnEA,cAAA,O6BuEF,4B/BowHF,2B+BlwHI,MAAA,KbxFF,iBAAA,QlBg2HF,oB+B7vHE,oBAEE,KAAA,EAAA,EAAA,KACA,WAAA,O/BgwHJ,yB+B3vHE,yBAEE,WAAA,EACA,UAAA,EACA,WAAA,OAMF,8B/BwvHF,mC+BvvHI,MAAA,KAUF,uBACE,QAAA,KAEF,qBACE,QAAA,MCxHJ,QACE,SAAA,SACA,QAAA,KACA,UAAA,KACA,YAAA,OACA,gBAAA,cACA,YAAA,MAEA,eAAA,MAOA,mBhCu2HF,yBAGA,sBADA,sBADA,sBAGA,sBACA,uBgC32HI,QAAA,KACA,UAAA,QACA,YAAA,OACA,gBAAA,cAoBJ,cACE,YAAA,SACA,eAAA,SACA,aAAA,K/B2OI,UAAA,Q+BzOJ,gBAAA,KACA,YAAA,OAaF,YACE,QAAA,KACA,eAAA,OACA,aAAA,EACA,cAAA,EACA,WAAA,KAEA,sBACE,cAAA,EACA,aAAA,EAGF,2BACE,SAAA,OASJ,aACE,YAAA,MACA,eAAA,MAYF,iBACE,WAAA,KACA,UAAA,EAGA,YAAA,OAIF,gBACE,QAAA,OAAA,O/B6KI,UAAA,Q+B3KJ,YAAA,EACA,iBAAA,YACA,OAAA,IAAA,MAAA,Y9BzGE,cAAA,OeHE,WAAA,WAAA,KAAA,YAIA,uCemGN,gBflGQ,WAAA,Me2GN,sBACE,gBAAA,KAGF,sBACE,gBAAA,KACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAMJ,qBACE,QAAA,aACA,MAAA,MACA,OAAA,MACA,eAAA,OACA,kBAAA,UACA,oBAAA,OACA,gBAAA,KAGF,mBACE,WAAA,6BACA,WAAA,KvB1FE,yBuBsGA,kBAEI,UAAA,OACA,gBAAA,WAEA,8BACE,eAAA,IAEA,6CACE,SAAA,SAGF,wCACE,cAAA,MACA,aAAA,MAIJ,qCACE,SAAA,QAGF,mCACE,QAAA,eACA,WAAA,KAGF,kCACE,QAAA,KAGF,oCACE,QAAA,KAGF,6BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhCgzHV,oCgC9yHQ,iCAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,kCACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,SvBhKN,yBuBsGA,kBAEI,UAAA,OACA,gBAAA,WAEA,8BACE,eAAA,IAEA,6CACE,SAAA,SAGF,wCACE,cAAA,MACA,aAAA,MAIJ,qCACE,SAAA,QAGF,mCACE,QAAA,eACA,WAAA,KAGF,kCACE,QAAA,KAGF,oCACE,QAAA,KAGF,6BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhCq2HV,oCgCn2HQ,iCAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,kCACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,SvBhKN,yBuBsGA,kBAEI,UAAA,OACA,gBAAA,WAEA,8BACE,eAAA,IAEA,6CACE,SAAA,SAGF,wCACE,cAAA,MACA,aAAA,MAIJ,qCACE,SAAA,QAGF,mCACE,QAAA,eACA,WAAA,KAGF,kCACE,QAAA,KAGF,oCACE,QAAA,KAGF,6BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhC05HV,oCgCx5HQ,iCAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,kCACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,SvBhKN,0BuBsGA,kBAEI,UAAA,OACA,gBAAA,WAEA,8BACE,eAAA,IAEA,6CACE,SAAA,SAGF,wCACE,cAAA,MACA,aAAA,MAIJ,qCACE,SAAA,QAGF,mCACE,QAAA,eACA,WAAA,KAGF,kCACE,QAAA,KAGF,oCACE,QAAA,KAGF,6BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhC+8HV,oCgC78HQ,iCAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,kCACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,SvBhKN,0BuBsGA,mBAEI,UAAA,OACA,gBAAA,WAEA,+BACE,eAAA,IAEA,8CACE,SAAA,SAGF,yCACE,cAAA,MACA,aAAA,MAIJ,sCACE,SAAA,QAGF,oCACE,QAAA,eACA,WAAA,KAGF,mCACE,QAAA,KAGF,qCACE,QAAA,KAGF,8BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhCogIV,qCgClgIQ,kCAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,mCACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,SA1DN,eAEI,UAAA,OACA,gBAAA,WAEA,2BACE,eAAA,IAEA,0CACE,SAAA,SAGF,qCACE,cAAA,MACA,aAAA,MAIJ,kCACE,SAAA,QAGF,gCACE,QAAA,eACA,WAAA,KAGF,+BACE,QAAA,KAGF,iCACE,QAAA,KAGF,0BACE,SAAA,QACA,OAAA,EACA,QAAA,KACA,UAAA,EACA,WAAA,kBACA,iBAAA,YACA,aAAA,EACA,YAAA,EfhMJ,WAAA,KekMI,UAAA,KhCwjIV,iCgCtjIQ,8BAEE,OAAA,KACA,WAAA,EACA,cAAA,EAGF,+BACE,QAAA,KACA,UAAA,EACA,QAAA,EACA,WAAA,QAcR,4BACE,MAAA,eAEA,kCAAA,kCAEE,MAAA,eAKF,oCACE,MAAA,gBAEA,0CAAA,0CAEE,MAAA,eAGF,6CACE,MAAA,ehCsiIR,2CgCliII,0CAEE,MAAA,eAIJ,8BACE,MAAA,gBACA,aAAA,eAGF,mCACE,iBAAA,4OAGF,2BACE,MAAA,gBAEA,6BhC+hIJ,mCADA,mCgC3hIM,MAAA,eAOJ,2BACE,MAAA,KAEA,iCAAA,iCAEE,MAAA,KAKF,mCACE,MAAA,sBAEA,yCAAA,yCAEE,MAAA,sBAGF,4CACE,MAAA,sBhCshIR,0CgClhII,yCAEE,MAAA,KAIJ,6BACE,MAAA,sBACA,aAAA,qBAGF,kCACE,iBAAA,kPAGF,0BACE,MAAA,sBACA,4BhCghIJ,kCADA,kCgC5gIM,MAAA,KCvUN,MACE,SAAA,SACA,QAAA,KACA,eAAA,OACA,UAAA,EAEA,UAAA,WACA,iBAAA,KACA,gBAAA,WACA,OAAA,IAAA,MAAA,iB/BME,cAAA,O+BFF,SACE,aAAA,EACA,YAAA,EAGF,kBACE,WAAA,QACA,cAAA,QAEA,8BACE,iBAAA,E/BCF,uBAAA,mBACA,wBAAA,mB+BEA,6BACE,oBAAA,E/BUF,2BAAA,mBACA,0BAAA,mB+BJF,+BjCm1IF,+BiCj1II,WAAA,EAIJ,WAGE,KAAA,EAAA,EAAA,KACA,QAAA,KAAA,KAIF,YACE,cAAA,MAGF,eACE,WAAA,QACA,cAAA,EAGF,sBACE,cAAA,EAQA,sBACE,YAAA,KAQJ,aACE,QAAA,MAAA,KACA,cAAA,EAEA,iBAAA,gBACA,cAAA,IAAA,MAAA,iBAEA,yB/BpEE,cAAA,mBAAA,mBAAA,EAAA,E+ByEJ,aACE,QAAA,MAAA,KAEA,iBAAA,gBACA,WAAA,IAAA,MAAA,iBAEA,wB/B/EE,cAAA,EAAA,EAAA,mBAAA,mB+ByFJ,kBACE,aAAA,OACA,cAAA,OACA,YAAA,OACA,cAAA,EAUF,mBACE,aAAA,OACA,YAAA,OAIF,kBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,K/BnHE,cAAA,mB+BuHJ,UjCqzIA,iBADA,ciCjzIE,MAAA,KAGF,UjCozIA,cEx6II,uBAAA,mBACA,wBAAA,mB+BwHJ,UjCqzIA,iBEh6II,2BAAA,mBACA,0BAAA,mB+BuHF,kBACE,cAAA,OxBpGA,yBwBgGJ,YAQI,QAAA,KACA,UAAA,IAAA,KAGA,kBAEE,KAAA,EAAA,EAAA,GACA,cAAA,EAEA,wBACE,YAAA,EACA,YAAA,EAKA,mC/BpJJ,wBAAA,EACA,2BAAA,EFg8IJ,gDiC1yIU,iDAGE,wBAAA,EjC2yIZ,gDiCzyIU,oDAGE,2BAAA,EAIJ,oC/BrJJ,uBAAA,EACA,0BAAA,EF87IJ,iDiCvyIU,kDAGE,uBAAA,EjCwyIZ,iDiCtyIU,qDAGE,0BAAA,GC7MZ,kBACE,SAAA,SACA,QAAA,KACA,YAAA,OACA,MAAA,KACA,QAAA,KAAA,QjC4RI,UAAA,KiC1RJ,MAAA,QACA,WAAA,KACA,iBAAA,KACA,OAAA,EhCKE,cAAA,EgCHF,gBAAA,KjBAI,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,WAAA,CAAA,cAAA,KAAA,KAIA,uCiBhBN,kBjBiBQ,WAAA,MiBFN,kCACE,MAAA,QACA,iBAAA,QACA,WAAA,MAAA,EAAA,KAAA,EAAA,iBAEA,yCACE,iBAAA,gRACA,UAAA,gBAKJ,yBACE,YAAA,EACA,MAAA,QACA,OAAA,QACA,YAAA,KACA,QAAA,GACA,iBAAA,gRACA,kBAAA,UACA,gBAAA,QjBvBE,WAAA,UAAA,IAAA,YAIA,uCiBWJ,yBjBVM,WAAA,MiBsBN,wBACE,QAAA,EAGF,wBACE,QAAA,EACA,aAAA,QACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAAA,qBAIJ,kBACE,cAAA,EAGF,gBACE,iBAAA,KACA,OAAA,IAAA,MAAA,iBAEA,8BhCnCE,uBAAA,OACA,wBAAA,OgCqCA,gDhCtCA,uBAAA,mBACA,wBAAA,mBgC0CF,oCACE,WAAA,EAIF,6BhClCE,2BAAA,OACA,0BAAA,OgCqCE,yDhCtCF,2BAAA,mBACA,0BAAA,mBgC0CA,iDhC3CA,2BAAA,OACA,0BAAA,OgCgDJ,gBACE,QAAA,KAAA,QASA,qCACE,aAAA,EAGF,iCACE,aAAA,EACA,YAAA,EhCxFA,cAAA,EgC2FA,6CAAgB,WAAA,EAChB,4CAAe,cAAA,EAEf,mDhC9FA,cAAA,EiCnBJ,YACE,QAAA,KACA,UAAA,KACA,QAAA,EAAA,EACA,cAAA,KAEA,WAAA,KAOA,kCACE,aAAA,MAEA,0CACE,MAAA,KACA,cAAA,MACA,MAAA,QACA,QAAA,kCAIJ,wBACE,MAAA,QCzBJ,YACE,QAAA,KhCGA,aAAA,EACA,WAAA,KgCAF,WACE,SAAA,SACA,QAAA,MACA,MAAA,QACA,gBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,QnBKI,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCmBfN,WnBgBQ,WAAA,MmBPN,iBACE,QAAA,EACA,MAAA,QAEA,iBAAA,QACA,aAAA,QAGF,iBACE,QAAA,EACA,MAAA,QACA,iBAAA,QACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAAA,qBAKF,wCACE,YAAA,KAGF,6BACE,QAAA,EACA,MAAA,KlBlCF,iBAAA,QkBoCE,aAAA,QAGF,+BACE,MAAA,QACA,eAAA,KACA,iBAAA,KACA,aAAA,QC3CF,WACE,QAAA,QAAA,OAOI,kCnCqCJ,uBAAA,OACA,0BAAA,OmChCI,iCnCiBJ,wBAAA,OACA,2BAAA,OmChCF,0BACE,QAAA,OAAA,OpCgSE,UAAA,QoCzRE,iDnCqCJ,uBAAA,MACA,0BAAA,MmChCI,gDnCiBJ,wBAAA,MACA,2BAAA,MmChCF,0BACE,QAAA,OAAA,MpCgSE,UAAA,QoCzRE,iDnCqCJ,uBAAA,MACA,0BAAA,MmChCI,gDnCiBJ,wBAAA,MACA,2BAAA,MoC/BJ,OACE,QAAA,aACA,QAAA,MAAA,MrC8RI,UAAA,MqC5RJ,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,eAAA,SpCKE,cAAA,OoCAF,aACE,QAAA,KAKJ,YACE,SAAA,SACA,IAAA,KCvBF,OACE,SAAA,SACA,QAAA,KAAA,KACA,cAAA,KACA,OAAA,IAAA,MAAA,YrCWE,cAAA,OqCNJ,eAEE,MAAA,QAIF,YACE,YAAA,IAQF,mBACE,cAAA,KAGA,8BACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,QAAA,EACA,QAAA,QAAA,KAeF,eClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,2BACE,MAAA,QD6CF,iBClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,6BACE,MAAA,QD6CF,eClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,2BACE,MAAA,QD6CF,YClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,wBACE,MAAA,QD6CF,eClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,2BACE,MAAA,QD6CF,cClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,0BACE,MAAA,QD6CF,aClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,yBACE,MAAA,QD6CF,YClDA,MAAA,QtBEA,iBAAA,QsBAA,aAAA,QAEA,wBACE,MAAA,QCHF,wCACE,GAAK,sBAAA,MADP,gCACE,GAAK,sBAAA,MAKT,UACE,QAAA,KACA,OAAA,KACA,SAAA,OxCwRI,UAAA,OwCtRJ,iBAAA,QvCIE,cAAA,OuCCJ,cACE,QAAA,KACA,eAAA,OACA,gBAAA,OACA,SAAA,OACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,iBAAA,QxBZI,WAAA,MAAA,IAAA,KAIA,uCwBAN,cxBCQ,WAAA,MwBWR,sBvBYE,iBAAA,iKuBVA,gBAAA,KAAA,KAIA,uBACE,kBAAA,GAAA,OAAA,SAAA,qBAAA,UAAA,GAAA,OAAA,SAAA,qBAGE,uCAJJ,uBAKM,kBAAA,KAAA,UAAA,MCvCR,YACE,QAAA,KACA,eAAA,OAGA,aAAA,EACA,cAAA,ExCSE,cAAA,OwCLJ,qBACE,gBAAA,KACA,cAAA,QAEA,gCAEE,QAAA,uBAAA,KACA,kBAAA,QAUJ,wBACE,MAAA,KACA,MAAA,QACA,WAAA,QAGA,8BAAA,8BAEE,QAAA,EACA,MAAA,QACA,gBAAA,KACA,iBAAA,QAGF,+BACE,MAAA,QACA,iBAAA,QASJ,iBACE,SAAA,SACA,QAAA,MACA,QAAA,MAAA,KACA,MAAA,QACA,gBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,iBAEA,6BxCrCE,uBAAA,QACA,wBAAA,QwCwCF,4BxC3BE,2BAAA,QACA,0BAAA,QwC8BF,0BAAA,0BAEE,MAAA,QACA,eAAA,KACA,iBAAA,KAIF,wBACE,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,kCACE,iBAAA,EAEA,yCACE,WAAA,KACA,iBAAA,IAcF,uBACE,eAAA,IAGE,oDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,mDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,+CACE,WAAA,EAGF,yDACE,iBAAA,IACA,kBAAA,EAEA,gEACE,YAAA,KACA,kBAAA,IjCpER,yBiC4CA,0BACE,eAAA,IAGE,uDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,sDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,kDACE,WAAA,EAGF,4DACE,iBAAA,IACA,kBAAA,EAEA,mEACE,YAAA,KACA,kBAAA,KjCpER,yBiC4CA,0BACE,eAAA,IAGE,uDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,sDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,kDACE,WAAA,EAGF,4DACE,iBAAA,IACA,kBAAA,EAEA,mEACE,YAAA,KACA,kBAAA,KjCpER,yBiC4CA,0BACE,eAAA,IAGE,uDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,sDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,kDACE,WAAA,EAGF,4DACE,iBAAA,IACA,kBAAA,EAEA,mEACE,YAAA,KACA,kBAAA,KjCpER,0BiC4CA,0BACE,eAAA,IAGE,uDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,sDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,kDACE,WAAA,EAGF,4DACE,iBAAA,IACA,kBAAA,EAEA,mEACE,YAAA,KACA,kBAAA,KjCpER,0BiC4CA,2BACE,eAAA,IAGE,wDxCrCJ,0BAAA,OAZA,wBAAA,EwCsDI,uDxCtDJ,wBAAA,OAYA,0BAAA,EwC+CI,mDACE,WAAA,EAGF,6DACE,iBAAA,IACA,kBAAA,EAEA,oEACE,YAAA,KACA,kBAAA,KAcZ,kBxC9HI,cAAA,EwCiIF,mCACE,aAAA,EAAA,EAAA,IAEA,8CACE,oBAAA,ECpJJ,yBACE,MAAA,QACA,iBAAA,QAGE,sDAAA,sDAEE,MAAA,QACA,iBAAA,QAGF,uDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,2BACE,MAAA,QACA,iBAAA,QAGE,wDAAA,wDAEE,MAAA,QACA,iBAAA,QAGF,yDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,yBACE,MAAA,QACA,iBAAA,QAGE,sDAAA,sDAEE,MAAA,QACA,iBAAA,QAGF,uDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,sBACE,MAAA,QACA,iBAAA,QAGE,mDAAA,mDAEE,MAAA,QACA,iBAAA,QAGF,oDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,yBACE,MAAA,QACA,iBAAA,QAGE,sDAAA,sDAEE,MAAA,QACA,iBAAA,QAGF,uDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,wBACE,MAAA,QACA,iBAAA,QAGE,qDAAA,qDAEE,MAAA,QACA,iBAAA,QAGF,sDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,uBACE,MAAA,QACA,iBAAA,QAGE,oDAAA,oDAEE,MAAA,QACA,iBAAA,QAGF,qDACE,MAAA,KACA,iBAAA,QACA,aAAA,QAdN,sBACE,MAAA,QACA,iBAAA,QAGE,mDAAA,mDAEE,MAAA,QACA,iBAAA,QAGF,oDACE,MAAA,KACA,iBAAA,QACA,aAAA,QCbR,WACE,WAAA,YACA,MAAA,IACA,OAAA,IACA,QAAA,MAAA,MACA,MAAA,KACA,WAAA,YAAA,0TAAA,MAAA,CAAA,IAAA,KAAA,UACA,OAAA,E1COE,cAAA,O0CLF,QAAA,GAGA,iBACE,MAAA,KACA,gBAAA,KACA,QAAA,IAGF,iBACE,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,OAAA,qBACA,QAAA,EAGF,oBAAA,oBAEE,eAAA,KACA,oBAAA,KAAA,iBAAA,KAAA,YAAA,KACA,QAAA,IAIJ,iBACE,OAAA,UAAA,gBAAA,iBCtCF,OACE,MAAA,MACA,UAAA,K5CmSI,UAAA,Q4ChSJ,eAAA,KACA,iBAAA,sBACA,gBAAA,YACA,OAAA,IAAA,MAAA,eACA,WAAA,EAAA,MAAA,KAAA,gB3CUE,cAAA,O2CPF,eACE,QAAA,EAGF,kBACE,QAAA,KAIJ,iBACE,MAAA,oBAAA,MAAA,iBAAA,MAAA,YACA,UAAA,KACA,eAAA,KAEA,mCACE,cAAA,OAIJ,cACE,QAAA,KACA,YAAA,OACA,QAAA,MAAA,OACA,MAAA,QACA,iBAAA,sBACA,gBAAA,YACA,cAAA,IAAA,MAAA,gB3CVE,uBAAA,mBACA,wBAAA,mB2CYF,yBACE,aAAA,SACA,YAAA,OAIJ,YACE,QAAA,OACA,UAAA,WC1CF,OACE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,OAAA,KACA,WAAA,OACA,WAAA,KAGA,QAAA,EAOF,cACE,SAAA,SACA,MAAA,KACA,OAAA,MAEA,eAAA,KAGA,0B7BlBI,WAAA,UAAA,IAAA,S6BoBF,UAAA,mB7BhBE,uC6BcJ,0B7BbM,WAAA,M6BiBN,0BACE,UAAA,KAIF,kCACE,UAAA,YAIJ,yBACE,OAAA,kBAEA,wCACE,WAAA,KACA,SAAA,OAGF,qCACE,WAAA,KAIJ,uBACE,QAAA,KACA,YAAA,OACA,WAAA,kBAIF,eACE,SAAA,SACA,QAAA,KACA,eAAA,OACA,MAAA,KAGA,eAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,e5C3DE,cAAA,M4C+DF,QAAA,EAIF,gBCpFE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,MAAA,MACA,OAAA,MACA,iBAAA,KAGA,qBAAS,QAAA,EACT,qBAAS,QAAA,GDgFX,cACE,QAAA,KACA,YAAA,EACA,YAAA,OACA,gBAAA,cACA,QAAA,KAAA,KACA,cAAA,IAAA,MAAA,Q5CtEE,uBAAA,kBACA,wBAAA,kB4CwEF,yBACE,QAAA,MAAA,MACA,OAAA,OAAA,OAAA,OAAA,KAKJ,aACE,cAAA,EACA,YAAA,IAKF,YACE,SAAA,SAGA,KAAA,EAAA,EAAA,KACA,QAAA,KAIF,cACE,QAAA,KACA,UAAA,KACA,YAAA,EACA,YAAA,OACA,gBAAA,SACA,QAAA,OACA,WAAA,IAAA,MAAA,Q5CzFE,2BAAA,kBACA,0BAAA,kB4C8FF,gBACE,OAAA,OrC3EA,yBqCkFF,cACE,UAAA,MACA,OAAA,QAAA,KAGF,yBACE,OAAA,oBAGF,uBACE,WAAA,oBAOF,UAAY,UAAA,OrCnGV,yBqCuGF,U9C0wKF,U8CxwKI,UAAA,OrCzGA,0BqC8GF,UAAY,UAAA,QASV,kBACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,iCACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,gC5C/KF,cAAA,E4CmLE,8BACE,WAAA,KAGF,gC5CvLF,cAAA,EOyDA,4BqC0GA,0BACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,yCACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,wC5C/KF,cAAA,E4CmLE,sCACE,WAAA,KAGF,wC5CvLF,cAAA,GOyDA,4BqC0GA,0BACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,yCACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,wC5C/KF,cAAA,E4CmLE,sCACE,WAAA,KAGF,wC5CvLF,cAAA,GOyDA,4BqC0GA,0BACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,yCACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,wC5C/KF,cAAA,E4CmLE,sCACE,WAAA,KAGF,wC5CvLF,cAAA,GOyDA,6BqC0GA,0BACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,yCACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,wC5C/KF,cAAA,E4CmLE,sCACE,WAAA,KAGF,wC5CvLF,cAAA,GOyDA,6BqC0GA,2BACE,MAAA,MACA,UAAA,KACA,OAAA,KACA,OAAA,EAEA,0CACE,OAAA,KACA,OAAA,E5C3KJ,cAAA,E4C+KE,yC5C/KF,cAAA,E4CmLE,uCACE,WAAA,KAGF,yC5CvLF,cAAA,G8ClBJ,SACE,SAAA,SACA,QAAA,KACA,QAAA,MACA,OAAA,ECJA,YAAA,0BAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,KhDsRI,UAAA,Q+C1RJ,UAAA,WACA,QAAA,EAEA,cAAS,QAAA,GAET,wBACE,SAAA,SACA,QAAA,MACA,MAAA,MACA,OAAA,MAEA,gCACE,SAAA,SACA,QAAA,GACA,aAAA,YACA,aAAA,MAKN,6CAAA,gBACE,QAAA,MAAA,EAEA,4DAAA,+BACE,OAAA,EAEA,oEAAA,uCACE,IAAA,KACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAKN,+CAAA,gBACE,QAAA,EAAA,MAEA,8DAAA,+BACE,KAAA,EACA,MAAA,MACA,OAAA,MAEA,sEAAA,uCACE,MAAA,KACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAKN,gDAAA,mBACE,QAAA,MAAA,EAEA,+DAAA,kCACE,IAAA,EAEA,uEAAA,0CACE,OAAA,KACA,aAAA,EAAA,MAAA,MACA,oBAAA,KAKN,8CAAA,kBACE,QAAA,EAAA,MAEA,6DAAA,iCACE,MAAA,EACA,MAAA,MACA,OAAA,MAEA,qEAAA,yCACE,KAAA,KACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAqBN,eACE,UAAA,MACA,QAAA,OAAA,MACA,MAAA,KACA,WAAA,OACA,iBAAA,K9C7FE,cAAA,OgDnBJ,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,MACA,UAAA,MDLA,YAAA,0BAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,KhDsRI,UAAA,QiDzRJ,UAAA,WACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,ehDIE,cAAA,MgDAF,wBACE,SAAA,SACA,QAAA,MACA,MAAA,KACA,OAAA,MAEA,+BAAA,gCAEE,SAAA,SACA,QAAA,MACA,QAAA,GACA,aAAA,YACA,aAAA,MAMJ,4DAAA,+BACE,OAAA,mBAEA,oEAAA,uCACE,OAAA,EACA,aAAA,MAAA,MAAA,EACA,iBAAA,gBAGF,mEAAA,sCACE,OAAA,IACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAMJ,8DAAA,+BACE,KAAA,mBACA,MAAA,MACA,OAAA,KAEA,sEAAA,uCACE,KAAA,EACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,gBAGF,qEAAA,sCACE,KAAA,IACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAMJ,+DAAA,kCACE,IAAA,mBAEA,uEAAA,0CACE,IAAA,EACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,gBAGF,sEAAA,yCACE,IAAA,IACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,KAKJ,wEAAA,2CACE,SAAA,SACA,IAAA,EACA,KAAA,IACA,QAAA,MACA,MAAA,KACA,YAAA,OACA,QAAA,GACA,cAAA,IAAA,MAAA,QAKF,6DAAA,iCACE,MAAA,mBACA,MAAA,MACA,OAAA,KAEA,qEAAA,yCACE,MAAA,EACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,gBAGF,oEAAA,wCACE,MAAA,IACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAqBN,gBACE,QAAA,MAAA,KACA,cAAA,EjDuJI,UAAA,KiDpJJ,iBAAA,QACA,cAAA,IAAA,MAAA,ehDtHE,uBAAA,kBACA,wBAAA,kBgDwHF,sBACE,QAAA,KAIJ,cACE,QAAA,KAAA,KACA,MAAA,QC/IF,UACE,SAAA,SAGF,wBACE,aAAA,MAGF,gBACE,SAAA,SACA,MAAA,KACA,SAAA,OCtBA,uBACE,QAAA,MACA,MAAA,KACA,QAAA,GDuBJ,eACE,SAAA,SACA,QAAA,KACA,MAAA,KACA,MAAA,KACA,aAAA,MACA,4BAAA,OAAA,oBAAA,OlClBI,WAAA,UAAA,IAAA,YAIA,uCkCQN,elCPQ,WAAA,MjBizLR,oBACA,oBmDjyLA,sBAGE,QAAA,MnDoyLF,0BmDhyLA,8CAEE,UAAA,iBnDmyLF,4BmDhyLA,4CAEE,UAAA,kBAWA,8BACE,QAAA,EACA,oBAAA,QACA,UAAA,KnD2xLJ,uDACA,qDmDzxLE,qCAGE,QAAA,EACA,QAAA,EnD0xLJ,yCmDvxLE,2CAEE,QAAA,EACA,QAAA,ElC/DE,WAAA,QAAA,GAAA,IAIA,uCjBs1LN,yCmD9xLE,2ClCvDM,WAAA,MjB21LR,uBmDvxLA,uBAEE,SAAA,SACA,IAAA,EACA,OAAA,EACA,QAAA,EAEA,QAAA,KACA,YAAA,OACA,gBAAA,OACA,MAAA,IACA,QAAA,EACA,MAAA,KACA,WAAA,OACA,WAAA,IACA,OAAA,EACA,QAAA,GlCzFI,WAAA,QAAA,KAAA,KAIA,uCjB+2LN,uBmD1yLA,uBlCpEQ,WAAA,MjBo3LR,6BADA,6BmD3xLE,6BAAA,6BAEE,MAAA,KACA,gBAAA,KACA,QAAA,EACA,QAAA,GAGJ,uBACE,KAAA,EAGF,uBACE,MAAA,EnD+xLF,4BmD1xLA,4BAEE,QAAA,aACA,MAAA,KACA,OAAA,KACA,kBAAA,UACA,oBAAA,IACA,gBAAA,KAAA,KAWF,4BACE,iBAAA,wPAEF,4BACE,iBAAA,yPAQF,qBACE,SAAA,SACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,EACA,QAAA,KACA,gBAAA,OACA,QAAA,EAEA,aAAA,IACA,cAAA,KACA,YAAA,IACA,WAAA,KAEA,sCACE,WAAA,YACA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,OAAA,IACA,QAAA,EACA,aAAA,IACA,YAAA,IACA,YAAA,OACA,OAAA,QACA,iBAAA,KACA,gBAAA,YACA,OAAA,EAEA,WAAA,KAAA,MAAA,YACA,cAAA,KAAA,MAAA,YACA,QAAA,GlC5KE,WAAA,QAAA,IAAA,KAIA,uCkCwJJ,sClCvJM,WAAA,MkC2KN,6BACE,QAAA,EASJ,kBACE,SAAA,SACA,MAAA,IACA,OAAA,QACA,KAAA,IACA,YAAA,QACA,eAAA,QACA,MAAA,KACA,WAAA,OnDqxLF,2CmD/wLE,2CAEE,OAAA,UAAA,eAGF,qDACE,iBAAA,KAGF,iCACE,MAAA,KE7NJ,kCACE,GAAK,UAAA,gBADP,0BACE,GAAK,UAAA,gBAIP,gBACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,QACA,OAAA,MAAA,MAAA,aACA,mBAAA,YAEA,cAAA,IACA,kBAAA,KAAA,OAAA,SAAA,eAAA,UAAA,KAAA,OAAA,SAAA,eAGF,mBACE,MAAA,KACA,OAAA,KACA,aAAA,KAQF,gCACE,GACE,UAAA,SAEF,IACE,QAAA,EACA,UAAA,MANJ,wBACE,GACE,UAAA,SAEF,IACE,QAAA,EACA,UAAA,MAKJ,cACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,QACA,iBAAA,aAEA,cAAA,IACA,QAAA,EACA,kBAAA,KAAA,OAAA,SAAA,aAAA,UAAA,KAAA,OAAA,SAAA,aAGF,iBACE,MAAA,KACA,OAAA,KAIA,uCACE,gBrDq/LJ,cqDn/LM,2BAAA,KAAA,mBAAA,MCjEN,WACE,SAAA,MACA,OAAA,EACA,QAAA,KACA,QAAA,KACA,eAAA,OACA,UAAA,KAEA,WAAA,OACA,iBAAA,KACA,gBAAA,YACA,QAAA,ErCKI,WAAA,UAAA,IAAA,YAIA,uCqCpBN,WrCqBQ,WAAA,MqCLR,oBPdE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,MAAA,MACA,OAAA,MACA,iBAAA,KAGA,yBAAS,QAAA,EACT,yBAAS,QAAA,GOQX,kBACE,QAAA,KACA,YAAA,OACA,gBAAA,cACA,QAAA,KAAA,KAEA,6BACE,QAAA,MAAA,MACA,WAAA,OACA,aAAA,OACA,cAAA,OAIJ,iBACE,cAAA,EACA,YAAA,IAGF,gBACE,UAAA,EACA,QAAA,KAAA,KACA,WAAA,KAGF,iBACE,IAAA,EACA,KAAA,EACA,MAAA,MACA,aAAA,IAAA,MAAA,eACA,UAAA,kBAGF,eACE,IAAA,EACA,MAAA,EACA,MAAA,MACA,YAAA,IAAA,MAAA,eACA,UAAA,iBAGF,eACE,IAAA,EACA,MAAA,EACA,KAAA,EACA,OAAA,KACA,WAAA,KACA,cAAA,IAAA,MAAA,eACA,UAAA,kBAGF,kBACE,MAAA,EACA,KAAA,EACA,OAAA,KACA,WAAA,KACA,WAAA,IAAA,MAAA,eACA,UAAA,iBAGF,gBACE,UAAA,KCjFF,aACE,QAAA,aACA,WAAA,IACA,eAAA,OACA,OAAA,KACA,iBAAA,aACA,QAAA,GAEA,yBACE,QAAA,aACA,QAAA,GAKJ,gBACE,WAAA,KAGF,gBACE,WAAA,KAGF,gBACE,WAAA,MAKA,+BACE,kBAAA,iBAAA,GAAA,YAAA,SAAA,UAAA,iBAAA,GAAA,YAAA,SAIJ,oCACE,IACE,QAAA,IAFJ,4BACE,IACE,QAAA,IAIJ,kBACE,mBAAA,8DAAA,WAAA,8DACA,kBAAA,KAAA,KAAA,UAAA,KAAA,KACA,kBAAA,iBAAA,GAAA,OAAA,SAAA,UAAA,iBAAA,GAAA,OAAA,SAGF,oCACE,KACE,sBAAA,MAAA,GAAA,cAAA,MAAA,IAFJ,4BACE,KACE,sBAAA,MAAA,GAAA,cAAA,MAAA,IH9CF,iBACE,QAAA,MACA,MAAA,KACA,QAAA,GIJF,cACE,MAAA,QAGE,oBAAA,oBAEE,MAAA,QANN,gBACE,MAAA,QAGE,sBAAA,sBAEE,MAAA,QANN,cACE,MAAA,QAGE,oBAAA,oBAEE,MAAA,QANN,WACE,MAAA,QAGE,iBAAA,iBAEE,MAAA,QANN,cACE,MAAA,QAGE,oBAAA,oBAEE,MAAA,QANN,aACE,MAAA,QAGE,mBAAA,mBAEE,MAAA,QANN,YACE,MAAA,QAGE,kBAAA,kBAEE,MAAA,QANN,WACE,MAAA,QAGE,iBAAA,iBAEE,MAAA,QCLR,OACE,SAAA,SACA,MAAA,KAEA,eACE,QAAA,MACA,YAAA,uBACA,QAAA,GAGF,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,MAAA,KACA,OAAA,KAKF,WACE,kBAAA,KADF,WACE,kBAAA,mBADF,YACE,kBAAA,oBADF,YACE,kBAAA,oBCrBJ,WACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,KAAA,EACA,QAAA,KAGF,cACE,SAAA,MACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KAQE,YACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,KjDqCF,yBiDxCA,eACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MjDqCF,yBiDxCA,eACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MjDqCF,yBiDxCA,eACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MjDqCF,0BiDxCA,eACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MjDqCF,0BiDxCA,gBACE,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MCzBN,QACE,QAAA,KACA,eAAA,IACA,YAAA,OACA,WAAA,QAGF,QACE,QAAA,KACA,KAAA,EAAA,EAAA,KACA,eAAA,OACA,WAAA,QCRF,iB5Dm4MA,0D6D/3ME,SAAA,mBACA,MAAA,cACA,OAAA,cACA,QAAA,YACA,OAAA,eACA,SAAA,iBACA,KAAA,wBACA,YAAA,iBACA,OAAA,YCXA,uBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,EACA,QAAA,GCRJ,eCAE,SAAA,OACA,cAAA,SACA,YAAA,OCNF,IACE,QAAA,aACA,WAAA,QACA,MAAA,IACA,WAAA,IACA,iBAAA,aACA,QAAA,ICyDM,gBAOI,eAAA,mBAPJ,WAOI,eAAA,cAPJ,cAOI,eAAA,iBAPJ,cAOI,eAAA,iBAPJ,mBAOI,eAAA,sBAPJ,gBAOI,eAAA,mBAPJ,aAOI,MAAA,eAPJ,WAOI,MAAA,gBAPJ,YAOI,MAAA,eAPJ,WAOI,QAAA,YAPJ,YAOI,QAAA,cAPJ,YAOI,QAAA,aAPJ,YAOI,QAAA,cAPJ,aAOI,QAAA,YAPJ,eAOI,SAAA,eAPJ,iBAOI,SAAA,iBAPJ,kBAOI,SAAA,kBAPJ,iBAOI,SAAA,iBAPJ,UAOI,QAAA,iBAPJ,gBAOI,QAAA,uBAPJ,SAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,SAOI,QAAA,gBAPJ,aAOI,QAAA,oBAPJ,cAOI,QAAA,qBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,QAOI,QAAA,eAPJ,QAOI,WAAA,EAAA,MAAA,KAAA,0BAPJ,WAOI,WAAA,EAAA,QAAA,OAAA,2BAPJ,WAOI,WAAA,EAAA,KAAA,KAAA,2BAPJ,aAOI,WAAA,eAPJ,iBAOI,SAAA,iBAPJ,mBAOI,SAAA,mBAPJ,mBAOI,SAAA,mBAPJ,gBAOI,SAAA,gBAPJ,iBAOI,SAAA,yBAAA,SAAA,iBAPJ,OAOI,IAAA,YAPJ,QAOI,IAAA,cAPJ,SAOI,IAAA,eAPJ,UAOI,OAAA,YAPJ,WAOI,OAAA,cAPJ,YAOI,OAAA,eAPJ,SAOI,KAAA,YAPJ,UAOI,KAAA,cAPJ,WAOI,KAAA,eAPJ,OAOI,MAAA,YAPJ,QAOI,MAAA,cAPJ,SAOI,MAAA,eAPJ,kBAOI,UAAA,+BAPJ,oBAOI,UAAA,2BAPJ,oBAOI,UAAA,2BAPJ,QAOI,OAAA,IAAA,MAAA,kBAPJ,UAOI,OAAA,YAPJ,YAOI,WAAA,IAAA,MAAA,kBAPJ,cAOI,WAAA,YAPJ,YAOI,aAAA,IAAA,MAAA,kBAPJ,cAOI,aAAA,YAPJ,eAOI,cAAA,IAAA,MAAA,kBAPJ,iBAOI,cAAA,YAPJ,cAOI,YAAA,IAAA,MAAA,kBAPJ,gBAOI,YAAA,YAPJ,gBAOI,aAAA,kBAPJ,kBAOI,aAAA,kBAPJ,gBAOI,aAAA,kBAPJ,aAOI,aAAA,kBAPJ,gBAOI,aAAA,kBAPJ,eAOI,aAAA,kBAPJ,cAOI,aAAA,kBAPJ,aAOI,aAAA,kBAPJ,cAOI,aAAA,eAPJ,UAOI,aAAA,cAPJ,UAOI,aAAA,cAPJ,UAOI,aAAA,cAPJ,UAOI,aAAA,cAPJ,UAOI,aAAA,cAPJ,MAOI,MAAA,cAPJ,MAOI,MAAA,cAPJ,MAOI,MAAA,cAPJ,OAOI,MAAA,eAPJ,QAOI,MAAA,eAPJ,QAOI,UAAA,eAPJ,QAOI,MAAA,gBAPJ,YAOI,UAAA,gBAPJ,MAOI,OAAA,cAPJ,MAOI,OAAA,cAPJ,MAOI,OAAA,cAPJ,OAOI,OAAA,eAPJ,QAOI,OAAA,eAPJ,QAOI,WAAA,eAPJ,QAOI,OAAA,gBAPJ,YAOI,WAAA,gBAPJ,WAOI,KAAA,EAAA,EAAA,eAPJ,UAOI,eAAA,cAPJ,aAOI,eAAA,iBAPJ,kBAOI,eAAA,sBAPJ,qBAOI,eAAA,yBAPJ,aAOI,UAAA,YAPJ,aAOI,UAAA,YAPJ,eAOI,YAAA,YAPJ,eAOI,YAAA,YAPJ,WAOI,UAAA,eAPJ,aAOI,UAAA,iBAPJ,mBAOI,UAAA,uBAPJ,OAOI,IAAA,YAPJ,OAOI,IAAA,iBAPJ,OAOI,IAAA,gBAPJ,OAOI,IAAA,eAPJ,OAOI,IAAA,iBAPJ,OAOI,IAAA,eAPJ,uBAOI,gBAAA,qBAPJ,qBAOI,gBAAA,mBAPJ,wBAOI,gBAAA,iBAPJ,yBAOI,gBAAA,wBAPJ,wBAOI,gBAAA,uBAPJ,wBAOI,gBAAA,uBAPJ,mBAOI,YAAA,qBAPJ,iBAOI,YAAA,mBAPJ,oBAOI,YAAA,iBAPJ,sBAOI,YAAA,mBAPJ,qBAOI,YAAA,kBAPJ,qBAOI,cAAA,qBAPJ,mBAOI,cAAA,mBAPJ,sBAOI,cAAA,iBAPJ,uBAOI,cAAA,wBAPJ,sBAOI,cAAA,uBAPJ,uBAOI,cAAA,kBAPJ,iBAOI,WAAA,eAPJ,kBAOI,WAAA,qBAPJ,gBAOI,WAAA,mBAPJ,mBAOI,WAAA,iBAPJ,qBAOI,WAAA,mBAPJ,oBAOI,WAAA,kBAPJ,aAOI,MAAA,aAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,KAOI,OAAA,YAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,gBAPJ,KAOI,OAAA,eAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,eAPJ,QAOI,OAAA,eAPJ,MAOI,aAAA,YAAA,YAAA,YAPJ,MAOI,aAAA,iBAAA,YAAA,iBAPJ,MAOI,aAAA,gBAAA,YAAA,gBAPJ,MAOI,aAAA,eAAA,YAAA,eAPJ,MAOI,aAAA,iBAAA,YAAA,iBAPJ,MAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,MAOI,WAAA,YAAA,cAAA,YAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,gBAAA,cAAA,gBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,YAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,gBAPJ,MAOI,WAAA,eAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,eAPJ,SAOI,WAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,SAOI,aAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,SAOI,cAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,SAOI,YAAA,eAPJ,KAOI,QAAA,YAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,gBAPJ,KAOI,QAAA,eAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,eAPJ,MAOI,cAAA,YAAA,aAAA,YAPJ,MAOI,cAAA,iBAAA,aAAA,iBAPJ,MAOI,cAAA,gBAAA,aAAA,gBAPJ,MAOI,cAAA,eAAA,aAAA,eAPJ,MAOI,cAAA,iBAAA,aAAA,iBAPJ,MAOI,cAAA,eAAA,aAAA,eAPJ,MAOI,YAAA,YAAA,eAAA,YAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,gBAAA,eAAA,gBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,MAOI,eAAA,YAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,gBAPJ,MAOI,eAAA,eAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,gBAOI,YAAA,mCAPJ,MAOI,UAAA,iCAPJ,MAOI,UAAA,gCAPJ,MAOI,UAAA,8BAPJ,MAOI,UAAA,gCAPJ,MAOI,UAAA,kBAPJ,MAOI,UAAA,eAPJ,YAOI,WAAA,iBAPJ,YAOI,WAAA,iBAPJ,UAOI,YAAA,cAPJ,YAOI,YAAA,kBAPJ,WAOI,YAAA,cAPJ,SAOI,YAAA,cAPJ,WAOI,YAAA,iBAPJ,MAOI,YAAA,YAPJ,OAOI,YAAA,eAPJ,SAOI,YAAA,cAPJ,OAOI,YAAA,YAPJ,YAOI,WAAA,eAPJ,UAOI,WAAA,gBAPJ,aAOI,WAAA,iBAPJ,sBAOI,gBAAA,eAPJ,2BAOI,gBAAA,oBAPJ,8BAOI,gBAAA,uBAPJ,gBAOI,eAAA,oBAPJ,gBAOI,eAAA,oBAPJ,iBAOI,eAAA,qBAPJ,WAOI,YAAA,iBAPJ,aAOI,YAAA,iBAPJ,YAOI,UAAA,qBAAA,WAAA,qBAPJ,cAIQ,kBAAA,EAGJ,MAAA,6DAPJ,gBAIQ,kBAAA,EAGJ,MAAA,+DAPJ,cAIQ,kBAAA,EAGJ,MAAA,6DAPJ,WAIQ,kBAAA,EAGJ,MAAA,0DAPJ,cAIQ,kBAAA,EAGJ,MAAA,6DAPJ,aAIQ,kBAAA,EAGJ,MAAA,4DAPJ,YAIQ,kBAAA,EAGJ,MAAA,2DAPJ,WAIQ,kBAAA,EAGJ,MAAA,0DAPJ,YAIQ,kBAAA,EAGJ,MAAA,2DAPJ,YAIQ,kBAAA,EAGJ,MAAA,2DAPJ,WAIQ,kBAAA,EAGJ,MAAA,gEAPJ,YAIQ,kBAAA,EAGJ,MAAA,kBAPJ,eAIQ,kBAAA,EAGJ,MAAA,yBAPJ,eAIQ,kBAAA,EAGJ,MAAA,+BAPJ,YAIQ,kBAAA,EAGJ,MAAA,kBAjBJ,iBACE,kBAAA,KADF,iBACE,kBAAA,IADF,iBACE,kBAAA,KADF,kBACE,kBAAA,EASF,YAIQ,gBAAA,EAGJ,iBAAA,2DAPJ,cAIQ,gBAAA,EAGJ,iBAAA,6DAPJ,YAIQ,gBAAA,EAGJ,iBAAA,2DAPJ,SAIQ,gBAAA,EAGJ,iBAAA,wDAPJ,YAIQ,gBAAA,EAGJ,iBAAA,2DAPJ,WAIQ,gBAAA,EAGJ,iBAAA,0DAPJ,UAIQ,gBAAA,EAGJ,iBAAA,yDAPJ,SAIQ,gBAAA,EAGJ,iBAAA,wDAPJ,UAIQ,gBAAA,EAGJ,iBAAA,yDAPJ,UAIQ,gBAAA,EAGJ,iBAAA,yDAPJ,SAIQ,gBAAA,EAGJ,iBAAA,2DAPJ,gBAIQ,gBAAA,EAGJ,iBAAA,sBAjBJ,eACE,gBAAA,IADF,eACE,gBAAA,KADF,eACE,gBAAA,IADF,eACE,gBAAA,KADF,gBACE,gBAAA,EASF,aAOI,iBAAA,6BAPJ,iBAOI,oBAAA,cAAA,iBAAA,cAAA,YAAA,cAPJ,kBAOI,oBAAA,eAAA,iBAAA,eAAA,YAAA,eAPJ,kBAOI,oBAAA,eAAA,iBAAA,eAAA,YAAA,eAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,iBAPJ,WAOI,cAAA,YAPJ,WAOI,cAAA,gBAPJ,WAOI,cAAA,iBAPJ,WAOI,cAAA,gBAPJ,gBAOI,cAAA,cAPJ,cAOI,cAAA,gBAPJ,aAOI,uBAAA,iBAAA,wBAAA,iBAPJ,aAOI,wBAAA,iBAAA,2BAAA,iBAPJ,gBAOI,2BAAA,iBAAA,0BAAA,iBAPJ,eAOI,0BAAA,iBAAA,uBAAA,iBAPJ,SAOI,WAAA,kBAPJ,WAOI,WAAA,iBzDPR,yByDAI,gBAOI,MAAA,eAPJ,cAOI,MAAA,gBAPJ,eAOI,MAAA,eAPJ,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,UAOI,IAAA,YAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,gBAPJ,UAOI,IAAA,eAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,eAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,eAOI,WAAA,eAPJ,aAOI,WAAA,gBAPJ,gBAOI,WAAA,kBzDPR,yByDAI,gBAOI,MAAA,eAPJ,cAOI,MAAA,gBAPJ,eAOI,MAAA,eAPJ,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,UAOI,IAAA,YAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,gBAPJ,UAOI,IAAA,eAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,eAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,eAOI,WAAA,eAPJ,aAOI,WAAA,gBAPJ,gBAOI,WAAA,kBzDPR,yByDAI,gBAOI,MAAA,eAPJ,cAOI,MAAA,gBAPJ,eAOI,MAAA,eAPJ,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,UAOI,IAAA,YAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,gBAPJ,UAOI,IAAA,eAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,eAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,eAOI,WAAA,eAPJ,aAOI,WAAA,gBAPJ,gBAOI,WAAA,kBzDPR,0ByDAI,gBAOI,MAAA,eAPJ,cAOI,MAAA,gBAPJ,eAOI,MAAA,eAPJ,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,UAOI,IAAA,YAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,gBAPJ,UAOI,IAAA,eAPJ,UAOI,IAAA,iBAPJ,UAOI,IAAA,eAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,eAOI,WAAA,eAPJ,aAOI,WAAA,gBAPJ,gBAOI,WAAA,kBzDPR,0ByDAI,iBAOI,MAAA,eAPJ,eAOI,MAAA,gBAPJ,gBAOI,MAAA,eAPJ,cAOI,QAAA,iBAPJ,oBAOI,QAAA,uBAPJ,aAOI,QAAA,gBAPJ,YAOI,QAAA,eAPJ,aAOI,QAAA,gBAPJ,iBAOI,QAAA,oBAPJ,kBAOI,QAAA,qBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,YAOI,QAAA,eAPJ,eAOI,KAAA,EAAA,EAAA,eAPJ,cAOI,eAAA,cAPJ,iBAOI,eAAA,iBAPJ,sBAOI,eAAA,sBAPJ,yBAOI,eAAA,yBAPJ,iBAOI,UAAA,YAPJ,iBAOI,UAAA,YAPJ,mBAOI,YAAA,YAPJ,mBAOI,YAAA,YAPJ,eAOI,UAAA,eAPJ,iBAOI,UAAA,iBAPJ,uBAOI,UAAA,uBAPJ,WAOI,IAAA,YAPJ,WAOI,IAAA,iBAPJ,WAOI,IAAA,gBAPJ,WAOI,IAAA,eAPJ,WAOI,IAAA,iBAPJ,WAOI,IAAA,eAPJ,2BAOI,gBAAA,qBAPJ,yBAOI,gBAAA,mBAPJ,4BAOI,gBAAA,iBAPJ,6BAOI,gBAAA,wBAPJ,4BAOI,gBAAA,uBAPJ,4BAOI,gBAAA,uBAPJ,uBAOI,YAAA,qBAPJ,qBAOI,YAAA,mBAPJ,wBAOI,YAAA,iBAPJ,0BAOI,YAAA,mBAPJ,yBAOI,YAAA,kBAPJ,yBAOI,cAAA,qBAPJ,uBAOI,cAAA,mBAPJ,0BAOI,cAAA,iBAPJ,2BAOI,cAAA,wBAPJ,0BAOI,cAAA,uBAPJ,2BAOI,cAAA,kBAPJ,qBAOI,WAAA,eAPJ,sBAOI,WAAA,qBAPJ,oBAOI,WAAA,mBAPJ,uBAOI,WAAA,iBAPJ,yBAOI,WAAA,mBAPJ,wBAOI,WAAA,kBAPJ,iBAOI,MAAA,aAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,gBAOI,MAAA,YAPJ,SAOI,OAAA,YAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,gBAPJ,SAOI,OAAA,eAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,eAPJ,YAOI,OAAA,eAPJ,UAOI,aAAA,YAAA,YAAA,YAPJ,UAOI,aAAA,iBAAA,YAAA,iBAPJ,UAOI,aAAA,gBAAA,YAAA,gBAPJ,UAOI,aAAA,eAAA,YAAA,eAPJ,UAOI,aAAA,iBAAA,YAAA,iBAPJ,UAOI,aAAA,eAAA,YAAA,eAPJ,aAOI,aAAA,eAAA,YAAA,eAPJ,UAOI,WAAA,YAAA,cAAA,YAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,gBAAA,cAAA,gBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,aAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,YAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,gBAPJ,UAOI,WAAA,eAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,eAPJ,aAOI,WAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,aAOI,aAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,aAOI,cAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,aAOI,YAAA,eAPJ,SAOI,QAAA,YAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,gBAPJ,SAOI,QAAA,eAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,eAPJ,UAOI,cAAA,YAAA,aAAA,YAPJ,UAOI,cAAA,iBAAA,aAAA,iBAPJ,UAOI,cAAA,gBAAA,aAAA,gBAPJ,UAOI,cAAA,eAAA,aAAA,eAPJ,UAOI,cAAA,iBAAA,aAAA,iBAPJ,UAOI,cAAA,eAAA,aAAA,eAPJ,UAOI,YAAA,YAAA,eAAA,YAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,gBAAA,eAAA,gBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,UAOI,eAAA,YAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,gBAPJ,UAOI,eAAA,eAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,gBAOI,WAAA,eAPJ,cAOI,WAAA,gBAPJ,iBAOI,WAAA,kBCnDZ,0BD4CQ,MAOI,UAAA,iBAPJ,MAOI,UAAA,eAPJ,MAOI,UAAA,kBAPJ,MAOI,UAAA,kBChCZ,aDyBQ,gBAOI,QAAA,iBAPJ,sBAOI,QAAA,uBAPJ,eAOI,QAAA,gBAPJ,cAOI,QAAA,eAPJ,eAOI,QAAA,gBAPJ,mBAOI,QAAA,oBAPJ,oBAOI,QAAA,qBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,cAOI,QAAA","sourcesContent":["/*!\n * Bootstrap v5.1.1 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$variable-prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`