make gifs animate on hover

This commit is contained in:
MathMan05 2025-04-08 20:51:57 -05:00
parent 25817fdaba
commit 4a9a17814e
10 changed files with 127 additions and 47 deletions

View file

@ -102,7 +102,7 @@ class Direct extends Guild {
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("flexltr", "liststyle"); div.classList.add("flexltr", "liststyle");
user.bind(div); user.bind(div);
div.append(user.buildpfp()); div.append(user.buildpfp(undefined, div));
const userinfos = document.createElement("div"); const userinfos = document.createElement("div");
userinfos.classList.add("flexttb"); userinfos.classList.add("flexttb");
@ -417,7 +417,7 @@ class Group extends Channel {
const myhtml = document.createElement("span"); const myhtml = document.createElement("span");
myhtml.classList.add("ellipsis"); myhtml.classList.add("ellipsis");
myhtml.textContent = this.name; myhtml.textContent = this.name;
div.appendChild(this.user.buildpfp()); div.appendChild(this.user.buildpfp(undefined, div));
div.appendChild(myhtml); div.appendChild(myhtml);
(div as any).myinfo = this; (div as any).myinfo = this;
div.onclick = (_) => { div.onclick = (_) => {

View file

@ -1,7 +1,7 @@
import {Message} from "./message.js"; import {Message} from "./message.js";
import {MarkDown} from "./markdown.js"; import {MarkDown} from "./markdown.js";
import {embedjson, invitejson} from "./jsontypes.js"; import {embedjson, invitejson} from "./jsontypes.js";
import {getapiurls, getBulkUsers, getInstances, Specialuser} from "./utils/utils.js"; import {createImg, getapiurls, getBulkUsers, getInstances, Specialuser} from "./utils/utils.js";
import {Guild} from "./guild.js"; import {Guild} from "./guild.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {ImagesDisplay} from "./disimg.js"; import {ImagesDisplay} from "./disimg.js";
@ -171,7 +171,7 @@ class Embed {
return div; return div;
} }
generateImage() { generateImage() {
const img = document.createElement("img"); const img = createImg(this.json.thumbnail.proxy_url);
img.classList.add("messageimg"); img.classList.add("messageimg");
img.onclick = function () { img.onclick = function () {
const full = new ImagesDisplay([ const full = new ImagesDisplay([
@ -179,7 +179,6 @@ class Embed {
]); ]);
full.show(); full.show();
}; };
img.src = this.json.thumbnail.proxy_url;
if (this.json.thumbnail.width) { if (this.json.thumbnail.width) {
let scale = 1; let scale = 1;
const max = 96 * 3; const max = 96 * 3;

View file

@ -3,6 +3,7 @@ import {filejson} from "./jsontypes.js";
import {ImagesDisplay} from "./disimg.js"; import {ImagesDisplay} from "./disimg.js";
import {makePlayBox, MediaPlayer} from "./media.js"; import {makePlayBox, MediaPlayer} from "./media.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {createImg} from "./utils/utils.js";
class File { class File {
owner: Message | null; owner: Message | null;
id: string; id: string;
@ -47,9 +48,10 @@ class File {
this.width /= scale; this.width /= scale;
this.height /= scale; this.height /= scale;
} }
if (this.content_type.startsWith("image/")) { if (this.content_type.startsWith("image/")) {
const div = document.createElement("div"); const div = document.createElement("div");
const img = document.createElement("img"); const img = createImg(src);
if (!fullScreen) { if (!fullScreen) {
img.classList.add("messageimg"); img.classList.add("messageimg");
div.classList.add("messageimgdiv"); div.classList.add("messageimgdiv");
@ -66,7 +68,6 @@ class File {
full.show(); full.show();
} }
}; };
img.src = src;
div.append(img); div.append(img);
if (this.width && !fullScreen) { if (this.width && !fullScreen) {
div.style.maxWidth = this.width + "px"; div.style.maxWidth = this.width + "px";

View file

@ -19,6 +19,7 @@ import {User} from "./user.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {Emoji} from "./emoji.js"; import {Emoji} from "./emoji.js";
import {webhookMenu} from "./webhooks.js"; import {webhookMenu} from "./webhooks.js";
import {createImg} from "./utils/utils.js";
class Guild extends SnowFlake { class Guild extends SnowFlake {
owner!: Localuser; owner!: Localuser;
@ -745,9 +746,8 @@ class Guild extends SnowFlake {
icon = guild.icon; icon = guild.icon;
} }
if (icon !== null) { if (icon !== null) {
const img = document.createElement("img"); const img = createImg(guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png");
img.classList.add("pfp", "servericon"); img.classList.add("pfp", "servericon");
img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png";
divy.appendChild(img); divy.appendChild(img);
if (guild instanceof Guild) { if (guild instanceof Guild) {
img.onclick = () => { img.onclick = () => {

View file

@ -3,7 +3,7 @@ import {Channel} from "./channel.js";
import {Direct} from "./direct.js"; import {Direct} from "./direct.js";
import {AVoice} from "./audio/voice.js"; import {AVoice} from "./audio/voice.js";
import {User} from "./user.js"; import {User} from "./user.js";
import {getapiurls, getBulkUsers, SW} from "./utils/utils.js"; import {createImg, getapiurls, getBulkUsers, SW} from "./utils/utils.js";
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js"; import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
import { import {
channeljson, channeljson,
@ -88,8 +88,7 @@ class Localuser {
const userInfo = document.createElement("div"); const userInfo = document.createElement("div");
userInfo.classList.add("flexltr", "switchtable"); userInfo.classList.add("flexltr", "switchtable");
const pfp = document.createElement("img"); const pfp = createImg(specialUser.pfpsrc);
pfp.src = specialUser.pfpsrc;
pfp.classList.add("pfp"); pfp.classList.add("pfp");
userInfo.append(pfp); userInfo.append(pfp);
@ -1244,24 +1243,26 @@ class Localuser {
content.classList.add("discovery-guild"); content.classList.add("discovery-guild");
if (guild.banner) { if (guild.banner) {
const banner = document.createElement("img"); const banner = createImg(
this.info.cdn + "/icons/" + guild.id + "/" + guild.banner + ".png?size=256",
);
banner.classList.add("banner"); banner.classList.add("banner");
banner.crossOrigin = "anonymous"; banner.crossOrigin = "anonymous";
banner.src = this.info.cdn + "/icons/" + guild.id + "/" + guild.banner + ".png?size=256";
banner.alt = ""; banner.alt = "";
content.appendChild(banner); content.appendChild(banner);
} }
const nameContainer = document.createElement("div"); const nameContainer = document.createElement("div");
nameContainer.classList.add("flex"); nameContainer.classList.add("flex");
const img = document.createElement("img"); const img = createImg(
this.info.cdn +
(guild.icon
? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48"
: "/embed/avatars/3.png"),
);
img.classList.add("icon"); img.classList.add("icon");
img.crossOrigin = "anonymous"; img.crossOrigin = "anonymous";
img.src =
this.info.cdn +
(guild.icon
? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48"
: "/embed/avatars/3.png");
img.alt = ""; img.alt = "";
nameContainer.appendChild(img); nameContainer.appendChild(img);
@ -1797,6 +1798,20 @@ class Localuser {
initState: !this.perminfo.user.disableColors, initState: !this.perminfo.user.disableColors,
}, },
); );
const gifSettings = ["hover", "always", "never"] as const;
accessibility.addSelect(
I18n.accessibility.playGif(),
(i) => {
localStorage.setItem("gifSetting", gifSettings[i]);
},
gifSettings.map((_) => I18n.accessibility.gifSettings[_]()),
{
defaultIndex:
((gifSettings as readonly string[]).indexOf(
localStorage.getItem("gifSetting") as string,
) + 1 || 1) - 1,
},
);
} }
{ {
const connections = settings.addButton(I18n.getTranslation("localuser.connections")); const connections = settings.addButton(I18n.getTranslation("localuser.connections"));
@ -1891,15 +1906,14 @@ class Localuser {
const container = document.createElement("div"); const container = document.createElement("div");
if (application.cover_image || application.icon) { if (application.cover_image || application.icon) {
const cover = document.createElement("img"); const cover = createImg(
cover.crossOrigin = "anonymous";
cover.src =
this.info.cdn + this.info.cdn +
"/app-icons/" + "/app-icons/" +
application.id + application.id +
"/" + "/" +
(application.cover_image || application.icon) + (application.cover_image || application.icon) +
".png?size=256"; ".png?size=256",
);
cover.alt = ""; cover.alt = "";
cover.loading = "lazy"; cover.loading = "lazy";
container.appendChild(cover); container.appendChild(cover);

View file

@ -574,7 +574,7 @@ class Message extends SnowFlake {
this.message_reference || this.message_reference ||
!messageTypes.has(premessage.type); !messageTypes.has(premessage.type);
if (combine) { if (combine) {
const pfp = this.author.buildpfp(); const pfp = this.author.buildpfp(undefined, div);
this.author.bind(pfp, this.guild, false); this.author.bind(pfp, this.guild, false);
pfpRow.appendChild(pfp); pfpRow.appendChild(pfp);
} }

View file

@ -11,6 +11,7 @@ import {I18n} from "./i18n.js";
import {Direct} from "./direct.js"; import {Direct} from "./direct.js";
import {Hover} from "./hover.js"; import {Hover} from "./hover.js";
import {Dialog} from "./settings.js"; import {Dialog} from "./settings.js";
import {createImg} from "./utils/utils.js";
class User extends SnowFlake { class User extends SnowFlake {
owner: Localuser; owner: Localuser;
@ -456,10 +457,9 @@ class User extends SnowFlake {
} }
} }
buildpfp(guild: Guild | void | Member | null): HTMLImageElement { buildpfp(guild: Guild | void | Member | null, hoverElm: void | HTMLElement): HTMLImageElement {
const pfp = document.createElement("img"); const pfp = createImg(this.getpfpsrc(), undefined, hoverElm);
pfp.loading = "lazy"; pfp.loading = "lazy";
pfp.src = this.getpfpsrc();
pfp.classList.add("pfp"); pfp.classList.add("pfp");
pfp.classList.add("userid:" + this.id); pfp.classList.add("userid:" + this.id);
if (guild) { if (guild) {
@ -467,9 +467,9 @@ class User extends SnowFlake {
if (guild instanceof Guild) { if (guild instanceof Guild) {
const memb = await Member.resolveMember(this, guild); const memb = await Member.resolveMember(this, guild);
if (!memb) return; if (!memb) return;
pfp.src = memb.getpfpsrc(); pfp.setSrcs(memb.getpfpsrc());
} else { } else {
pfp.src = guild.getpfpsrc(); pfp.setSrcs(guild.getpfpsrc());
} }
})(); })();
} }
@ -479,7 +479,7 @@ class User extends SnowFlake {
async buildstatuspfp(guild: Guild | void | Member | null): Promise<HTMLDivElement> { async buildstatuspfp(guild: Guild | void | Member | null): Promise<HTMLDivElement> {
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("pfpDiv"); div.classList.add("pfpDiv");
const pfp = this.buildpfp(guild); const pfp = this.buildpfp(guild, div);
div.append(pfp); div.append(pfp);
const status = document.createElement("div"); const status = document.createElement("div");
status.classList.add("statusDiv"); status.classList.add("statusDiv");
@ -560,9 +560,10 @@ class User extends SnowFlake {
changepfp(update: string | null): void { changepfp(update: string | null): void {
this.avatar = update; this.avatar = update;
this.hypotheticalpfp = false; this.hypotheticalpfp = false;
const src = this.getpfpsrc(); //const src = this.getpfpsrc();
Array.from(document.getElementsByClassName("userid:" + this.id)).forEach((element) => { Array.from(document.getElementsByClassName("userid:" + this.id)).forEach((_element) => {
(element as HTMLImageElement).src = src; //(element as HTMLImageElement).src = src;
//FIXME
}); });
} }
@ -719,12 +720,14 @@ class User extends SnowFlake {
for (const badgejson of badges) { for (const badgejson of badges) {
const badge = document.createElement(badgejson.link ? "a" : "div"); const badge = document.createElement(badgejson.link ? "a" : "div");
badge.classList.add("badge"); badge.classList.add("badge");
const img = document.createElement("img"); let src: string;
if (URL.canParse(badgejson.icon)) { if (URL.canParse(badgejson.icon)) {
img.src = badgejson.icon; src = badgejson.icon;
} else { } else {
img.src = this.info.cdn + "/badge-icons/" + badgejson.icon + ".png"; src = this.info.cdn + "/badge-icons/" + badgejson.icon + ".png";
} }
const img = createImg(src, undefined, badgediv);
badge.append(img); badge.append(img);
let hovertxt: string; let hovertxt: string;
if (badgejson.translate) { if (badgejson.translate) {
@ -810,11 +813,11 @@ class User extends SnowFlake {
return div; return div;
} }
getBanner(guild: Guild | null | Member): HTMLImageElement { getBanner(guild: Guild | null | Member): HTMLImageElement {
const banner = document.createElement("img"); const banner = createImg(undefined);
const bsrc = this.getBannerUrl(); const bsrc = this.getBannerUrl();
if (bsrc) { if (bsrc) {
banner.src = bsrc; banner.setSrcs(bsrc);
banner.classList.add("banner"); banner.classList.add("banner");
} }
@ -822,7 +825,7 @@ class User extends SnowFlake {
if (guild instanceof Member) { if (guild instanceof Member) {
const bsrc = guild.getBannerUrl(); const bsrc = guild.getBannerUrl();
if (bsrc) { if (bsrc) {
banner.src = bsrc; banner.setSrcs(bsrc);
banner.classList.add("banner"); banner.classList.add("banner");
} }
} else { } else {
@ -830,7 +833,7 @@ class User extends SnowFlake {
if (!memb) return; if (!memb) return;
const bsrc = memb.getBannerUrl(); const bsrc = memb.getBannerUrl();
if (bsrc) { if (bsrc) {
banner.src = bsrc; banner.setSrcs(bsrc);
banner.classList.add("banner"); banner.classList.add("banner");
} }
}); });

View file

@ -583,6 +583,63 @@ export async function getapiurls(str: string): Promise<
} }
return false; return false;
} }
function isAnimated(src: string) {
return src.endsWith(".apng") || src.endsWith(".gif");
}
const staticImgMap = new Map<string, string | Promise<string>>();
export function createImg(
src: string | undefined,
staticsrc: string | void,
elm: HTMLElement | void,
) {
const settings =
localStorage.getItem("gifSetting") || ("hover" as "hover") || "always" || "never";
const img = document.createElement("img");
elm ||= img;
img.crossOrigin = "anonymous";
img.onload = async () => {
if (settings === "always") return;
if (!src) return;
if (isAnimated(src) && !staticsrc) {
let s = staticImgMap.get(src);
if (s) {
staticsrc = await s;
} else {
staticImgMap.set(
src,
new Promise(async (res) => {
const c = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
const ctx = c.getContext("2d");
if (!ctx) return;
ctx.drawImage(img, 0, 0);
const blob = await c.convertToBlob();
res(URL.createObjectURL(blob));
}),
);
staticsrc = (await staticImgMap.get(src)) as string;
}
img.src = staticsrc;
}
};
elm.onmouseover = () => {
if (settings === "never") return;
if (img.src !== src && src) {
img.src = src;
}
};
elm.onmouseleave = () => {
if (staticsrc && settings !== "always") {
img.src = staticsrc;
}
};
img.src = settings !== "always" ? staticsrc || src || "" : src || "";
return Object.assign(img, {
setSrcs: (nsrc: string, nstaticsrc: string | void) => {
src = nsrc;
staticsrc = nstaticsrc;
},
});
}
/** /**
* *
* This function takes in a string and checks if the string is a valid instance * This function takes in a string and checks if the string is a valid instance

View file

@ -155,7 +155,7 @@ async function webhookMenu(
const nameBox = document.createElement("div"); const nameBox = document.createElement("div");
nameBox.classList.add("flexttb"); nameBox.classList.add("flexttb");
nameBox.append(name); nameBox.append(name);
const pfp = user.buildpfp(); const pfp = user.buildpfp(undefined, div);
div.append(pfp, nameBox); div.append(pfp, nameBox);
form.addHTMLArea(div); form.addHTMLArea(div);

View file

@ -147,7 +147,13 @@
"spoiler": "Spoiler", "spoiler": "Spoiler",
"accessibility": { "accessibility": {
"name": "Accessibility", "name": "Accessibility",
"roleColors": "Disable role colors" "roleColors": "Disable role colors",
"playGif": "Play gifs:",
"gifSettings": {
"hover": "Hover",
"always": "Always",
"never": "Never"
}
}, },
"searchGifs": "Search Tenor", "searchGifs": "Search Tenor",
"channel": { "channel": {