From ea7769ce85e16d4058c9a5caedeac2b5bfdc9b3b Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 2 Apr 2025 10:49:06 -0500 Subject: [PATCH] add image spoilers --- src/webpage/file.ts | 70 ++++++++++++++++++++++++++++++--- src/webpage/guild.ts | 28 +++++++++++++ src/webpage/icons/spoiler.svg | 1 + src/webpage/icons/unspoiler.svg | 1 + src/webpage/style.css | 33 ++++++++++++++++ translations/en.json | 4 ++ 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 src/webpage/icons/spoiler.svg create mode 100644 src/webpage/icons/unspoiler.svg diff --git a/src/webpage/file.ts b/src/webpage/file.ts index 05d8706..6663eb6 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -2,6 +2,7 @@ import {Message} from "./message.js"; import {filejson} from "./jsontypes.js"; import {ImagesDisplay} from "./disimg.js"; import {makePlayBox, MediaPlayer} from "./media.js"; +import {I18n} from "./i18n.js"; class File { owner: Message | null; id: string; @@ -24,7 +25,17 @@ class File { this.content_type = fileJSON.content_type; this.size = fileJSON.size; } - getHTML(temp: boolean = false, fullScreen = false): HTMLElement { + getHTML(temp: boolean = false, fullScreen = false, OSpoiler = false): HTMLElement { + function makeSpoilerHTML(): HTMLElement { + const spoil = document.createElement("div"); + spoil.classList.add("fSpoil"); + const stext = document.createElement("span"); + stext.textContent = I18n.spoiler(); + spoil.append(stext); + spoil.onclick = () => spoil.remove(); + return spoil; + } + OSpoiler ||= this.filename.startsWith("SPOILER_"); const src = this.proxy_url || this.url; if (this.width && this.height) { let scale = 1; @@ -60,6 +71,9 @@ class File { div.style.height = this.height + "px"; } if (!fullScreen) { + if (OSpoiler) { + div.append(makeSpoilerHTML()); + } return div; } else { return img; @@ -75,11 +89,25 @@ class File { video.width = this.width; video.height = this.height; } + if (OSpoiler) { + const div = document.createElement("div"); + div.style.setProperty("position", "relative"); + div.append(video, makeSpoilerHTML()); + return div; + } return video; } else if (this.content_type.startsWith("audio/")) { - return this.getAudioHTML(); + const a = this.getAudioHTML(); + if (OSpoiler) { + a.append(makeSpoilerHTML()); + } + return a; } else { - return this.createunknown(); + const uk = this.createunknown(); + if (OSpoiler) { + uk.append(makeSpoilerHTML()); + } + return uk; } } private getAudioHTML() { @@ -88,10 +116,12 @@ class File { } upHTML(files: Blob[], file: globalThis.File): HTMLElement { const div = document.createElement("div"); - const contained = this.getHTML(true); + let contained = this.getHTML(true, false, file.name.startsWith("SPOILER_")); div.classList.add("containedFile"); div.append(contained); const controls = document.createElement("div"); + controls.classList.add("controls"); + const garbage = document.createElement("button"); const icon = document.createElement("span"); icon.classList.add("svgicon", "svg-delete"); @@ -100,9 +130,37 @@ class File { div.remove(); files.splice(files.indexOf(file), 1); }; - controls.classList.add("controls"); + + const spoiler = document.createElement("button"); + const sicon = document.createElement("span"); + sicon.classList.add( + "svgicon", + file.name.startsWith("SPOILER_") ? "svg-unspoiler" : "svg-spoiler", + ); + spoiler.append(sicon); + spoiler.onclick = (_) => { + if (file.name.startsWith("SPOILER_")) { + const name = file.name.split("SPOILER_"); + name.shift(); + file = files[files.indexOf(file)] = new globalThis.File([file], name.join("SPOILER_"), { + type: file.type, + }); + sicon.classList.add("svg-spoiler"); + sicon.classList.remove("svg-unspoiler"); + } else { + file = files[files.indexOf(file)] = new globalThis.File([file], "SPOILER_" + file.name, { + type: file.type, + }); + sicon.classList.add("svg-unspoiler"); + sicon.classList.remove("svg-spoiler"); + } + contained.remove(); + contained = this.getHTML(true, false, file.name.startsWith("SPOILER_")); + div.append(contained); + }; + div.append(controls); - controls.append(garbage); + controls.append(spoiler, garbage); return div; } static initFromBlob(file: globalThis.File) { diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 67eb539..551713c 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -317,6 +317,34 @@ class Guild extends SnowFlake { genDiv(); emoji.addHTMLArea(containdiv); } + (async () => { + const widgetMenu = settings.addButton(I18n.widget()); + const cur = (await ( + await fetch(this.info.api + "/guilds/" + this.id + "/widget", { + headers: this.headers, + }) + ).json()) as { + enabled: boolean; + channel_id?: null | string; + }; + const form = widgetMenu.addForm("", () => {}, { + traditionalSubmit: true, + fetchURL: this.info.api + "/guilds/" + this.id + "/widget", + headers: this.headers, + method: "PATCH", + }); + form.addCheckboxInput(I18n.widgetEnabled(), "enabled", {initState: cur.enabled}); + const channels = this.channels.filter((_) => _.type !== 4); + form.addSelect( + I18n.channel.name(), + "channel_id", + channels.map((_) => _.name), + { + defaultIndex: channels.findIndex((_) => _.id == cur.channel_id), + }, + channels.map((_) => _.id), + ); + })(); const webhooks = settings.addButton(I18n.webhooks.base()); webhookMenu(this, this.info.api + `/guilds/${this.id}/webhooks`, webhooks); settings.show(); diff --git a/src/webpage/icons/spoiler.svg b/src/webpage/icons/spoiler.svg new file mode 100644 index 0000000..f45543f --- /dev/null +++ b/src/webpage/icons/spoiler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/unspoiler.svg b/src/webpage/icons/unspoiler.svg new file mode 100644 index 0000000..032375f --- /dev/null +++ b/src/webpage/icons/unspoiler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/style.css b/src/webpage/style.css index 56a114a..aa5053d 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -36,6 +36,7 @@ body { flex-grow: 0; flex-shrink: 0; height: 52px; + position: relative; * { margin: 2px; @@ -333,6 +334,12 @@ textarea { display: block; mask-size: cover !important; } +.svg-spoiler { + mask: url(/icons/spoiler.svg); +} +.svg-unspoiler { + mask: url(/icons/unspoiler.svg); +} .svg-soundMore { mask: url(/icons/soundMore.svg); } @@ -1094,6 +1101,7 @@ span.instanceStatus { } .messageimgdiv { height: 100%; + position: relative; } .messageimg { height: 100%; @@ -1223,6 +1231,29 @@ span.instanceStatus { .messagediv:hover { background: var(--primary-hover); } +.fSpoil { + position: absolute; + top: 0; + width: 100%; + height: 100%; + backdrop-filter: blur(20px); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: backdrop-filter 0.4s; + span { + background: var(--spoiler-bg); + padding: 3px 6px; + border-radius: 6px; + } +} +.fSpoil:hover { + backdrop-filter: blur(15px); + span { + background: var(--spoiler-hover); + } +} .messageButtons, .controls { position: absolute; @@ -1262,6 +1293,7 @@ span.instanceStatus { top: 6px; right: 6px; box-shadow: 0 0 1.5px var(--primary-text-soft); + z-index: 1; } .message { padding-right: 28px; @@ -1603,6 +1635,7 @@ img.bigembedimg { padding: 4px; background: var(--secondary-bg); border-radius: 4px; + position: relative; } .acceptinvbutton { width: calc(100% - 24px); diff --git a/translations/en.json b/translations/en.json index 0efe2e6..67f8265 100644 --- a/translations/en.json +++ b/translations/en.json @@ -144,7 +144,9 @@ "typing": "$2 {{PLURAL:$1|is|are}} typing", "noMessages": "No messages appear to be here, be the first to say something!", "blankMessage": "Blank Message", + "spoiler": "Spoiler", "channel": { + "name": "Channel", "copyId": "Copy channel id", "markRead": "Mark as read", "settings": "Settings", @@ -509,6 +511,8 @@ "name:": "Name:", "confirmDel": "Are you sure you want to delete this emoji?" }, + "widget": "Guild Widget", + "widgetEnabled": "Widget enabled", "incorrectURLS": "## This instance has likely sent the incorrect URLs.\n### If you're the instance owner please see [here](https://docs.spacebar.chat/setup/server/) under *Connecting from remote machines* to correct the issue.\n Would you like Jank Client to automatically try to fix this error to let you connect to the instance?", "jankInfo": "Client Information", "clientDesc": "Client version: $1\n\n[Join the official Jank Client guild]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Help translate Jank Client](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Help create Jank client](https://github.com/MathMan05/JankClient)\n\n[Help maintain the server jank client relies on](https://github.com/spacebarchat/server)\n\nCalculated rights: $3",