291 lines
11 KiB
Text
291 lines
11 KiB
Text
@page "/mod/emojis"
|
|
@using Iceshrimp.Frontend.Core.Services
|
|
@using Iceshrimp.Frontend.Localization
|
|
@using Microsoft.AspNetCore.Authorization
|
|
@using Microsoft.Extensions.Localization
|
|
@using Microsoft.AspNetCore.Components.Sections
|
|
@using Iceshrimp.Assets.PhosphorIcons
|
|
@using Iceshrimp.Frontend.Core.Miscellaneous
|
|
@using Iceshrimp.Shared.Schemas.Web
|
|
@using Iceshrimp.Frontend.Components
|
|
@attribute [Authorize(Roles = "moderator")]
|
|
@layout ModerationLayout
|
|
@inject ApiService Api;
|
|
@inject GlobalComponentSvc Global;
|
|
@inject IJSRuntime Js;
|
|
@inject IStringLocalizer<Localization> Loc;
|
|
@inject ILogger<CustomEmojis> Logger;
|
|
|
|
<HeadTitle Text="@Loc["Custom Emojis"]"/>
|
|
|
|
<SectionContent SectionName="top-bar">
|
|
<Icon Name="Icons.Smiley"></Icon>
|
|
@Loc["Custom Emojis"]
|
|
@if (State is State.Empty or State.Loaded && Source == "local")
|
|
{
|
|
<span class="action btn" @onclick="OpenUpload" title="@Loc["Upload emoji"]">
|
|
<Icon Name="Icons.Upload"/>
|
|
</span>
|
|
<span class="action btn" @onclick="OpenImport" title="@Loc["Import emoji pack"]">
|
|
<Icon Name="Icons.FileArrowUp"/>
|
|
</span>
|
|
}
|
|
</SectionContent>
|
|
|
|
<div class="body">
|
|
@if (State is State.Empty or State.Error or State.Loaded)
|
|
{
|
|
<div class="emoji-search">
|
|
<input @bind="EmojiFilter" @bind:event="oninput" @bind:after="FilterEmojis" class="search" type="text"
|
|
placeholder="Search" aria-label="search"/>
|
|
<select class="search-from" @bind="Source" @bind:after="GetEmojis">
|
|
<option value="local">@Loc["Local"]</option>
|
|
<option value="remote">@Loc["Remote"]</option>
|
|
</select>
|
|
<select class="display-type" @bind=_displayType @bind:after="GetEmojis">
|
|
<option value="@DisplayType.All">@Loc["List"]</option>
|
|
<option value="@DisplayType.Categories">@Loc["Categories"]</option>
|
|
</select>
|
|
</div>
|
|
}
|
|
@if (State is State.Loaded)
|
|
{
|
|
@if (_displayType is DisplayType.All )
|
|
{
|
|
<div class="emoji-list">
|
|
@foreach (var emoji in DisplayedEmojis)
|
|
{
|
|
<EmojiManagementEntry Emoji="@emoji" Source="@Source" GetEmojis="GetEmojis"/>
|
|
}
|
|
</div>
|
|
@if (PaginationData is { Next: not null } && Source == "remote")
|
|
{
|
|
<ScrollEnd ManualLoad="FetchMore" IntersectionChange="FetchMore"></ScrollEnd>
|
|
}
|
|
}
|
|
|
|
@if (_displayType is DisplayType.Categories && Source == "local")
|
|
{
|
|
foreach (var el in LocalEmojiCategories)
|
|
{
|
|
<span class="category-name">@el.Key</span>
|
|
<div class="emoji-list">
|
|
@foreach (var emoji in el.Value)
|
|
{
|
|
<EmojiManagementEntry Emoji="emoji" Source="remote"/>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
}
|
|
|
|
@if (_displayType is DisplayType.Categories && Source == "remote")
|
|
{
|
|
@foreach (var category in DisplayedCategories)
|
|
{
|
|
<HostEmojiEntry Host="@category"/>
|
|
}
|
|
|
|
<ScrollEnd IntersectionChange="FetchHosts" ManualLoad="FetchHosts"/>
|
|
}
|
|
}
|
|
@if (State is State.Empty)
|
|
{
|
|
<i>This instance has no emojis</i>
|
|
}
|
|
@if (State is State.Loading)
|
|
{
|
|
<div class="loading">
|
|
<LoadingSpinner Scale="2"/>
|
|
</div>
|
|
}
|
|
|
|
<div class="file-input">
|
|
<InputFile @ref="UploadInput" OnChange="Upload" accept="image/*">Upload</InputFile>
|
|
<InputFile @ref="ImportInput" OnChange="Import" accept=".zip">Import</InputFile>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private List<EmojiResponse> DisplayedEmojis { get; set; } = [];
|
|
private List<EmojiResponse> StoredLocalEmojis { get; set; } = [];
|
|
private List<EmojiResponse> StoredRemoteEmojis { get; set; } = [];
|
|
private Dictionary<string, List<EmojiResponse>> LocalEmojiCategories { get; set; } = new();
|
|
private HashSet<string> Categories { get; set; } = [];
|
|
private HashSet<string> DisplayedCategories { get; set; } = [];
|
|
private string EmojiFilter { get; set; } = "";
|
|
private string Source { get; set; } = "local";
|
|
private State State { get; set; }
|
|
private InputFile UploadInput { get; set; } = null!;
|
|
private IBrowserFile UploadFile { get; set; } = null!;
|
|
private InputFile ImportInput { get; set; } = null!;
|
|
private IBrowserFile ImportFile { get; set; } = null!;
|
|
private PaginationData? PaginationData { get; set; }
|
|
private IJSObjectReference _module = null!;
|
|
private DisplayType _displayType = DisplayType.All;
|
|
|
|
private void FilterEmojis()
|
|
{
|
|
if (Source == "remote" && _displayType == DisplayType.All || Source == "local")
|
|
{
|
|
DisplayedEmojis = StoredRemoteEmojis.Where(p => p.Name.Contains(EmojiFilter.Trim()) || p.Tags.Count(a => a.Contains(EmojiFilter.Trim())) > 0).ToList();
|
|
}
|
|
|
|
if (Source == "remote" && _displayType == DisplayType.Categories)
|
|
{
|
|
DisplayedCategories = Categories.Where(p => p.Contains(EmojiFilter.Trim())).ToHashSet();
|
|
}
|
|
|
|
if (Source == "local" && _displayType == DisplayType.Categories)
|
|
{
|
|
LocalEmojiCategories = StoredLocalEmojis
|
|
.Where(p => p.Name.Contains(EmojiFilter.Trim()) || p.Tags.Count(a => a.Contains(EmojiFilter.Trim())) != 0)
|
|
.OrderBy(p => p.Name)
|
|
.ThenBy(p => p.Id)
|
|
.GroupBy(p => p.Category)
|
|
.OrderBy(p => string.IsNullOrEmpty(p.Key))
|
|
.ThenBy(p => p.Key)
|
|
.ToDictionary(p => p.Key ?? "Other", p => p.ToList());
|
|
}
|
|
}
|
|
|
|
private enum DisplayType
|
|
{
|
|
Categories,
|
|
All
|
|
}
|
|
|
|
private async Task GetEmojis()
|
|
{
|
|
if (Source == "local")
|
|
{
|
|
State = State.Loading;
|
|
|
|
StoredLocalEmojis = await Api.Emoji.GetAllEmojiAsync();
|
|
FilterEmojis();
|
|
|
|
State = StoredLocalEmojis.Count == 0 ? State.Empty : State.Loaded;
|
|
}
|
|
|
|
if (Source == "remote")
|
|
{
|
|
if (_displayType is DisplayType.Categories)
|
|
{
|
|
await FetchHosts();
|
|
}
|
|
else
|
|
{
|
|
var res = await Api.Emoji.GetRemoteEmojiAsync(new PaginationQuery());
|
|
PaginationData = res.Links;
|
|
StoredRemoteEmojis = res.Data;
|
|
}
|
|
}
|
|
|
|
if (EmojiFilter.Length == 0)
|
|
{
|
|
switch (Source)
|
|
{
|
|
case "remote":
|
|
DisplayedEmojis = StoredRemoteEmojis;
|
|
break;
|
|
case "local":
|
|
DisplayedEmojis = StoredLocalEmojis;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task FetchHosts()
|
|
{
|
|
// Due to query weirdness, MinID and MaxID are reversed here
|
|
var pq = new PaginationQuery { MinId = Categories.LastOrDefault(), Limit = 250 };
|
|
var res = await Api.Emoji.GetRemoteEmojiHostsAsync(pq);
|
|
foreach (var el in res)
|
|
{
|
|
var op = Categories.Add(el.Entity);
|
|
if (!op) Logger.LogWarning($"Duplicate host entry: {el.Entity}");
|
|
}
|
|
|
|
if (EmojiFilter.Length == 0) DisplayedCategories = Categories;
|
|
}
|
|
|
|
private async Task FetchMore()
|
|
{
|
|
if (PaginationData?.Next == null) return;
|
|
var pq = new PaginationQuery { MaxId = PaginationData.Next?.Split('=')[1], Limit = PaginationData.Limit };
|
|
var res = await Api.Emoji.GetRemoteEmojiAsync(pq);
|
|
PaginationData = res.Links;
|
|
StoredRemoteEmojis.AddRange(res.Data);
|
|
if (EmojiFilter.Length == 0)
|
|
{
|
|
DisplayedEmojis = StoredRemoteEmojis;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
// The <InputFile> Component is hidden, and triggered by a sepperate button.
|
|
// That way we get it's functionality, without the styling limitations of the InputFile component
|
|
private async Task OpenUpload()
|
|
{
|
|
await _module.InvokeVoidAsync("openUpload", UploadInput.Element);
|
|
}
|
|
|
|
private async Task OpenImport()
|
|
{
|
|
await _module.InvokeVoidAsync("openUpload", ImportInput.Element);
|
|
}
|
|
|
|
private async Task Upload(InputFileChangeEventArgs e)
|
|
{
|
|
if (!e.File.ContentType.StartsWith("image/")) return;
|
|
UploadFile = e.File;
|
|
await Global.PromptDialog?.Prompt(new EventCallback<string?>(this, UploadCallback), Loc["Set emoji name"], "", e.File.Name.Split(".")[0], buttonText: Loc["Upload"])!;
|
|
}
|
|
|
|
private async Task UploadCallback(string? name)
|
|
{
|
|
if (name == null) return;
|
|
|
|
try
|
|
{
|
|
await Api.Emoji.UploadEmojiAsync(UploadFile, name);
|
|
await Global.NoticeDialog?.Display(Loc["Successfully uploaded {0}", name])!;
|
|
await GetEmojis();
|
|
}
|
|
catch (ApiException e)
|
|
{
|
|
await Global.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
|
|
}
|
|
}
|
|
|
|
private async Task Import(InputFileChangeEventArgs e)
|
|
{
|
|
if (e.File.ContentType != "application/zip") return;
|
|
ImportFile = e.File;
|
|
await Global.ConfirmDialog?.Confirm(new EventCallback<bool>(this, ImportCallback), Loc["Import {0}?", e.File.Name], Icons.FileArrowUp, Loc["Import"])!;
|
|
}
|
|
|
|
private async Task ImportCallback(bool import)
|
|
{
|
|
if (!import) return;
|
|
|
|
try
|
|
{
|
|
await Api.Emoji.ImportEmojiAsync(ImportFile);
|
|
await Global.NoticeDialog?.Display(Loc["Successfully imported emoji pack"])!;
|
|
await GetEmojis();
|
|
}
|
|
catch (ApiException e)
|
|
{
|
|
await Global.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
|
|
}
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_module = await Js.InvokeAsync<IJSObjectReference>("import",
|
|
"./Pages/Moderation/CustomEmojis.razor.js");
|
|
await GetEmojis();
|
|
}
|
|
}
|