FIx preview page uploader. Add progress when downloading videos. Fix large video downloads.
This commit is contained in:
parent
f9abb08141
commit
71de97e04f
13 changed files with 282 additions and 42 deletions
|
|
@ -11,6 +11,9 @@ namespace LANCommander.Server.Services.Abstractions
|
|||
Task<IEnumerable<MediaGrabberResult>> SearchAsync(MediaType type, string keywords);
|
||||
Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result);
|
||||
|
||||
Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result, IProgress<MediaDownloadProgress>? progress)
|
||||
=> DownloadAsync(result);
|
||||
|
||||
async IAsyncEnumerable<IEnumerable<MediaGrabberResult>> SearchStreamAsync(
|
||||
MediaType type, string keywords,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
|
|
|
|||
|
|
@ -66,11 +66,16 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
}
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
{
|
||||
return await DownloadAsync(result, null);
|
||||
}
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result, IProgress<MediaDownloadProgress>? progress)
|
||||
{
|
||||
var grabber = _grabbers.FirstOrDefault(g => g.Name == result.GrabberName)
|
||||
?? _grabbers.First();
|
||||
|
||||
return await grabber.DownloadAsync(result);
|
||||
return await grabber.DownloadAsync(result, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,10 +69,19 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
return results;
|
||||
}
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
public Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
=> DownloadAsync(result, null);
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result, IProgress<MediaDownloadProgress>? progress)
|
||||
{
|
||||
var http = new HttpClient();
|
||||
var stream = await http.GetStreamAsync(result.SourceUrl);
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(30) };
|
||||
|
||||
using var response = await http.GetAsync(result.SourceUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength;
|
||||
var stream = await ProgressStream.CopyToTempFileAsync(
|
||||
await response.Content.ReadAsStreamAsync(), totalBytes, progress);
|
||||
|
||||
return new MediaGrabberDownload
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,10 +43,19 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
public Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
=> DownloadAsync(result, null);
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result, IProgress<MediaDownloadProgress>? progress)
|
||||
{
|
||||
var http = new HttpClient();
|
||||
var stream = await http.GetStreamAsync(result.SourceUrl);
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(30) };
|
||||
|
||||
using var response = await http.GetAsync(result.SourceUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength;
|
||||
var stream = await ProgressStream.CopyToTempFileAsync(
|
||||
await response.Content.ReadAsStreamAsync(), totalBytes, progress);
|
||||
|
||||
return new MediaGrabberDownload
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using YoutubeExplode.Common;
|
|||
|
||||
namespace LANCommander.Server.Services.MediaGrabbers
|
||||
{
|
||||
public class YouTubeMediaGrabber(ILogger<YouTubeMediaGrabber> logger) : IMediaGrabberService
|
||||
public partial class YouTubeMediaGrabber(ILogger<YouTubeMediaGrabber> logger) : IMediaGrabberService
|
||||
{
|
||||
public string Name => "YouTube";
|
||||
|
||||
|
|
@ -39,12 +39,21 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
return results;
|
||||
}
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
public Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result)
|
||||
=> DownloadAsync(result, null);
|
||||
|
||||
public async Task<MediaGrabberDownload> DownloadAsync(MediaGrabberResult result, IProgress<MediaDownloadProgress>? progress)
|
||||
{
|
||||
if (!IsYouTubeUrl(result.SourceUrl))
|
||||
{
|
||||
var http = new HttpClient();
|
||||
var stream = await http.GetStreamAsync(result.SourceUrl);
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(30) };
|
||||
|
||||
using var response = await http.GetAsync(result.SourceUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength;
|
||||
var stream = await ProgressStream.CopyToTempFileAsync(
|
||||
await response.Content.ReadAsStreamAsync(), totalBytes, progress);
|
||||
|
||||
return new MediaGrabberDownload
|
||||
{
|
||||
|
|
@ -60,11 +69,13 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
|
||||
var outputTemplate = Path.Combine(tempDir, "video.%(ext)s");
|
||||
|
||||
progress?.Report(new MediaDownloadProgress { Status = "Starting yt-dlp..." });
|
||||
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ytdlpPath,
|
||||
Arguments = $"--no-playlist -f \"bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best\" --merge-output-format mp4 -o \"{outputTemplate}\" \"{result.SourceUrl}\"",
|
||||
Arguments = $"--no-playlist --newline -f \"bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best\" --merge-output-format mp4 -o \"{outputTemplate}\" \"{result.SourceUrl}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
|
|
@ -72,16 +83,49 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
// Parse yt-dlp stdout for progress lines like "[download] 45.2% of 120.00MiB ..."
|
||||
var stdoutTask = Task.Run(async () =>
|
||||
{
|
||||
while (await process.StandardOutput.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (progress == null)
|
||||
continue;
|
||||
|
||||
var match = YtDlpProgressRegex().Match(line);
|
||||
if (match.Success && double.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var pct))
|
||||
{
|
||||
progress.Report(new MediaDownloadProgress
|
||||
{
|
||||
BytesTransferred = (long)pct,
|
||||
TotalBytes = 100,
|
||||
Status = $"Downloading video... {pct:F1}%"
|
||||
});
|
||||
}
|
||||
else if (line.Contains("[Merger]") || line.Contains("Merging"))
|
||||
{
|
||||
progress.Report(new MediaDownloadProgress
|
||||
{
|
||||
BytesTransferred = 100,
|
||||
TotalBytes = 100,
|
||||
Status = "Merging audio and video..."
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var stderrTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
await stdoutTask;
|
||||
var stderr = await stderrTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
|
||||
throw new Exception($"yt-dlp failed with exit code {process.ExitCode}: {error}");
|
||||
throw new Exception($"yt-dlp failed with exit code {process.ExitCode}: {stderr}");
|
||||
}
|
||||
|
||||
var outputFile = Directory.GetFiles(tempDir).FirstOrDefault();
|
||||
|
|
@ -111,6 +155,9 @@ namespace LANCommander.Server.Services.MediaGrabbers
|
|||
};
|
||||
}
|
||||
|
||||
[System.Text.RegularExpressions.GeneratedRegex(@"\[download\]\s+([\d.]+)%")]
|
||||
private static partial System.Text.RegularExpressions.Regex YtDlpProgressRegex();
|
||||
|
||||
private static bool IsYouTubeUrl(string url) =>
|
||||
url.Contains("youtube.com/") || url.Contains("youtu.be/");
|
||||
|
||||
|
|
|
|||
13
LANCommander.Server.Services/Models/MediaDownloadProgress.cs
Normal file
13
LANCommander.Server.Services/Models/MediaDownloadProgress.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace LANCommander.Server.Services.Models
|
||||
{
|
||||
public class MediaDownloadProgress
|
||||
{
|
||||
public long BytesTransferred { get; set; }
|
||||
public long? TotalBytes { get; set; }
|
||||
public string Status { get; set; } = "Downloading...";
|
||||
|
||||
public double? Percent => TotalBytes > 0
|
||||
? Math.Round((double)BytesTransferred / TotalBytes.Value * 100, 1)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace LANCommander.Server.Services.Models
|
||||
{
|
||||
public class MediaGrabberDownloadResult
|
||||
{
|
||||
public MediaGrabberResult Result { get; set; }
|
||||
public MediaGrabberDownload Download { get; set; }
|
||||
}
|
||||
}
|
||||
72
LANCommander.Server.Services/Models/ProgressStream.cs
Normal file
72
LANCommander.Server.Services/Models/ProgressStream.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
namespace LANCommander.Server.Services.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies a source stream to a temporary file while reporting download progress.
|
||||
/// The returned stream owns the temp file and deletes it on dispose.
|
||||
/// </summary>
|
||||
public static class ProgressStream
|
||||
{
|
||||
private const int BufferSize = 1024 * 1024; // 1 MB
|
||||
|
||||
public static async Task<Stream> CopyToTempFileAsync(
|
||||
Stream source, long? totalBytes, IProgress<MediaDownloadProgress>? progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tempPath = Path.GetTempFileName();
|
||||
long bytesTransferred = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize))
|
||||
{
|
||||
var buffer = new byte[BufferSize];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
||||
bytesTransferred += bytesRead;
|
||||
|
||||
progress?.Report(new MediaDownloadProgress
|
||||
{
|
||||
BytesTransferred = bytesTransferred,
|
||||
TotalBytes = totalBytes,
|
||||
Status = "Downloading..."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new TempFileStream(tempPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
File.Delete(tempPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TempFileStream : FileStream
|
||||
{
|
||||
private readonly string _path;
|
||||
|
||||
public TempFileStream(string path)
|
||||
: base(path, FileMode.Open, FileAccess.Read, FileShare.Read)
|
||||
{
|
||||
_path = path;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(_path))
|
||||
File.Delete(_path);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
@inherits FeedbackComponent<MediaGrabberOptions, MediaGrabberResult>
|
||||
@inherits FeedbackComponent<MediaGrabberOptions, MediaGrabberDownloadResult>
|
||||
@using LANCommander.Server.Services.Abstractions
|
||||
@using LANCommander.Server.Services.Models
|
||||
@using LANCommander.SDK.Enums
|
||||
@inject IMediaGrabberService MediaGrabberService
|
||||
@inject IMessageService MessageService
|
||||
|
|
@ -89,6 +90,17 @@
|
|||
<Empty Description="@($"No results for \"{Search}\"")" />
|
||||
}
|
||||
|
||||
@if (_isDownloading)
|
||||
{
|
||||
<div style="position: absolute; inset: 0; background: rgba(0,0,0,0.75); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10; gap: 12px; border-radius: 8px;">
|
||||
<Spin Spinning="true" />
|
||||
<span>@_downloadStatus</span>
|
||||
<div style="width: 60%;">
|
||||
<Progress Percent="_downloadPercent" Size="ProgressSize.Small" Status="@ProgressStatus.Active" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string Search { get; set; }
|
||||
[Parameter] public MediaType Type { get; set; }
|
||||
|
|
@ -98,6 +110,9 @@
|
|||
|
||||
double Size { get; set; } = 200;
|
||||
bool Loading { get; set; } = true;
|
||||
bool _isDownloading;
|
||||
double _downloadPercent;
|
||||
string _downloadStatus = "Downloading...";
|
||||
|
||||
CancellationTokenSource _searchCts;
|
||||
List<MediaGrabberResult> _allResults = new();
|
||||
|
|
@ -175,9 +190,7 @@
|
|||
private async Task OnResultPicked(MediaGrabberResult result)
|
||||
{
|
||||
Media = result;
|
||||
|
||||
await base.OkCancelRefWithResult!.OnOk(Media);
|
||||
await CloseFeedbackAsync();
|
||||
await DownloadAndClose();
|
||||
}
|
||||
|
||||
private void OnImageSelected(string uniqueKey)
|
||||
|
|
@ -188,9 +201,7 @@
|
|||
private async Task OnImagePicked(string uniqueKey)
|
||||
{
|
||||
OnImageSelected(uniqueKey);
|
||||
|
||||
await base.OkCancelRefWithResult!.OnOk(Media);
|
||||
await CloseFeedbackAsync();
|
||||
await DownloadAndClose();
|
||||
}
|
||||
|
||||
private void PreviewVideo(MediaGrabberResult result)
|
||||
|
|
@ -211,8 +222,54 @@
|
|||
|
||||
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
|
||||
{
|
||||
await base.OkCancelRefWithResult!.OnOk(Media);
|
||||
await CloseFeedbackAsync();
|
||||
args.Cancel = true;
|
||||
await DownloadAndClose();
|
||||
}
|
||||
|
||||
public override async Task OnFeedbackCancelAsync(ModalClosingEventArgs args)
|
||||
{
|
||||
_preview = null;
|
||||
await base.OnFeedbackCancelAsync(args);
|
||||
}
|
||||
|
||||
private async Task DownloadAndClose()
|
||||
{
|
||||
if (Media == null)
|
||||
return;
|
||||
|
||||
_preview = null;
|
||||
_isDownloading = true;
|
||||
_downloadPercent = 0;
|
||||
_downloadStatus = "Downloading...";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<MediaDownloadProgress>(p =>
|
||||
{
|
||||
_downloadPercent = p.Percent ?? 0;
|
||||
_downloadStatus = p.Status ?? "Downloading...";
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
var download = await MediaGrabberService.DownloadAsync(Media, progress);
|
||||
|
||||
var result = new MediaGrabberDownloadResult
|
||||
{
|
||||
Result = Media,
|
||||
Download = download
|
||||
};
|
||||
|
||||
await base.OkCancelRefWithResult!.OnOk(result);
|
||||
await CloseFeedbackAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_isDownloading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
MessageService.Error("Download failed. Please try again.");
|
||||
Logger.LogError(ex, "Failed to download media");
|
||||
}
|
||||
}
|
||||
|
||||
private class GroupedResults
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using LANCommander.Server.Settings
|
||||
@using LANCommander.Server.Services.Abstractions
|
||||
@using LANCommander.Server.Services.Models
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Net.Mime
|
||||
@using LANCommander.UI.Providers
|
||||
@inject MediaService MediaService
|
||||
@inject GameService GameService
|
||||
@inject IMediaGrabberService MediaGrabberService
|
||||
|
|
@ -10,7 +12,7 @@
|
|||
@inject IMessageService MessageService
|
||||
@inject IOptions<Settings> Settings
|
||||
@inject ILogger<GameDetailPreview> Logger
|
||||
@inject IJSRuntime JS
|
||||
@inject ScriptProvider ScriptProvider
|
||||
|
||||
<div class="game-detail-preview">
|
||||
<!-- ══ Hero Section ═══════════════════════════════════════════ -->
|
||||
|
|
@ -322,6 +324,7 @@
|
|||
|
||||
List<Media> _media = new();
|
||||
Game _game;
|
||||
IJSObjectReference _domHelper;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
|
@ -341,8 +344,14 @@
|
|||
_media = _game.Media?.ToList() ?? new();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
_domHelper = await ScriptProvider.ImportModuleAsync("DomHelper");
|
||||
}
|
||||
|
||||
async Task ClickFileInput(string inputId) =>
|
||||
await JS.InvokeVoidAsync("clickElement", inputId);
|
||||
await _domHelper.InvokeVoidAsync("ClickElement", inputId);
|
||||
|
||||
Media? GetMedia(MediaType type) =>
|
||||
_media.FirstOrDefault(m => m.Type == type);
|
||||
|
|
@ -431,13 +440,11 @@
|
|||
Search = _game.Title
|
||||
};
|
||||
|
||||
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberResult>(modalOptions, grabberOptions);
|
||||
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberDownloadResult>(modalOptions, grabberOptions);
|
||||
|
||||
modalRef.OnOk = async (result) =>
|
||||
modalRef.OnOk = async (downloadResult) =>
|
||||
{
|
||||
modalRef.Config.ConfirmLoading = true;
|
||||
|
||||
using var download = await MediaGrabberService.DownloadAsync(result);
|
||||
using var download = downloadResult.Download;
|
||||
|
||||
var isSingular = type != MediaType.Screenshot && type != MediaType.Video;
|
||||
var existing = isSingular ? GetMedia(type) : null;
|
||||
|
|
@ -447,7 +454,7 @@
|
|||
if (existing != null)
|
||||
{
|
||||
media = existing;
|
||||
media.SourceUrl = result.SourceUrl;
|
||||
media.SourceUrl = downloadResult.Result.SourceUrl;
|
||||
media.MimeType = download.MimeType;
|
||||
|
||||
MediaService.DeleteLocalMediaFile(media);
|
||||
|
|
@ -459,7 +466,7 @@
|
|||
{
|
||||
GameId = GameId,
|
||||
Type = type,
|
||||
SourceUrl = result.SourceUrl,
|
||||
SourceUrl = downloadResult.Result.SourceUrl,
|
||||
MimeType = download.MimeType,
|
||||
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
|
||||
Crc32 = string.Empty,
|
||||
|
|
@ -470,7 +477,6 @@
|
|||
}
|
||||
|
||||
await RefreshMedia();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using LANCommander.Server.Settings
|
||||
@using LANCommander.Server.Services.Abstractions
|
||||
@using LANCommander.Server.Services.Models
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Net.Mime
|
||||
@inject MediaService MediaService
|
||||
|
|
@ -203,15 +204,13 @@
|
|||
Search = _game.Title
|
||||
};
|
||||
|
||||
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberResult>(modalOptions, grabberOptions);
|
||||
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberDownloadResult>(modalOptions, grabberOptions);
|
||||
|
||||
modalRef.OnOk = async (result) =>
|
||||
modalRef.OnOk = async (downloadResult) =>
|
||||
{
|
||||
modalRef.Config.ConfirmLoading = true;
|
||||
using var download = downloadResult.Download;
|
||||
|
||||
using var download = await MediaGrabberService.DownloadAsync(result);
|
||||
|
||||
media.SourceUrl = result.SourceUrl;
|
||||
media.SourceUrl = downloadResult.Result.SourceUrl;
|
||||
media.MimeType = download.MimeType;
|
||||
|
||||
if (media.Id == Guid.Empty)
|
||||
|
|
@ -221,7 +220,6 @@
|
|||
else
|
||||
{
|
||||
MediaService.DeleteLocalMediaFile(media);
|
||||
|
||||
media = await MediaService.WriteToFileAsync(media, download.Stream);
|
||||
}
|
||||
|
||||
|
|
|
|||
12
LANCommander.UI/Components/DomHelper/DomHelper.ts
Normal file
12
LANCommander.UI/Components/DomHelper/DomHelper.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export class DomHelper
|
||||
{
|
||||
public static Create(): DomHelper
|
||||
{
|
||||
return new DomHelper();
|
||||
}
|
||||
|
||||
public ClickElement(id: string): void
|
||||
{
|
||||
document.getElementById(id)?.click();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ export { InfiniteScroll } from "./Components/InfiniteLoader/InfiniteScroll";
|
|||
export { ChunkUploader } from "./Components/ChunkUploader/ChunkUploader";
|
||||
export { SplitPane } from "./Components/SplitPane/SplitPane";
|
||||
export { TimeProvider } from "./Components/LocalTime/TimeProvider";
|
||||
export { Terminal } from "./Components/Terminal/Terminal";
|
||||
export { Terminal } from "./Components/Terminal/Terminal";
|
||||
export { DomHelper } from "./Components/DomHelper/DomHelper";
|
||||
Loading…
Add table
Add a link
Reference in a new issue