feat(nitroBypass): add sticker bypass (#184)
Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
parent
d69dfd6205
commit
7d5ade21fc
|
@ -37,25 +37,33 @@ export interface MessageObject {
|
||||||
validNonShortcutEmojis: Emoji[];
|
validNonShortcutEmojis: Emoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
export interface MessageExtra {
|
||||||
|
stickerIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean };
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channelId, messageObj, extra);
|
const result = listener(channelId, messageObj, extra);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
if (result && result.cancel === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e); }
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channeld, messageId, messageObj);
|
listener(channelId, messageId, messageObj);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
} catch (e) { MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +98,7 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
try {
|
try {
|
||||||
listener(message, channel, event);
|
listener(message, channel, event);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
} catch (e) { MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
|
@ -21,7 +21,7 @@ declare global {
|
||||||
/**
|
/**
|
||||||
* This exists only at build time, so references to it in patches should insert it
|
* This exists only at build time, so references to it in patches should insert it
|
||||||
* via String interpolation OR use different replacement code based on this
|
* via String interpolation OR use different replacement code based on this
|
||||||
* but NEVER refrence it inside the patched code
|
* but NEVER reference it inside the patched code
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // BAD
|
* // BAD
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default definePlugin({
|
||||||
find: "sendMessage:function",
|
find: "sendMessage:function",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
||||||
replace: "{Vencord.Api.MessageEvents._handlePreSend(...arguments);"
|
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
|
||||||
}, {
|
}, {
|
||||||
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
||||||
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||||
|
|
|
@ -16,18 +16,50 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addPreEditListener, addPreSendListener, removePreEditListener,removePreSendListener } from "../api/MessageEvents";
|
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "../api/MessageEvents";
|
||||||
|
import { lazyWebpack } from "../utils";
|
||||||
import { Devs } from "../utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
|
import { ApngDisposeOp, getGifEncoder, importApngJs } from "../utils/dependencies";
|
||||||
import definePlugin, { OptionType } from "../utils/types";
|
import definePlugin, { OptionType } from "../utils/types";
|
||||||
import { Settings } from "../Vencord";
|
import { Settings } from "../Vencord";
|
||||||
import { findByProps } from "../webpack";
|
import { filters } from "../webpack";
|
||||||
import { UserStore } from "../webpack/common";
|
import { ChannelStore, UserStore } from "../webpack/common";
|
||||||
|
|
||||||
|
const DRAFT_TYPE = 0;
|
||||||
|
const promptToUpload = lazyWebpack(filters.byCode("UPLOAD_FILE_LIMIT_ERROR"));
|
||||||
|
|
||||||
|
interface Sticker {
|
||||||
|
available: boolean;
|
||||||
|
description: string;
|
||||||
|
format_type: number;
|
||||||
|
guild_id: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tags: string;
|
||||||
|
type: number;
|
||||||
|
_notAvailable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StickerPack {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sku_id: string;
|
||||||
|
description: string;
|
||||||
|
cover_sticker_id: string;
|
||||||
|
banner_asset_id: string;
|
||||||
|
stickers: Sticker[];
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NitroBypass",
|
name: "NitroBypass",
|
||||||
authors: [Devs.Arjix],
|
authors: [
|
||||||
description: "Allows you to stream in nitro quality and send fake emojis.",
|
Devs.Arjix,
|
||||||
|
Devs.D3SOX,
|
||||||
|
Devs.Ven
|
||||||
|
],
|
||||||
|
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "canUseAnimatedEmojis:function",
|
find: "canUseAnimatedEmojis:function",
|
||||||
|
@ -38,10 +70,25 @@ export default definePlugin({
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(.+?}`),
|
match: new RegExp(`${func}:function\\(.+?}`),
|
||||||
replace: `${func}:function (e) { return true; }`
|
replace: `${func}:function(e){return true;}`
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
find: "canUseAnimatedEmojis:function",
|
||||||
|
predicate: () => Settings.plugins.NitroBypass.enableStickerBypass === true,
|
||||||
|
replacement: {
|
||||||
|
match: /canUseStickersEverywhere:function\(.+?}/,
|
||||||
|
replace: "canUseStickersEverywhere:function(e){return true;}"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "\"SENDABLE\"",
|
||||||
|
replacement: {
|
||||||
|
match: /(\w+)\.available\?/,
|
||||||
|
replace: "true?"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
find: "canUseAnimatedEmojis:function",
|
find: "canUseAnimatedEmojis:function",
|
||||||
predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true,
|
predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true,
|
||||||
|
@ -52,7 +99,7 @@ export default definePlugin({
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(.+?}`),
|
match: new RegExp(`${func}:function\\(.+?}`),
|
||||||
replace: `${func}:function (e) { return true; }`
|
replace: `${func}:function(e){return true;}`
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -63,8 +110,9 @@ export default definePlugin({
|
||||||
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
||||||
replace: ""
|
replace: ""
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
enableEmojiBypass: {
|
enableEmojiBypass: {
|
||||||
description: "Allow sending fake emojis",
|
description: "Allow sending fake emojis",
|
||||||
|
@ -72,6 +120,18 @@ export default definePlugin({
|
||||||
default: true,
|
default: true,
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
},
|
},
|
||||||
|
enableStickerBypass: {
|
||||||
|
description: "Allow sending fake stickers",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
stickerSize: {
|
||||||
|
description: "Size of the stickers when sending",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
default: 160,
|
||||||
|
markers: [32, 64, 128, 160, 256, 512],
|
||||||
|
},
|
||||||
enableStreamQualityBypass: {
|
enableStreamQualityBypass: {
|
||||||
description: "Allow streaming in nitro quality",
|
description: "Allow streaming in nitro quality",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -88,50 +148,162 @@ export default definePlugin({
|
||||||
return Boolean(UserStore.getCurrentUser().premiumType);
|
return Boolean(UserStore.getCurrentUser().premiumType);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getStickerLink(stickerId: string) {
|
||||||
|
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.NitroBypass.stickerSize}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
||||||
|
const [{ parseURL }, {
|
||||||
|
GIFEncoder,
|
||||||
|
quantize,
|
||||||
|
applyPalette
|
||||||
|
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
||||||
|
|
||||||
|
const { frames, width, height } = await parseURL(stickerLink);
|
||||||
|
|
||||||
|
const gif = new GIFEncoder();
|
||||||
|
const resolution = Settings.plugins.NitroBypass.stickerSize;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d", {
|
||||||
|
willReadFrequently: true
|
||||||
|
})!;
|
||||||
|
|
||||||
|
const scale = resolution / width;
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
let lastImg: HTMLImageElement | null = null;
|
||||||
|
for (const { left, top, width, height, disposeOp, img, delay } of frames) {
|
||||||
|
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
||||||
|
ctx.clearRect(left, top, width, height);
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, left, top, width, height);
|
||||||
|
|
||||||
|
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
||||||
|
|
||||||
|
const palette = quantize(data, 256);
|
||||||
|
const index = applyPalette(data, palette);
|
||||||
|
|
||||||
|
gif.writeFrame(index, resolution, resolution, {
|
||||||
|
transparent: true,
|
||||||
|
palette,
|
||||||
|
delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
|
||||||
|
ctx.drawImage(lastImg, left, top, width, height);
|
||||||
|
}
|
||||||
|
lastImg = img;
|
||||||
|
}
|
||||||
|
|
||||||
|
gif.finish();
|
||||||
|
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||||
|
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
||||||
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Settings.plugins.NitroBypass.enableEmojiBypass) {
|
const settings = Settings.plugins.NitroBypass;
|
||||||
|
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.canUseEmotes) {
|
const EmojiStore = lazyWebpack(filters.byProps("getCustomEmojiById"));
|
||||||
console.info("[NitroBypass] Skipping start because you have nitro");
|
const StickerStore = lazyWebpack(filters.byProps("getAllGuildStickers")) as {
|
||||||
return;
|
getPremiumPacks(): StickerPack[];
|
||||||
}
|
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||||
|
getStickerById(id: string): Sticker | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const { getCustomEmojiById } = findByProps("getCustomEmojiById");
|
function getWordBoundary(origStr: string, offset: number) {
|
||||||
|
|
||||||
function getWordBoundary(origStr, offset) {
|
|
||||||
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preSend = addPreSendListener((_, messageObj) => {
|
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
|
||||||
const { guildId } = this;
|
|
||||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
|
||||||
|
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
|
||||||
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48");
|
|
||||||
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.preEdit = addPreEditListener((_, __, messageObj) => {
|
|
||||||
const { guildId } = this;
|
const { guildId } = this;
|
||||||
|
|
||||||
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
if (settings.enableStickerBypass) {
|
||||||
const emoji = getCustomEmojiById(emojiId);
|
const stickerId = extra?.stickerIds?.[0];
|
||||||
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
|
|
||||||
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48");
|
if (stickerId) {
|
||||||
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
let stickerLink = this.getStickerLink(stickerId);
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
let sticker: Sticker | undefined;
|
||||||
});
|
|
||||||
|
const discordStickerPack = StickerStore.getPremiumPacks().find(pack => {
|
||||||
|
const discordSticker = pack.stickers.find(sticker => sticker.id === stickerId);
|
||||||
|
if (discordSticker) {
|
||||||
|
sticker = discordSticker;
|
||||||
|
}
|
||||||
|
return discordSticker;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (discordStickerPack) {
|
||||||
|
// discord stickers provided by the Distok project
|
||||||
|
stickerLink = `https://distok.top/stickers/${discordStickerPack.id}/${stickerId}.gif`;
|
||||||
|
} else {
|
||||||
|
// guild stickers
|
||||||
|
sticker = StickerStore.getStickerById(stickerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sticker) {
|
||||||
|
// when the user has Nitro and the sticker is available, send the sticker normally
|
||||||
|
if (this.canUseEmotes && sticker.available) {
|
||||||
|
return { cancel: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// only modify if sticker is not from current guild
|
||||||
|
if (sticker.guild_id !== guildId) {
|
||||||
|
// if it's an animated guild sticker, download it, convert to gif and send it
|
||||||
|
const isAnimated = sticker.format_type === 2;
|
||||||
|
if (!discordStickerPack && isAnimated) {
|
||||||
|
this.sendAnimatedSticker(stickerLink, stickerId, channelId);
|
||||||
|
return { cancel: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageObj.content)
|
||||||
|
messageObj.content += " ";
|
||||||
|
messageObj.content += stickerLink;
|
||||||
|
|
||||||
|
delete extra.stickerIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||||
|
for (const emoji of messageObj.validNonShortcutEmojis) {
|
||||||
|
if (!emoji.require_colons) continue;
|
||||||
|
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||||
|
|
||||||
|
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
||||||
|
const url = emoji.url.replace(/\?size=\d+/, "?size=48");
|
||||||
|
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
||||||
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cancel: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||||
|
this.preEdit = addPreEditListener((_, __, messageObj) => {
|
||||||
|
const { guildId } = this;
|
||||||
|
|
||||||
|
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
||||||
|
const emoji = EmojiStore.getCustomEmojiById(emojiId);
|
||||||
|
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
||||||
|
if (!emoji.require_colons) continue;
|
||||||
|
|
||||||
|
const url = emoji.url.replace(/\?size=\d+/, "?size=48");
|
||||||
|
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
||||||
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
|
|
@ -17,8 +17,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption } from "../api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption } from "../api/Commands";
|
||||||
|
import { lazyWebpack, makeLazy } from "../utils";
|
||||||
import { Devs } from "../utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import { lazyWebpack, makeLazy } from "../utils/misc";
|
import { getGifEncoder } from "../utils/dependencies";
|
||||||
import definePlugin from "../utils/types";
|
import definePlugin from "../utils/types";
|
||||||
import { filters } from "../webpack";
|
import { filters } from "../webpack";
|
||||||
|
|
||||||
|
@ -27,11 +28,6 @@ const DEFAULT_DELAY = 20;
|
||||||
const DEFAULT_RESOLUTION = 128;
|
const DEFAULT_RESOLUTION = 128;
|
||||||
const FRAMES = 10;
|
const FRAMES = 10;
|
||||||
|
|
||||||
// https://github.com/mattdesl/gifenc
|
|
||||||
// this lib is way better than gif.js and all other libs, they're all so terrible but this one is nice
|
|
||||||
// @ts-ignore ts mad
|
|
||||||
const getGifEncoder = makeLazy(() => import("https://unpkg.com/gifenc@1.0.3/dist/gifenc.esm.js"));
|
|
||||||
|
|
||||||
const getFrames = makeLazy(() => Promise.all(
|
const getFrames = makeLazy(() => Promise.all(
|
||||||
Array.from(
|
Array.from(
|
||||||
{ length: FRAMES },
|
{ length: FRAMES },
|
||||||
|
|
76
src/utils/dependencies.ts
Normal file
76
src/utils/dependencies.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { makeLazy } from "./misc";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add dynamically loaded dependencies for plugins here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// https://github.com/mattdesl/gifenc
|
||||||
|
// this lib is way better than gif.js and all other libs, they're all so terrible but this one is nice
|
||||||
|
// @ts-ignore ts mad
|
||||||
|
export const getGifEncoder = makeLazy(() => import("https://unpkg.com/gifenc@1.0.3/dist/gifenc.esm.js"));
|
||||||
|
|
||||||
|
// needed to parse APNGs in the nitroBypass plugin
|
||||||
|
export const importApngJs = makeLazy(async () => {
|
||||||
|
const exports = {};
|
||||||
|
const winProxy = new Proxy(window, { set: (_, k, v) => exports[k] = v });
|
||||||
|
Function("self", await fetch("https://cdnjs.cloudflare.com/ajax/libs/apng-canvas/2.1.1/apng-canvas.min.js").then(r => r.text()))(winProxy);
|
||||||
|
// @ts-ignore
|
||||||
|
return exports.APNG as { parseURL(url: string): Promise<ApngFrameData>; };
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
|
||||||
|
export enum ApngDisposeOp {
|
||||||
|
/**
|
||||||
|
* no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||||
|
*/
|
||||||
|
NONE,
|
||||||
|
/**
|
||||||
|
* the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||||
|
*/
|
||||||
|
BACKGROUND,
|
||||||
|
/**
|
||||||
|
* the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||||
|
*/
|
||||||
|
PREVIOUS
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Might need to somehow implement this
|
||||||
|
export enum ApngBlendOp {
|
||||||
|
SOURCE,
|
||||||
|
OVER
|
||||||
|
}
|
||||||
|
export interface ApngFrame {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
img: HTMLImageElement;
|
||||||
|
delay: number;
|
||||||
|
blendOp: ApngBlendOp;
|
||||||
|
disposeOp: ApngDisposeOp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApngFrameData {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
frames: ApngFrame[];
|
||||||
|
playTime: number;
|
||||||
|
}
|
Loading…
Reference in a new issue