Skip to content

Commit e8d6d5f

Browse files
Merge pull request #429 from NeoCoderMatrix86/413-add-timespan-display-formats
Add timespan display formats
2 parents 8f50790 + c54c96c commit e8d6d5f

8 files changed

Lines changed: 185 additions & 13 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//This file is part of AudioCuesheetEditor.
2+
3+
//AudioCuesheetEditor is free software: you can redistribute it and/or modify
4+
//it under the terms of the GNU General Public License as published by
5+
//the Free Software Foundation, either version 3 of the License, or
6+
//(at your option) any later version.
7+
8+
//AudioCuesheetEditor is distributed in the hope that it will be useful,
9+
//but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
//GNU General Public License for more details.
12+
13+
//You should have received a copy of the GNU General Public License
14+
//along with Foobar. If not, see
15+
//<http: //www.gnu.org/licenses />.
16+
using AudioCuesheetEditor.Data.Options;
17+
using AudioCuesheetEditor.Model.AudioCuesheet;
18+
using AudioCuesheetEditor.Model.Options;
19+
using AudioCuesheetEditor.Services.UI;
20+
using Microsoft.VisualStudio.TestTools.UnitTesting;
21+
using Moq;
22+
using System;
23+
using System.Linq.Expressions;
24+
using System.Threading.Tasks;
25+
26+
namespace AudioCuesheetEditor.Tests.Services.UI
27+
{
28+
[TestClass()]
29+
public class ApplicationOptionsTimeSpanParserTests
30+
{
31+
[TestMethod]
32+
public async Task TimespanTextChanged_ValidInput_SetsPropertyCorrectly()
33+
{
34+
// Arrange
35+
var options = new ApplicationOptions()
36+
{
37+
TimeSpanFormat = new()
38+
{
39+
Scheme = "Minutes:Seconds"
40+
}
41+
};
42+
var mockOptionsProvider = new Mock<ILocalStorageOptionsProvider>();
43+
mockOptionsProvider
44+
.Setup(p => p.GetOptions<ApplicationOptions>())
45+
.ReturnsAsync(options);
46+
var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object);
47+
await Task.Delay(50);
48+
var track = new Track();
49+
// Act
50+
await parser.TimespanTextChanged(track, x => x.Begin, "92:12");
51+
// Assert
52+
Assert.AreEqual(new TimeSpan(1, 32, 12), track.Begin);
53+
}
54+
55+
[TestMethod]
56+
public async Task TimespanTextChanged_InvalidInput_SetsNull()
57+
{
58+
var options = new ApplicationOptions()
59+
{
60+
TimeSpanFormat = new()
61+
{
62+
Scheme = "Minutes:Seconds"
63+
}
64+
};
65+
var mockOptionsProvider = new Mock<ILocalStorageOptionsProvider>();
66+
mockOptionsProvider
67+
.Setup(p => p.GetOptions<ApplicationOptions>())
68+
.ReturnsAsync(options);
69+
var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object);
70+
await Task.Delay(50);
71+
var track = new Track();
72+
// Act
73+
await parser.TimespanTextChanged(track, x => x.End, "not a time");
74+
// Assert
75+
Assert.IsNull(track.End);
76+
}
77+
78+
[TestMethod()]
79+
public async Task GetTimespanFormatted_ValidFormat_ReturnsCorrectString()
80+
{
81+
// Arrange
82+
var options = new ApplicationOptions()
83+
{
84+
DisplayTimeSpanFormat = @"hh\:mm\:ss"
85+
};
86+
var mockOptionsProvider = new Mock<ILocalStorageOptionsProvider>();
87+
mockOptionsProvider
88+
.Setup(p => p.GetOptions<ApplicationOptions>())
89+
.ReturnsAsync(options);
90+
var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object);
91+
await Task.Delay(50);
92+
// Act
93+
var result = parser.GetTimespanFormatted(new TimeSpan(0, 1, 30, 27, 200, 103));
94+
// Assert
95+
Assert.AreEqual("01:30:27", result);
96+
}
97+
98+
[TestMethod()]
99+
public async Task GetTimespanFormatted_InvalidFormat_FallbackToDefault()
100+
{
101+
// Arrange
102+
var options = new ApplicationOptions()
103+
{
104+
DisplayTimeSpanFormat = "INVALID_FORMAT"
105+
};
106+
var mockOptionsProvider = new Mock<ILocalStorageOptionsProvider>();
107+
mockOptionsProvider
108+
.Setup(p => p.GetOptions<ApplicationOptions>())
109+
.ReturnsAsync(options);
110+
var parser = new ApplicationOptionsTimeSpanParser(mockOptionsProvider.Object);
111+
await Task.Delay(50);
112+
// Act
113+
var result = parser.GetTimespanFormatted(new TimeSpan(0, 1, 30, 27, 200, 103));
114+
// Assert
115+
Assert.AreEqual("01:30:27.2001030", result);
116+
}
117+
}
118+
}

AudioCuesheetEditor/Model/Options/ApplicationOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public String? ProjectFilename
105105
public TimeSpanFormat ImportTimeSpanFormat { get; set; } = new();
106106
public uint RecordCountdownTimer { get; set; } = 5;
107107
public Boolean FixedTracksTableHeader { get; set; } = false;
108+
public String? DisplayTimeSpanFormat { get; set; }
108109

