role icon setting and image upload improvements

This commit is contained in:
MathMan05
2025-07-16 16:35:59 -05:00
parent 54b1f43078
commit adbc99fbd4
7 changed files with 335 additions and 121 deletions
+86 -80
View File
@@ -18,7 +18,7 @@ class Emoji {
id?: string;
emoji?: string;
animated: boolean;
owner: Guild | Localuser;
owner?: Guild | Localuser;
get guild() {
if (this.owner instanceof Guild) {
return this.owner;
@@ -32,10 +32,7 @@ class Emoji {
return this.owner;
}
}
get info() {
return this.owner.info;
}
constructor(json: emojijson, owner: Guild | Localuser) {
constructor(json: emojijson, owner?: Guild | Localuser) {
this.name = json.name;
this.id = json.id;
this.animated = json.animated || false;
@@ -53,12 +50,18 @@ class Emoji {
}
getHTML(bigemoji: boolean = false) {
if (this.id) {
if (!this.owner) throw new Error("owner is missing for custom emoji!");
const emojiElem = document.createElement("img");
emojiElem.classList.add("md-emoji");
emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji");
emojiElem.crossOrigin = "anonymous";
emojiElem.src =
this.info.cdn + "/emojis/" + this.id + "." + (this.animated ? "gif" : "png") + "?size=32";
this.owner.info.cdn +
"/emojis/" +
this.id +
"." +
(this.animated ? "gif" : "png") +
"?size=32";
emojiElem.alt = this.name;
emojiElem.loading = "lazy";
@@ -147,7 +150,7 @@ class Emoji {
this: typeof Emoji,
x: number,
y: number,
localuser: Localuser,
localuser?: Localuser,
): Promise<Emoji> {
let res: (r: Emoji) => void;
this;
@@ -231,76 +234,77 @@ class Emoji {
body.classList.add("emojiBody");
let isFirst = true;
if (localuser) {
[
localuser.lookingguild,
...localuser.guilds.filter((guild) => guild !== localuser.lookingguild),
]
.filter((_) => _ !== undefined)
.filter((guild) => guild.id != "@me" && guild.emojis.length > 0)
.forEach((guild) => {
const select = document.createElement("div");
select.classList.add("emojiSelect");
[
localuser.lookingguild,
...localuser.guilds.filter((guild) => guild !== localuser.lookingguild),
]
.filter((_) => _ !== undefined)
.filter((guild) => guild.id != "@me" && guild.emojis.length > 0)
.forEach((guild) => {
const select = document.createElement("div");
select.classList.add("emojiSelect");
if (guild.properties.icon) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon", "emoji-server");
img.crossOrigin = "anonymous";
img.src =
localuser.info.cdn +
"/icons/" +
guild.properties.id +
"/" +
guild.properties.icon +
".png?size=48";
img.alt = "Server: " + guild.properties.name;
select.appendChild(img);
} else {
const div = document.createElement("span");
div.textContent = guild.properties.name
.replace(/'s /g, " ")
.replace(/\w+/g, (word) => word[0])
.replace(/\s/g, "");
select.append(div);
}
selection.append(select);
const clickEvent = () => {
search.value = "";
updateSearch.call(this);
title.textContent = guild.properties.name;
body.innerHTML = "";
for (const emojit of guild.emojis) {
const emojiElem = document.createElement("div");
emojiElem.classList.add("emojiSelect");
const emojiClass = new Emoji(
{
id: emojit.id as string,
name: emojit.name,
animated: emojit.animated as boolean,
},
localuser,
);
emojiElem.append(emojiClass.getHTML());
body.append(emojiElem);
emojiElem.addEventListener("click", () => {
res(emojiClass);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
});
if (guild.properties.icon) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon", "emoji-server");
img.crossOrigin = "anonymous";
img.src =
localuser.info.cdn +
"/icons/" +
guild.properties.id +
"/" +
guild.properties.icon +
".png?size=48";
img.alt = "Server: " + guild.properties.name;
select.appendChild(img);
} else {
const div = document.createElement("span");
div.textContent = guild.properties.name
.replace(/'s /g, " ")
.replace(/\w+/g, (word) => word[0])
.replace(/\s/g, "");
select.append(div);
}
};
select.addEventListener("click", clickEvent);
if (isFirst) {
clickEvent();
isFirst = false;
}
});
selection.append(select);
const clickEvent = () => {
search.value = "";
updateSearch.call(this);
title.textContent = guild.properties.name;
body.innerHTML = "";
for (const emojit of guild.emojis) {
const emojiElem = document.createElement("div");
emojiElem.classList.add("emojiSelect");
const emojiClass = new Emoji(
{
id: emojit.id as string,
name: emojit.name,
animated: emojit.animated as boolean,
},
localuser,
);
emojiElem.append(emojiClass.getHTML());
body.append(emojiElem);
emojiElem.addEventListener("click", () => {
res(emojiClass);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
});
}
};
select.addEventListener("click", clickEvent);
if (isFirst) {
clickEvent();
isFirst = false;
}
});
}
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
@@ -344,7 +348,7 @@ class Emoji {
search.focus();
return promise;
}
static searchEmoji(search: string, localuser: Localuser, results = 50): [Emoji, number][] {
static searchEmoji(search: string, localuser?: Localuser, results = 50): [Emoji, number][] {
//NOTE this function is used for searching in the emoji picker for reactions, and the emoji auto-fill
const ranked: [emojijson, number][] = [];
function similar(json: emojijson) {
@@ -364,11 +368,13 @@ class Emoji {
}
}
const weakGuild = new WeakMap<emojijson, Guild>();
for (const guild of localuser.guilds) {
if (guild.id !== "@me" && guild.emojis.length !== 0) {
for (const emoji of guild.emojis) {
if (similar(emoji)) {
weakGuild.set(emoji, guild);
if (localuser) {
for (const guild of localuser.guilds) {
if (guild.id !== "@me" && guild.emojis.length !== 0) {
for (const emoji of guild.emojis) {
if (similar(emoji)) {
weakGuild.set(emoji, guild);
}
}
}
}
+14 -2
View File
@@ -287,8 +287,20 @@ class Guild extends SnowFlake {
initText: this.properties.name,
});
form.addFileInput(I18n.getTranslation("guild.banner:"), "banner", {clear: true});
form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {clear: true});
form.addImageInput(I18n.getTranslation("guild.banner:"), "banner", {
clear: true,
width: 96 * 3,
initImg: this.banner
? this.info.cdn + "/icons/" + this.id + "/" + this.banner + ".png?size=256"
: "",
objectFit: "cover",
});
form.addImageInput(I18n.getTranslation("guild.icon:"), "icon", {
clear: true,
initImg: this.properties.icon
? this.info.cdn + "/icons/" + this.id + "/" + this.properties.icon + ".png"
: "",
});
form.addHR();
+23 -9
View File
@@ -1295,7 +1295,9 @@ class Localuser {
}
});
});
form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one"});
form.addImageInput(I18n.getTranslation("guild.icon:"), "icon", {
clear: true,
});
form.addTextInput(I18n.getTranslation("guild.name:"), "name", {required: true});
const loading = new Dialog("");
loading.float.options.addTitle(I18n.guild.creating());
@@ -1332,7 +1334,7 @@ class Localuser {
const template = form.addTextInput(I18n.guild.template(), "template", {
initText: templateID || "",
});
form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one"});
form.addImageInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one", clear: true});
form.addTextInput(I18n.getTranslation("guild.name:"), "name", {required: true});
const loading = new Dialog("");
@@ -1533,14 +1535,14 @@ class Localuser {
const settingsRight = userOptions.addOptions("");
settingsRight.addHTMLArea(hypotheticalProfile);
const finput = settingsLeft.addFileInput(
const finput = settingsLeft.addImageInput(
I18n.getTranslation("uploadPfp"),
(_) => {
if (file) {
this.updatepfp(file);
}
},
{clear: true},
{clear: true, initImg: this.user.getpfpsrc()},
);
finput.watchForChange((_) => {
if (!_) {
@@ -1559,14 +1561,19 @@ class Localuser {
}
});
let bfile: undefined | File | null;
const binput = settingsLeft.addFileInput(
const binput = settingsLeft.addImageInput(
I18n.getTranslation("uploadBanner"),
(_) => {
if (bfile !== undefined) {
this.updatebanner(bfile);
}
},
{clear: true},
{
clear: true,
width: 96 * 3,
initImg: this.user.banner ? this.user.getBannerUrl() : "",
objectFit: "cover",
},
);
binput.watchForChange((_) => {
if (!_) {
@@ -2391,6 +2398,7 @@ class Localuser {
headers: this.headers,
});
const json = await res.json();
console.error(json);
const form = container.addSubForm(json.name, () => {}, {
fetchURL: this.info.api + "/applications/" + appId,
method: "PATCH",
@@ -2401,7 +2409,10 @@ class Localuser {
form.addMDInput(I18n.getTranslation("localuser.description"), "description", {
initText: json.description,
});
form.addFileInput("Icon:", "icon");
form.addImageInput("Icon:", "icon", {
clear: true,
initImg: json.icon ? this.info.cdn + "/app-icons/" + appId + "/" + json.icon : "",
});
form.addTextInput(I18n.getTranslation("localuser.privacyPolcyURL"), "privacy_policy_url", {
initText: json.privacy_policy_url,
});
@@ -2441,7 +2452,7 @@ class Localuser {
if (!json.bot) {
return alert(I18n.getTranslation("localuser.confuseNoBot"));
}
const bot: mainuserjson = json.bot;
const bot: User = new User(json.bot, this);
const form = container.addSubForm(
I18n.getTranslation("localuser.editingBot", bot.username),
(out) => {
@@ -2457,7 +2468,10 @@ class Localuser {
form.addTextInput(I18n.getTranslation("localuser.botUsername"), "username", {
initText: bot.username,
});
form.addFileInput(I18n.getTranslation("localuser.botAvatar"), "avatar");
form.addImageInput(I18n.getTranslation("localuser.botAvatar"), "avatar", {
initImg: bot.getpfpsrc(),
clear: true,
});
form.addButtonInput("", I18n.getTranslation("localuser.resetToken"), async () => {
if (!confirm(I18n.getTranslation("localuser.confirmReset"))) {
return;
+26 -6
View File
@@ -86,6 +86,7 @@ export {Role};
import {Options} from "./settings.js";
import {createImg} from "./utils/utils.js";
import {Hover} from "./hover.js";
import {Emoji} from "./emoji.js";
class PermissionToggle implements OptionsElement<number> {
readonly rolejson: {
name: string;
@@ -226,11 +227,11 @@ class RoleList extends Buttons {
this.redoButtons();
}
makeguildmenus(option: Options) {
option.addButtonInput("", I18n.getTranslation("role.displaySettings"), () => {
option.addButtonInput("", I18n.role.displaySettings(), () => {
const role = this.guild.roleids.get(this.curid as string);
if (!role) return;
const form = option.addSubForm(
I18n.getTranslation("role.displaySettings"),
I18n.role.displaySettings(),
(e) => {
if ("name" in e && typeof e.name === "string") {
option.name = e.name;
@@ -245,19 +246,38 @@ class RoleList extends Buttons {
traditionalSubmit: true,
},
);
form.addTextInput(I18n.getTranslation("role.name"), "name", {
form.addTextInput(I18n.role.name(), "name", {
initText: role.name,
});
form.addCheckboxInput(I18n.getTranslation("role.hoisted"), "hoist", {
form.addCheckboxInput(I18n.role.hoisted(), "hoist", {
initState: role.hoist,
});
form.addCheckboxInput(I18n.getTranslation("role.mentionable"), "mentionable", {
form.addCheckboxInput(I18n.role.mentionable(), "mentionable", {
initState: role.mentionable,
});
const color = "#" + role.color.toString(16).padStart(6, "0");
const colorI = form.addColorInput(I18n.getTranslation("role.color"), "color", {
const colorI = form.addColorInput(I18n.role.color(), "color", {
initColor: color,
});
form.addEmojiInput(I18n.role.roleEmoji(), "unicode_emoji", undefined, {
initEmoji: role.unicode_emoji
? new Emoji(
{
name: "Emoji",
emoji: role.unicode_emoji,
},
undefined,
)
: undefined,
required: false,
clear: true,
});
form.addImageInput(I18n.role.roleFileIcon(), "icon", {
initImg: role.icon
? role.info.cdn + "/role-icons/" + role.id + "/" + role.icon + ".webp"
: "",
clear: true,
});
form.addPreprocessor((obj: any) => {
obj.color = Number("0x" + colorI.colorContent.substring(1));
+160 -22
View File
@@ -564,27 +564,31 @@ class MDInput implements OptionsElement<string> {
this.onSubmit(this.value);
}
}
class EmojiInput implements OptionsElement<Emoji | undefined> {
class EmojiInput implements OptionsElement<Emoji | undefined | null> {
readonly label: string;
readonly owner: Options;
readonly onSubmit: (str: Emoji | undefined) => void;
readonly onSubmit: (str: Emoji | undefined | null) => void;
input!: WeakRef<HTMLInputElement>;
value!: Emoji | undefined;
localuser: Localuser;
value!: Emoji | undefined | null;
localuser?: Localuser;
clear: boolean;
constructor(
label: string,
onSubmit: (str: Emoji | undefined) => void,
onSubmit: (str: Emoji | undefined | null) => void,
owner: Options,
localuser: Localuser,
{initEmoji = undefined}: {initEmoji: undefined | Emoji},
localuser?: Localuser,
{initEmoji = undefined, clear = false}: {initEmoji?: undefined | Emoji; clear?: boolean} = {},
) {
this.label = label;
this.owner = owner;
this.onSubmit = onSubmit;
this.value = initEmoji;
this.localuser = localuser;
this.clear = !!clear;
}
generateHTML(): HTMLElement {
const outDiv = document.createElement("div");
outDiv.classList.add("flexltr");
const div = document.createElement("div");
div.classList.add("flexltr", "emojiForm");
const label = document.createElement("span");
@@ -614,22 +618,39 @@ class EmojiInput implements OptionsElement<Emoji | undefined> {
})();
};
div.append(label, emoji);
return div;
outDiv.append(div);
if (this.clear) {
const button = document.createElement("button");
button.textContent = I18n.settings.clear();
button.onclick = () => {
this.value = null;
emoji.remove();
this.onchange(null);
this.owner.changed();
emoji = document.createElement("span");
emoji.classList.add("emptyEmoji");
div.append(emoji);
};
outDiv.append(button);
}
return outDiv;
}
onchange = (_: Emoji | undefined) => {};
watchForChange(func: (arg1: Emoji | undefined) => void) {
onchange = (_: Emoji | undefined | null) => {};
watchForChange(func: (arg1: Emoji | undefined | null) => void) {
this.onchange = func;
}
submit() {
this.onSubmit(this.value);
}
}
class FileInput implements OptionsElement<FileList | null> {
class FileInput implements OptionsElement<FileList | null | undefined> {
readonly label: string;
readonly owner: Options;
readonly onSubmit: (str: FileList | null) => void;
input!: WeakRef<HTMLInputElement>;
value!: FileList | null;
value: FileList | null | undefined = undefined;
clear: boolean;
constructor(
label: string,
@@ -656,7 +677,7 @@ class FileInput implements OptionsElement<FileList | null> {
innerDiv.append(input);
if (this.clear) {
const button = document.createElement("button");
button.textContent = "Clear";
button.textContent = I18n.settings.clear();
button.onclick = (_) => {
if (this.onchange) {
this.onchange(null);
@@ -679,8 +700,8 @@ class FileInput implements OptionsElement<FileList | null> {
}
}
}
onchange: ((str: FileList | null) => void) | null = null;
watchForChange(func: (str: FileList | null) => void) {
onchange: ((str: FileList | null | undefined) => void) | null = null;
watchForChange(func: (str: FileList | null | undefined) => void) {
this.onchange = func;
}
submit() {
@@ -690,6 +711,95 @@ class FileInput implements OptionsElement<FileList | null> {
}
}
}
class ImageInput extends FileInput {
img: HTMLElement;
constructor(
label: string,
onSubmit: (str: FileList | null) => void,
owner: Options,
{clear = false, initImg = "", width = -1, objectFit = ""} = {},
) {
super(label, onSubmit, owner, {clear});
console.log(initImg);
let hasimg = "" !== initImg;
const input = document.createElement("input");
input.type = "file";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
input.accept = "image/*";
const img = document.createElement("img");
img.src = initImg;
img.onclick = () => {
input.click();
};
const button = document.createElement("button");
button.textContent = I18n.settings.clear();
button.onclick = (_) => {
img.src = "";
if (this.onchange) {
this.onchange(null);
}
this.value = null;
this.owner.changed();
hasimg = false;
genImg();
};
this.clearbutton = button;
input.addEventListener("change", () => {
if (!input.files) return;
const reader = new FileReader();
reader.onload = (imgf) => {
const res = imgf.target?.result;
if (!res) return;
if (typeof res !== "string") return;
img.src = res;
hasimg = true;
genImg();
};
reader.readAsDataURL(input.files[0]);
});
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = I18n.settings.img();
const genImg = () => {
console.warn(hasimg);
if (hasimg) {
div.append(img);
span.remove();
} else {
div.append(span);
img.remove();
}
};
if (width !== -1) {
div.style.width = width + "px";
img.style.width = width + "px";
}
if (objectFit) img.style.objectFit = objectFit;
console.warn(objectFit);
this.img = div;
genImg();
}
clearbutton: HTMLButtonElement;
generateHTML(): HTMLDivElement {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const innerDiv = document.createElement("div");
innerDiv.classList.add("flexltr", "fileinputdiv");
innerDiv.append(this.img);
if (this.clear) {
innerDiv.append(this.clearbutton);
}
div.append(innerDiv);
return div;
}
}
class HtmlArea implements OptionsElement<void> {
submit: () => void;
@@ -988,11 +1098,12 @@ class Options implements OptionsElement<void> {
addEmojiInput(
label: string,
onSubmit: (str: Emoji | undefined) => void,
localuser: Localuser,
{initEmoji = undefined} = {} as {initEmoji?: Emoji},
localuser?: Localuser,
{initEmoji = undefined, clear = false} = {} as {initEmoji?: Emoji; clear?: boolean},
) {
const emoji = new EmojiInput(label, onSubmit, this, localuser, {
initEmoji: initEmoji,
clear,
});
this.options.push(emoji);
this.generate(emoji);
@@ -1025,6 +1136,16 @@ class Options implements OptionsElement<void> {
this.generate(select);
return select;
}
addImageInput(
label: string,
onSubmit: (files: FileList | null) => void,
{clear = false, initImg = "", width = -1, objectFit = ""} = {},
) {
const FI = new ImageInput(label, onSubmit, this, {clear, initImg, width, objectFit});
this.options.push(FI);
this.generate(FI);
return FI;
}
addFileInput(label: string, onSubmit: (files: FileList | null) => void, {clear = false} = {}) {
const FI = new FileInput(label, onSubmit, this, {clear});
this.options.push(FI);
@@ -1547,14 +1668,33 @@ class Form implements OptionsElement<object> {
}
return FI;
}
addImageInput(
label: string,
formName: string,
{required = false, files = "one", clear = false, initImg = "", width = -1, objectFit = ""} = {},
) {
const FI = this.options.addImageInput(label, (_) => {}, {clear, initImg, width, objectFit});
if (files !== "one" && files !== "multi") throw new Error("files should equal one or multi");
this.fileOptions.set(FI, {files});
this.names.set(formName, FI);
if (required) {
this.required.add(FI);
}
return FI;
}
addEmojiInput(
label: string,
formName: string,
localuser: Localuser,
{initEmoji = undefined, required = false} = {} as {initEmoji?: Emoji; required: boolean},
localuser?: Localuser,
{initEmoji = undefined, required = false, clear = false} = {} as {
initEmoji?: Emoji;
required: boolean;
clear?: boolean;
},
) {
const emoji = this.options.addEmojiInput(label, () => {}, localuser, {
initEmoji: initEmoji,
clear,
});
if (required) {
this.required.add(emoji);
@@ -1732,15 +1872,13 @@ class Form implements OptionsElement<object> {
});
promises.push(promise);
continue;
} else if (input.value === undefined) {
continue;
}
} else {
console.error(options.files + " is not currently implemented");
}
} else if (input instanceof EmojiInput) {
if (!input.value) {
(build as any)[thing] = undefined;
(build as any)[thing] = input.value;
} else if (input.value.id) {
(build as any)[thing] = input.value.id;
} else if (input.value.emoji) {
+20
View File
@@ -1733,6 +1733,26 @@ span.instanceStatus {
gap: 4px;
width: 100%;
}
.fileinputdiv > div {
width: 64px;
height: 64px;
object-fit: contain;
background: var(--secondary-bg);
border-radius: 16px;
cursor: pointer;
img {
width: 64px;
height: 64px;
object-fit: contain;
border-radius: 16px;
}
display: flex;
justify-content: center;
align-items: center;
span {
text-align: center;
}
}
.username {
margin-top: auto;
font-weight: bold;
+6 -2
View File
@@ -338,11 +338,15 @@
"color": "Color",
"remove": "Remove role",
"delete": "Delete Role",
"confirmDelete": "Are you sure you want to delete $1?"
"confirmDelete": "Are you sure you want to delete $1?",
"roleEmoji": "Role emoji:",
"roleFileIcon": "Role Icon:"
},
"settings": {
"unsaved": "Careful, you have unsaved changes",
"save": "Save changes"
"save": "Save changes",
"img": "Upload image",
"clear": "Clear"
},
"trace": {
"totalTime": "$2: $1ms",