Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions XMOJ.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,8 @@ async function main() {
}, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, {
"ID": "BBSPopup", "Type": "A", "Name": "讨论提醒"
}, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, {
"ID": "ImageEnlarger", "Type": "A", "Name": "图片放大功能"
}, {
"ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)"
}, {
"ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)"
Expand Down Expand Up @@ -5572,6 +5574,275 @@ int main()
}
}
}

// Image Enlargement Feature
if (UtilityEnabled("ImageEnlarger")) {
try {
// Add CSS styles for the enlarger
let EnlargerStyle = document.createElement("style");
EnlargerStyle.textContent = `
.xmoj-image-preview {
cursor: pointer;
}

.xmoj-image-preview:hover {
opacity: 0.8;
transition: opacity 0.2s ease;
}

.xmoj-image-preview::after {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
content: "点击放大";
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.xmoj-image-preview:hover::after {
opacity: 1;
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hover hint relies on .xmoj-image-preview::after. Many browsers do not render ::before/::after on replaced elements like <img>, so the “点击放大” overlay may never appear. If you want a reliable overlay, wrap the image in a positioned container and apply the pseudo-element to that container (or use a tooltip/title-based approach).

Suggested change
.xmoj-image-preview::after {
content: "点击放大";
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.xmoj-image-preview:hover::after {
opacity: 1;
}

Copilot uses AI. Check for mistakes.
.xmoj-image-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
}

.xmoj-image-modal.show {
display: flex;
flex-direction: column;
}

.xmoj-image-modal-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
}

.xmoj-image-modal-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}

.xmoj-image-modal-toolbar {
display: flex;
justify-content: center;
gap: 10px;
padding: 15px;
background-color: rgba(0, 0, 0, 0.5);
flex-wrap: wrap;
}

.xmoj-image-modal-toolbar button {
padding: 8px 16px;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
}

.xmoj-image-modal-toolbar button:hover {
background-color: #0b5ed7;
}

.xmoj-image-modal-toolbar button:active {
background-color: #0a58ca;
}

.xmoj-image-modal-close {
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 40px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s ease;
}

.xmoj-image-modal-close:hover {
color: #ccc;
}
`;
document.head.appendChild(EnlargerStyle);

// Create modal element
let ImageModal = document.createElement("div");
ImageModal.className = "xmoj-image-modal";
ImageModal.id = "xmoj-image-modal";

let CloseButton = document.createElement("span");
CloseButton.className = "xmoj-image-modal-close";

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close control is a <span> with click handling only; it is not keyboard-focusable and has no accessible name/role. Use a <button type='button'> (or add role='button', tabindex='0', and an aria-label) so screen readers and keyboard users can operate it reliably.

Suggested change
let CloseButton = document.createElement("span");
CloseButton.className = "xmoj-image-modal-close";
let CloseButton = document.createElement("button");
CloseButton.className = "xmoj-image-modal-close";
CloseButton.type = "button";
CloseButton.setAttribute("aria-label", "关闭图片");
CloseButton.title = "关闭图片";

Copilot uses AI. Check for mistakes.
CloseButton.innerHTML = "&times;";
ImageModal.appendChild(CloseButton);

let ModalContent = document.createElement("div");
ModalContent.className = "xmoj-image-modal-content";
let ModalImage = document.createElement("img");
ModalImage.className = "xmoj-image-modal-image";
ModalContent.appendChild(ModalImage);
ImageModal.appendChild(ModalContent);

let Toolbar = document.createElement("div");
Toolbar.className = "xmoj-image-modal-toolbar";

let ZoomInBtn = document.createElement("button");
ZoomInBtn.innerHTML = "放大 (+)";
ZoomInBtn.type = "button";
Toolbar.appendChild(ZoomInBtn);

let ZoomOutBtn = document.createElement("button");
ZoomOutBtn.innerHTML = "缩小 (-)";
ZoomOutBtn.type = "button";
Toolbar.appendChild(ZoomOutBtn);

let ResetZoomBtn = document.createElement("button");
ResetZoomBtn.innerHTML = "重置大小";
ResetZoomBtn.type = "button";
Toolbar.appendChild(ResetZoomBtn);

let SaveBtn = document.createElement("button");
SaveBtn.innerHTML = "保存图片";
SaveBtn.type = "button";
Toolbar.appendChild(SaveBtn);

ImageModal.appendChild(Toolbar);
document.body.appendChild(ImageModal);

// Zoom level state
let CurrentZoom = 1;
const ZoomStep = 0.1;
const MinZoom = 0.1;
const MaxZoom = 5;

// Function to update image size
let UpdateImageSize = () => {
ModalImage.style.transform = `scale(${CurrentZoom})`;
ModalImage.style.transition = "transform 0.2s ease";
};

// Function to open modal
let OpenImageModal = (imgSrc) => {
CurrentZoom = 1;
ModalImage.src = imgSrc;
ImageModal.classList.add("show");
UpdateImageSize();
};

// Function to close modal
let CloseImageModal = () => {
ImageModal.classList.remove("show");
};

// Close button click
CloseButton.addEventListener("click", CloseImageModal);

// Close when clicking outside the image
ImageModal.addEventListener("click", (e) => {
if (e.target === ImageModal || e.target === ModalContent) {
CloseImageModal();
}
});

// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (ImageModal.classList.contains("show")) {
if (e.key === "Escape") {
CloseImageModal();
} else if (e.key === "+") {
ZoomInBtn.click();
} else if (e.key === "-") {
ZoomOutBtn.click();
}
}
});