109110
public override ValidationResult Validate(string property)
110111
{

AudioCuesheetEditor/Services/UI/ApplicationOptionsTimeSpanParser.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ public async Task TimespanTextChanged<T, TProperty>(T entity, Expression<Func<T,
5656
}
5757
}
5858

59+
public string? GetTimespanFormatted(TimeSpan? timeSpan)
60+
{
61+
string? formatted = null;
62+
if (timeSpan.HasValue)
63+
{
64+
try
65+
{
66+
formatted = timeSpan.Value.ToString(applicationOptions?.DisplayTimeSpanFormat);
67+
}
68+
catch (FormatException)
69+
{
70+
formatted = timeSpan.Value.ToString();
71+
}
72+
}
73+
return formatted;
74+
}
75+
5976
protected virtual void Dispose(bool disposing)
6077
{
6178
if (!disposedValue)

AudioCuesheetEditor/Shared/Cuesheet/EditSections.razor

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ along with Foobar. If not, see
4545
</MudButtonGroup>
4646

4747
<MudDataGrid Items="Cuesheet?.Sections" ReadOnly="false" Bordered EditTrigger="DataGridEditTrigger.OnRowClick" EditMode="DataGridEditMode.Cell" ColumnResizeMode="ResizeMode.Column"
48-
MultiSelection SelectOnRowClick="false" @bind-SelectedItems="selectedSections">
48+
MultiSelection SelectOnRowClick="false" @bind-SelectedItems="selectedSections">
4949
<Columns>
5050
<SelectColumn />
5151
<PropertyColumn Property="x => x.Begin" Title="@_localizer["Begin"]">
5252
<EditTemplate>
53-
<MudTextField Value="@context.Item.Begin.ToString()" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<CuesheetSection, TimeSpan?>(context.Item, x => x.Begin, value)"
54-
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(CuesheetSection.Begin)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(CuesheetSection.Begin))" />
53+
<MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<CuesheetSection, TimeSpan?>(context.Item, x => x.Begin, value)"
54+
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(CuesheetSection.Begin)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(CuesheetSection.Begin))" />
5555
</EditTemplate>
5656
</PropertyColumn>
5757
<PropertyColumn Property="x => x.End" Title="@_localizer["End"]">
5858
<EditTemplate>
59-
<MudTextField Value="@context.Item.End.ToString()" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<CuesheetSection, TimeSpan?>(context.Item, x => x.End, value)"
60-
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(CuesheetSection.End)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(CuesheetSection.End))" />
59+
<MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<CuesheetSection, TimeSpan?>(context.Item, x => x.End, value)"
60+
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(CuesheetSection.End)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(CuesheetSection.End))" />
6161
</EditTemplate>
6262
</PropertyColumn>
6363
<PropertyColumn Property="x => x.Artist" Title="@_localizer["Cuesheet artist"]">
@@ -73,7 +73,7 @@ MultiSelection SelectOnRowClick="false" @bind-SelectedItems="selectedSections">
7373
<PropertyColumn Property="x => x.AudiofileName" Title="@_localizer["Audio file"]">
7474
<EditTemplate>
7575
<FileInput Label="@_localizer["Audiofile"]" FileName="@context.Item.AudiofileName" OnFileSelected="(file) => AudiofileSelected(context.Item, file)" Error="@GetValidationErrorMessage(context.Item, nameof(CuesheetSection.AudiofileName))" Filter="@String.Join(",", Audiofile.AudioCodecs.Select(x => x.MimeType))"
76-
DisplayMenu="false" />
76+
DisplayMenu="false" />
7777
</EditTemplate>
7878
</PropertyColumn>
7979
</Columns>

AudioCuesheetEditor/Shared/Dialogs/OptionsDialog.de.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
<data name="Days" xml:space="preserve">
127127
<value>Tage</value>
128128
</data>
129+
<data name="Display" xml:space="preserve">
130+
<value>Anzeige</value>
131+
</data>
129132
<data name="Hours" xml:space="preserve">
130133
<value>Stunden</value>
131134
</data>
@@ -141,7 +144,13 @@
141144
<data name="Seconds" xml:space="preserve">
142145
<value>Sekunden</value>
143146
</data>
147+
<data name="Time display format" xml:space="preserve">
148+
<value>Anzeigeformat Zeit</value>
149+
</data>
144150
<data name="Time input format" xml:space="preserve">
145151
<value>Eingabeformat Zeit</value>
146152
</data>
153+
<data name="Uses .NET format, check help for more information" xml:space="preserve">
154+
<value>Benutzt .NET format, bitte prüfen sie die Hilfe für mehr Informationen</value>
155+
</data>
147156
</root>

AudioCuesheetEditor/Shared/Dialogs/OptionsDialog.razor

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,23 @@ along with Foobar. If not, see
2222

