Merge branch 'dev'
This commit is contained in:
commit
289cf41bb6
5 changed files with 146 additions and 15 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "equicord",
|
"name": "equicord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.14.2.0",
|
"version": "1.14.2.1",
|
||||||
"description": "The other cutest Discord client mod",
|
"description": "The other cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
@ -122,4 +122,4 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ async function runReporter() {
|
||||||
|
|
||||||
await loadLazyChunksDone;
|
await loadLazyChunksDone;
|
||||||
|
|
||||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
|
if (IS_REPORTER && IS_WEB && !IS_VESKTOP && !IS_EQUIBOP) {
|
||||||
console.log("[REPORTER_META]", {
|
console.log("[REPORTER_META]", {
|
||||||
buildNumber: getBuildNumber(),
|
buildNumber: getBuildNumber(),
|
||||||
buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
|
buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { User } from "@vencord/discord-types";
|
import { User } from "@vencord/discord-types";
|
||||||
import { UserStore } from "@webpack/common";
|
import { IconUtils, UserStore } from "@webpack/common";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
|
|
||||||
import { CANVAS_CONFIG, CanvasConfig, FONT_SIZES, FontSizeCalculation, QuoteFont, QuoteImageOptions, SPACING } from "./types";
|
import { CANVAS_CONFIG, CanvasConfig, FONT_SIZES, FontSizeCalculation, QuoteFont, QuoteImageOptions, SPACING } from "./types";
|
||||||
|
|
||||||
|
const CUSTOM_EMOJI_REGEX = /<a?:(\w+):(\d+)>/g;
|
||||||
|
const CUSTOM_EMOJI_PLACEHOLDER = "\uFFFC";
|
||||||
|
const CUSTOM_EMOJI_SIZE_MULTIPLIER = 1.15;
|
||||||
|
const CUSTOM_EMOJI_BASELINE_OFFSET = 0.85;
|
||||||
|
|
||||||
|
interface CustomEmojiToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
animated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function sizeUpgrade(url: string): string {
|
export function sizeUpgrade(url: string): string {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
u.searchParams.set("size", "512");
|
u.searchParams.set("size", "512");
|
||||||
|
|
@ -32,7 +43,7 @@ export async function fetchImageAsBlob(url: string): Promise<Blob> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fixUpQuote(quote: string): string {
|
export function fixUpQuote(quote: string): string {
|
||||||
let result = quote.replace(/<a?:(\w+):(\d+)>/g, "");
|
let result = quote;
|
||||||
const mentionMatches = result.match(/<@!?\d+>/g);
|
const mentionMatches = result.match(/<@!?\d+>/g);
|
||||||
if (mentionMatches) {
|
if (mentionMatches) {
|
||||||
mentionMatches.forEach(match => {
|
mentionMatches.forEach(match => {
|
||||||
|
|
@ -141,6 +152,89 @@ function drawGradientOverlay(ctx: CanvasRenderingContext2D, config: CanvasConfig
|
||||||
ctx.fillRect(config.height - SPACING.gradientWidth, 0, SPACING.gradientWidth, config.height);
|
ctx.fillRect(config.height - SPACING.gradientWidth, 0, SPACING.gradientWidth, config.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractCustomEmojis(text: string): { text: string; emojis: CustomEmojiToken[]; } {
|
||||||
|
const emojis: CustomEmojiToken[] = [];
|
||||||
|
const cleanText = text.replace(CUSTOM_EMOJI_REGEX, (match, name: string, id: string) => {
|
||||||
|
emojis.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
animated: match.startsWith("<a:")
|
||||||
|
});
|
||||||
|
return CUSTOM_EMOJI_PLACEHOLDER;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { text: cleanText, emojis };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImageFromBlob(blob: Blob): Promise<HTMLImageElement> {
|
||||||
|
const image = new Image();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
image.onload = () => resolve();
|
||||||
|
image.onerror = () => reject(new Error("Failed to load image"));
|
||||||
|
image.src = blobUrl;
|
||||||
|
});
|
||||||
|
return image;
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCustomEmojiImage(emoji: CustomEmojiToken): Promise<HTMLImageElement | null> {
|
||||||
|
const animatedVariants = emoji.animated ? [true, false] : [false];
|
||||||
|
for (const animated of animatedVariants) {
|
||||||
|
try {
|
||||||
|
const blob = await fetchImageAsBlob(IconUtils.getEmojiURL({
|
||||||
|
id: emoji.id,
|
||||||
|
animated,
|
||||||
|
size: 96
|
||||||
|
}));
|
||||||
|
return await loadImageFromBlob(blob);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCustomEmojiImages(emojis: CustomEmojiToken[]): Promise<Map<string, HTMLImageElement>> {
|
||||||
|
if (!emojis.length) return new Map<string, HTMLImageElement>();
|
||||||
|
|
||||||
|
const unique = new Map<string, CustomEmojiToken>();
|
||||||
|
emojis.forEach(emoji => {
|
||||||
|
const key = `${emoji.id}:${emoji.animated ? "a" : "s"}`;
|
||||||
|
if (!unique.has(key)) unique.set(key, emoji);
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = await Promise.all(
|
||||||
|
Array.from(unique.entries()).map(async ([key, emoji]) => [key, await loadCustomEmojiImage(emoji)] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
return entries.reduce((acc, [key, image]) => {
|
||||||
|
if (image) acc.set(key, image);
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, HTMLImageElement>());
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureTextWithCustomEmojis(ctx: CanvasRenderingContext2D, text: string, fontSize: number): number {
|
||||||
|
const emojiSize = fontSize * CUSTOM_EMOJI_SIZE_MULTIPLIER;
|
||||||
|
let width = 0;
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (text[i] !== CUSTOM_EMOJI_PLACEHOLDER) continue;
|
||||||
|
width += ctx.measureText(text.slice(start, i)).width;
|
||||||
|
width += emojiSize;
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
width += ctx.measureText(text.slice(start)).width;
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
function calculateTextLines(
|
function calculateTextLines(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
|
|
@ -154,7 +248,7 @@ function calculateTextLines(
|
||||||
let currentLine: string[] = [];
|
let currentLine: string[] = [];
|
||||||
|
|
||||||
words.forEach(word => {
|
words.forEach(word => {
|
||||||
if (ctx.measureText(word).width > maxWidth) {
|
if (measureTextWithCustomEmojis(ctx, word, fontSize) > maxWidth) {
|
||||||
if (currentLine.length) {
|
if (currentLine.length) {
|
||||||
lines.push(currentLine.join(" "));
|
lines.push(currentLine.join(" "));
|
||||||
currentLine = [];
|
currentLine = [];
|
||||||
|
|
@ -163,7 +257,7 @@ function calculateTextLines(
|
||||||
let chunk = "";
|
let chunk = "";
|
||||||
for (const char of word) {
|
for (const char of word) {
|
||||||
const testChunk = chunk + char;
|
const testChunk = chunk + char;
|
||||||
if (ctx.measureText(testChunk).width > maxWidth) {
|
if (measureTextWithCustomEmojis(ctx, testChunk, fontSize) > maxWidth) {
|
||||||
if (chunk) lines.push(chunk);
|
if (chunk) lines.push(chunk);
|
||||||
chunk = char;
|
chunk = char;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -173,7 +267,7 @@ function calculateTextLines(
|
||||||
if (chunk) lines.push(chunk);
|
if (chunk) lines.push(chunk);
|
||||||
} else {
|
} else {
|
||||||
const testLine = [...currentLine, word].join(" ");
|
const testLine = [...currentLine, word].join(" ");
|
||||||
if (ctx.measureText(testLine).width > maxWidth && currentLine.length) {
|
if (measureTextWithCustomEmojis(ctx, testLine, fontSize) > maxWidth && currentLine.length) {
|
||||||
lines.push(currentLine.join(" "));
|
lines.push(currentLine.join(" "));
|
||||||
currentLine = [word];
|
currentLine = [word];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -224,17 +318,52 @@ function drawQuoteText(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
calculation: FontSizeCalculation,
|
calculation: FontSizeCalculation,
|
||||||
font: QuoteFont,
|
font: QuoteFont,
|
||||||
config: CanvasConfig
|
config: CanvasConfig,
|
||||||
|
emojis: CustomEmojiToken[],
|
||||||
|
emojiImages: Map<string, HTMLImageElement>
|
||||||
): number {
|
): number {
|
||||||
ctx.fillStyle = "#fff";
|
ctx.fillStyle = "#fff";
|
||||||
ctx.font = `300 ${calculation.fontSize}px '${font}', sans-serif`;
|
ctx.font = `300 ${calculation.fontSize}px '${font}', sans-serif`;
|
||||||
|
const emojiSize = calculation.fontSize * CUSTOM_EMOJI_SIZE_MULTIPLIER;
|
||||||
|
|
||||||
let quoteY = (config.height - calculation.totalHeight) / 2;
|
let quoteY = (config.height - calculation.totalHeight) / 2;
|
||||||
|
let emojiIndex = 0;
|
||||||
|
|
||||||
calculation.lines.forEach(line => {
|
calculation.lines.forEach(line => {
|
||||||
const xOffset = (config.quoteAreaWidth - ctx.measureText(line).width) / 2;
|
const xOffset = (config.quoteAreaWidth - measureTextWithCustomEmojis(ctx, line, calculation.fontSize)) / 2;
|
||||||
|
let x = config.quoteAreaX + xOffset;
|
||||||
|
let segmentStart = 0;
|
||||||
quoteY += calculation.lineHeight;
|
quoteY += calculation.lineHeight;
|
||||||
ctx.fillText(line, config.quoteAreaX + xOffset, quoteY);
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
if (line[i] !== CUSTOM_EMOJI_PLACEHOLDER) continue;
|
||||||
|
|
||||||
|
const segment = line.slice(segmentStart, i);
|
||||||
|
if (segment) {
|
||||||
|
ctx.fillText(segment, x, quoteY);
|
||||||
|
x += ctx.measureText(segment).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = emojis[emojiIndex++];
|
||||||
|
if (emoji) {
|
||||||
|
const key = `${emoji.id}:${emoji.animated ? "a" : "s"}`;
|
||||||
|
const image = emojiImages.get(key);
|
||||||
|
if (image) {
|
||||||
|
const y = quoteY - emojiSize * CUSTOM_EMOJI_BASELINE_OFFSET;
|
||||||
|
ctx.drawImage(image, x, y, emojiSize, emojiSize);
|
||||||
|
} else {
|
||||||
|
ctx.fillText("□", x, quoteY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x += emojiSize;
|
||||||
|
segmentStart = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tail = line.slice(segmentStart);
|
||||||
|
if (tail) {
|
||||||
|
ctx.fillText(tail, x, quoteY);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return quoteY;
|
return quoteY;
|
||||||
|
|
@ -283,6 +412,8 @@ export async function createQuoteImage(options: QuoteImageOptions): Promise<Blob
|
||||||
await ensureFontLoaded();
|
await ensureFontLoaded();
|
||||||
|
|
||||||
const quote = fixUpQuote(rawQuote);
|
const quote = fixUpQuote(rawQuote);
|
||||||
|
const { text: quoteText, emojis } = extractCustomEmojis(quote);
|
||||||
|
const emojiImages = await loadCustomEmojiImages(emojis);
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) throw new Error("Failed to get 2D rendering context");
|
if (!ctx) throw new Error("Failed to get 2D rendering context");
|
||||||
|
|
@ -302,8 +433,8 @@ export async function createQuoteImage(options: QuoteImageOptions): Promise<Blob
|
||||||
|
|
||||||
drawGradientOverlay(ctx, CANVAS_CONFIG);
|
drawGradientOverlay(ctx, CANVAS_CONFIG);
|
||||||
|
|
||||||
const calculation = calculateOptimalFontSize(ctx, quote, quoteFont, CANVAS_CONFIG);
|
const calculation = calculateOptimalFontSize(ctx, quoteText, quoteFont, CANVAS_CONFIG);
|
||||||
const quoteEndY = drawQuoteText(ctx, calculation, quoteFont, CANVAS_CONFIG);
|
const quoteEndY = drawQuoteText(ctx, calculation, quoteFont, CANVAS_CONFIG, emojis, emojiImages);
|
||||||
drawAuthorInfo(ctx, author, calculation, CANVAS_CONFIG, quoteEndY);
|
drawAuthorInfo(ctx, author, calculation, CANVAS_CONFIG, quoteEndY);
|
||||||
|
|
||||||
if (showWatermark && watermark) {
|
if (showWatermark && watermark) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class Logger {
|
||||||
constructor(public name: string, public color: string = "white") { }
|
constructor(public name: string, public color: string = "white") { }
|
||||||
|
|
||||||
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
||||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
|
if (IS_REPORTER && IS_WEB && !IS_VESKTOP && !IS_EQUIBOP) {
|
||||||
console[level]("[Equicord]", this.name + ":", ...args);
|
console[level]("[Equicord]", this.name + ":", ...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
export function relaunch() {
|
export function relaunch() {
|
||||||
if (IS_DISCORD_DESKTOP)
|
if (IS_DISCORD_DESKTOP)
|
||||||
window.DiscordNative.app.relaunch();
|
window.DiscordNative.app.relaunch();
|
||||||
else if (IS_VESKTOP)
|
else if (IS_VESKTOP || IS_EQUIBOP)
|
||||||
window.VesktopNative.app.relaunch();
|
window.VesktopNative.app.relaunch();
|
||||||
else
|
else
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue