mirror of
https://github.com/Equicord/Equicord.git
synced 2025-05-10 17:35:37 +02:00
Added Metadata to imageZoom (#254)
* Added Metadata to imageZoom * Added to devs to prevent Lint * Some Changes * Some Modifications --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
fea47af302
commit
b897a97095
3 changed files with 213 additions and 12 deletions
|
@ -20,7 +20,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { makeRange } from "@components/PluginSettings/components";
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { createRoot, Menu } from "@webpack/common";
|
import { createRoot, Menu } from "@webpack/common";
|
||||||
|
@ -31,6 +31,19 @@ import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||||
import { ELEMENT_ID } from "./constants";
|
import { ELEMENT_ID } from "./constants";
|
||||||
import managedStyle from "./styles.css?managed";
|
import managedStyle from "./styles.css?managed";
|
||||||
|
|
||||||
|
|
||||||
|
interface ImageMetadata {
|
||||||
|
filename: string;
|
||||||
|
dimensions: string;
|
||||||
|
size?: string;
|
||||||
|
fetching?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageMetadataCache = new Map<string, ImageMetadata>();
|
||||||
|
|
||||||
|
let lastClickTime = 0;
|
||||||
|
const DOUBLE_CLICK_THRESHOLD = 300;
|
||||||
|
|
||||||
export const settings = definePluginSettings({
|
export const settings = definePluginSettings({
|
||||||
saveZoomValues: {
|
saveZoomValues: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
@ -78,6 +91,12 @@ export const settings = definePluginSettings({
|
||||||
default: 0.5,
|
default: 0.5,
|
||||||
stickToMarkers: false,
|
stickToMarkers: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showMetadata: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show image metadata when double clicking on selected image",
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +106,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
||||||
// emojis in user statuses
|
// emojis in user statuses
|
||||||
if (props.target?.classList?.contains("emoji")) return;
|
if (props.target?.classList?.contains("emoji")) return;
|
||||||
|
|
||||||
const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]);
|
const { square, nearestNeighbour, showMetadata } = settings.use(["square", "nearestNeighbour", "showMetadata"]);
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuGroup id="image-zoom">
|
<Menu.MenuGroup id="image-zoom">
|
||||||
|
@ -150,14 +169,138 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Menu.MenuSeparator />
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-show-metadata"
|
||||||
|
label="Show Image Metadata"
|
||||||
|
checked={showMetadata}
|
||||||
|
action={() => {
|
||||||
|
settings.store.showMetadata = !showMetadata;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-view-metadata"
|
||||||
|
label="View Metadata"
|
||||||
|
action={() => {
|
||||||
|
const target = props.target as HTMLImageElement;
|
||||||
|
if (target && target.src) {
|
||||||
|
toggleMetadata(target);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Menu.MenuGroup>
|
</Menu.MenuGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function toggleMetadata(imgElement: HTMLImageElement) {
|
||||||
|
if (!imgElement || !imgElement.src) return;
|
||||||
|
const parent = imgElement.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const metadataContainer = parent.querySelector(".vc-image-metadata");
|
||||||
|
if (metadataContainer) {
|
||||||
|
metadataContainer.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createMetadataDisplay(imgElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMetadataDisplay(imgElement: HTMLImageElement) {
|
||||||
|
if (!imgElement || !imgElement.src) return;
|
||||||
|
|
||||||
|
const { src } = imgElement;
|
||||||
|
const parent = imgElement.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "vc-image-wrapper";
|
||||||
|
parent.insertBefore(wrapper, imgElement);
|
||||||
|
wrapper.appendChild(imgElement);
|
||||||
|
|
||||||
|
let metadata = imageMetadataCache.get(src);
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
metadata = {
|
||||||
|
filename: getFilenameFromURL(src),
|
||||||
|
dimensions: `${imgElement.naturalWidth || imgElement.width} × ${imgElement.naturalHeight || imgElement.height} px`,
|
||||||
|
fetching: true
|
||||||
|
};
|
||||||
|
|
||||||
|
imageMetadataCache.set(src, metadata);
|
||||||
|
fetchFileSize(src).then(size => {
|
||||||
|
if (size !== undefined) {
|
||||||
|
const cachedMetadata = imageMetadataCache.get(src);
|
||||||
|
if (cachedMetadata) {
|
||||||
|
cachedMetadata.size = formatFileSize(size);
|
||||||
|
cachedMetadata.fetching = false;
|
||||||
|
imageMetadataCache.set(src, cachedMetadata);
|
||||||
|
|
||||||
|
const container = parent.querySelector(".vc-image-metadata");
|
||||||
|
if (container) {
|
||||||
|
const sizeElement = container.querySelector(".vc-metadata-row:last-child span:last-child");
|
||||||
|
if (sizeElement) {
|
||||||
|
sizeElement.textContent = formatFileSize(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "vc-image-metadata";
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="vc-metadata-row">
|
||||||
|
<span class="vc-metadata-label">Filename:</span>
|
||||||
|
<span>${metadata.filename}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vc-metadata-row">
|
||||||
|
<span class="vc-metadata-label">Dimensions:</span>
|
||||||
|
<span>${metadata.dimensions}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vc-metadata-row">
|
||||||
|
<span class="vc-metadata-label">Size:</span>
|
||||||
|
<span>${metadata.size || "Loading..."}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
wrapper.appendChild(container);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilenameFromURL(url: string): string {
|
||||||
|
try {
|
||||||
|
const cleanUrl = url.split("?")[0];
|
||||||
|
const parts = cleanUrl.split("/");
|
||||||
|
return decodeURIComponent(parts[parts.length - 1]);
|
||||||
|
} catch {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFileSize(url: string): Promise<number | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: "HEAD" });
|
||||||
|
return parseInt(response.headers.get("content-length") || "0");
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ImageZoom",
|
name: "ImageZoom",
|
||||||
description: "Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size",
|
description: "Lets you zoom in to images and gifs as well as displays image metadata. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius.",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria, EquicordDevs.Campfire],
|
||||||
tags: ["ImageUtilities"],
|
tags: ["ImageUtilities"],
|
||||||
|
|
||||||
managedStyle,
|
managedStyle,
|
||||||
|
@ -171,7 +314,6 @@ export default definePlugin({
|
||||||
replace: `id:"${ELEMENT_ID}",$&`
|
replace: `id:"${ELEMENT_ID}",$&`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// This patch needs to be above the next one as it uses the zoomed class as an anchor
|
|
||||||
match: /\.zoomed]:.+?,(?=children:)/,
|
match: /\.zoomed]:.+?,(?=children:)/,
|
||||||
replace: "$&onClick:()=>{},"
|
replace: "$&onClick:()=>{},"
|
||||||
},
|
},
|
||||||
|
@ -181,7 +323,6 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Make media viewer options not hide when zoomed in with the default Discord feature
|
|
||||||
{
|
{
|
||||||
find: '="FOCUS_SENSITIVE",',
|
find: '="FOCUS_SENSITIVE",',
|
||||||
replacement: {
|
replacement: {
|
||||||
|
@ -189,7 +330,6 @@ export default definePlugin({
|
||||||
replace: "false"
|
replace: "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
find: ".handleImageLoad)",
|
find: ".handleImageLoad)",
|
||||||
replacement: [
|
replacement: [
|
||||||
|
@ -197,17 +337,14 @@ export default definePlugin({
|
||||||
match: /placeholderVersion:\i,(?=.{0,50}children:)/,
|
match: /placeholderVersion:\i,(?=.{0,50}children:)/,
|
||||||
replace: "...$self.makeProps(this),$&"
|
replace: "...$self.makeProps(this),$&"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
match: /componentDidMount\(\){/,
|
match: /componentDidMount\(\){/,
|
||||||
replace: "$&$self.renderMagnifier(this);",
|
replace: "$&$self.renderMagnifier(this);",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
match: /componentWillUnmount\(\){/,
|
match: /componentWillUnmount\(\){/,
|
||||||
replace: "$&$self.unMountMagnifier();"
|
replace: "$&$self.unMountMagnifier();"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
match: /componentDidUpdate\(\i\){/,
|
match: /componentDidUpdate\(\i\){/,
|
||||||
replace: "$&$self.updateMagnifier(this);"
|
replace: "$&$self.updateMagnifier(this);"
|
||||||
|
@ -221,22 +358,35 @@ export default definePlugin({
|
||||||
"image-context": imageContextMenuPatch
|
"image-context": imageContextMenuPatch
|
||||||
},
|
},
|
||||||
|
|
||||||
// to stop from rendering twice /shrug
|
|
||||||
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
||||||
element: null as HTMLDivElement | null,
|
element: null as HTMLDivElement | null,
|
||||||
|
|
||||||
Magnifier,
|
Magnifier,
|
||||||
root: null as Root | null,
|
root: null as Root | null,
|
||||||
|
|
||||||
makeProps(instance) {
|
makeProps(instance) {
|
||||||
return {
|
return {
|
||||||
onMouseOver: () => this.onMouseOver(instance),
|
onMouseOver: () => this.onMouseOver(instance),
|
||||||
onMouseOut: () => this.onMouseOut(instance),
|
onMouseOut: () => this.onMouseOut(instance),
|
||||||
onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance),
|
onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance),
|
||||||
onMouseUp: () => this.onMouseUp(instance),
|
onMouseUp: () => this.onMouseUp(instance),
|
||||||
|
onClick: (e: React.MouseEvent) => this.handleImageClick(e, instance),
|
||||||
id: instance.props.id,
|
id: instance.props.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleImageClick(e: React.MouseEvent | MouseEvent, instance: any) {
|
||||||
|
if (!settings.store.showMetadata) return;
|
||||||
|
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
if (target && target.tagName === "IMG" && target.src) {
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
if (currentTime - lastClickTime < DOUBLE_CLICK_THRESHOLD) {
|
||||||
|
toggleMetadata(target);
|
||||||
|
}
|
||||||
|
lastClickTime = currentTime;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderMagnifier(instance) {
|
renderMagnifier(instance) {
|
||||||
try {
|
try {
|
||||||
if (instance.props.id === ELEMENT_ID) {
|
if (instance.props.id === ELEMENT_ID) {
|
||||||
|
@ -280,11 +430,41 @@ export default definePlugin({
|
||||||
this.element = document.createElement("div");
|
this.element = document.createElement("div");
|
||||||
this.element.classList.add("MagnifierContainer");
|
this.element.classList.add("MagnifierContainer");
|
||||||
document.body.appendChild(this.element);
|
document.body.appendChild(this.element);
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "image-metadata-styles";
|
||||||
|
style.textContent = `
|
||||||
|
.vc-image-metadata {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-normal);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-metadata-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-metadata-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
// so componenetWillUnMount gets called if Magnifier component is still alive
|
// so componenetWillUnMount gets called if Magnifier component is still alive
|
||||||
this.root && this.root.unmount();
|
this.root && this.root.unmount();
|
||||||
this.element?.remove();
|
this.element?.remove();
|
||||||
|
|
||||||
|
document.getElementById("image-metadata-styles")?.remove();
|
||||||
|
document.querySelectorAll(".vc-image-metadata").forEach(el => el.remove());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,3 +21,20 @@
|
||||||
|
|
||||||
/* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
|
/* https://googlechrome.github.io/samples/image-rendering-pixelated/index.html */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-image-metadata {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgb(0 0 0 / 60%);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
|
@ -1061,6 +1061,10 @@ export const EquicordDevs = Object.freeze({
|
||||||
Reycko: {
|
Reycko: {
|
||||||
name: "Reycko",
|
name: "Reycko",
|
||||||
id: 1123725368004726794n,
|
id: 1123725368004726794n,
|
||||||
|
},
|
||||||
|
Campfire: {
|
||||||
|
name: "Campfire",
|
||||||
|
id: 376414446840578081n,
|
||||||
}
|
}
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue