Add sticker manager in non admin section of admin api test client

This commit is contained in:
Rory&
2025-12-29 11:40:02 +01:00
parent bb9075667c
commit 16e90d228e
3 changed files with 252 additions and 3 deletions

View File

@@ -0,0 +1,236 @@
@page "/NonAdmin/Guilds/{GuildId}/StickerManager"
@using System.Diagnostics
@using System.Diagnostics.CodeAnalysis
@using System.Net.Http.Headers
@using System.Text.Json.Serialization
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Blazor.Components.Services
@using ArcaneLibs.Extensions
@using Microsoft.JSInterop.WebAssembly
@using Spacebar.AdminApi.TestClient.Services
@inject Config Config
@inject WebAssemblyJSRuntime JSRuntime
@inject JsConsoleService JsConsole
<h3>StickerManager</h3>
@* drop zone for uploads *@
<InputFile OnChange="@HandleFilesDropped" multiple="true"
style="border-radius: 16px; border: 2px dashed gray; padding: 16px; text-align: center; cursor: pointer; margin-bottom: 16px;">
<p>Drag and drop files here, or click to select.</p>
</InputFile>
<p>
@Stickers.Count stickers, of which @Stickers.Count(x => x is LocalSticker) are not yet uploaded.
<LinkButton OnClickAsync="@SaveAll">Save all</LinkButton>
</p>
@if (SaveProgress.End.Value > 0) {
<progress value="@SaveProgress.Start.Value" max="@Stickers.Count"></progress>
<span>@SaveProgress.Start.Value / @Stickers.Count</span>
}
@foreach (var sticker in Stickers.OrderBy(x => ulong.Parse(x.Id))) {
<div style="border-radius: 16px; border: 1px solid gray; padding: 8px; margin-bottom: 8px;">
<img style="aspect-ratio: 1; width: 15cqh;" alt="" src="@(sticker is LocalSticker ls ? ls.BlobUri : $"{Config.CdnUrl}/stickers/{sticker.Id}")"/>
<div style="display: inline-block; vertical-align: middle;">
<span>ID: @sticker.Id</span><br/>
<span>Name: <FancyTextBox @bind-Value="@sticker.Name"/></span><br/>
<span>Description: <FancyTextBox @bind-Value="@sticker.Description"/></span><br/>
<span>Tags: <FancyTextBox @bind-Value="@sticker.Tags"/></span><br/>
<span>Available: <InputCheckbox @bind-Value="@sticker.Available"/></span><br/>
<span>Type: @sticker.Type.ToString()</span><br/>
<span>Format Type: @sticker.FormatType.ToString()</span><br/>
@if (sticker is LocalSticker ls2) {
<LinkButton OnClickAsync="@(() => UploadAsync(ls2))">Upload</LinkButton>
}
else {
<LinkButton OnClickAsync="@(() => SaveChangesAsync(sticker))">Save</LinkButton>
}
<LinkButton OnClickAsync="@(() => DeleteAsync(sticker))" Color="#FF0000">Delete</LinkButton>
</div>
<br/>
</div>
}
@code {
[Parameter]
public required string GuildId { get; set; }
private List<Sticker> Stickers { get; set; } = [];
private Range SaveProgress { get; set; } = ..0;
protected override async Task OnInitializedAsync() {
var hc = new StreamingHttpClient();
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
Stickers = await hc.GetFromJsonAsync<List<Sticker>>($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers");
}
private class Sticker {
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("tags")]
public string Tags { get; set; }
[JsonPropertyName("available")]
public bool Available { get; set; }
[JsonPropertyName("type")]
public StickerType Type { get; set; }
[JsonPropertyName("format_type")]
public StickerFormatType FormatType { get; set; }
}
enum StickerType {
Standard = 1,
Guild = 2
}
enum StickerFormatType {
Png = 1,
Apng = 2,
Lottie = 3,
Gif = 4
}
private class LocalSticker : Sticker {
[JsonIgnore]
public required byte[] Data { get; set; }
[JsonIgnore]
public required string FileName { get; set; }
[JsonIgnore]
public required string ContentType { get; set; }
[JsonIgnore]
public required string BlobUri { get; set; }
}
private async Task<Sticker?> SaveChangesAsync(Sticker sticker) {
var hc = new StreamingHttpClient();
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
var res = await hc.PatchAsJsonAsync($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers/{sticker.Id}", new {
name = sticker.Name,
description = sticker.Description,
tags = sticker.Tags
// available = sticker.Available
});
return await res.Content.ReadFromJsonAsync<Sticker>();
}
private async Task DeleteAsync(Sticker sticker) {
var hc = new StreamingHttpClient();
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
if ((await hc.DeleteAsync($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers/{sticker.Id}")).IsSuccessStatusCode)
Stickers.Remove(sticker);
StateHasChanged();
}
private async Task<Sticker?> UploadAsync(LocalSticker sticker) {
var hc = new StreamingHttpClient();
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken);
var req = new HttpRequestMessage(HttpMethod.Post, $"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers");
MultipartFormDataContent content;
req.Content = content = new MultipartFormDataContent();
content.Add(new StringContent(sticker.Name), "name");
content.Add(new StringContent(sticker.Description ?? ""), "description");
content.Add(new StringContent(sticker.Tags ?? ""), "tags");
content.Add(new ByteArrayContent(sticker.Data) {
Headers = {
ContentType = new MediaTypeHeaderValue(sticker.ContentType)
}
}, "file", sticker.FileName);
var res = await hc.SendAsync(req);
return await res.Content.ReadFromJsonAsync<Sticker>();
}
private async Task HandleFilesDropped(InputFileChangeEventArgs arg) {
var tasks = arg.GetMultipleFiles(10000).Select(async file => {
await using var ms = new MemoryStream();
await using var stream = file.OpenReadStream(10 * 1024 * 1024);
await stream.CopyToAsync(ms);
return new LocalSticker {
Id = "0",
Name = Path.GetFileNameWithoutExtension(file.Name),
Available = true,
Type = StickerType.Guild,
FormatType = file.ContentType switch {
"image/png" => StickerFormatType.Png,
"image/apng" => StickerFormatType.Apng,
"application/json" => StickerFormatType.Lottie,
"image/gif" => StickerFormatType.Gif,
_ => throw new Exception("Unsupported sticker format: " + file.ContentType)
},
FileName = file.Name,
ContentType = file.ContentType,
Data = ms.ToArray(),
// DataUri = $"data:{file.ContentType};base64,{Convert.ToBase64String(ms.ToArray())}"
BlobUri = await GetBlobUriAsync(ms.ToArray(), file.ContentType)
};
}).ToList().ToAsyncResultEnumerable();
await foreach (var sticker in tasks) {
Stickers.Add(sticker);
if (Stickers.Count % 25 == 0) StateHasChanged();
}
StateHasChanged();
}
private async Task<string> GetBlobUriAsync(byte[] data, string type = "application/octet-stream") {
await using var jsBlob = JSRuntime.InvokeConstructor("Blob", (byte[][])[data], new { type });
var uri = JSRuntime.Invoke<string>("URL.createObjectURL", jsBlob);
return uri;
}
private async Task SaveAll() {
SaveProgress = ..Stickers.Count;
StateHasChanged();
var ss = new SemaphoreSlim(32, 32);
var tasks = Stickers.ToList().Select(async s => {
return await ProcessSticker();
async Task<(Sticker Old, Sticker? New)> ProcessSticker() {
await ss.WaitAsync();
try {
return (Old: s, New: await (s is LocalSticker ls ? UploadAsync(ls) : SaveChangesAsync(s)));
}
finally {
ss.Release();
}
}
}).ToList().ToAsyncResultEnumerable();
var sw = Stopwatch.StartNew();
await foreach (var r in tasks) {
SaveProgress = (SaveProgress.Start.Value + 1)..SaveProgress.End.Value;
Stickers[Stickers.IndexOf(r.Old)] = r.New ?? r.Old;
if (r.Old is LocalSticker ls)
JSRuntime.InvokeVoid("URL.revokeObjectURL", ls.BlobUri);
if (sw.ElapsedMilliseconds > 2000) {
StateHasChanged();
await Task.Delay(100);
sw.Restart();
}
}
SaveProgress = ..0;
StateHasChanged();
}
}

View File

@@ -2,9 +2,12 @@ using System.Net;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using ArcaneLibs.Blazor.Components.Services;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using Microsoft.JSInterop.WebAssembly;
using Spacebar.AdminApi.TestClient;
using Spacebar.AdminApi.TestClient.Services;
@@ -52,8 +55,11 @@ builder.Services.AddBlazoredLocalStorageAsSingleton(config => {
config = new Config();
await localStorage.SetItemAsync("sb_admin_tc_config", config);
}
builder.Services.AddSingleton(config);
}
builder.Services.AddSingleton<JsConsoleService>();
builder.Services.AddSingleton(sp => (WebAssemblyJSRuntime)sp.GetRequiredService<IJSRuntime>());
builder.Services.AddSingleton(sp => (IJSInProcessRuntime)sp.GetRequiredService<IJSRuntime>());
await builder.Build().RunAsync();

View File

@@ -282,9 +282,9 @@ public class StreamingHttpClient {
return await SendAsync(request, cancellationToken);
}
public async Task DeleteAsync(string url) {
public async Task<HttpResponseMessage> DeleteAsync(string url) {
var request = new HttpRequestMessage(HttpMethod.Delete, url);
await SendAsync(request);
return await SendAsync(request);
}
public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) {
@@ -293,5 +293,12 @@ public class StreamingHttpClient {
};
return await SendAsync(request);
}
public async Task<HttpResponseMessage> PatchAsJsonAsync<T>(string url, T payload) {
var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
return await SendAsync(request);
}
}
#endif