diff --git a/AudioCuesheetEditor/AudioCuesheetEditor.csproj b/AudioCuesheetEditor/AudioCuesheetEditor.csproj index ddce9620..7fbf0ec5 100644 --- a/AudioCuesheetEditor/AudioCuesheetEditor.csproj +++ b/AudioCuesheetEditor/AudioCuesheetEditor.csproj @@ -7,10 +7,11 @@ enable https://github.com/NeoCoderMatrix86/AudioCuesheetEditor 3.0 - 5.0.0 + 6.0.0 false true true + service-worker-assets.js @@ -26,6 +27,8 @@ + + @@ -34,6 +37,10 @@ + + + + @@ -65,6 +72,8 @@ + + @@ -73,6 +82,10 @@ + + + + @@ -98,9 +111,10 @@ + - - + + @@ -108,8 +122,12 @@ - - + + + + + + diff --git a/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs b/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs index 53311e85..62cd9f52 100644 --- a/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs +++ b/AudioCuesheetEditor/Data/Options/LocalStorageOptionsProvider.cs @@ -13,27 +13,25 @@ //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.Options; using Microsoft.JSInterop; +using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; namespace AudioCuesheetEditor.Data.Options { - public class LocalStorageOptionsProvider + public class LocalStorageOptionsProvider(IJSRuntime jsRuntime) { public event EventHandler? OptionSaved; - private readonly IJSRuntime _jsRuntime; + private readonly IJSRuntime _jsRuntime = jsRuntime; - public LocalStorageOptionsProvider(IJSRuntime jsRuntime) + private readonly JsonSerializerOptions SerializerOptions = new() { - if (jsRuntime is null) - { - throw new ArgumentNullException(nameof(jsRuntime)); - } - - _jsRuntime = jsRuntime; - } + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; public async Task GetOptions() where T : IOptions { @@ -65,17 +63,51 @@ public async Task GetOptions() where T : IOptions public async Task SaveOptions(IOptions options) { - if (options == null) + var optionsJson = JsonSerializer.Serialize(options, SerializerOptions); + await _jsRuntime.InvokeVoidAsync(String.Format("{0}.set", options.GetType().Name), optionsJson); + OptionSaved?.Invoke(this, options); + } + + public async Task SaveOptionsValue(Expression> propertyExpression, object value) where T : class, IOptions, new() + { + var options = await GetOptions(); + if (propertyExpression.Body is MemberExpression memberExpression) { - throw new ArgumentNullException(nameof(options)); + 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."); + } } - var serializerOptions = new JsonSerializerOptions + else if (propertyExpression.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression unaryMemberExpression) { - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }; - var optionsJson = JsonSerializer.Serialize(options, serializerOptions); - await _jsRuntime.InvokeVoidAsync(String.Format("{0}.set", options.GetType().Name), optionsJson); - OptionSaved?.Invoke(this, options); + 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."); + } + } + else + { + throw new ArgumentException("The provided expression does not reference a valid property."); + } + Boolean saveOptions = true; + if (options is IValidateable validateable) + { + saveOptions = validateable.Validate(propertyExpression).Status != ValidationStatus.Error; + } + if (saveOptions) + { + await SaveOptions(options); + } } } } diff --git a/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs b/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs index 7ee78d1a..834c5df1 100644 --- a/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs +++ b/AudioCuesheetEditor/Data/Services/MusicBrainzDataProvider.cs @@ -36,8 +36,10 @@ public class MusicBrainzTrack public TimeSpan? Length { get; init; } public String? Disambiguation { get; init; } } - public class MusicBrainzDataProvider + public class MusicBrainzDataProvider(ILogger logger) { + private readonly ILogger _logger = logger; + public const String Application = "AudioCuesheetEditor"; public const String ProjectUrl = "https://github.com/NeoCoderMatrix86/AudioCuesheetEditor"; @@ -45,82 +47,103 @@ public class MusicBrainzDataProvider public async Task> SearchArtistAsync(String searchString) { - List artistSearchResult = new(); - if (String.IsNullOrEmpty(searchString) == false) + List artistSearchResult = []; + try + { + if (String.IsNullOrEmpty(searchString) == false) + { + using var query = new Query(Application, ApplicationVersion, ProjectUrl); + var findArtistsResult = await query.FindArtistsAsync(searchString, simple: true); + artistSearchResult = findArtistsResult.Results.ToList().ConvertAll(x => new MusicBrainzArtist() { Id = x.Item.Id, Name = x.Item.Name, Disambiguation = x.Item.Disambiguation }); + } + } + catch (HttpRequestException hre) { - using var query = new Query(Application, ApplicationVersion, ProjectUrl); - var findArtistsResult = await query.FindArtistsAsync(searchString, simple: true); - artistSearchResult = findArtistsResult.Results.ToList().ConvertAll(x => new MusicBrainzArtist() { Id = x.Item.Id, Name = x.Item.Name, Disambiguation = x.Item.Disambiguation }); + _logger.LogError(hre, "Error getting response from MusicBrainz"); } return artistSearchResult.AsReadOnly(); } public async Task> SearchTitleAsync(String searchString, String? artist = null) { - List titleSearchResult = new(); - if (String.IsNullOrEmpty(searchString) == false) + List titleSearchResult = []; + try { - using var query = new Query(Application, ApplicationVersion, ProjectUrl); - ISearchResults> findRecordingsResult; - if (String.IsNullOrEmpty(artist)) + if (String.IsNullOrEmpty(searchString) == false) { - findRecordingsResult = await query.FindRecordingsAsync(searchString, simple: true); - } - else - { - findRecordingsResult = await query.FindRecordingsAsync(String.Format("{0} AND artistname:{1}", searchString, artist)); - } - foreach (var result in findRecordingsResult.Results) - { - String artistString = String.Empty; - if (result.Item.ArtistCredit != null) + using var query = new Query(Application, ApplicationVersion, ProjectUrl); + ISearchResults> findRecordingsResult; + if (String.IsNullOrEmpty(artist)) { - foreach (var artistCredit in result.Item.ArtistCredit) + findRecordingsResult = await query.FindRecordingsAsync(searchString, simple: true); + } + else + { + findRecordingsResult = await query.FindRecordingsAsync(String.Format("{0} AND artistname:{1}", searchString, artist)); + } + foreach (var result in findRecordingsResult.Results) + { + String artistString = String.Empty; + if (result.Item.ArtistCredit != null) { - artistString += artistCredit.Name; - if (String.IsNullOrEmpty(artistCredit.JoinPhrase) == false) + foreach (var artistCredit in result.Item.ArtistCredit) { - artistString += artistCredit.JoinPhrase; + artistString += artistCredit.Name; + if (String.IsNullOrEmpty(artistCredit.JoinPhrase) == false) + { + artistString += artistCredit.JoinPhrase; + } } } + titleSearchResult.Add(new MusicBrainzTrack() + { + Id = result.Item.Id, + Artist = artistString, + Title = result.Item.Title, + Length = result.Item.Length, + Disambiguation = result.Item.Disambiguation + }); } - titleSearchResult.Add(new MusicBrainzTrack() - { - Id = result.Item.Id, - Artist = artistString, - Title = result.Item.Title, - Length = result.Item.Length, - Disambiguation = result.Item.Disambiguation - }); } } + catch(HttpRequestException hre) + { + _logger.LogError(hre, "Error getting response from MusicBrainz"); + } return titleSearchResult.AsReadOnly(); } public async Task GetDetailsAsync(Guid id) { MusicBrainzTrack? track = null; - if (id != Guid.Empty) + try { - var query = new Query(Application, ApplicationVersion, ProjectUrl); - var recording = await query.LookupRecordingAsync(id, Include.Artists); - if (recording != null) + if (id != Guid.Empty) { - String artist = String.Empty; - if (recording.ArtistCredit != null) + var query = new Query(Application, ApplicationVersion, ProjectUrl); + var recording = await query.LookupRecordingAsync(id, Include.Artists); + if (recording != null) { - foreach (var artistCredit in recording.ArtistCredit) + String artist = String.Empty; + if (recording.ArtistCredit != null) { - artist += artistCredit.Name; - if (String.IsNullOrEmpty(artistCredit.JoinPhrase) == false) + foreach (var artistCredit in recording.ArtistCredit) { - artist += artistCredit.JoinPhrase; + 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 }; } - 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; } diff --git a/AudioCuesheetEditor/Extensions/SessionStateContainer.cs b/AudioCuesheetEditor/Extensions/SessionStateContainer.cs index d46385f6..cbc900d4 100644 --- a/AudioCuesheetEditor/Extensions/SessionStateContainer.cs +++ b/AudioCuesheetEditor/Extensions/SessionStateContainer.cs @@ -92,9 +92,14 @@ public CuesheetImportfile? CuesheetImportFile get { return cuesheetImportFile; } set { + if (cuesheetImportFile != null) + { + cuesheetImportFile.AnalysisFinished -= CuesheetImportFile_AnalysisFinished; + } cuesheetImportFile = value; if ((CuesheetImportFile != null) && (CuesheetImportFile.Cuesheet != null)) { + CuesheetImportFile.AnalysisFinished += CuesheetImportFile_AnalysisFinished; ImportCuesheet = CuesheetImportFile.Cuesheet; } else @@ -128,6 +133,22 @@ public ViewMode CurrentViewMode } } + public IImportfile? Importfile + { + get + { + if (TextImportFile != null) + { + return TextImportFile; + } + if (CuesheetImportFile != null) + { + return CuesheetImportFile; + } + return null; + } + } + public void ResetImport() { TextImportFile = null; @@ -169,5 +190,17 @@ private void Cuesheet_CuesheetImported(object? sender, EventArgs e) { CuesheetChanged?.Invoke(this, EventArgs.Empty); } + + void CuesheetImportFile_AnalysisFinished(object? sender, EventArgs e) + { + if (CuesheetImportFile != null) + { + ImportCuesheet = CuesheetImportFile.Cuesheet; + } + else + { + ImportCuesheet = null; + } + } } } diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs b/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs index b2eb9594..4b17102c 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/Cuesheet.cs @@ -239,7 +239,7 @@ public void RemoveSection(CuesheetSection section) return previousLinkedTrack; } - public void AddTrack(Track track, ApplicationOptions? applicationOptions = null) + public void AddTrack(Track track, ApplicationOptions? applicationOptions = null, RecordOptions? recordOptions = null) { if (track.IsCloned) { @@ -247,13 +247,14 @@ 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.Begin = CalculateTimeSpanWithSensitivity(DateTime.UtcNow - recordingStart.Value, applicationOptions.RecordTimeSensitivity); - } track.IsLinkedToPreviousTrack = applicationOptions.LinkTracksWithPreviousOne; } tracks.Add(track); @@ -418,13 +419,13 @@ public void StartRecording() recordingStart = DateTime.UtcNow; } - public void StopRecording(ApplicationOptions applicationOptions) + public void StopRecording(RecordOptions recordOptions) { //Set end of last track var lastTrack = Tracks.LastOrDefault(); if ((lastTrack != null) && (recordingStart.HasValue)) { - lastTrack.End = CalculateTimeSpanWithSensitivity(DateTime.UtcNow - recordingStart.Value, applicationOptions.RecordTimeSensitivity); + lastTrack.End = CalculateTimeSpanWithSensitivity(DateTime.UtcNow - recordingStart.Value, recordOptions.RecordTimeSensitivity); } recordingStart = null; } diff --git a/AudioCuesheetEditor/Model/Entity/Validateable.cs b/AudioCuesheetEditor/Model/Entity/Validateable.cs index 6c82a554..d0c78b3d 100644 --- a/AudioCuesheetEditor/Model/Entity/Validateable.cs +++ b/AudioCuesheetEditor/Model/Entity/Validateable.cs @@ -13,7 +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.Linq.Expressions; using System.Reflection; @@ -25,11 +24,18 @@ public abstract class Validateable : IValidateable public ValidationResult Validate(Expression> expression) { - if (expression.Body is not MemberExpression body) + if (expression.Body is MemberExpression memberExpression) { - throw new ArgumentException("'expression' should be a member expression"); + 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."); } - return Validate(body.Member.Name); } public ValidationResult Validate() @@ -40,7 +46,7 @@ public ValidationResult Validate() var result = Validate(property.Name); if (result.ValidationMessages != null) { - validationResult.ValidationMessages ??= new(); + validationResult.ValidationMessages ??= []; validationResult.ValidationMessages.AddRange(result.ValidationMessages); } switch (validationResult.Status) diff --git a/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs b/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs index 8fc38bc7..7107f45a 100644 --- a/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs +++ b/AudioCuesheetEditor/Model/IO/Export/CuesheetSection.cs @@ -38,7 +38,7 @@ public CuesheetSection(Cuesheet cuesheet) title = Cuesheet.Title; audiofileName = Cuesheet.Audiofile?.Name; //Try to set begin - begin = Cuesheet.Sections?.FirstOrDefault()?.End; + begin = Cuesheet.Sections?.LastOrDefault()?.End; if (Begin.HasValue == false) { begin = Cuesheet.Tracks.Min(x => x.Begin); diff --git a/AudioCuesheetEditor/Model/IO/Import/CuesheetImportfile.cs b/AudioCuesheetEditor/Model/IO/Import/CuesheetImportfile.cs index f9868c04..c5656c54 100644 --- a/AudioCuesheetEditor/Model/IO/Import/CuesheetImportfile.cs +++ b/AudioCuesheetEditor/Model/IO/Import/CuesheetImportfile.cs @@ -20,27 +20,49 @@ namespace AudioCuesheetEditor.Model.IO.Import { - public class CuesheetImportfile + public class CuesheetImportfile : IImportfile { - /// - /// File content (each element is a file line) - /// - public IReadOnlyCollection? FileContent { get; private set; } + private IEnumerable fileContent; - /// - /// File content with marking which passages has been reconized by scheme - /// - public IReadOnlyCollection? FileContentRecognized { get; private set; } + public EventHandler? AnalysisFinished; + + /// + public IEnumerable FileContent + { + get => fileContent; + set + { + fileContent = value; + Analyse(); + } + } + + /// + public IEnumerable FileContentRecognized { get; private set; } public Exception? AnalyseException { get; private set; } public Cuesheet? Cuesheet { get; private set; } + public ApplicationOptions ApplicationOptions { get; private set; } + + public CuesheetImportfile(MemoryStream fileContentStream, ApplicationOptions applicationOptions) + { + FileContentRecognized = []; + fileContentStream.Position = 0; + using var reader = new StreamReader(fileContentStream); + List lines = []; + while (reader.EndOfStream == false) + { + lines.Add(reader.ReadLine()); + } + fileContent = lines.AsReadOnly(); + ApplicationOptions = applicationOptions; + Analyse(); + } - public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicationOptions) + private void Analyse() { try { Cuesheet = new Cuesheet(); - fileContent.Position = 0; - using var reader = new StreamReader(fileContent); var cuesheetArtistGroupName = "CuesheetArtist"; var cuesheetTitleGroupName = "CuesheetTitle"; var cuesheetFileNameGroupName = "CuesheetFileName"; @@ -65,11 +87,10 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati var regexCDTextfile = new Regex("^" + CuesheetConstants.CuesheetCDTextfile + " \"(?'" + cuesheetCDTextfileGroupName + "'.{0,})\""); var regexCatalogueNumber = new Regex("^" + CuesheetConstants.CuesheetCatalogueNumber + " (?'" + cuesheetCatalogueNumberGroupName + "'.{0,})"); Track? track = null; - List lines = new(); - List? recognizedLines = new(); - while (reader.EndOfStream == false) + List lines = []; + List? recognizedLines = []; + foreach (var line in FileContent) { - var line = reader.ReadLine(); lines.Add(line); String? recognizedLine = line; if (String.IsNullOrEmpty(line) == false) @@ -214,9 +235,9 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati var matchGroup = match.Groups.GetValueOrDefault(trackPreGapGroupName); if (matchGroup != null) { - var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(":"))); - var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(":") + 1, 2)); - var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(":") + 1)); + var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(':'))); + var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(':') + 1, 2)); + var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(':') + 1)); if (track != null) { track.PreGap = new TimeSpan(0, 0, minutes, seconds, Convert.ToInt32((frames / 75.0) * 1000)); @@ -238,9 +259,9 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati var matchGroup = match.Groups.GetValueOrDefault(trackIndex01GroupName); if (matchGroup != null) { - var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(":"))); - var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(":") + 1, 2)); - var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(":") + 1)); + var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(':'))); + var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(':') + 1, 2)); + var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(':') + 1)); if (track != null) { track.Begin = new TimeSpan(0, 0, minutes, seconds, Convert.ToInt32((frames / 75.0) * 1000)); @@ -256,7 +277,7 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati } if (track != null) { - Cuesheet.AddTrack(track, applicationOptions); + Cuesheet.AddTrack(track, ApplicationOptions); } else { @@ -270,9 +291,9 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati var matchGroup = match.Groups.GetValueOrDefault(trackPostGapGroupName); if (matchGroup != null) { - var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(":"))); - var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(":") + 1, 2)); - var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(":") + 1)); + var minutes = int.Parse(matchGroup.Value.Substring(0, matchGroup.Value.IndexOf(':'))); + var seconds = int.Parse(matchGroup.Value.Substring(matchGroup.Value.IndexOf(':') + 1, 2)); + var frames = int.Parse(matchGroup.Value.Substring(matchGroup.Value.LastIndexOf(':') + 1)); if (track != null) { track.PostGap = new TimeSpan(0, 0, minutes, seconds, Convert.ToInt32((frames / 75.0) * 1000)); @@ -290,15 +311,16 @@ public CuesheetImportfile(MemoryStream fileContent, ApplicationOptions applicati } recognizedLines.Add(recognizedLine); } - FileContent = lines.AsReadOnly(); + fileContent = lines.AsReadOnly(); FileContentRecognized = recognizedLines.AsReadOnly(); } - catch(Exception ex) + catch (Exception ex) { AnalyseException = ex; Cuesheet = null; - FileContentRecognized = null; + FileContentRecognized = FileContent; } + AnalysisFinished?.Invoke(this, EventArgs.Empty); } } } diff --git a/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs b/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs new file mode 100644 index 00000000..44bd3490 --- /dev/null +++ b/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs @@ -0,0 +1,29 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. +namespace AudioCuesheetEditor.Model.IO.Import +{ + public interface IImportfile + { + /// + /// File content (each element is a file line) + /// + public IEnumerable FileContent { get; set; } + /// + /// File content with marking which passages has been reconized by scheme + /// + public IEnumerable FileContentRecognized { get; } + } +} diff --git a/AudioCuesheetEditor/Model/IO/Import/TextImportfile.cs b/AudioCuesheetEditor/Model/IO/Import/TextImportfile.cs index 9cacc17f..3e38db41 100644 --- a/AudioCuesheetEditor/Model/IO/Import/TextImportfile.cs +++ b/AudioCuesheetEditor/Model/IO/Import/TextImportfile.cs @@ -17,18 +17,12 @@ using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.Utility; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace AudioCuesheetEditor.Model.IO.Import { - public class TextImportfile : IDisposable + public class TextImportfile : IImportfile, IDisposable { public const String MimeType = "text/plain"; public const String FileExtension = ".txt"; @@ -38,12 +32,15 @@ public class TextImportfile : IDisposable private TextImportScheme textImportScheme; private TimeSpanFormat? timeSpanFormat; private bool disposedValue; + private IEnumerable fileContent; - public TextImportfile(MemoryStream fileContent, ImportOptions? importOptions = null) + public TextImportfile(MemoryStream fileContentStream, ImportOptions? importOptions = null) { + FileContentRecognized = []; textImportScheme = new TextImportScheme(); - fileContent.Position = 0; - using var reader = new StreamReader(fileContent); + fileContent = []; + fileContentStream.Position = 0; + using var reader = new StreamReader(fileContentStream); List lines = []; while (reader.EndOfStream == false) { @@ -65,15 +62,19 @@ public TextImportfile(MemoryStream fileContent, ImportOptions? importOptions = n } } - /// - /// File content (each element is a file line) - /// - public IReadOnlyCollection FileContent { get; private set; } + /// + public IEnumerable FileContent + { + get => fileContent; + set + { + fileContent = value; + Analyse(); + } + } - /// - /// File content with marking which passages has been reconized by scheme - /// - public IReadOnlyCollection? FileContentRecognized { get; private set; } + /// + public IEnumerable FileContentRecognized { get; private set; } public TextImportScheme TextImportScheme { @@ -129,7 +130,7 @@ private void Analyse() try { Cuesheet = new Cuesheet(); - FileContentRecognized = null; + FileContentRecognized = []; AnalyseException = null; Boolean cuesheetRecognized = false; List recognizedFileContent = []; diff --git a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs index cb76aa6a..2f33d15d 100644 --- a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs +++ b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs @@ -16,7 +16,6 @@ using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.Entity; using AudioCuesheetEditor.Model.IO; -using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Export; using AudioCuesheetEditor.Model.Utility; using System.Globalization; @@ -91,27 +90,8 @@ public String? ViewModename } } } - public String RecordedAudiofilename { get; set; } = Audiofile.RecordingFileName; public Boolean LinkTracksWithPreviousOne { get; set; } = true; public String? ProjectFilename { get; set; } = Projectfile.DefaultFilename; - public uint RecordCountdownTimer { get; set; } = 5; - [JsonIgnore] - public TimeSensitivityMode RecordTimeSensitivity { get; set; } - 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)); - } - } - } public TimeSpanFormat? TimeSpanFormat { get; set; } protected override ValidationResult Validate(string property) @@ -143,29 +123,6 @@ protected override ValidationResult Validate(string property) } } break; - 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; case nameof(ProjectFilename): validationStatus = ValidationStatus.Success; if (String.IsNullOrEmpty(ProjectFilename)) diff --git a/AudioCuesheetEditor/Model/Options/RecordOptions.cs b/AudioCuesheetEditor/Model/Options/RecordOptions.cs new file mode 100644 index 00000000..2a5f365d --- /dev/null +++ b/AudioCuesheetEditor/Model/Options/RecordOptions.cs @@ -0,0 +1,76 @@ +//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/Pages/Help.razor b/AudioCuesheetEditor/Pages/Help.razor index 8562dc68..fcfd2b33 100644 --- a/AudioCuesheetEditor/Pages/Help.razor +++ b/AudioCuesheetEditor/Pages/Help.razor @@ -57,79 +57,85 @@ along with Foobar. If not, see
@_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"]) diff --git a/AudioCuesheetEditor/Pages/ImportFileView.razor b/AudioCuesheetEditor/Pages/ImportFileView.razor new file mode 100644 index 00000000..f88c801c --- /dev/null +++ b/AudioCuesheetEditor/Pages/ImportFileView.razor @@ -0,0 +1,101 @@ + +@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 != 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/RecordControl.razor b/AudioCuesheetEditor/Pages/RecordControl.razor new file mode 100644 index 00000000..3fded530 --- /dev/null +++ b/AudioCuesheetEditor/Pages/RecordControl.razor @@ -0,0 +1,294 @@ + +@implements IDisposable + +@inject SessionStateContainer _sessionStateContainer +@inject ITextLocalizer _localizer +@inject ITextLocalizerService _localizationService +@inject LocalStorageOptionsProvider _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 void Dispose() + { + hotKeysContext?.Dispose(); + _localizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionsSaved; + } + + 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 index 54005a68..758628e8 100644 --- a/AudioCuesheetEditor/Pages/ViewModeImport.razor +++ b/AudioCuesheetEditor/Pages/ViewModeImport.razor @@ -119,27 +119,14 @@ along with Foobar. If not, see - @if (FileContentRecognized != null) - { - - - - - - -
-                                                @foreach(var line in FileContentRecognized)
-                                                {
-                                                    if (line != null)
-                                                    {
-                                                        @((MarkupString)String.Format("{0}
", line)) - } - } -
-
-
-
- } + + + + + + + + @if (_sessionStateContainer.TextImportFile != null) { @@ -328,22 +315,6 @@ along with Foobar. If not, see HotKeysContext? hotKeysContext; Validations? validations; - public IReadOnlyCollection? FileContentRecognized - { - get - { - if (_sessionStateContainer.TextImportFile != null) - { - return _sessionStateContainer.TextImportFile.FileContentRecognized; - } - if (_sessionStateContainer.CuesheetImportFile != null) - { - return _sessionStateContainer.CuesheetImportFile.FileContentRecognized; - } - return null; - } - } - public void Dispose() { hotKeysContext?.Dispose(); diff --git a/AudioCuesheetEditor/Pages/ViewModeRecord.razor b/AudioCuesheetEditor/Pages/ViewModeRecord.razor index 3f9c6638..b68a38fd 100644 --- a/AudioCuesheetEditor/Pages/ViewModeRecord.razor +++ b/AudioCuesheetEditor/Pages/ViewModeRecord.razor @@ -29,60 +29,20 @@ along with Foobar. If not, see + + + + + + + + - @if (_sessionStateContainer.Cuesheet.IsRecording == true) - { - var backgroundCSS = _sessionStateContainer.Cuesheet.IsRecording ? "BackgroundBlink rounded" : "rounded"; -
- @_localizer["Record running!"] -
- } - @if ((startRecordTimer != null) && (startRecordTimer.Enabled)) - { - - } - - - - - - - @_localizer["Start record 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) - } - - - - - +
@@ -169,33 +129,33 @@ along with Foobar. If not, see }
- - @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 async Task AudioRecordingFinished(String url) + public Task AudioRecordingFinished(String url) { - var options = await _localStorageOptionsProvider.GetOptions(); - var audiofile = new Audiofile(options.RecordedAudiofilename, url, Audiofile.AudioCodecWEBM, _httpClient, true); + 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; } - Timer updateGUITimer = default!; - Timer? startRecordTimer; - - DateTime recordTimerStarted; - - ModalDialog? modalDialog; - Autocomplete? autocompleteArtist; Autocomplete? autocompleteTitle; @@ -205,6 +165,7 @@ along with Foobar. If not, see Boolean recordControlVisible = true; Boolean enterNewTrackVisible = true; Boolean cuesheetTracksVisible = true; + Boolean recordOptionsVisible = false; IEnumerable? autocompleteArtists; IEnumerable? autocompleteTitles; @@ -215,86 +176,12 @@ along with Foobar. If not, see 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; - - updateGUITimer = new Timer(500); - 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(); - } - }; - var options = await _localStorageOptionsProvider.GetOptions(); - startRecordTimer = new Timer(options.RecordCountdownTimer * 1000); - startRecordTimer.Elapsed += async delegate - { - await StartRecordingClicked(); - startRecordTimer.Stop(); - }; - } - - private 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 _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(); - } - } - } - - private async Task StopRecordingClicked() - { - await _jsRuntime.InvokeVoidAsync("stopAudioRecording"); - var options = await _localStorageOptionsProvider.GetOptions(); - _sessionStateContainer.Cuesheet.StopRecording(options); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionsSaved; } async Task OnKeyDownRecordArtist(KeyboardEventArgs args) @@ -331,8 +218,7 @@ along with Foobar. If not, see { if (_sessionStateContainer.Cuesheet.IsRecording == true) { - var options = await _localStorageOptionsProvider.GetOptions(); - _sessionStateContainer.Cuesheet.AddTrack(currentRecordingTrack, options); + _sessionStateContainer.Cuesheet.AddTrack(currentRecordingTrack, applicationOptions, recordOptions); currentRecordingTrack = new Track(); if (autocompleteTitle != null) { @@ -347,16 +233,6 @@ along with Foobar. If not, see } } - private void StartRecordCountdownTimer() - { - recordTimerStarted = DateTime.Now; - if (startRecordTimer != null) - { - startRecordTimer.Start(); - } - updateGUITimer.Start(); - } - private void LocalizationService_LocalizationChanged(object? sender, EventArgs args) { StateHasChanged(); @@ -402,4 +278,35 @@ along with Foobar. If not, see } } } + + 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 45bdad06..287ee61f 100644 --- a/AudioCuesheetEditor/Program.cs +++ b/AudioCuesheetEditor/Program.cs @@ -17,12 +17,12 @@ using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Data.Services; using AudioCuesheetEditor.Extensions; -using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.UI; using AudioCuesheetEditor.Model.Utility; 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; @@ -39,7 +39,8 @@ options.Debounce = true; options.DebounceInterval = 300; }) -.AddBootstrap5Providers(); +.AddBootstrap5Providers() +.AddFontAwesomeIcons(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json b/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json new file mode 100644 index 00000000..30df6a1e --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/de.json @@ -0,0 +1,12 @@ +{ + "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 new file mode 100644 index 00000000..efe746c3 --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/EditRecordOptions/en.json @@ -0,0 +1,11 @@ +{ + "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/Help/de.json b/AudioCuesheetEditor/Resources/Localization/Help/de.json index c605718f..a9531a2c 100644 --- a/AudioCuesheetEditor/Resources/Localization/Help/de.json +++ b/AudioCuesheetEditor/Resources/Localization/Help/de.json @@ -16,14 +16,14 @@ "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
", + "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 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.

Dateiname für aufgenommene Audiodaten

Gibt den standardmäßig genutzten Dateinamen für die Audiodatei bei Benutzung des Aufnahmemodus an.

Aufnahmecountdown in Sekunden

Geben Sie an, wie viele Sekunden der Countdown laufen soll, bevor die Aufnahme aktiv geschaltet wird.

Genauigkeit der Aufnahmezeiten

Wählen Sie aus, wie die Aufnahmezeiten dargestellt werden sollen.", + "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", @@ -31,6 +31,8 @@ "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." + "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 index 58e14c3d..e2a4ecf5 100644 --- a/AudioCuesheetEditor/Resources/Localization/Help/en.json +++ b/AudioCuesheetEditor/Resources/Localization/Help/en.json @@ -16,14 +16,14 @@ "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
", + "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.", + "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 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.

Audio filename of recording

Sets the default filename used for recording if audio capture is activated.

Record countdown timer in seconds

Enter how much seconds you want to use for countdown, if using countdown timer record mode.

Recordtime sensitivity

Select how recording times will be represented.", + "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", @@ -31,6 +31,8 @@ "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." + "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 new file mode 100644 index 00000000..1eab9393 --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/ImportFileView/de.json @@ -0,0 +1,7 @@ +{ + "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 new file mode 100644 index 00000000..1a2e8cea --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/ImportFileView/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "translations": { + "Preview": "Preview", + "Edit": "Edit" + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json b/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json new file mode 100644 index 00000000..4de5cadf --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/RecordControl/de.json @@ -0,0 +1,16 @@ +{ + "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 new file mode 100644 index 00000000..20daa93e --- /dev/null +++ b/AudioCuesheetEditor/Resources/Localization/RecordControl/en.json @@ -0,0 +1,16 @@ +{ + "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/ViewModeRecord/de.json b/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json index 7df97489..9fd846dc 100644 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json +++ b/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/de.json @@ -27,6 +27,7 @@ "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 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 index b358eda7..c84a3a9f 100644 --- a/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/en.json +++ b/AudioCuesheetEditor/Resources/Localization/ViewModeRecord/en.json @@ -27,6 +27,7 @@ "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 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/Shared/EditRecordOptions.razor b/AudioCuesheetEditor/Shared/EditRecordOptions.razor new file mode 100644 index 00000000..4414f1f0 --- /dev/null +++ b/AudioCuesheetEditor/Shared/EditRecordOptions.razor @@ -0,0 +1,111 @@ + +@implements IDisposable + +@inject ITextLocalizer _localizer +@inject LocalStorageOptionsProvider _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/MainLayout.razor b/AudioCuesheetEditor/Shared/MainLayout.razor index 0d9acf2d..b79f7e52 100644 --- a/AudioCuesheetEditor/Shared/MainLayout.razor +++ b/AudioCuesheetEditor/Shared/MainLayout.razor @@ -461,9 +461,9 @@ along with Foobar. If not, see _traceChangeManager.UndoDone += TraceChangeManager_UndoDone; _traceChangeManager.RedoDone += TraceChangeManager_RedoDone; hotKeysContext = _hotKeys.CreateContext() - .Add(ModKey.Ctrl, Key.h, () => _navigationManager.NavigateTo("Help")) - .Add(ModKey.Ctrl, Key.e, OnStrgEKeyDown) - .Add(ModKey.Ctrl, Key.s, OnStrgSKeyDown) + .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(); @@ -643,14 +643,26 @@ along with Foobar. If not, see } } - async ValueTask OnStrgEKeyDown() + ValueTask OnCtrlHKeyDown() { - await OnDisplayExportProfilesClicked(); + if (ShortCutsEnabled) + { + _navigationManager.NavigateTo("Help"); + } + return ValueTask.CompletedTask; } - async ValueTask OnStrgSKeyDown() + async ValueTask OnCtrlEKeyDown() { - if (modalDownloadProjectfile != null) + if (ShortCutsEnabled) + { + await OnDisplayExportProfilesClicked(); + } + } + + async ValueTask OnCtrlSKeyDown() + { + if ((ShortCutsEnabled) && (modalDownloadProjectfile != null)) { await modalDownloadProjectfile.Show(); } @@ -885,4 +897,12 @@ along with Foobar. If not, see } 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/OptionsDialog.razor b/AudioCuesheetEditor/Shared/OptionsDialog.razor index d1e23330..94820ff4 100644 --- a/AudioCuesheetEditor/Shared/OptionsDialog.razor +++ b/AudioCuesheetEditor/Shared/OptionsDialog.razor @@ -42,7 +42,6 @@ along with Foobar. If not, see @_localizer["Common settings"] - @_localizer["Record settings"] @@ -135,37 +134,6 @@ along with Foobar. If not, see - - - - @_localizer["Filename for recorded audio"] - - - - - - - - - - - @_localizer["Record countdown timer in seconds"] - - - - - - @_localizer["Record time sensitivity"] - - - - - diff --git a/AudioCuesheetEditor/wwwroot/icon-192.png b/AudioCuesheetEditor/wwwroot/icon-192.png new file mode 100644 index 00000000..700add90 Binary files /dev/null and b/AudioCuesheetEditor/wwwroot/icon-192.png differ diff --git a/AudioCuesheetEditor/wwwroot/icon-512.png b/AudioCuesheetEditor/wwwroot/icon-512.png new file mode 100644 index 00000000..751bdc2b Binary files /dev/null and b/AudioCuesheetEditor/wwwroot/icon-512.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_de.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_de.png index 411859e1..288c30e6 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_de.png and b/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_de.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_en.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_en.png index 398aaba3..1a036753 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_en.png and b/AudioCuesheetEditor/wwwroot/images/ImportTextfile2_en.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_de.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_de.png deleted file mode 100644 index f940c499..00000000 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_de.png and /dev/null differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_en.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_en.png deleted file mode 100644 index 7979c23f..00000000 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile6_en.png and /dev/null differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_de.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_de.png index 038bd0d3..506c549d 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_de.png and b/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_de.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_en.png b/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_en.png index c7dccdf5..e2a3aadb 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_en.png and b/AudioCuesheetEditor/wwwroot/images/ImportTextfile9_en.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/Options1_de.png b/AudioCuesheetEditor/wwwroot/images/Options1_de.png index 2beb0379..8d745c11 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/Options1_de.png and b/AudioCuesheetEditor/wwwroot/images/Options1_de.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/Options1_en.png b/AudioCuesheetEditor/wwwroot/images/Options1_en.png index b4fc3e99..0e8b1e5e 100644 Binary files a/AudioCuesheetEditor/wwwroot/images/Options1_en.png and b/AudioCuesheetEditor/wwwroot/images/Options1_en.png differ diff --git a/AudioCuesheetEditor/wwwroot/images/Options3_de.png b/AudioCuesheetEditor/wwwroot/images/Options3_de.png deleted file mode 100644 index ef2fc313..00000000 Binary files a/AudioCuesheetEditor/wwwroot/images/Options3_de.png and /dev/null differ diff --git a/AudioCuesheetEditor/wwwroot/images/Options3_en.png b/AudioCuesheetEditor/wwwroot/images/Options3_en.png deleted file mode 100644 index 92085569..00000000 Binary files a/AudioCuesheetEditor/wwwroot/images/Options3_en.png and /dev/null differ diff --git a/AudioCuesheetEditor/wwwroot/index.html b/AudioCuesheetEditor/wwwroot/index.html index 68465a1f..952379fc 100644 --- a/AudioCuesheetEditor/wwwroot/index.html +++ b/AudioCuesheetEditor/wwwroot/index.html @@ -6,15 +6,44 @@ AudioCuesheetEditor + + + + + + + + + + + + + + + + + + + + + + + + + + + + +