mirror of https://github.com/ErsatzTV/ErsatzTV.git
21 changed files with 644 additions and 3 deletions
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public record SyncCollectionFromTraktList |
||||
(int CollectionId, string TraktListUrl) : IRequest<Either<BaseError, Unit>>; |
||||
} |
||||
@ -0,0 +1,269 @@
@@ -0,0 +1,269 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.RegularExpressions; |
||||
using System.Threading; |
||||
using System.Threading.Channels; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.Playouts.Commands; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Trakt; |
||||
using ErsatzTV.Core.Trakt; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
using Unit = LanguageExt.Unit; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public class |
||||
SyncCollectionFromTraktListHandler : IRequestHandler<SyncCollectionFromTraktList, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly ITraktApiClient _traktApiClient; |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly ILogger<SyncCollectionFromTraktListHandler> _logger; |
||||
|
||||
public SyncCollectionFromTraktListHandler( |
||||
ITraktApiClient traktApiClient, |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
ILogger<SyncCollectionFromTraktListHandler> logger) |
||||
{ |
||||
_traktApiClient = traktApiClient; |
||||
_dbContextFactory = dbContextFactory; |
||||
_mediaCollectionRepository = mediaCollectionRepository; |
||||
_channel = channel; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
SyncCollectionFromTraktList request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
// validate and parse user/list from URL
|
||||
const string PATTERN = @"users\/([\w\-_]+)\/lists\/([\w\-_]+)"; |
||||
Match match = Regex.Match(request.TraktListUrl, PATTERN); |
||||
if (match.Success) |
||||
{ |
||||
string user = match.Groups[1].Value; |
||||
string list = match.Groups[2].Value; |
||||
|
||||
Either<BaseError, List<TraktListItemWithGuids>> maybeItems = |
||||
await _traktApiClient.GetUserListItems(user, list); |
||||
return await maybeItems.Match( |
||||
async items => await SyncCollectionFromItems(request.CollectionId, items), |
||||
error => Task.FromResult(Left<BaseError, Unit>(error))); |
||||
} |
||||
|
||||
return BaseError.New("Invalid Trakt List URL"); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, Unit>> SyncCollectionFromItems(int collectionId, List<TraktListItemWithGuids> items) |
||||
{ |
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); |
||||
|
||||
Option<Collection> maybeCollection = await dbContext.Collections |
||||
.Include(c => c.MediaItems) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == collectionId); |
||||
|
||||
foreach (Collection collection in maybeCollection) |
||||
{ |
||||
var movieIds = new System.Collections.Generic.HashSet<int>(); |
||||
foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Movie)) |
||||
{ |
||||
foreach (int movieId in await IdentifyMovie(dbContext, item)) |
||||
{ |
||||
movieIds.Add(movieId); |
||||
} |
||||
} |
||||
|
||||
var showIds = new System.Collections.Generic.HashSet<int>(); |
||||
foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Show)) |
||||
{ |
||||
foreach (int showId in await IdentifyShow(dbContext, item)) |
||||
{ |
||||
showIds.Add(showId); |
||||
} |
||||
} |
||||
|
||||
var seasonIds = new System.Collections.Generic.HashSet<int>(); |
||||
foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Season)) |
||||
{ |
||||
foreach (int seasonId in await IdentifySeason(dbContext, item)) |
||||
{ |
||||
seasonIds.Add(seasonId); |
||||
} |
||||
} |
||||
|
||||
var episodeIds = new System.Collections.Generic.HashSet<int>(); |
||||
foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Episode)) |
||||
{ |
||||
foreach (int episodeId in await IdentifyEpisode(dbContext, item)) |
||||
{ |
||||
episodeIds.Add(episodeId); |
||||
} |
||||
} |
||||
|
||||
var allIds = movieIds |
||||
.Append(showIds) |
||||
.Append(seasonIds) |
||||
.Append(episodeIds) |
||||
.ToList(); |
||||
|
||||
collection.MediaItems.RemoveAll(mi => !allIds.Contains(mi.Id)); |
||||
|
||||
List<MediaItem> toAdd = await dbContext.MediaItems |
||||
.Filter(mi => allIds.Contains(mi.Id)) |
||||
.ToListAsync(); |
||||
|
||||
collection.MediaItems.AddRange(toAdd); |
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0) |
||||
{ |
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collectionId)) |
||||
{ |
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Option<int>> IdentifyMovie(TvContext dbContext, TraktListItemWithGuids item) |
||||
{ |
||||
Option<int> maybeMovieByGuid = await dbContext.MovieMetadata |
||||
.Filter(mm => mm.Guids.Any(g => item.Guids.Contains(g.Guid))) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(mm => mm.MovieId); |
||||
|
||||
foreach (int movieId in maybeMovieByGuid) |
||||
{ |
||||
_logger.LogDebug("Located trakt movie {Title} by id", item.DisplayTitle); |
||||
return movieId; |
||||
} |
||||
|
||||
Option<int> maybeMovieByTitleYear = await dbContext.MovieMetadata |
||||
.Filter(mm => mm.Title == item.Title && mm.Year == item.Year) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(mm => mm.MovieId); |
||||
|
||||
foreach (int movieId in maybeMovieByTitleYear) |
||||
{ |
||||
_logger.LogDebug("Located trakt movie {Title} by title/year", item.DisplayTitle); |
||||
return movieId; |
||||
} |
||||
|
||||
_logger.LogDebug("Unable to locate trakt movie {Title}", item.DisplayTitle); |
||||
|
||||
return None; |
||||
} |
||||
|
||||
private async Task<Option<int>> IdentifyShow(TvContext dbContext, TraktListItemWithGuids item) |
||||
{ |
||||
Option<int> maybeShowByGuid = await dbContext.ShowMetadata |
||||
.Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.ShowId); |
||||
|
||||
foreach (int showId in maybeShowByGuid) |
||||
{ |
||||
_logger.LogDebug("Located trakt show {Title} by id", item.DisplayTitle); |
||||
return showId; |
||||
} |
||||
|
||||
Option<int> maybeShowByTitleYear = await dbContext.ShowMetadata |
||||
.Filter(sm => sm.Title == item.Title && sm.Year == item.Year) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.ShowId); |
||||
|
||||
foreach (int showId in maybeShowByTitleYear) |
||||
{ |
||||
_logger.LogDebug("Located trakt show {Title} by title/year", item.Title); |
||||
return showId; |
||||
} |
||||
|
||||
_logger.LogDebug("Unable to locate trakt show {Title}", item.DisplayTitle); |
||||
|
||||
return None; |
||||
} |
||||
|
||||
private async Task<Option<int>> IdentifySeason(TvContext dbContext, TraktListItemWithGuids item) |
||||
{ |
||||
Option<int> maybeSeasonByGuid = await dbContext.SeasonMetadata |
||||
.Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.SeasonId); |
||||
|
||||
foreach (int seasonId in maybeSeasonByGuid) |
||||
{ |
||||
_logger.LogDebug("Located trakt season {Title} by id", item.DisplayTitle); |
||||
return seasonId; |
||||
} |
||||
|
||||
Option<int> maybeSeasonByTitleYear = await dbContext.SeasonMetadata |
||||
.Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) |
||||
.Filter(sm => sm.Season.SeasonNumber == item.Season) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.SeasonId); |
||||
|
||||
foreach (int seasonId in maybeSeasonByTitleYear) |
||||
{ |
||||
_logger.LogDebug("Located trakt season {Title} by title/year/season", item.DisplayTitle); |
||||
return seasonId; |
||||
} |
||||
|
||||
_logger.LogDebug("Unable to locate trakt season {Title}", item.DisplayTitle); |
||||
|
||||
return None; |
||||
} |
||||
|
||||
private async Task<Option<int>> IdentifyEpisode(TvContext dbContext, TraktListItemWithGuids item) |
||||
{ |
||||
Option<int> maybeEpisodeByGuid = await dbContext.EpisodeMetadata |
||||
.Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.EpisodeId); |
||||
|
||||
foreach (int episodeId in maybeEpisodeByGuid) |
||||
{ |
||||
_logger.LogDebug("Located trakt episode {Title} by id", item.DisplayTitle); |
||||
return episodeId; |
||||
} |
||||
|
||||
Option<int> maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata |
||||
.Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) |
||||
.Filter(em => em.Episode.Season.SeasonNumber == item.Season) |
||||
.Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode)) |
||||
.FirstOrDefaultAsync() |
||||
.Map(Optional) |
||||
.MapT(sm => sm.EpisodeId); |
||||
|
||||
foreach (int episodeId in maybeEpisodeByTitleYear) |
||||
{ |
||||
_logger.LogDebug("Located trakt episode {Title} by title/year/season/episode", item.DisplayTitle); |
||||
return episodeId; |
||||
} |
||||
|
||||
_logger.LogDebug("Unable to locate trakt episode {Title}", item.DisplayTitle); |
||||
|
||||
return None; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Trakt; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Trakt |
||||
{ |
||||
public interface ITraktApiClient |
||||
{ |
||||
Task<Either<BaseError, List<TraktListItemWithGuids>>> GetUserListItems( |
||||
string user, |
||||
string list); |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Trakt |
||||
{ |
||||
public class TraktConfiguration |
||||
{ |
||||
public string ClientId { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Trakt |
||||
{ |
||||
public enum TraktListItemKind |
||||
{ |
||||
Movie, |
||||
Show, |
||||
Season, |
||||
Episode |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Trakt |
||||
{ |
||||
public record TraktListItemWithGuids( |
||||
string DisplayTitle, |
||||
string Title, |
||||
int Year, |
||||
int Season, |
||||
int Episode, |
||||
TraktListItemKind Kind, |
||||
List<string> Guids); |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Infrastructure.Trakt.Models; |
||||
using Refit; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Trakt |
||||
{ |
||||
[Headers("Accept: application/json", "trakt-api-version: 2")] |
||||
public interface ITraktApi |
||||
{ |
||||
[Get("/users/{user}/lists/{list}/items")] |
||||
Task<List<TraktListItem>> GetUserListItems( |
||||
[Header("trakt-api-key")] |
||||
string clientId, |
||||
string user, |
||||
string list); |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItem |
||||
{ |
||||
public int Rank { get; set; } |
||||
public int Id { get; set; } |
||||
public string Type { get; set; } |
||||
public TraktListItemMovie Movie { get; set; } |
||||
public TraktListItemShow Show { get; set; } |
||||
public TraktListItemSeason Season { get; set; } |
||||
public TraktListItemEpisode Episode { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItemEpisode |
||||
{ |
||||
public int Season { get; set; } |
||||
public int Number { get; set; } |
||||
public string Title { get; set; } |
||||
public TraktListItemIds Ids { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItemIds |
||||
{ |
||||
public int Trakt { get; set; } |
||||
public int? Tvdb { get; set; } |
||||
public string Imdb { get; set; } |
||||
public int? Tmdb { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItemMovie |
||||
{ |
||||
public string Title { get; set; } |
||||
public int Year { get; set; } |
||||
public TraktListItemIds Ids { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItemSeason |
||||
{ |
||||
public int Number { get; set; } |
||||
public TraktListItemIds Ids { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Trakt.Models |
||||
{ |
||||
public class TraktListItemShow |
||||
{ |
||||
public string Title { get; set; } |
||||
public int Year { get; set; } |
||||
public TraktListItemIds Ids { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Interfaces.Trakt; |
||||
using ErsatzTV.Core.Trakt; |
||||
using ErsatzTV.Infrastructure.Trakt.Models; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Logging; |
||||
using Microsoft.Extensions.Options; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Trakt |
||||
{ |
||||
public class TraktApiClient : ITraktApiClient |
||||
{ |
||||
private readonly ITraktApi _traktApi; |
||||
private readonly IOptions<TraktConfiguration> _traktConfiguration; |
||||
private readonly ILogger<TraktApiClient> _logger; |
||||
|
||||
public TraktApiClient( |
||||
ITraktApi traktApi, |
||||
IOptions<TraktConfiguration> traktConfiguration, |
||||
ILogger<TraktApiClient> logger) |
||||
{ |
||||
_traktApi = traktApi; |
||||
_traktConfiguration = traktConfiguration; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, List<TraktListItemWithGuids>>> GetUserListItems( |
||||
string user, |
||||
string list) |
||||
{ |
||||
try |
||||
{ |
||||
var result = new List<TraktListItemWithGuids>(); |
||||
|
||||
List<TraktListItem> apiItems = await _traktApi.GetUserListItems( |
||||
_traktConfiguration.Value.ClientId, |
||||
user, |
||||
list); |
||||
|
||||
foreach (TraktListItem apiItem in apiItems) |
||||
{ |
||||
TraktListItemWithGuids item = apiItem.Type.ToLowerInvariant() switch |
||||
{ |
||||
"movie" => new TraktListItemWithGuids( |
||||
$"{apiItem.Movie.Title} ({apiItem.Movie.Year})", |
||||
apiItem.Movie.Title, |
||||
apiItem.Movie.Year, |
||||
0, |
||||
0, |
||||
TraktListItemKind.Movie, |
||||
GuidsFromIds(apiItem.Movie.Ids)), |
||||
"show" => new TraktListItemWithGuids( |
||||
$"{apiItem.Show.Title} ({apiItem.Show.Year})", |
||||
apiItem.Show.Title, |
||||
apiItem.Show.Year, |
||||
0, |
||||
0, |
||||
TraktListItemKind.Show, |
||||
GuidsFromIds(apiItem.Show.Ids)), |
||||
"season" => new TraktListItemWithGuids( |
||||
$"{apiItem.Show.Title} ({apiItem.Show.Year}) S{apiItem.Season.Number:00}", |
||||
apiItem.Show.Title, |
||||
apiItem.Show.Year, |
||||
apiItem.Season.Number, |
||||
0, |
||||
TraktListItemKind.Season, |
||||
GuidsFromIds(apiItem.Season.Ids)), |
||||
"episode" => new TraktListItemWithGuids( |
||||
$"{apiItem.Show.Title} ({apiItem.Show.Year}) S{apiItem.Episode.Season:00}E{apiItem.Episode.Number:00}", |
||||
apiItem.Show.Title, |
||||
apiItem.Show.Year, |
||||
apiItem.Episode.Season, |
||||
apiItem.Episode.Number, |
||||
TraktListItemKind.Episode, |
||||
GuidsFromIds(apiItem.Episode.Ids)), |
||||
_ => null |
||||
}; |
||||
|
||||
if (item != null) |
||||
{ |
||||
result.Add(item); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError(ex, "Error getting trakt list items"); |
||||
return BaseError.New(ex.Message); |
||||
} |
||||
} |
||||
|
||||
private static List<string> GuidsFromIds(TraktListItemIds ids) |
||||
{ |
||||
var result = new List<string>(); |
||||
|
||||
if (!string.IsNullOrWhiteSpace(ids.Imdb)) |
||||
{ |
||||
result.Add($"imdb://{ids.Imdb}"); |
||||
} |
||||
|
||||
if (ids.Tmdb.HasValue) |
||||
{ |
||||
result.Add($"tmdb://{ids.Tmdb.Value}"); |
||||
} |
||||
|
||||
if (ids.Tvdb.HasValue) |
||||
{ |
||||
result.Add($"tvdb://{ids.Tvdb.Value}"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
@using Microsoft.Extensions.Caching.Memory |
||||
@inject IMediator _mediator |
||||
@inject IMemoryCache _memoryCache |
||||
@inject ISnackbar _snackbar |
||||
@inject ILogger<SyncFromTraktListDialog> _logger |
||||
|
||||
<MudDialog> |
||||
<DialogContent> |
||||
<EditForm Model="@_dummyModel" OnSubmit="@(_ => Submit())"> |
||||
<MudContainer Class="mb-6"> |
||||
<MudText> |
||||
Enter Trakt List URL |
||||
</MudText> |
||||
</MudContainer> |
||||
<MudTextFieldString Label="Trakt List URL" |
||||
Placeholder="https://trakt.tv/users/username/lists/list-name" |
||||
@bind-Text="@_traktListUrl" |
||||
Style="min-width: 400px" |
||||
Class="mb-6 mx-4"> |
||||
</MudTextFieldString> |
||||
</EditForm> |
||||
</DialogContent> |
||||
<DialogActions> |
||||
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton> |
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit"> |
||||
Sync |
||||
</MudButton> |
||||
</DialogActions> |
||||
</MudDialog> |
||||
|
||||
|
||||
@code { |
||||
|
||||
[CascadingParameter] |
||||
MudDialogInstance MudDialog { get; set; } |
||||
|
||||
private string _traktListUrl; |
||||
|
||||
private record DummyModel; |
||||
|
||||
private readonly DummyModel _dummyModel = new(); |
||||
|
||||
private bool CanSubmit() => !string.IsNullOrWhiteSpace(_traktListUrl); |
||||
|
||||
private void Submit() |
||||
{ |
||||
if (!CanSubmit()) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
MudDialog.Close(DialogResult.Ok(_traktListUrl)); |
||||
} |
||||
|
||||
private void Cancel(MouseEventArgs e) |
||||
{ |
||||
// this is gross, but [enter] seems to sometimes trigger cancel instead of submit |
||||
if (e.Detail == 0) |
||||
{ |
||||
Submit(); |
||||
} |
||||
else |
||||
{ |
||||
MudDialog.Cancel(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue