mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-08 22:50:00 +08:00
feat(commandline): add download screenshot command (@torturado) (#6532)
### Description This Pull Request introduces the functionality to download the result screenshot directly as a PNG file, in addition to the existing option to copy it to the clipboard. This is achieved by: 1. Refactoring the screenshot logic in `test-ui.ts`. 2. Adding a new `Download screenshot` command to the result screen command list. **Changes Made:** * **`frontend/src/ts/test/test-ui.ts`**: * Created an internal function `generateScreenshotCanvas` that encapsulates UI preparation, canvas generation with `html2canvas`, and UI restoration. * Modified the exported `screenshot` function to use `generateScreenshotCanvas` and **only** handle copying the resulting Blob to the clipboard (with the fallback of opening in a new tab). * Added a new exported function `getScreenshotBlob` that uses `generateScreenshotCanvas` and returns the resulting image Blob. * **`frontend/src/ts/commandline/lists/result-screen.ts`**: * Renamed the `saveScreenshot` command to `copyScreenshot` (updating `id`, `icon`, and `alias`) to more accurately reflect its action. It still uses `TestUI.screenshot()`. * Added a new `downloadScreenshot` command (`id`, `display`, `icon`, `alias`) that: * Calls `TestUI.getScreenshotBlob()` to get the image data. * If a Blob is obtained, creates a temporary object URL. * Creates a temporary `<a>` element, sets the `href` to the object URL, and the `download` attribute with a filename (e.g., `monkeytype-result-TIMESTAMP.png`). * Simulates a click on the link to initiate the file download. * Revokes the object URL. * Displays success or error notifications. ### Checks - [ ] Adding quotes? - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language or a theme? - [ ] If is a language, did you edit `_list.json`, `_groups.json` and add `languages.json`? - [ ] If is a theme, did you add the theme.css? - Also please add a screenshot of the theme, it would be extra awesome if you do so! - [ ] Check if any open issues are related to this PR; if so, be sure to tag them below. - [x] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [ ] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. <!-- Remember to add your username here! --> --------- Co-authored-by: fehmer <3728838+fehmer@users.noreply.github.com> Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
be62681c32
commit
44a67db9fc
3 changed files with 304 additions and 190 deletions
|
@ -7,6 +7,7 @@ import * as TestWords from "../../test/test-words";
|
|||
import Config from "../../config";
|
||||
import * as PractiseWords from "../../test/practise-words";
|
||||
import { Command, CommandsSubgroup } from "../types";
|
||||
import * as TestScreenshot from "../../test/test-screenshot";
|
||||
|
||||
const practiceSubgroup: CommandsSubgroup = {
|
||||
title: "Practice words...",
|
||||
|
@ -92,13 +93,27 @@ const commands: Command[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
id: "saveScreenshot",
|
||||
id: "copyScreenshot",
|
||||
display: "Copy screenshot to clipboard",
|
||||
icon: "fa-image",
|
||||
alias: "save",
|
||||
icon: "fa-copy",
|
||||
alias: "copy image clipboard",
|
||||
exec: (): void => {
|
||||
setTimeout(() => {
|
||||
void TestUI.screenshot();
|
||||
void TestScreenshot.copyToClipboard();
|
||||
}, 500);
|
||||
},
|
||||
available: (): boolean => {
|
||||
return TestUI.resultVisible;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "downloadScreenshot",
|
||||
display: "Download screenshot",
|
||||
icon: "fa-download",
|
||||
alias: "save image download file",
|
||||
exec: (): void => {
|
||||
setTimeout(async () => {
|
||||
void TestScreenshot.download();
|
||||
}, 500);
|
||||
},
|
||||
available: (): boolean => {
|
||||
|
|
284
frontend/src/ts/test/test-screenshot.ts
Normal file
284
frontend/src/ts/test/test-screenshot.ts
Normal file
|
@ -0,0 +1,284 @@
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as Replay from "./replay";
|
||||
import * as Misc from "../utils/misc";
|
||||
import { isAuthenticated } from "../firebase";
|
||||
import { getActiveFunboxesWithFunction } from "./funbox/list";
|
||||
import * as DB from "../db";
|
||||
import * as ThemeColors from "../elements/theme-colors";
|
||||
import { format } from "date-fns/format";
|
||||
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
|
||||
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
|
||||
return (await import("html2canvas")).default;
|
||||
}
|
||||
|
||||
let revealReplay = false;
|
||||
let revertCookie = false;
|
||||
|
||||
function revert(): void {
|
||||
Loader.hide();
|
||||
$("#ad-result-wrapper").removeClass("hidden");
|
||||
$("#ad-result-small-wrapper").removeClass("hidden");
|
||||
$("#testConfig").removeClass("hidden");
|
||||
$(".pageTest .screenshotSpacer").remove();
|
||||
$("#notificationCenter").removeClass("hidden");
|
||||
$("#commandLineMobileButton").removeClass("hidden");
|
||||
$(".pageTest .ssWatermark").addClass("hidden");
|
||||
$(".pageTest .ssWatermark").text("monkeytype.com"); // Reset watermark text
|
||||
$(".pageTest .buttons").removeClass("hidden");
|
||||
$("noscript").removeClass("hidden");
|
||||
$("#nocss").removeClass("hidden");
|
||||
$("header, footer").removeClass("invisible");
|
||||
$("#result").removeClass("noBalloons");
|
||||
$(".wordInputHighlight").removeClass("hidden");
|
||||
$(".highlightContainer").removeClass("hidden");
|
||||
if (revertCookie) $("#cookiesModal").removeClass("hidden");
|
||||
if (revealReplay) $("#resultReplay").removeClass("hidden");
|
||||
if (!isAuthenticated()) {
|
||||
$(".pageTest .loginTip").removeClass("hidden");
|
||||
}
|
||||
(document.querySelector("html") as HTMLElement).style.scrollBehavior =
|
||||
"smooth";
|
||||
for (const fb of getActiveFunboxesWithFunction("applyGlobalCSS")) {
|
||||
fb.functions.applyGlobalCSS();
|
||||
}
|
||||
}
|
||||
|
||||
let firefoxClipboardNotificatoinShown = false;
|
||||
|
||||
/**
|
||||
* Prepares UI, generates screenshot canvas using html2canvas, and reverts UI changes.
|
||||
* Returns the generated canvas element or null on failure.
|
||||
* Handles its own loader and basic error notifications for canvas generation.
|
||||
*/
|
||||
async function generateCanvas(): Promise<HTMLCanvasElement | null> {
|
||||
Loader.show();
|
||||
|
||||
if (!$("#resultReplay").hasClass("hidden")) {
|
||||
revealReplay = true;
|
||||
Replay.pauseReplay();
|
||||
}
|
||||
if (
|
||||
Misc.isElementVisible("#cookiesModal") ||
|
||||
document.contains(document.querySelector("#cookiesModal"))
|
||||
) {
|
||||
revertCookie = true;
|
||||
}
|
||||
|
||||
// --- UI Preparation ---
|
||||
const dateNow = new Date(Date.now());
|
||||
$("#resultReplay").addClass("hidden");
|
||||
$(".pageTest .ssWatermark").removeClass("hidden");
|
||||
|
||||
const snapshot = DB.getSnapshot();
|
||||
const ssWatermark = [format(dateNow, "dd MMM yyyy HH:mm"), "monkeytype.com"];
|
||||
if (snapshot?.name !== undefined) {
|
||||
const userText = `${snapshot?.name}${getHtmlByUserFlags(snapshot, {
|
||||
iconsOnly: true,
|
||||
})}`;
|
||||
ssWatermark.unshift(userText);
|
||||
}
|
||||
$(".pageTest .ssWatermark").html(
|
||||
ssWatermark
|
||||
.map((el) => `<span>${el}</span>`)
|
||||
.join("<span class='pipe'>|</span>")
|
||||
);
|
||||
$(".pageTest .buttons").addClass("hidden");
|
||||
$("#notificationCenter").addClass("hidden");
|
||||
$("#commandLineMobileButton").addClass("hidden");
|
||||
$(".pageTest .loginTip").addClass("hidden");
|
||||
$("noscript").addClass("hidden");
|
||||
$("#nocss").addClass("hidden");
|
||||
$("#ad-result-wrapper").addClass("hidden");
|
||||
$("#ad-result-small-wrapper").addClass("hidden");
|
||||
$("#testConfig").addClass("hidden");
|
||||
// Ensure spacer is removed before adding a new one if function is called rapidly
|
||||
$(".pageTest .screenshotSpacer").remove();
|
||||
$(".page.pageTest").prepend("<div class='screenshotSpacer'></div>");
|
||||
$("header, footer").addClass("invisible");
|
||||
$("#result").addClass("noBalloons");
|
||||
$(".wordInputHighlight").addClass("hidden");
|
||||
$(".highlightContainer").addClass("hidden");
|
||||
if (revertCookie) $("#cookiesModal").addClass("hidden");
|
||||
|
||||
for (const fb of getActiveFunboxesWithFunction("clearGlobal")) {
|
||||
fb.functions.clearGlobal();
|
||||
}
|
||||
|
||||
(document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto";
|
||||
window.scrollTo({ top: 0, behavior: "instant" as ScrollBehavior }); // Use instant scroll
|
||||
|
||||
// --- Target Element Calculation ---
|
||||
const src = $("#result .wrapper");
|
||||
if (!src.length) {
|
||||
console.error("Result wrapper not found for screenshot");
|
||||
Notifications.add("Screenshot target element not found", -1);
|
||||
revert();
|
||||
return null;
|
||||
}
|
||||
// Ensure offset calculations happen *after* potential layout shifts from UI prep
|
||||
await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for render updates
|
||||
|
||||
const sourceX = src.offset()?.left ?? 0;
|
||||
const sourceY = src.offset()?.top ?? 0;
|
||||
const sourceWidth = src.outerWidth(true) as number;
|
||||
const sourceHeight = src.outerHeight(true) as number;
|
||||
|
||||
// --- Canvas Generation ---
|
||||
try {
|
||||
const paddingX = convertRemToPixels(2);
|
||||
const paddingY = convertRemToPixels(2);
|
||||
|
||||
const canvas = await (
|
||||
await gethtml2canvas()
|
||||
)(document.body, {
|
||||
backgroundColor: await ThemeColors.get("bg"),
|
||||
width: sourceWidth + paddingX * 2,
|
||||
height: sourceHeight + paddingY * 2,
|
||||
x: sourceX - paddingX,
|
||||
y: sourceY - paddingY,
|
||||
logging: false, // Suppress html2canvas logs in console
|
||||
useCORS: true, // May be needed if user flags/icons are external
|
||||
});
|
||||
|
||||
revert(); // Revert UI *after* canvas is successfully generated
|
||||
return canvas;
|
||||
} catch (e) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Error creating screenshot canvas"),
|
||||
-1
|
||||
);
|
||||
revert(); // Ensure UI is reverted on error
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates screenshot and attempts to copy it to the clipboard.
|
||||
* Falls back to opening in a new tab if clipboard access fails.
|
||||
* Handles notifications related to the copy action.
|
||||
* (This function should be used by the 'copy' command or the original button)
|
||||
*/
|
||||
export async function copyToClipboard(): Promise<void> {
|
||||
const canvas = await generateCanvas();
|
||||
if (!canvas) {
|
||||
// Error notification handled by generateScreenshotCanvas
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.toBlob(async (blob) => {
|
||||
if (!blob) {
|
||||
Notifications.add("Failed to generate image data (blob is null)", -1);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Attempt to copy using ClipboardItem API
|
||||
const clipItem = new ClipboardItem(
|
||||
Object.defineProperty({}, blob.type, {
|
||||
value: blob,
|
||||
enumerable: true,
|
||||
})
|
||||
);
|
||||
await navigator.clipboard.write([clipItem]);
|
||||
Notifications.add("Copied screenshot to clipboard", 1, { duration: 2 });
|
||||
} catch (e) {
|
||||
// Handle clipboard write error
|
||||
console.error("Error saving image to clipboard", e);
|
||||
|
||||
// Firefox specific message (only show once)
|
||||
if (
|
||||
navigator.userAgent.toLowerCase().includes("firefox") &&
|
||||
!firefoxClipboardNotificatoinShown
|
||||
) {
|
||||
firefoxClipboardNotificatoinShown = true;
|
||||
Notifications.add(
|
||||
"On Firefox you can enable the asyncClipboard.clipboardItem permission in about:config to enable copying straight to the clipboard",
|
||||
0,
|
||||
{ duration: 10 }
|
||||
);
|
||||
}
|
||||
|
||||
// General fallback notification and action
|
||||
Notifications.add(
|
||||
"Could not copy screenshot to clipboard. Opening in new tab instead (make sure popups are allowed)",
|
||||
0,
|
||||
{ duration: 5 }
|
||||
);
|
||||
try {
|
||||
// Fallback: Open blob in a new tab
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
window.open(blobUrl);
|
||||
// No need to revoke URL immediately as the new tab needs it.
|
||||
// Browser usually handles cleanup when tab is closed or navigated away.
|
||||
} catch (openError) {
|
||||
Notifications.add("Failed to open screenshot in new tab", -1);
|
||||
console.error("Error opening blob URL:", openError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates screenshot canvas and returns the image data as a Blob.
|
||||
* Handles notifications for canvas/blob generation errors.
|
||||
* (This function is intended to be used by the 'download' command)
|
||||
*/
|
||||
async function getBlob(): Promise<Blob | null> {
|
||||
const canvas = await generateCanvas();
|
||||
if (!canvas) {
|
||||
// Notification already handled by generateScreenshotCanvas
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
Notifications.add("Failed to convert canvas to Blob for download", -1);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(blob); // Return the generated blob
|
||||
}
|
||||
}, "image/png"); // Explicitly request PNG format
|
||||
});
|
||||
}
|
||||
|
||||
export async function download(): Promise<void> {
|
||||
try {
|
||||
const blob = await getBlob();
|
||||
|
||||
if (!blob) {
|
||||
Notifications.add("Failed to generate screenshot data", -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
link.download = `monkeytype-result-${timestamp}.png`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
Notifications.add("Screenshot download started", 1);
|
||||
} catch (error) {
|
||||
console.error("Error downloading screenshot:", error);
|
||||
Notifications.add("Failed to download screenshot", -1);
|
||||
}
|
||||
}
|
||||
|
||||
$(".pageTest").on("click", "#saveScreenshotButton", (event) => {
|
||||
if (event.shiftKey) {
|
||||
void download();
|
||||
} else {
|
||||
void copyToClipboard();
|
||||
}
|
||||
});
|
|
@ -1,13 +1,11 @@
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import * as ThemeColors from "../elements/theme-colors";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import * as DB from "../db";
|
||||
import * as TestWords from "./test-words";
|
||||
import * as TestInput from "./test-input";
|
||||
import * as CustomText from "./custom-text";
|
||||
import * as Caret from "./caret";
|
||||
import * as OutOfFocus from "./out-of-focus";
|
||||
import * as Replay from "./replay";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as Strings from "../utils/strings";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
|
@ -17,29 +15,18 @@ import * as SlowTimer from "../states/slow-timer";
|
|||
import * as CompositionState from "../states/composition";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as Hangul from "hangul-js";
|
||||
import { format } from "date-fns/format";
|
||||
import { isAuthenticated } from "../firebase";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as ResultWordHighlight from "../elements/result-word-highlight";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import Format from "../utils/format";
|
||||
import * as Loader from "../elements/loader";
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
import {
|
||||
TimerColor,
|
||||
TimerOpacity,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
import {
|
||||
findSingleActiveFunboxWithFunction,
|
||||
getActiveFunboxesWithFunction,
|
||||
} from "./funbox/list";
|
||||
import { findSingleActiveFunboxWithFunction } from "./funbox/list";
|
||||
import * as TestState from "./test-state";
|
||||
|
||||
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
|
||||
return (await import("html2canvas")).default;
|
||||
}
|
||||
|
||||
function createHintsHtml(
|
||||
incorrectLtrIndices: number[][],
|
||||
activeWordLetters: NodeListOf<Element>,
|
||||
|
@ -627,174 +614,6 @@ export function colorful(tc: boolean): void {
|
|||
}
|
||||
}
|
||||
|
||||
let firefoxClipboardNotificatoinShown = false;
|
||||
export async function screenshot(): Promise<void> {
|
||||
Loader.show();
|
||||
let revealReplay = false;
|
||||
|
||||
let revertCookie = false;
|
||||
if (
|
||||
Misc.isElementVisible("#cookiesModal") ||
|
||||
document.contains(document.querySelector("#cookiesModal"))
|
||||
) {
|
||||
revertCookie = true;
|
||||
}
|
||||
|
||||
function revertScreenshot(): void {
|
||||
Loader.hide();
|
||||
$("#ad-result-wrapper").removeClass("hidden");
|
||||
$("#ad-result-small-wrapper").removeClass("hidden");
|
||||
$("#testConfig").removeClass("hidden");
|
||||
$(".pageTest .screenshotSpacer").remove();
|
||||
$("#notificationCenter").removeClass("hidden");
|
||||
$("#commandLineMobileButton").removeClass("hidden");
|
||||
$(".pageTest .ssWatermark").addClass("hidden");
|
||||
$(".pageTest .ssWatermark").text("monkeytype.com");
|
||||
$(".pageTest .buttons").removeClass("hidden");
|
||||
$("noscript").removeClass("hidden");
|
||||
$("#nocss").removeClass("hidden");
|
||||
$("header, footer").removeClass("invisible");
|
||||
$("#result").removeClass("noBalloons");
|
||||
$(".wordInputHighlight").removeClass("hidden");
|
||||
$(".highlightContainer").removeClass("hidden");
|
||||
if (revertCookie) $("#cookiesModal").removeClass("hidden");
|
||||
if (revealReplay) $("#resultReplay").removeClass("hidden");
|
||||
if (!isAuthenticated()) {
|
||||
$(".pageTest .loginTip").removeClass("hidden");
|
||||
}
|
||||
(document.querySelector("html") as HTMLElement).style.scrollBehavior =
|
||||
"smooth";
|
||||
for (const fb of getActiveFunboxesWithFunction("applyGlobalCSS")) {
|
||||
fb.functions.applyGlobalCSS();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$("#resultReplay").hasClass("hidden")) {
|
||||
revealReplay = true;
|
||||
Replay.pauseReplay();
|
||||
}
|
||||
const dateNow = new Date(Date.now());
|
||||
$("#resultReplay").addClass("hidden");
|
||||
$(".pageTest .ssWatermark").removeClass("hidden");
|
||||
|
||||
const snapshot = DB.getSnapshot();
|
||||
const ssWatermark = [format(dateNow, "dd MMM yyyy HH:mm"), "monkeytype.com"];
|
||||
if (snapshot?.name !== undefined) {
|
||||
const userText = `${snapshot?.name}${getHtmlByUserFlags(snapshot, {
|
||||
iconsOnly: true,
|
||||
})}`;
|
||||
ssWatermark.unshift(userText);
|
||||
}
|
||||
$(".pageTest .ssWatermark").html(
|
||||
ssWatermark
|
||||
.map((el) => `<span>${el}</span>`)
|
||||
.join("<span class='pipe'>|</span>")
|
||||
);
|
||||
$(".pageTest .buttons").addClass("hidden");
|
||||
$("#notificationCenter").addClass("hidden");
|
||||
$("#commandLineMobileButton").addClass("hidden");
|
||||
$(".pageTest .loginTip").addClass("hidden");
|
||||
$("noscript").addClass("hidden");
|
||||
$("#nocss").addClass("hidden");
|
||||
$("#ad-result-wrapper").addClass("hidden");
|
||||
$("#ad-result-small-wrapper").addClass("hidden");
|
||||
$("#testConfig").addClass("hidden");
|
||||
$(".page.pageTest").prepend("<div class='screenshotSpacer'></div>");
|
||||
$("header, footer").addClass("invisible");
|
||||
$("#result").addClass("noBalloons");
|
||||
$(".wordInputHighlight").addClass("hidden");
|
||||
$(".highlightContainer").addClass("hidden");
|
||||
if (revertCookie) $("#cookiesModal").addClass("hidden");
|
||||
|
||||
for (const fb of getActiveFunboxesWithFunction("clearGlobal")) {
|
||||
fb.functions.clearGlobal();
|
||||
}
|
||||
|
||||
(document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto";
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
const src = $("#result .wrapper");
|
||||
const sourceX = src.offset()?.left ?? 0; /*X position from div#target*/
|
||||
const sourceY = src.offset()?.top ?? 0; /*Y position from div#target*/
|
||||
const sourceWidth = src.outerWidth(
|
||||
true
|
||||
) as number; /*clientWidth/offsetWidth from div#target*/
|
||||
const sourceHeight = src.outerHeight(
|
||||
true
|
||||
) as number; /*clientHeight/offsetHeight from div#target*/
|
||||
try {
|
||||
const paddingX = convertRemToPixels(2);
|
||||
const paddingY = convertRemToPixels(2);
|
||||
|
||||
const canvas = await (
|
||||
await gethtml2canvas()
|
||||
)(document.body, {
|
||||
backgroundColor: await ThemeColors.get("bg"),
|
||||
width: sourceWidth + paddingX * 2,
|
||||
height: sourceHeight + paddingY * 2,
|
||||
x: sourceX - paddingX,
|
||||
y: sourceY - paddingY,
|
||||
});
|
||||
canvas.toBlob(async (blob) => {
|
||||
try {
|
||||
if (blob === null) {
|
||||
throw new Error("Could not create image, blob is null");
|
||||
}
|
||||
const clipItem = new ClipboardItem(
|
||||
Object.defineProperty({}, blob.type, {
|
||||
value: blob,
|
||||
enumerable: true,
|
||||
})
|
||||
);
|
||||
await navigator.clipboard.write([clipItem]);
|
||||
Notifications.add("Copied to clipboard", 1, {
|
||||
duration: 2,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error while saving image to clipboard", e);
|
||||
if (blob) {
|
||||
//check if on firefox
|
||||
if (
|
||||
navigator.userAgent.toLowerCase().includes("firefox") &&
|
||||
!firefoxClipboardNotificatoinShown
|
||||
) {
|
||||
firefoxClipboardNotificatoinShown = true;
|
||||
Notifications.add(
|
||||
"On Firefox you can enable the asyncClipboard.clipboardItem permission in about:config to enable copying straight to the clipboard",
|
||||
0,
|
||||
{
|
||||
duration: 10,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Notifications.add(
|
||||
"Could not save image to clipboard. Opening in new tab instead (make sure popups are allowed)",
|
||||
0,
|
||||
{
|
||||
duration: 5,
|
||||
}
|
||||
);
|
||||
open(URL.createObjectURL(blob));
|
||||
} else {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Error saving image to clipboard"),
|
||||
-1
|
||||
);
|
||||
}
|
||||
}
|
||||
revertScreenshot();
|
||||
});
|
||||
} catch (e) {
|
||||
Notifications.add(Misc.createErrorMessage(e, "Error creating image"), -1);
|
||||
revertScreenshot();
|
||||
}
|
||||
setTimeout(() => {
|
||||
revertScreenshot();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
export async function updateActiveWordLetters(
|
||||
inputOverride?: string
|
||||
): Promise<void> {
|
||||
|
@ -1727,10 +1546,6 @@ function updateLiveStatsColor(value: TimerColor): void {
|
|||
}
|
||||
}
|
||||
|
||||
$(".pageTest").on("click", "#saveScreenshotButton", () => {
|
||||
void screenshot();
|
||||
});
|
||||
|
||||
$(".pageTest #copyWordsListButton").on("click", async () => {
|
||||
let words;
|
||||
if (Config.mode === "zen") {
|
||||
|
|
Loading…
Add table
Reference in a new issue