diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 594da89..eb40363 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -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 { 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(); - 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); + } } } } diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 469834c..20b37c4 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -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(); diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 0ce4e62..08998e1 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -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; diff --git a/src/webpage/role.ts b/src/webpage/role.ts index 385f363..cd26f80 100644 --- a/src/webpage/role.ts +++ b/src/webpage/role.ts @@ -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 { 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)); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 0ebaa3c..ab5f297 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -564,27 +564,31 @@ class MDInput implements OptionsElement { this.onSubmit(this.value); } } -class EmojiInput implements OptionsElement { +class EmojiInput implements OptionsElement { readonly label: string; readonly owner: Options; - readonly onSubmit: (str: Emoji | undefined) => void; + readonly onSubmit: (str: Emoji | undefined | null) => void; input!: WeakRef; - 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 { })(); }; 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 { + +class FileInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: FileList | null) => void; input!: WeakRef; - value!: FileList | null; + value: FileList | null | undefined = undefined; clear: boolean; constructor( label: string, @@ -656,7 +677,7 @@ class FileInput implements OptionsElement { 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 { } } } - 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 { } } } +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 { submit: () => void; @@ -988,11 +1098,12 @@ class Options implements OptionsElement { 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 { 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 { } 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 { }); 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) { diff --git a/src/webpage/style.css b/src/webpage/style.css index 389b938..603e08e 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -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; diff --git a/translations/en.json b/translations/en.json index 9741106..fa13e0a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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",