diff --git a/src/webpage/disimg.ts b/src/webpage/disimg.ts index e37978e..857f349 100644 --- a/src/webpage/disimg.ts +++ b/src/webpage/disimg.ts @@ -1,8 +1,10 @@ +import {File} from "./file.js"; + class ImagesDisplay { - images: string[]; + files: File[]; index = 0; - constructor(srcs: string[], index = 0) { - this.images = srcs; + constructor(files: File[], index = 0) { + this.files = files; this.index = index; } weakbg = new WeakRef(document.createElement("div")); @@ -14,19 +16,64 @@ class ImagesDisplay { } makeHTML(): HTMLElement { //TODO this should be able to display more than one image at a time lol - const image = document.createElement("img"); - image.src = this.images[this.index]; + const image = this.files[this.index].getHTML(false, true); image.classList.add("imgfit", "centeritem"); return image; } show() { this.background = document.createElement("div"); this.background.classList.add("background"); - this.background.appendChild(this.makeHTML()); + let cur = this.makeHTML(); + if (this.files.length !== 1) { + const right = document.createElement("span"); + right.classList.add("rightArrow", "svg-intoMenu"); + right.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.index++; + this.index %= this.files.length; + cur.remove(); + cur = this.makeHTML(); + if (this.background) { + this.background.appendChild(cur); + } + }; + + const left = document.createElement("span"); + left.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.index += this.files.length - 1; + this.index %= this.files.length; + cur.remove(); + cur = this.makeHTML(); + if (this.background) { + this.background.appendChild(cur); + } + }; + left.classList.add("leftArrow", "svg-leftArrow"); + this.background.append(right, left); + this.background.addEventListener("keydown", (e) => { + if (e.key === "ArrowRight") { + e.preventDefault(); + e.stopImmediatePropagation(); + right.click(); + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + e.stopImmediatePropagation(); + right.click(); + } + }); + } + + this.background.appendChild(cur); this.background.onclick = (_) => { this.hide(); }; document.body.append(this.background); + this.background.setAttribute("tabindex", "0"); + this.background.focus(); } hide() { if (this.background) { diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts index 01fde34..1f0d205 100644 --- a/src/webpage/embed.ts +++ b/src/webpage/embed.ts @@ -5,6 +5,7 @@ import {getapiurls, getInstances} from "./utils/utils.js"; import {Guild} from "./guild.js"; import {I18n} from "./i18n.js"; import {ImagesDisplay} from "./disimg.js"; +import {File} from "./file.js"; class Embed { type: string; @@ -169,7 +170,9 @@ class Embed { const img = document.createElement("img"); img.classList.add("messageimg"); img.onclick = function () { - const full = new ImagesDisplay([img.src]); + const full = new ImagesDisplay([ + new File({id: "", filename: "", url: img.src, size: -1, content_type: "image"}, null), + ]); full.show(); }; img.src = this.json.thumbnail.proxy_url; @@ -205,7 +208,9 @@ class Embed { if (this.json.thumbnail) { img.classList.add("embedimg"); img.onclick = function () { - const full = new ImagesDisplay([img.src]); + const full = new ImagesDisplay([ + new File({id: "", filename: "", url: img.src, size: -1, content_type: "image"}, null), + ]); full.show(); }; img.src = this.json.thumbnail.proxy_url; @@ -381,7 +386,9 @@ class Embed { }; } else { img.onclick = async () => { - const full = new ImagesDisplay([img.src]); + const full = new ImagesDisplay([ + new File({id: "", filename: "", url: img.src, size: -1, content_type: "image"}, null), + ]); full.show(); }; } diff --git a/src/webpage/file.ts b/src/webpage/file.ts index f394ad4..05d8706 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -1,7 +1,7 @@ import {Message} from "./message.js"; import {filejson} from "./jsontypes.js"; import {ImagesDisplay} from "./disimg.js"; - +import {makePlayBox, MediaPlayer} from "./media.js"; class File { owner: Message | null; id: string; @@ -24,7 +24,7 @@ class File { this.content_type = fileJSON.content_type; this.size = fileJSON.size; } - getHTML(temp: boolean = false): HTMLElement { + getHTML(temp: boolean = false, fullScreen = false): HTMLElement { const src = this.proxy_url || this.url; if (this.width && this.height) { let scale = 1; @@ -37,19 +37,33 @@ class File { if (this.content_type.startsWith("image/")) { const div = document.createElement("div"); const img = document.createElement("img"); - img.classList.add("messageimg"); - div.classList.add("messageimgdiv"); - img.onclick = function () { - const full = new ImagesDisplay([img.src]); - full.show(); + if (!fullScreen) { + img.classList.add("messageimg"); + div.classList.add("messageimgdiv"); + } + img.onclick = () => { + if (this.owner) { + const full = new ImagesDisplay( + this.owner.attachments, + this.owner.attachments.indexOf(this), + ); + full.show(); + } else { + const full = new ImagesDisplay([this]); + full.show(); + } }; img.src = src; div.append(img); - if (this.width) { + if (this.width && !fullScreen) { div.style.width = this.width + "px"; div.style.height = this.height + "px"; } - return div; + if (!fullScreen) { + return div; + } else { + return img; + } } else if (this.content_type.startsWith("video/")) { const video = document.createElement("video"); const source = document.createElement("source"); @@ -63,17 +77,15 @@ class File { } return video; } else if (this.content_type.startsWith("audio/")) { - const audio = document.createElement("audio"); - const source = document.createElement("source"); - source.src = src; - audio.append(source); - source.type = this.content_type; - audio.controls = !temp; - return audio; + return this.getAudioHTML(); } else { return this.createunknown(); } } + private getAudioHTML() { + const src = this.proxy_url || this.url; + return makePlayBox(src, player); + } upHTML(files: Blob[], file: globalThis.File): HTMLElement { const div = document.createElement("div"); const contained = this.getHTML(true); @@ -149,4 +161,6 @@ class File { ); } } + +const player = new MediaPlayer(); export {File}; diff --git a/src/webpage/icons/leftArrow.svg b/src/webpage/icons/leftArrow.svg new file mode 100644 index 0000000..186769b --- /dev/null +++ b/src/webpage/icons/leftArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/pause.svg b/src/webpage/icons/pause.svg new file mode 100644 index 0000000..ab767c6 --- /dev/null +++ b/src/webpage/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/play.svg b/src/webpage/icons/play.svg new file mode 100644 index 0000000..6bac363 --- /dev/null +++ b/src/webpage/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index 584d027..6465f49 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -107,7 +107,10 @@ -
+
+
+
+
diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 7e1ffc3..e737a6e 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -357,7 +357,7 @@ type filejson = { content_type: string; width?: number; height?: number; - proxy_url: string | undefined; + proxy_url?: string; url: string; size: number; }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 10d3bdc..e84241f 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -2326,9 +2326,10 @@ class Localuser { }) .filter((_) => _ !== undefined); const sideDiv = document.getElementById("sideDiv"); - if (!sideDiv) return; + const sideContainDiv = document.getElementById("sideContainDiv"); + if (!sideDiv || !sideContainDiv) return; sideDiv.innerHTML = ""; - sideDiv.classList.add("searchDiv"); + sideContainDiv.classList.add("searchDiv"); let channel: Channel | undefined = undefined; for (const message of messages) { if (channel !== message.channel) { diff --git a/src/webpage/media.ts b/src/webpage/media.ts new file mode 100644 index 0000000..dc0d532 --- /dev/null +++ b/src/webpage/media.ts @@ -0,0 +1,507 @@ +import {I18n} from "./i18n.js"; + +type mediaEvents = + | { + type: "play"; + } + | { + type: "pause"; + } + | { + type: "playing"; + time: number; + } + | { + type: "done"; + } + | { + type: "audio"; + t: "start" | "stop"; + } + | { + type: "audio"; + t: "skip"; + time: number; + } + | { + type: "end"; + }; + +function makePlayBox(mor: string | media, player: MediaPlayer) { + const div = document.createElement("div"); + div.classList.add("flexltr", "Mplayer"); + + const button = document.createElement("img"); + button.classList.add("svg-mediaButton", "svg-play"); + + const vDiv = document.createElement("div"); + vDiv.classList.add("flexttb"); + + const title = document.createElement("span"); + title.textContent = I18n.media.loading(); + + const barDiv = document.createElement("div"); + barDiv.classList.add("flexltr"); + + const bar = document.createElement("input"); + bar.type = "range"; + bar.disabled = true; + bar.value = "0"; + bar.min = "0"; + + const time = document.createElement("span"); + time.textContent = "0:00/..:.."; + + barDiv.append(bar, time); + vDiv.append(title, barDiv); + div.append(button, vDiv); + MediaPlayer.IdentifyFile(mor).then((thing) => { + let audio: HTMLAudioElement | undefined = undefined; + + if (!thing) { + const span = document.createElement("span"); + span.textContent = I18n.media.notFound(); + return; + } + player.addListener(thing.src, followUpdates, div); + let int = setInterval((_) => {}, 1000); + if (mor instanceof Object) { + const audioo = new Audio(mor.src); + audioo.load(); + audioo.autoplay = true; + int = setInterval(() => { + if (button.classList.contains("svg-pause")) { + player.addUpdate(mor.src, {type: "playing", time: audioo.currentTime * 1000}); + } + }, 100) as unknown as number; + audioo.onplay = () => { + player.addUpdate(mor.src, {type: "play"}); + }; + audioo.onpause = () => { + player.addUpdate(thing.src, {type: "pause"}); + }; + audioo.onloadeddata = () => { + audio = audioo; + }; + audioo.onended = () => { + player.addUpdate(mor.src, {type: "end"}); + }; + } + button.onclick = () => { + if (!player.isPlaying(thing.src)) { + player.setToTopList(thing); + } else { + player.addUpdate(thing.src, { + type: "audio", + t: button.classList.contains("svg-play") ? "start" : "stop", + }); + } + }; + function followUpdates(cur: mediaEvents) { + if (audio && cur.type !== "playing") { + console.log(cur); + } + if (cur.type == "audio" && audio) { + if (cur.t == "start") { + audio.play(); + } else if (cur.t == "stop") { + audio.pause(); + } else if (cur.t == "skip" && audio) { + audio.currentTime = cur.time / 1000; + } + } + if (cur.type == "playing") { + regenTime(cur.time); + bar.value = "" + cur.time / 1000; + button.classList.add("svg-pause"); + button.classList.remove("svg-play"); + } else if (cur.type === "play") { + button.classList.add("svg-pause"); + button.classList.remove("svg-play"); + } else if (cur.type === "pause") { + button.classList.add("svg-play"); + button.classList.remove("svg-pause"); + } else if (cur.type === "end") { + clearInterval(int); + if (audio) { + audio.pause(); + audio.src = ""; + player.end(); + } + button.classList.add("svg-play"); + button.classList.remove("svg-pause"); + regenTime(); + } + } + + const med = thing; + if (med.img) { + let img: HTMLImageElement; + if (mor instanceof Object) { + img = button; + } else { + img = document.createElement("img"); + div.append(img); + } + img.classList.add("media-small"); + img.src = med.img.url; + } + function timeToString(time: number) { + const minutes = Math.floor(time / 1000 / 60); + const seconds = Math.round(time / 1000 - minutes * 60) + ""; + return `${minutes}:${seconds.padStart(2, "0")}`; + } + bar.oninput = () => { + player.addUpdate(thing.src, { + type: "audio", + t: "skip", + time: +bar.value * 1000, + }); + regenTime(+bar.value * 1000); + }; + async function regenTime(curTime: number = 0) { + console.log(med.length); + const len = await med.length; + bar.disabled = false; + bar.max = "" + len / 1000; + + time.textContent = `${timeToString(curTime)}/${timeToString(len)}`; + } + regenTime(); + title.textContent = thing.title; + console.log(thing); + }); + return div; +} + +interface media { + src: string; + img?: { + url: string; + description?: string; + }; + title: string; + artist?: string; + composer?: string; + sourcy?: string; + year?: number; + copyright?: string; + length: number | Promise; +} +class MediaPlayer { + lists: media[] = []; + curAudio?: HTMLElement; + private listeners: [(e: mediaEvents) => void, string][] = []; + elm: HTMLDivElement; + cur = 0; + constructor() { + this.elm = document.getElementById("player") as HTMLDivElement; + } + addElmToList(audio: media) { + this.lists.unshift(audio); + } + addUpdate(e: string, update: mediaEvents) { + for (const thing of this.listeners) { + if (thing[1] === e) { + thing[0](update); + } + } + } + addListener(audio: string, updates: (e: mediaEvents) => void, elm: HTMLElement) { + this.listeners.push([updates, audio]); + const int = setInterval(() => { + if (!document.contains(elm)) { + clearInterval(int); + this.listeners = this.listeners.filter((_) => _[0] !== updates); + console.log("cleared data"); + } + }, 1000); + } + isPlaying(str: string) { + const med = this.lists[this.cur]; + if (!med) return false; + return med.src === str; + } + setToTopList(audio: media) { + const med = this.lists[this.cur]; + if (med) { + this.addUpdate(med.src, {type: "end"}); + } + this.lists.splice(this.cur, 0, audio); + this.regenPlayer(); + } + end() { + if (this.curAudio) { + this.curAudio.remove(); + this.cur++; + this.regenPlayer(); + } + } + regenPlayer() { + this.elm.innerHTML = ""; + if (this.lists.length > this.cur) { + const audio = this.lists[this.cur]; + this.elm.append((this.curAudio = makePlayBox(audio, this))); + } + } + static cache = new Map>(); + static async IdentifyFile(url: string | media): Promise { + if (url instanceof Object) { + return url; + } + const cValue = this.cache.get(url); + if (cValue) { + return cValue; + } + let resMedio = (_: media) => {}; + this.cache.set(url, new Promise((res) => (resMedio = res))); + const controller = new AbortController(); + + const f = await fetch(url, { + method: "get", + signal: controller.signal, + }); + if (!f.ok || !f.body) { + return null; + } + + let index = 0; + const read = f.body.getReader(); + let cbuff = (await read.read()).value; + const output: Partial = { + src: url, + }; + try { + let sizeLeft = 0; + async function next() { + return (await get8BitArray(1))[0]; + } + async function get8BitArray(size: number) { + sizeLeft -= size; + const arr = new Uint8Array(size); + let arri = 0; + while (size > 0) { + if (!cbuff) throw Error("ran out of file to read"); + let itter = Math.min(size, cbuff.length - index); + size -= itter; + for (let i = 0; i < itter; i++, arri++, index++) { + arr[arri] = cbuff[index]; + } + + if (size !== 0) { + cbuff = (await read.read()).value; + index = 0; + console.log("got more buffer", index, arri, size); + } + } + return arr; + } + const head = String.fromCharCode(await next(), await next(), await next()); + if (head === "ID3") { + const version = (await next()) + (await next()) * 256; + + if (version === 2) { + //TODO I'm like 90% I can ignore *all* of the flags, but I need to check more sometime + await next(); + //debugger; + const sizes = await get8BitArray(4); + sizeLeft = (sizes[0] << 21) + (sizes[1] << 14) + (sizes[2] << 7) + sizes[3]; + const mappy = new Map(); + while (sizeLeft > 0) { + const Identify = String.fromCharCode(await next(), await next(), await next()); + const sizeArr = await get8BitArray(3); + const size = (sizeArr[0] << 16) + (sizeArr[1] << 8) + sizeArr[2]; + console.log(sizeLeft, size, index); + if (Identify === String.fromCharCode(0, 0, 0)) { + break; + } + if (!size) { + throw Error("weirdness"); + } + if (!Identify.match(/[A-Z0-9]/gm)) { + console.error(`tried to get ${Identify} which doesn't exist`); + break; + } + if (mappy.has(Identify)) { + await get8BitArray(size); + //console.warn("Got dupe", Identify); + } else { + mappy.set(Identify, await get8BitArray(size)); + } + console.warn(sizeLeft); + } + const pic = mappy.get("PIC"); + if (pic) { + let i = 5; //skipping info I don't need right now + const desc: number[] = []; + for (; pic[i]; i++) { + desc.push(pic[i]); + } + const description = new TextDecoder().decode(new Uint8Array(desc)); + i++; + console.warn(pic, i); + const blob = new Blob([pic.slice(i, pic.length).buffer], {type: "image/jpeg"}); + const urlmaker = window.URL || window.webkitURL; + const url = urlmaker.createObjectURL(blob); + output.img = {url, description}; + } + function decodeText(buf: ArrayBuffer) { + let str = new TextDecoder().decode(buf); + if (str.startsWith("\u0000")) { + str = str.slice(1, str.length); + } + if (str.endsWith("\u0000")) { + str = str.slice(0, str.length - 1); + } + return str; + } + const mapmap = { + TT2: "title", + TP1: "artist", + TCM: "composer", + TAL: "sourcy", + TCO: "type", + TEN: "copyright", + } as const; + for (const [key, ind] of Object.entries(mapmap)) { + const temp = mappy.get(key); + if (temp) { + //@ts-ignore TS is being weird about this idk why + output[ind] = decodeText(temp); + } + } + const tye = mappy.get("TYE"); + if (tye) { + output.year = +decodeText(tye); + } + //TODO more thoroughly check if these two are the same format + } else if (version === 3 || version === 4) { + const flags = await next(); + if (flags & 0b01000000) { + //TODO deal with the extended header + } + //debugger; + const sizes = await get8BitArray(4); + sizeLeft = (sizes[0] << 21) + (sizes[1] << 14) + (sizes[2] << 7) + sizes[3]; + const mappy = new Map(); + while (sizeLeft > 0) { + const Identify = String.fromCharCode( + await next(), + await next(), + await next(), + await next(), + ); + const sizeArr = await get8BitArray(4); + const size = (sizeArr[0] << 24) + (sizeArr[1] << 16) + (sizeArr[2] << 8) + sizeArr[3]; + + const flags = await get8BitArray(2); + const compression = !!(flags[1] & 0b10000000); + if (compression) { + //TODO Honestly, I don't know if I can do this with normal JS + continue; + } + const encyption = !!(flags[1] & 0b01000000); + if (encyption) { + //TODO not sure what this would even do + continue; + } + + console.log(sizeLeft, size, index); + if (Identify === String.fromCharCode(0, 0, 0, 0)) { + break; + } + if (!size) { + //throw Error("weirdness"); + } + if (!Identify.match(/[A-Z0-9]/gm)) { + console.error(`tried to get ${Identify} which doesn't exist`); + break; + } + if (mappy.has(Identify)) { + await get8BitArray(size); + //console.warn("Got dupe", Identify); + } else { + mappy.set(Identify, await get8BitArray(size)); + } + console.warn(sizeLeft); + } + const pic = mappy.get("APIC"); + if (pic) { + const encoding = pic[0]; + let i = 1; //skipping info I don't need right now + for (; pic[i]; i++) {} + i += 2; + let desc: number[] = []; + for (; pic[i]; i++) { + desc.push(pic[i]); + } + const description = new TextDecoder().decode(new Uint8Array(desc)); + i++; + const blob = new Blob([pic.slice(i, pic.length).buffer], {type: "image/jpeg"}); + const urlmaker = window.URL || window.webkitURL; + const url = urlmaker.createObjectURL(blob); + output.img = {url, description}; + } + function decodeText(buf: ArrayBuffer) { + let str = new TextDecoder().decode(buf); + if (str.startsWith("\u0000")) { + str = str.slice(1, str.length); + } + if (str.endsWith("\u0000")) { + str = str.slice(0, str.length - 1); + } + return str; + } + const mapmap = { + TIT2: "title", + TPE1: "artist", + TCOM: "composer", + TALB: "sourcy", + TMED: "type", + TENC: "copyright", + } as const; + for (const [key, ind] of Object.entries(mapmap)) { + const temp = mappy.get(key); + if (temp) { + //@ts-ignore TS is being weird about this idk why + output[ind] = decodeText(temp); + } + } + const TYER = mappy.get("TYER"); + if (TYER) { + console.log(decodeText(TYER)); + output.year = +decodeText(TYER); + } + const TLEN = mappy.get("TLEN"); + if (TLEN) { + output.length = +decodeText(TLEN); + } + console.log(mappy); + } + } //TODO implement more metadata types + } catch (e) { + console.error(e); + } finally { + controller.abort(); + if (!output.length) { + output.length = new Promise(async (res) => { + const audio = document.createElement("audio"); + audio.src = url; + audio.onloadeddata = (_) => { + console.log("Loaded!", audio.duration * 1000); + output.length = audio.duration * 1000; + res(audio.duration * 1000); + }; + audio.load(); + console.log(audio); + }); + } + if (!output.title) { + output.title = url.split("/").at(-1); + } + } + resMedio(output as media); + return output as media; + } +} +export {MediaPlayer, media, makePlayBox}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 80cfef5..2331a06 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -28,6 +28,19 @@ body { margin-right: 0.15in; } } +.Mplayer { + padding: 4px; + border-radius: 3px; + background: var(--secondary-bg); + align-items: center; + *{ + margin:2px; + } + .flexttb { + display: flex; + align-items: center; + } +} .flexltr { min-height: 0; display: flex; @@ -70,6 +83,12 @@ body { padding: 0; border: 0; } +#player { + height: 128px; +} +#player:empty { + height: 0px; +} .messageEditContainer { position: relative; width: 100%; @@ -266,6 +285,42 @@ textarea { cursor: pointer; user-select: none; } +.leftArrow { + z-index: 1; + width: 42px; + height: 42px; + display: block; + background: var(--primary-text-soft); + position: absolute; + top: calc(50vh - 42px); + left: 4px; + cursor: pointer; +} +.rightArrow { + z-index: 1; + width: 42px; + height: 42px; + display: block; + background: var(--primary-text-soft); + position: absolute; + top: calc(50vh - 42px); + right: 4px; + cursor: pointer; +} +.svg-mediaButton { + width: 30px; + height: 30px; + background: var(--primary-text); + cursor: pointer; + display: block; + mask-size: cover !important; +} +.svg-play { + mask: url(/icons/play.svg); +} +.svg-pause { + mask: url(/icons/pause.svg); +} .svg-announce { mask: url(/icons/announce.svg); } @@ -287,6 +342,9 @@ textarea { .svg-intoMenu { mask: url(/icons/intoMenu.svg); } +.svg-leftArrow { + mask: url(/icons/leftArrow.svg); +} .svg-channel { mask: url(/icons/channel.svg); } @@ -427,15 +485,18 @@ textarea { ::-webkit-scrollbar-thumb:hover { background: var(--primary-text-soft); } -#sideDiv:empty { +#sideContainDiv:empty { width: 0px; padding: 0; } #servers::-webkit-scrollbar, #channels::-webkit-scrollbar, -#sideDiv::-webkit-scrollbar { +#sideContainDiv::-webkit-scrollbar { display: none; } +#sideContainDiv { + width: 0; +} #servers, #channels, #sideDiv { @@ -1255,6 +1316,11 @@ span.instanceStatus { margin-top: 4px; border-radius: 4px; } +.media-small { + max-width: 0.6in !important; + max-height: 0.4in; + object-fit: contain; +} .message img { max-width: 100%; } @@ -1525,7 +1591,7 @@ img.bigembedimg { .acceptinvbutton:disabled { background: color-mix(in hsl, var(--green) 80%, var(--black)); } -#sideDiv.searchDiv { +#sideContainDiv.searchDiv { width: 30vw; .topMessage { margin-top: 2px; @@ -1541,13 +1607,13 @@ img.bigembedimg { } /* Sidebar */ #sideDiv { - display: none; flex: none; - width: 240px; padding: 16px 8px; background: var(--sidebar-bg); overflow-y: auto; box-sizing: border-box; + flex-grow: 1; + flex-shrink: 1; } .memberList { @@ -1578,8 +1644,9 @@ img.bigembedimg { height: 16px; width: 16px; } -#page:has(#memberlisttoggle:checked) #sideDiv { - display: block; +#page:has(#memberlisttoggle:checked) #sideContainDiv { + width: 240px; + flex-shrink: 0; } #page:has(#memberlisttoggle:checked) #memberlisttoggleicon span { background: var(--primary-text-prominent); @@ -2201,7 +2268,7 @@ fieldset input[type="radio"] { } @media screen and (max-width: 1000px) { - #sideDiv { + #sideContainDiv { position: absolute; right: 0; height: calc(100svh - 50px); @@ -2240,7 +2307,7 @@ fieldset input[type="radio"] { background: var(--primary-bg); transition: left 0.3s; } - #sideDiv { + #sideContainDiv { display: block; right: -100svw; width: 100svw; @@ -2269,7 +2336,7 @@ fieldset input[type="radio"] { #page:has(#maintoggle:checked) #mainarea { left: 0; } - #page:has(#memberlisttoggle:checked) #sideDiv { + #page:has(#memberlisttoggle:checked) #sideContainDiv { right: 0; } #page:has(#maintoggle:checked) #maintoggleicon { @@ -2476,3 +2543,4 @@ fieldset input[type="radio"] { right: 0.2in; cursor: pointer; } + diff --git a/translations/en.json b/translations/en.json index efcad7b..6aec589 100644 --- a/translations/en.json +++ b/translations/en.json @@ -10,6 +10,10 @@ "reply": "Reply", "copyrawtext": "Copy raw text", "copymessageid": "Copy message id", + "media": { + "notFound": "Media could not be found", + "loading": "Loading" + }, "permissions": { "descriptions": { "CREATE_INSTANT_INVITE": "Allows the user to create invites for the guild",