706 lines
20 KiB
TypeScript
706 lines
20 KiB
TypeScript
|
import type { DragPosition } from "components/system/Files/FileManager/useDraggableEntries";
|
||
|
import type { Size } from "components/system/Window/RndWindow/useResizable";
|
||
|
import type { Processes, RelativePosition } from "contexts/process/types";
|
||
|
import type {
|
||
|
IconPosition,
|
||
|
IconPositions,
|
||
|
SortOrders,
|
||
|
} from "contexts/session/types";
|
||
|
import type { Position } from "eruda";
|
||
|
import type HtmlToImage from "html-to-image";
|
||
|
import { basename, dirname, extname, join } from "path";
|
||
|
import {
|
||
|
DEFAULT_LOCALE,
|
||
|
HIGH_PRIORITY_REQUEST,
|
||
|
MAX_RES_ICON_OVERRIDE,
|
||
|
ONE_TIME_PASSIVE_EVENT,
|
||
|
TASKBAR_HEIGHT,
|
||
|
TIMESTAMP_DATE_FORMAT,
|
||
|
} from "utils/constants";
|
||
|
|
||
|
export const GOOGLE_SEARCH_QUERY = "https://www.google.com/search?igu=1&q=";
|
||
|
|
||
|
export const bufferToBlob = (buffer: Buffer, type?: string): Blob =>
|
||
|
new Blob([buffer], type ? { type } : undefined);
|
||
|
|
||
|
export const bufferToUrl = (buffer: Buffer, mimeType?: string): string =>
|
||
|
mimeType
|
||
|
? `data:${mimeType};base64,${buffer.toString("base64")}`
|
||
|
: URL.createObjectURL(bufferToBlob(buffer));
|
||
|
|
||
|
let dpi: number;
|
||
|
|
||
|
export const getDpi = (): number => {
|
||
|
if (typeof dpi === "number") return dpi;
|
||
|
|
||
|
dpi = Math.min(Math.ceil(window.devicePixelRatio), 3);
|
||
|
|
||
|
return dpi;
|
||
|
};
|
||
|
|
||
|
export const sendMouseClick = (target: HTMLElement, count = 1): void => {
|
||
|
if (count === 0) return;
|
||
|
|
||
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||
|
|
||
|
sendMouseClick(target, count - 1);
|
||
|
};
|
||
|
|
||
|
export const toggleFullScreen = async (): Promise<void> => {
|
||
|
try {
|
||
|
await (document.fullscreenElement
|
||
|
? document.exitFullscreen()
|
||
|
: document.documentElement.requestFullscreen());
|
||
|
} catch {
|
||
|
// Ignore failure to enter fullscreen
|
||
|
}
|
||
|
};
|
||
|
|
||
|
export const toggleShowDesktop = (
|
||
|
processes: Processes,
|
||
|
minimize: (id: string) => void
|
||
|
): void => {
|
||
|
const processArray = Object.entries(processes);
|
||
|
const allWindowsMinimized =
|
||
|
processArray.length > 0 &&
|
||
|
!processArray.some(([, { minimized }]) => !minimized);
|
||
|
|
||
|
processArray.forEach(
|
||
|
([pid, { minimized }]) =>
|
||
|
(allWindowsMinimized || (!allWindowsMinimized && !minimized)) &&
|
||
|
minimize(pid)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export const imageSrc = (
|
||
|
imagePath: string,
|
||
|
size: number,
|
||
|
ratio: number,
|
||
|
extension: string
|
||
|
): string => {
|
||
|
const imageName = basename(imagePath, ".webp");
|
||
|
const [expectedSize, maxIconSize] = MAX_RES_ICON_OVERRIDE[imageName] || [];
|
||
|
const ratioSize = size * ratio;
|
||
|
const imageSize =
|
||
|
expectedSize === size ? Math.min(maxIconSize, ratioSize) : ratioSize;
|
||
|
|
||
|
return `${join(
|
||
|
dirname(imagePath),
|
||
|
`${imageSize}x${imageSize}`,
|
||
|
`${imageName}${extension}`
|
||
|
).replace(/\\/g, "/")}${ratio > 1 ? ` ${ratio}x` : ""}`;
|
||
|
};
|
||
|
|
||
|
export const imageSrcs = (
|
||
|
imagePath: string,
|
||
|
size: number,
|
||
|
extension: string,
|
||
|
failedUrls?: string[]
|
||
|
): string => {
|
||
|
return [
|
||
|
imageSrc(imagePath, size, 1, extension),
|
||
|
imageSrc(imagePath, size, 2, extension),
|
||
|
imageSrc(imagePath, size, 3, extension),
|
||
|
]
|
||
|
.filter(
|
||
|
(url) => !failedUrls?.length || failedUrls?.includes(url.split(" ")[0])
|
||
|
)
|
||
|
.join(", ");
|
||
|
};
|
||
|
|
||
|
export const imageToBufferUrl = (
|
||
|
path: string,
|
||
|
buffer: Buffer | string
|
||
|
): string =>
|
||
|
extname(path) === ".svg"
|
||
|
? `data:image/svg+xml;base64,${window.btoa(buffer.toString())}`
|
||
|
: `data:image/png;base64,${buffer.toString("base64")}`;
|
||
|
|
||
|
export const blobToBase64 = (blob: Blob): Promise<string> =>
|
||
|
new Promise((resolve) => {
|
||
|
const fileReader = new FileReader();
|
||
|
|
||
|
fileReader.readAsDataURL(blob);
|
||
|
fileReader.onloadend = () => resolve(fileReader.result as string);
|
||
|
});
|
||
|
|
||
|
type JxlDecodeResponse = { data: { imgData: ImageData } };
|
||
|
|
||
|
export const decodeJxl = async (image: Buffer): Promise<ImageData> =>
|
||
|
new Promise((resolve) => {
|
||
|
const worker = new Worker("System/JXL.js/jxl_dec.js");
|
||
|
|
||
|
worker.postMessage({ image, jxlSrc: "image.jxl" });
|
||
|
worker.addEventListener("message", (message: JxlDecodeResponse) =>
|
||
|
resolve(message?.data?.imgData)
|
||
|
);
|
||
|
});
|
||
|
|
||
|
export const imgDataToBuffer = (imageData: ImageData): Buffer => {
|
||
|
const canvas = document.createElement("canvas");
|
||
|
|
||
|
canvas.width = imageData.width;
|
||
|
canvas.height = imageData.height;
|
||
|
canvas.getContext("2d")?.putImageData(imageData, 0, 0);
|
||
|
|
||
|
return Buffer.from(
|
||
|
canvas?.toDataURL("image/png").replace("data:image/png;base64,", ""),
|
||
|
"base64"
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export const cleanUpBufferUrl = (url: string): void => URL.revokeObjectURL(url);
|
||
|
|
||
|
const loadScript = (
|
||
|
src: string,
|
||
|
defer?: boolean,
|
||
|
force?: boolean,
|
||
|
asModule?: boolean
|
||
|
): Promise<Event> =>
|
||
|
new Promise((resolve, reject) => {
|
||
|
const loadedScripts = [...document.scripts];
|
||
|
const currentScript = loadedScripts.find((loadedScript) =>
|
||
|
loadedScript.src.endsWith(src)
|
||
|
);
|
||
|
|
||
|
if (currentScript) {
|
||
|
if (!force) {
|
||
|
resolve(new Event("Already loaded."));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
currentScript.remove();
|
||
|
}
|
||
|
|
||
|
const script = document.createElement(
|
||
|
"script"
|
||
|
) as HTMLElementWithPriority<HTMLScriptElement>;
|
||
|
|
||
|
script.async = false;
|
||
|
if (defer) script.defer = true;
|
||
|
if (asModule) script.type = "module";
|
||
|
script.fetchPriority = "high";
|
||
|
script.src = src;
|
||
|
script.addEventListener("error", reject, ONE_TIME_PASSIVE_EVENT);
|
||
|
script.addEventListener("load", resolve, ONE_TIME_PASSIVE_EVENT);
|
||
|
|
||
|
document.head.append(script);
|
||
|
});
|
||
|
|
||
|
const loadStyle = (href: string): Promise<Event> =>
|
||
|
new Promise((resolve, reject) => {
|
||
|
const loadedStyles = [
|
||
|
...document.querySelectorAll("link[rel=stylesheet]"),
|
||
|
] as HTMLLinkElement[];
|
||
|
|
||
|
if (loadedStyles.some((loadedStyle) => loadedStyle.href.endsWith(href))) {
|
||
|
resolve(new Event("Already loaded."));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const link = document.createElement(
|
||
|
"link"
|
||
|
) as HTMLElementWithPriority<HTMLLinkElement>;
|
||
|
|
||
|
link.rel = "stylesheet";
|
||
|
link.fetchPriority = "high";
|
||
|
link.href = href;
|
||
|
link.addEventListener("error", reject, ONE_TIME_PASSIVE_EVENT);
|
||
|
link.addEventListener("load", resolve, ONE_TIME_PASSIVE_EVENT);
|
||
|
|
||
|
document.head.append(link);
|
||
|
});
|
||
|
|
||
|
export const loadFiles = async (
|
||
|
files?: string[],
|
||
|
defer?: boolean,
|
||
|
force?: boolean,
|
||
|
asModule?: boolean
|
||
|
): Promise<void> =>
|
||
|
!files || files.length === 0
|
||
|
? Promise.resolve()
|
||
|
: files.reduce(async (_promise, file) => {
|
||
|
await (extname(file).toLowerCase() === ".css"
|
||
|
? loadStyle(encodeURI(file))
|
||
|
: loadScript(encodeURI(file), defer, force, asModule));
|
||
|
}, Promise.resolve());
|
||
|
|
||
|
export const getHtmlToImage = async (): Promise<
|
||
|
typeof HtmlToImage | undefined
|
||
|
> => {
|
||
|
await loadFiles(["/System/html-to-image/html-to-image.js"]);
|
||
|
|
||
|
const { htmlToImage } = window as unknown as Window & {
|
||
|
htmlToImage: typeof HtmlToImage;
|
||
|
};
|
||
|
|
||
|
return htmlToImage;
|
||
|
};
|
||
|
|
||
|
export const pxToNum = (value: number | string = 0): number =>
|
||
|
typeof value === "number" ? value : Number.parseFloat(value);
|
||
|
|
||
|
export const viewHeight = (): number => window.innerHeight;
|
||
|
|
||
|
export const viewWidth = (): number => window.innerWidth;
|
||
|
|
||
|
export const getWindowViewport = (): Position => ({
|
||
|
x: viewWidth(),
|
||
|
y: viewHeight() - TASKBAR_HEIGHT,
|
||
|
});
|
||
|
|
||
|
export const calcInitialPosition = (
|
||
|
relativePosition: RelativePosition,
|
||
|
container: HTMLElement
|
||
|
): Position => ({
|
||
|
x: relativePosition.left || viewWidth() - (relativePosition.right || 0),
|
||
|
y:
|
||
|
relativePosition.top ||
|
||
|
viewHeight() - (relativePosition.bottom || 0) - container.offsetHeight,
|
||
|
});
|
||
|
|
||
|
const GRID_TEMPLATE_ROWS = "grid-template-rows";
|
||
|
|
||
|
const calcGridDropPosition = (
|
||
|
gridElement: HTMLElement | null,
|
||
|
{ x = 0, y = 0 }: DragPosition
|
||
|
): IconPosition => {
|
||
|
if (!gridElement) return Object.create(null) as IconPosition;
|
||
|
|
||
|
const gridComputedStyle = window.getComputedStyle(gridElement);
|
||
|
const gridTemplateRows = gridComputedStyle
|
||
|
.getPropertyValue(GRID_TEMPLATE_ROWS)
|
||
|
.split(" ");
|
||
|
const gridTemplateColumns = gridComputedStyle
|
||
|
.getPropertyValue("grid-template-columns")
|
||
|
.split(" ");
|
||
|
const gridRowHeight = pxToNum(gridTemplateRows[0]);
|
||
|
const gridColumnWidth = pxToNum(gridTemplateColumns[0]);
|
||
|
const gridColumnGap = pxToNum(
|
||
|
gridComputedStyle.getPropertyValue("grid-column-gap")
|
||
|
);
|
||
|
const gridRowGap = pxToNum(
|
||
|
gridComputedStyle.getPropertyValue("grid-row-gap")
|
||
|
);
|
||
|
const paddingTop = pxToNum(gridComputedStyle.getPropertyValue("padding-top"));
|
||
|
|
||
|
return {
|
||
|
gridColumnStart: Math.min(
|
||
|
Math.ceil(x / (gridColumnWidth + gridColumnGap)),
|
||
|
gridTemplateColumns.length
|
||
|
),
|
||
|
gridRowStart: Math.min(
|
||
|
Math.ceil((y - paddingTop) / (gridRowHeight + gridRowGap)),
|
||
|
gridTemplateRows.length
|
||
|
),
|
||
|
};
|
||
|
};
|
||
|
|
||
|
const updateIconPositionsIfEmpty = (
|
||
|
url: string,
|
||
|
gridElement: HTMLElement | null,
|
||
|
iconPositions: IconPositions,
|
||
|
sortOrders: SortOrders
|
||
|
): IconPositions => {
|
||
|
if (!gridElement) return iconPositions;
|
||
|
|
||
|
const [fileOrder = []] = sortOrders[url] || [];
|
||
|
const newIconPositions: IconPositions = {};
|
||
|
const gridComputedStyle = window.getComputedStyle(gridElement);
|
||
|
const gridTemplateRowCount = gridComputedStyle
|
||
|
.getPropertyValue(GRID_TEMPLATE_ROWS)
|
||
|
.split(" ").length;
|
||
|
|
||
|
fileOrder.forEach((entry, index) => {
|
||
|
const entryUrl = join(url, entry);
|
||
|
|
||
|
if (!iconPositions[entryUrl]) {
|
||
|
const gridEntry = [...gridElement.children].find((element) =>
|
||
|
element.querySelector(`button[aria-label="${entry}"]`)
|
||
|
);
|
||
|
|
||
|
if (gridEntry instanceof HTMLElement) {
|
||
|
const { x, y, height, width } = gridEntry.getBoundingClientRect();
|
||
|
|
||
|
newIconPositions[entryUrl] = calcGridDropPosition(gridElement, {
|
||
|
x: x - width,
|
||
|
y: y + height,
|
||
|
});
|
||
|
} else {
|
||
|
const position = index + 1;
|
||
|
const gridColumnStart = Math.ceil(position / gridTemplateRowCount);
|
||
|
const gridRowStart =
|
||
|
position - gridTemplateRowCount * (gridColumnStart - 1);
|
||
|
|
||
|
newIconPositions[entryUrl] = { gridColumnStart, gridRowStart };
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return Object.keys(newIconPositions).length > 0
|
||
|
? { ...newIconPositions, ...iconPositions }
|
||
|
: iconPositions;
|
||
|
};
|
||
|
|
||
|
const calcGridPositionOffset = (
|
||
|
url: string,
|
||
|
targetUrl: string,
|
||
|
currentIconPositions: IconPositions,
|
||
|
gridDropPosition: IconPosition,
|
||
|
[, ...draggedEntries]: string[],
|
||
|
gridElement: HTMLElement
|
||
|
): IconPosition => {
|
||
|
if (currentIconPositions[url] && currentIconPositions[targetUrl]) {
|
||
|
return {
|
||
|
gridColumnStart:
|
||
|
currentIconPositions[url].gridColumnStart +
|
||
|
(gridDropPosition.gridColumnStart -
|
||
|
currentIconPositions[targetUrl].gridColumnStart),
|
||
|
gridRowStart:
|
||
|
currentIconPositions[url].gridRowStart +
|
||
|
(gridDropPosition.gridRowStart -
|
||
|
currentIconPositions[targetUrl].gridRowStart),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const gridComputedStyle = window.getComputedStyle(gridElement);
|
||
|
const gridTemplateRowCount = gridComputedStyle
|
||
|
.getPropertyValue(GRID_TEMPLATE_ROWS)
|
||
|
.split(" ").length;
|
||
|
const {
|
||
|
gridColumnStart: targetGridColumnStart,
|
||
|
gridRowStart: targetGridRowStart,
|
||
|
} = gridDropPosition;
|
||
|
const gridRowStart =
|
||
|
targetGridRowStart + draggedEntries.indexOf(basename(url)) + 1;
|
||
|
|
||
|
return gridRowStart > gridTemplateRowCount
|
||
|
? {
|
||
|
gridColumnStart:
|
||
|
targetGridColumnStart +
|
||
|
Math.ceil(gridRowStart / gridTemplateRowCount) -
|
||
|
1,
|
||
|
gridRowStart:
|
||
|
gridRowStart % gridTemplateRowCount || gridTemplateRowCount,
|
||
|
}
|
||
|
: {
|
||
|
gridColumnStart: targetGridColumnStart,
|
||
|
gridRowStart,
|
||
|
};
|
||
|
};
|
||
|
|
||
|
export const updateIconPositions = (
|
||
|
directory: string,
|
||
|
gridElement: HTMLElement | null,
|
||
|
iconPositions: IconPositions,
|
||
|
sortOrders: SortOrders,
|
||
|
dragPosition: DragPosition,
|
||
|
draggedEntries: string[],
|
||
|
setIconPositions: React.Dispatch<React.SetStateAction<IconPositions>>
|
||
|
): void => {
|
||
|
if (!gridElement) return;
|
||
|
|
||
|
const currentIconPositions = updateIconPositionsIfEmpty(
|
||
|
directory,
|
||
|
gridElement,
|
||
|
iconPositions,
|
||
|
sortOrders
|
||
|
);
|
||
|
const gridDropPosition = calcGridDropPosition(gridElement, dragPosition);
|
||
|
|
||
|
if (
|
||
|
draggedEntries.length > 0 &&
|
||
|
!Object.values(currentIconPositions).some(
|
||
|
({ gridColumnStart, gridRowStart }) =>
|
||
|
gridColumnStart === gridDropPosition.gridColumnStart &&
|
||
|
gridRowStart === gridDropPosition.gridRowStart
|
||
|
)
|
||
|
) {
|
||
|
const targetFile =
|
||
|
draggedEntries.find((entry) =>
|
||
|
entry.startsWith(document.activeElement?.textContent || "")
|
||
|
) || draggedEntries[0];
|
||
|
const targetUrl = join(directory, targetFile);
|
||
|
const adjustDraggedEntries = [
|
||
|
targetFile,
|
||
|
...draggedEntries.filter((entry) => entry !== targetFile),
|
||
|
];
|
||
|
const newIconPositions = Object.fromEntries(
|
||
|
adjustDraggedEntries
|
||
|
.map<[string, IconPosition]>((entryFile) => {
|
||
|
const url = join(directory, entryFile);
|
||
|
|
||
|
return [
|
||
|
url,
|
||
|
url === targetUrl
|
||
|
? gridDropPosition
|
||
|
: calcGridPositionOffset(
|
||
|
url,
|
||
|
targetUrl,
|
||
|
currentIconPositions,
|
||
|
gridDropPosition,
|
||
|
adjustDraggedEntries,
|
||
|
gridElement
|
||
|
),
|
||
|
];
|
||
|
})
|
||
|
.filter(
|
||
|
([, { gridColumnStart, gridRowStart }]) =>
|
||
|
gridColumnStart >= 1 && gridRowStart >= 1
|
||
|
)
|
||
|
);
|
||
|
|
||
|
setIconPositions({
|
||
|
...currentIconPositions,
|
||
|
...Object.fromEntries(
|
||
|
Object.entries(newIconPositions).filter(
|
||
|
([, { gridColumnStart, gridRowStart }]) =>
|
||
|
!Object.values(currentIconPositions).some(
|
||
|
({
|
||
|
gridColumnStart: currentGridColumnStart,
|
||
|
gridRowStart: currentRowColumnStart,
|
||
|
}) =>
|
||
|
gridColumnStart === currentGridColumnStart &&
|
||
|
gridRowStart === currentRowColumnStart
|
||
|
)
|
||
|
)
|
||
|
),
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
export const isCanvasDrawn = (canvas?: HTMLCanvasElement | null): boolean =>
|
||
|
canvas instanceof HTMLCanvasElement &&
|
||
|
Boolean(
|
||
|
canvas
|
||
|
.getContext("2d", {
|
||
|
willReadFrequently: true,
|
||
|
})
|
||
|
?.getImageData(0, 0, canvas.width, canvas.height)
|
||
|
.data.some((channel) => channel !== 0)
|
||
|
);
|
||
|
|
||
|
const bytesInKB = 1024;
|
||
|
const bytesInMB = 1022976; // 1024 * 999
|
||
|
const bytesInGB = 1047527424; // 1024 * 1024 * 999
|
||
|
const bytesInTB = 1072668082176; // 1024 * 1024 * 1024 * 999
|
||
|
|
||
|
const formatNumber = (number: number): string =>
|
||
|
new Intl.NumberFormat("en-US", {
|
||
|
maximumSignificantDigits: number < 1 ? 2 : 3,
|
||
|
minimumSignificantDigits: number < 1 ? 2 : 3,
|
||
|
}).format(Number(number.toFixed(4).slice(0, -2)));
|
||
|
|
||
|
export const getFormattedSize = (size = 0): string => {
|
||
|
if (size === 1) return "1 byte";
|
||
|
if (size < bytesInKB) return `${size} bytes`;
|
||
|
if (size < bytesInMB) return `${formatNumber(size / bytesInKB)} KB`;
|
||
|
if (size < bytesInGB) {
|
||
|
return `${formatNumber(size / bytesInKB / bytesInKB)} MB`;
|
||
|
}
|
||
|
if (size < bytesInTB) {
|
||
|
return `${formatNumber(size / bytesInKB / bytesInKB / bytesInKB)} GB`;
|
||
|
}
|
||
|
|
||
|
return `${size} bytes`;
|
||
|
};
|
||
|
|
||
|
export const getTZOffsetISOString = (): string => {
|
||
|
const date = new Date();
|
||
|
|
||
|
return new Date(
|
||
|
date.getTime() - date.getTimezoneOffset() * 60000
|
||
|
).toISOString();
|
||
|
};
|
||
|
|
||
|
export const getUrlOrSearch = async (input: string): Promise<string> => {
|
||
|
const isIpfs = input.startsWith("ipfs://");
|
||
|
const hasHttpSchema =
|
||
|
input.startsWith("http://") || input.startsWith("https://");
|
||
|
const hasTld =
|
||
|
input.endsWith(".com") ||
|
||
|
input.endsWith(".ca") ||
|
||
|
input.endsWith(".net") ||
|
||
|
input.endsWith(".org");
|
||
|
|
||
|
try {
|
||
|
const { href } = new URL(
|
||
|
hasHttpSchema || !hasTld || isIpfs ? input : `https://${input}`
|
||
|
);
|
||
|
|
||
|
if (isIpfs) {
|
||
|
const { getIpfsGatewayUrl } = await import("utils/ipfs");
|
||
|
|
||
|
return await getIpfsGatewayUrl(href);
|
||
|
}
|
||
|
|
||
|
return href;
|
||
|
} catch {
|
||
|
return `${GOOGLE_SEARCH_QUERY}${input}`;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let IS_FIREFOX: boolean;
|
||
|
|
||
|
export const isFirefox = (): boolean => {
|
||
|
if (typeof window === "undefined") return false;
|
||
|
if (IS_FIREFOX ?? false) return IS_FIREFOX;
|
||
|
|
||
|
IS_FIREFOX = /firefox/i.test(window.navigator.userAgent);
|
||
|
|
||
|
return IS_FIREFOX;
|
||
|
};
|
||
|
|
||
|
let IS_SAFARI: boolean;
|
||
|
|
||
|
export const isSafari = (): boolean => {
|
||
|
if (typeof window === "undefined") return false;
|
||
|
if (IS_SAFARI ?? false) return IS_SAFARI;
|
||
|
|
||
|
IS_SAFARI = /^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);
|
||
|
|
||
|
return IS_SAFARI;
|
||
|
};
|
||
|
|
||
|
export const haltEvent = (
|
||
|
event:
|
||
|
| Event
|
||
|
| React.DragEvent
|
||
|
| React.FocusEvent
|
||
|
| React.KeyboardEvent
|
||
|
| React.MouseEvent
|
||
|
): void => {
|
||
|
try {
|
||
|
if (event.cancelable) {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
}
|
||
|
} catch {
|
||
|
// Ignore failured to halt event
|
||
|
}
|
||
|
};
|
||
|
|
||
|
export const createOffscreenCanvas = (
|
||
|
containerElement: HTMLElement,
|
||
|
devicePixelRatio = 1,
|
||
|
customSize: Size = Object.create(null) as Size
|
||
|
): OffscreenCanvas => {
|
||
|
const canvas = document.createElement("canvas");
|
||
|
const height = Number(customSize?.height) || containerElement.offsetHeight;
|
||
|
const width = Number(customSize?.width) || containerElement.offsetWidth;
|
||
|
|
||
|
canvas.style.height = `${height}px`;
|
||
|
canvas.style.width = `${width}px`;
|
||
|
|
||
|
canvas.height = Math.floor(height * devicePixelRatio);
|
||
|
canvas.width = Math.floor(width * devicePixelRatio);
|
||
|
|
||
|
containerElement.append(canvas);
|
||
|
|
||
|
return canvas.transferControlToOffscreen();
|
||
|
};
|
||
|
|
||
|
export const getSearchParam = (param: string): string =>
|
||
|
new URLSearchParams(window.location.search).get(param) || "";
|
||
|
|
||
|
export const clsx = (classes: Record<string, boolean>): string =>
|
||
|
Object.entries(classes)
|
||
|
.filter(([, isActive]) => isActive)
|
||
|
.map(([className]) => className)
|
||
|
.join(" ");
|
||
|
|
||
|
export const label = (value: string): React.HTMLAttributes<HTMLElement> => ({
|
||
|
"aria-label": value,
|
||
|
title: value,
|
||
|
});
|
||
|
|
||
|
export const isYouTubeUrl = (url: string): boolean =>
|
||
|
url.includes("youtube.com/") || url.includes("youtu.be/");
|
||
|
|
||
|
export const getYouTubeUrlId = (url: string): string => {
|
||
|
try {
|
||
|
const { pathname, searchParams } = new URL(url);
|
||
|
|
||
|
return searchParams.get("v") || pathname.split("/").pop() || "";
|
||
|
} catch {
|
||
|
// URL parsing failed
|
||
|
}
|
||
|
|
||
|
return "";
|
||
|
};
|
||
|
|
||
|
export const preloadLibs = (libs: string[] = []): void => {
|
||
|
const scripts = [...document.scripts];
|
||
|
const preloadedLinks = [
|
||
|
...document.querySelectorAll("link[rel=preload]"),
|
||
|
] as HTMLLinkElement[];
|
||
|
|
||
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||
|
libs.map(encodeURI).forEach((lib) => {
|
||
|
if (
|
||
|
scripts.some((script) => script.src.endsWith(lib)) ||
|
||
|
preloadedLinks.some((preloadedLink) => preloadedLink.href.endsWith(lib))
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const link = document.createElement(
|
||
|
"link"
|
||
|
) as HTMLElementWithPriority<HTMLLinkElement>;
|
||
|
|
||
|
link.fetchPriority = "high";
|
||
|
link.rel = "preload";
|
||
|
link.href = lib;
|
||
|
|
||
|
switch (extname(lib).toLowerCase()) {
|
||
|
case ".css":
|
||
|
link.as = "style";
|
||
|
break;
|
||
|
case ".htm":
|
||
|
case ".html":
|
||
|
link.rel = "prerender";
|
||
|
break;
|
||
|
case ".url":
|
||
|
link.as = "fetch";
|
||
|
link.crossOrigin = "anonymous";
|
||
|
break;
|
||
|
default:
|
||
|
link.as = "script";
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
document.head.append(link);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
export const getGifJs = async (): Promise<GIF> => {
|
||
|
const { default: GIFInstance } = await import("gif.js");
|
||
|
|
||
|
return new GIFInstance({
|
||
|
quality: 10,
|
||
|
workerScript: "Program Files/gif.js/gif.worker.js",
|
||
|
workers: Math.max(Math.floor(navigator.hardwareConcurrency / 4), 1),
|
||
|
});
|
||
|
};
|
||
|
|
||
|
export const jsonFetch = async (
|
||
|
url: string
|
||
|
): Promise<Record<string, unknown>> => {
|
||
|
const response = await fetch(url, HIGH_PRIORITY_REQUEST);
|
||
|
const json = (await response.json()) as Record<string, unknown>;
|
||
|
|
||
|
return json || {};
|
||
|
};
|
||
|
|
||
|
export const isCanvasEmpty = (canvas: HTMLCanvasElement): boolean =>
|
||
|
!canvas
|
||
|
.getContext("2d")
|
||
|
?.getImageData(0, 0, canvas.width, canvas.height)
|
||
|
.data.some(Boolean);
|
||
|
|
||
|
export const generatePrettyTimestamp = (): string =>
|
||
|
new Intl.DateTimeFormat(DEFAULT_LOCALE, TIMESTAMP_DATE_FORMAT)
|
||
|
.format(new Date())
|
||
|
.replace(/[/:]/g, "-")
|
||
|
.replace(",", "");
|