|
| 1 | +@inherits MelodeeComponentBase |
| 2 | +@using Melodee.Blazor.Controllers.Melodee.Models |
| 3 | +@using Melodee.Common.Models |
| 4 | +@using Microsoft.AspNetCore.Components.Forms |
| 5 | +@using Microsoft.Extensions.Logging |
| 6 | + |
| 7 | +@inject PlaylistService PlaylistService |
| 8 | +@inject NotificationService NotificationService |
| 9 | +@inject DialogService DialogService |
| 10 | +@inject IJSRuntime JS |
| 11 | +@inject ILogger<M3UPlaylistImportDialog> Logger |
| 12 | + |
| 13 | +<RadzenStack Gap="1rem"> |
| 14 | + <RadzenAlert AlertStyle="AlertStyle.Info" Shade="Shade.Lighter" AllowClose="false"> |
| 15 | + <RadzenText TextStyle="TextStyle.Subtitle2">@L("M3UImportDialog.Title")</RadzenText> |
| 16 | + <RadzenText TextStyle="TextStyle.Body2"> |
| 17 | + @L("M3UImportDialog.Description") |
| 18 | + </RadzenText> |
| 19 | + <RadzenText TextStyle="TextStyle.Body2"> |
| 20 | + <strong>@L("M3UImportDialog.SupportedFormats"):</strong> |
| 21 | + <ul style="margin: 0.5rem 0; padding-left: 1.5rem;"> |
| 22 | + <li>.m3u</li> |
| 23 | + <li>.m3u8</li> |
| 24 | + </ul> |
| 25 | + </RadzenText> |
| 26 | + </RadzenAlert> |
| 27 | + |
| 28 | + <RadzenFormField Text="@L("M3UImportDialog.SelectFile")" Variant="Variant.Outlined" Style="width: 100%;"> |
| 29 | + <RadzenStack Orientation="Orientation.Horizontal" Gap="0.5rem" AlignItems="AlignItems.Center"> |
| 30 | + <InputFile OnChange="@OnFileSelected" accept=".m3u,.m3u8" style="display: none;" id="m3uFileInput"/> |
| 31 | + <RadzenButton Text="@L("Actions.Upload")" Icon="upload_file" ButtonStyle="ButtonStyle.Primary" Click="@TriggerFileInput" Disabled="@_isUploading"/> |
| 32 | + @if (!string.IsNullOrEmpty(_fileName)) |
| 33 | + { |
| 34 | + <RadzenText TextStyle="TextStyle.Body2" Style="align-self: center;">@_fileName</RadzenText> |
| 35 | + <RadzenText TextStyle="TextStyle.Caption" Style="align-self: center;">(@_fileSize)</RadzenText> |
| 36 | + } |
| 37 | + </RadzenStack> |
| 38 | + </RadzenFormField> |
| 39 | + |
| 40 | + @if (_isUploading) |
| 41 | + { |
| 42 | + <RadzenProgressBar Value="100" ShowValue="false" Mode="ProgressBarMode.Indeterminate" Style="margin-top: 1rem;"/> |
| 43 | + <RadzenText TextStyle="TextStyle.Body2" Style="text-align: center;">@L("M3UImportDialog.Uploading")</RadzenText> |
| 44 | + } |
| 45 | + |
| 46 | + @if (_validationErrors.Any()) |
| 47 | + { |
| 48 | + <RadzenAlert AlertStyle="AlertStyle.Danger" Shade="Shade.Lighter" AllowClose="false"> |
| 49 | + <RadzenText TextStyle="TextStyle.Subtitle2">@L("M3UImportDialog.ValidationErrors")</RadzenText> |
| 50 | + <ul style="margin: 0.5rem 0; padding-left: 1.5rem;"> |
| 51 | + @foreach (var error in _validationErrors) |
| 52 | + { |
| 53 | + <li><RadzenText TextStyle="TextStyle.Body2">@error</RadzenText></li> |
| 54 | + } |
| 55 | + </ul> |
| 56 | + </RadzenAlert> |
| 57 | + } |
| 58 | + |
| 59 | + @if (_importResult != null) |
| 60 | + { |
| 61 | + <RadzenAlert AlertStyle="AlertStyle.Success" Shade="Shade.Lighter" AllowClose="false"> |
| 62 | + <RadzenText TextStyle="TextStyle.Subtitle2">@L("M3UImportDialog.ImportSuccess")</RadzenText> |
| 63 | + <RadzenText TextStyle="TextStyle.Body2"> |
| 64 | + <strong>@L("Common.Name"):</strong> @_importResult.PlaylistName<br/> |
| 65 | + <strong>@L("M3UImportDialog.TotalEntries"):</strong> @_importResult.TotalEntries<br/> |
| 66 | + <strong>@L("M3UImportDialog.MatchedSongs"):</strong> @_importResult.MatchedEntries (@GetMatchPercentage()%)<br/> |
| 67 | + <strong>@L("M3UImportDialog.MissingSongs"):</strong> @_importResult.MissingEntries<br/> |
| 68 | + </RadzenText> |
| 69 | + @if (_importResult.MissingEntries > 0) |
| 70 | + { |
| 71 | + <RadzenText TextStyle="TextStyle.Caption" Style="margin-top: 0.5rem;"> |
| 72 | + @L("M3UImportDialog.MissingItemsNote") |
| 73 | + </RadzenText> |
| 74 | + } |
| 75 | + </RadzenAlert> |
| 76 | + } |
| 77 | + |
| 78 | + <RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.End" Gap="0.5rem"> |
| 79 | + <RadzenButton Text="@L("Actions.Cancel")" ButtonStyle="ButtonStyle.Light" Click="@CancelClick"/> |
| 80 | + <RadzenButton Text="@L("Actions.Close")" ButtonStyle="ButtonStyle.Primary" Click="@CloseClick" Visible="@(_importResult != null)"/> |
| 81 | + </RadzenStack> |
| 82 | +</RadzenStack> |
| 83 | + |
| 84 | +@code { |
| 85 | + private string _fileName = string.Empty; |
| 86 | + private string _fileSize = string.Empty; |
| 87 | + private IBrowserFile? _selectedFile; |
| 88 | + private bool _isUploading; |
| 89 | + private readonly List<string> _validationErrors = new(); |
| 90 | + private PlaylistImportResponse? _importResult; |
| 91 | + |
| 92 | + private const long MaxFileSize = 10 * 1024 * 1024; // 10MB |
| 93 | +
|
| 94 | + private async Task TriggerFileInput() |
| 95 | + { |
| 96 | + await JS.InvokeVoidAsync("document.getElementById('m3uFileInput').click"); |
| 97 | + } |
| 98 | + |
| 99 | + private async Task OnFileSelected(InputFileChangeEventArgs e) |
| 100 | + { |
| 101 | + _selectedFile = e.File; |
| 102 | + _fileName = _selectedFile.Name; |
| 103 | + _fileSize = FormatFileSize(_selectedFile.Size); |
| 104 | + _validationErrors.Clear(); |
| 105 | + _importResult = null; |
| 106 | + |
| 107 | + // Validate file |
| 108 | + var extension = Path.GetExtension(_fileName).ToLowerInvariant(); |
| 109 | + if (extension != ".m3u" && extension != ".m3u8") |
| 110 | + { |
| 111 | + _validationErrors.Add(L("M3UImportDialog.InvalidFileType")); |
| 112 | + _selectedFile = null; |
| 113 | + _fileName = string.Empty; |
| 114 | + _fileSize = string.Empty; |
| 115 | + StateHasChanged(); |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + if (_selectedFile.Size > MaxFileSize) |
| 120 | + { |
| 121 | + _validationErrors.Add(L("M3UImportDialog.FileTooLarge")); |
| 122 | + _selectedFile = null; |
| 123 | + _fileName = string.Empty; |
| 124 | + _fileSize = string.Empty; |
| 125 | + StateHasChanged(); |
| 126 | + return; |
| 127 | + } |
| 128 | + |
| 129 | + // Auto-upload if validation passes |
| 130 | + await UploadFile(); |
| 131 | + } |
| 132 | + |
| 133 | + private async Task UploadFile() |
| 134 | + { |
| 135 | + if (_selectedFile == null || _isUploading) |
| 136 | + { |
| 137 | + return; |
| 138 | + } |
| 139 | + |
| 140 | + try |
| 141 | + { |
| 142 | + _isUploading = true; |
| 143 | + _validationErrors.Clear(); |
| 144 | + StateHasChanged(); |
| 145 | + |
| 146 | + using var stream = _selectedFile.OpenReadStream(MaxFileSize); |
| 147 | + using var memoryStream = new MemoryStream(); |
| 148 | + await stream.CopyToAsync(memoryStream); |
| 149 | + memoryStream.Position = 0; |
| 150 | + |
| 151 | + var result = await PlaylistService.ImportPlaylistAsync( |
| 152 | + CurrentUser!.UserId(), |
| 153 | + memoryStream, |
| 154 | + _selectedFile.Name, |
| 155 | + CancellationToken.None); |
| 156 | + |
| 157 | + if (!result.IsSuccess || result.Data == null) |
| 158 | + { |
| 159 | + var errorMessage = result.Errors?.FirstOrDefault()?.Message ?? L("M3UImportDialog.ImportFailed"); |
| 160 | + _validationErrors.Add(errorMessage); |
| 161 | + return; |
| 162 | + } |
| 163 | + |
| 164 | + _importResult = new PlaylistImportResponse |
| 165 | + { |
| 166 | + PlaylistId = result.Data.PlaylistApiKey, |
| 167 | + PlaylistName = result.Data.PlaylistName, |
| 168 | + TotalEntries = result.Data.TotalEntries, |
| 169 | + MatchedEntries = result.Data.MatchedEntries, |
| 170 | + MissingEntries = result.Data.MissingEntries |
| 171 | + }; |
| 172 | + |
| 173 | + NotificationService.Notify(new NotificationMessage |
| 174 | + { |
| 175 | + Severity = NotificationSeverity.Success, |
| 176 | + Summary = L("M3UImportDialog.ImportSuccess"), |
| 177 | + Detail = L("M3UImportDialog.ImportSummary", _importResult.MatchedEntries.ToString(), _importResult.TotalEntries.ToString()), |
| 178 | + Duration = 5000 |
| 179 | + }); |
| 180 | + } |
| 181 | + catch (Exception ex) |
| 182 | + { |
| 183 | + Logger.LogError(ex, "Failed to import M3U playlist"); |
| 184 | + _validationErrors.Add(L("M3UImportDialog.ImportFailed") + ": " + ex.Message); |
| 185 | + NotificationService.Notify(new NotificationMessage |
| 186 | + { |
| 187 | + Severity = NotificationSeverity.Error, |
| 188 | + Summary = L("M3UImportDialog.ImportFailed"), |
| 189 | + Detail = ex.Message, |
| 190 | + Duration = 8000 |
| 191 | + }); |
| 192 | + } |
| 193 | + finally |
| 194 | + { |
| 195 | + _isUploading = false; |
| 196 | + StateHasChanged(); |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + private string FormatFileSize(long bytes) |
| 201 | + { |
| 202 | + string[] sizes = { "B", "KB", "MB", "GB" }; |
| 203 | + double len = bytes; |
| 204 | + int order = 0; |
| 205 | + while (len >= 1024 && order < sizes.Length - 1) |
| 206 | + { |
| 207 | + order++; |
| 208 | + len = len / 1024; |
| 209 | + } |
| 210 | + return $"{len:0.##} {sizes[order]}"; |
| 211 | + } |
| 212 | + |
| 213 | + private string GetMatchPercentage() |
| 214 | + { |
| 215 | + if (_importResult == null || _importResult.TotalEntries == 0) |
| 216 | + { |
| 217 | + return "0"; |
| 218 | + } |
| 219 | + return ((double)_importResult.MatchedEntries / _importResult.TotalEntries * 100).ToString("0.#"); |
| 220 | + } |
| 221 | + |
| 222 | + private void CancelClick() |
| 223 | + { |
| 224 | + DialogService.Close(false); |
| 225 | + } |
| 226 | + |
| 227 | + private void CloseClick() |
| 228 | + { |
| 229 | + DialogService.Close(true); |
| 230 | + } |
| 231 | +} |
0 commit comments