2323
<MudDialog>
2424
<DialogContent>
25-
<MudText Typo="Typo.h6"><b>@_localizer["Input"]</b></MudText>
26-
<MudSwitch T="Boolean?" Value="ApplicationOptions?.LinkTracks" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValue<ApplicationOptions>(x => x.LinkTracks, newValue)" Label="@_localizer["Automatically link tracks"]" Color="Color.Primary" />
25+
<MudText Typo="Typo.subtitle1"><b>@_localizer["Input"]</b></MudText>
26+
<MudSwitch T="Boolean?" Value="ApplicationOptions?.LinkTracks" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValue<ApplicationOptions>(x => x.LinkTracks, newValue)"
27+
Label="@_localizer["Automatically link tracks"]" Color="Color.Primary" />
2728
<MudStack Row AlignItems="AlignItems.Baseline">
2829
<MudTextField T="string" @ref="timeInputFormatTextField" Label="@_localizer["Time input format"]" Text="@ApplicationOptions?.TimeSpanFormat?.Scheme" TextChanged="TimeInputFormatChangedAsync"
29-
Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))) == false"
30-
ErrorText="@GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))" Variant="Variant.Outlined" />
30+
Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))) == false"
31+
ErrorText="@GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))" Variant="Variant.Outlined" />
3132
<MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown" Class="mt-1" Style="height: 56px;">
3233
@foreach(var scheme in TimeSpanFormat.AvailableTimespanScheme)
3334
{
3435
<MudMenuItem OnClick="() => AppendPlaceholderToTimeInputFormatTextField(scheme)">@_localizer[scheme]</MudMenuItem>
3536
}
3637
</MudMenu>
3738
</MudStack>
39+
<MudText Typo="Typo.subtitle1"><b>@_localizer["Display"]</b></MudText>
40+
<MudTextField T="string" Label="@_localizer["Time display format"]" Text="@ApplicationOptions?.DisplayTimeSpanFormat" TextChanged="DisplayTimeSpanFormatChangedAsync"
41+
Clearable Variant="Variant.Outlined" HelperText="@_localizer["Uses .NET format, check help for more information"]" />
3842
</DialogContent>
3943
</MudDialog>
4044

@@ -59,6 +63,11 @@ along with Foobar. If not, see
5963
await LocalStorageOptionsProvider.SaveOptionsValue<ApplicationOptions>(x => x.TimeSpanFormat!, timeSpanFormat);
6064
}
6165

66+
async Task DisplayTimeSpanFormatChangedAsync(string newValue)
67+
{
68+
await LocalStorageOptionsProvider.SaveOptionsValue<ApplicationOptions>(x => x.DisplayTimeSpanFormat, newValue);
69+
}
70+
6271
void AppendPlaceholderToTimeInputFormatTextField(string placeholder)
6372
{
6473
timeInputFormatTextField?.SetText($"{timeInputFormatTextField.Text}{placeholder}");

AudioCuesheetEditor/Shared/Dialogs/OptionsDialog.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
<data name="Days" xml:space="preserve">
127127
<value>Days</value>
128128
</data>
129+
<data name="Display" xml:space="preserve">
130+
<value>Display</value>
131+
</data>
129132
<data name="Hours" xml:space="preserve">
130133
<value>Hours</value>
131134
</data>
@@ -141,7 +144,13 @@
141144
<data name="Seconds" xml:space="preserve">
142145
<value>Seconds</value>
143146
</data>
147+
<data name="Time display format" xml:space="preserve">
148+
<value>Time display format</value>
149+
</data>
144150
<data name="Time input format" xml:space="preserve">
145151
<value>Time input format</value>
146152
</data>
153+
<data name="Uses .NET format, check help for more information" xml:space="preserve">
154+
<value>Uses .NET format, check help for more information</value>
155+
</data>
147156
</root>

AudioCuesheetEditor/Shared/TrackList/TrackList.razor

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,29 @@ along with Foobar. If not, see
115115
</EditTemplate>
116116
</PropertyColumn>
117117
<PropertyColumn Property="x => x.Begin" Title="@_localizer["Begin"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;">
118+
<CellTemplate>
119+
@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin)
120+
</CellTemplate>
118121
<EditTemplate>
119-
<MudTextField Value="@context.Item.Begin.ToString()" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Begin, value)"
122+
<MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Begin, value)"
120123
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Begin)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Begin))" />
121124
</EditTemplate>
122125
</PropertyColumn>
123126
<PropertyColumn Property="x => x.End" Title="@_localizer["End"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;">
127+
<CellTemplate>
128+
@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End)
129+
</CellTemplate>
124130
<EditTemplate>
125-
<MudTextField Value="@context.Item.End.ToString()" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.End, value)"
131+
<MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.End, value)"
126132
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.End)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.End))" />
127133
</EditTemplate>
128134
</PropertyColumn>
129135
<PropertyColumn Property="x => x.Length" Title="@_localizer["Length"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;">
136+
<CellTemplate>
137+
@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length)
138+
</CellTemplate>
130139
<EditTemplate>
131-
<MudTextField Value="@context.Item.Length.ToString()" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Length, value)"
140+
<MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Length, value)"
132141
Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Length)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Length))" />
133142
</EditTemplate>
134143
</PropertyColumn>

0 commit comments

Comments
 (0)