// Zoom controls
ZoomInBtn.addEventListener("click", () => {
CurrentZoom = Math.min(CurrentZoom + ZoomStep, MaxZoom);
UpdateImageSize();
});

ZoomOutBtn.addEventListener("click", () => {
CurrentZoom = Math.max(CurrentZoom - ZoomStep, MinZoom);
UpdateImageSize();
});

ResetZoomBtn.addEventListener("click", () => {
CurrentZoom = 1;
UpdateImageSize();
});

// Save/Download image
SaveBtn.addEventListener("click", () => {
let Link = document.createElement("a");
Link.href = ModalImage.src;
Link.download = ModalImage.src.split("/").pop() || "image.png";
document.body.appendChild(Link);
Link.click();
document.body.removeChild(Link);
});

// Apply to all images on the page
let ApplyEnlargerToImages = () => {
let Images = document.querySelectorAll("img");
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
Images.forEach((img) => {
if (!img.classList.contains("xmoj-image-preview") &&
!img.parentElement.classList.contains("xmoj-image-modal") &&

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplyEnlargerToImages() will also match the modal’s own <img> (and any images inside the modal) because it only excludes images whose parent has xmoj-image-modal. The modal image’s parent is xmoj-image-modal-content, so it gets the preview class and a click handler, which can reset zoom/reopen the modal when the user clicks the enlarged image. Exclude any image with closest('.xmoj-image-modal') (or explicitly skip .xmoj-image-modal-image) instead of checking only parentElement.

Suggested change
!img.parentElement.classList.contains("xmoj-image-modal") &&
!img.closest(".xmoj-image-modal") &&

Copilot uses AI. Check for mistakes.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
img.src &&
!img.src.includes("gravatar") &&
!img.src.includes("cravatar")) {

img.classList.add("xmoj-image-preview");
img.style.position = "relative";
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
img.addEventListener("click", (e) => {
e.stopPropagation();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
OpenImageModal(img.src);
});

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Click handling uses img.src to open the modal. For responsive images (srcset/sizes) or some lazy-loading setups, img.currentSrc is the actually-displayed resource and may differ from img.src. Using currentSrc || src will make enlargement/download match what the user sees.

Copilot uses AI. Check for mistakes.
}
});
};

// Apply to existing images
ApplyEnlargerToImages();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

// Apply to dynamically added images
let Observer = new MutationObserver((mutations) => {
ApplyEnlargerToImages();
});

Observer.observe(document.body, {
childList: true,
subtree: true
});
Comment on lines +6037 to +6053

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MutationObserver callback calls ApplyEnlargerToImages(), which scans all images on every DOM mutation. On pages with frequent updates this can become an O(n) hot loop and noticeably degrade performance. Consider iterating only over mutation.addedNodes (and their descendant imgs), or throttling/debouncing the full scan.

Copilot uses AI. Check for mistakes.

} catch (e) {
console.error(e);
if (UtilityEnabled("DebugMode")) {
SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode.");
}
}
}
} catch (e) {
console.error(e);
if (UtilityEnabled("DebugMode")) {
Expand Down
Loading