diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
new file mode 100644
index 000000000..73ce4ec2d
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
@@ -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
+
StickerManager
+
+@* drop zone for uploads *@
+
+
+ Drag and drop files here, or click to select.
+
+
+
+ @Stickers.Count stickers, of which @Stickers.Count(x => x is LocalSticker) are not yet uploaded.
+ Save all
+
+
+@if (SaveProgress.End.Value > 0) {
+
+ @SaveProgress.Start.Value / @Stickers.Count
+}
+
+@foreach (var sticker in Stickers.OrderBy(x => ulong.Parse(x.Id))) {
+
+

+
+ ID: @sticker.Id
+ Name:
+ Description:
+ Tags:
+ Available:
+ Type: @sticker.Type.ToString()
+ Format Type: @sticker.FormatType.ToString()
+ @if (sticker is LocalSticker ls2) {
+ Upload
+ }
+ else {
+ Save
+ }
+ Delete
+
+
+
+}
+
+@code {
+
+ [Parameter]
+ public required string GuildId { get; set; }
+
+ private List 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>($"{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 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();
+ }
+
+ 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 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();
+ }
+
+ 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 GetBlobUriAsync(byte[] data, string type = "application/octet-stream") {
+ await using var jsBlob = JSRuntime.InvokeConstructor("Blob", (byte[][])[data], new { type });
+ var uri = JSRuntime.Invoke("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();
+ }
+
+}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
index d28066da6..ba12a172c 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
@@ -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();
+builder.Services.AddSingleton(sp => (WebAssemblyJSRuntime)sp.GetRequiredService());
+builder.Services.AddSingleton(sp => (IJSInProcessRuntime)sp.GetRequiredService());
await builder.Build().RunAsync();
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
index 0f2ade713..311eb4579 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
@@ -282,9 +282,9 @@ public class StreamingHttpClient {
return await SendAsync(request, cancellationToken);
}
- public async Task DeleteAsync(string url) {
+ public async Task DeleteAsync(string url) {
var request = new HttpRequestMessage(HttpMethod.Delete, url);
- await SendAsync(request);
+ return await SendAsync(request);
}
public async Task DeleteAsJsonAsync(string url, T payload) {
@@ -293,5 +293,12 @@ public class StreamingHttpClient {
};
return await SendAsync(request);
}
+
+ public async Task PatchAsJsonAsync(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
\ No newline at end of file