From dc90ba62ec7de76baff23ca1e0761d4961ab3dbd Mon Sep 17 00:00:00 2001
From: Cyrus Yiu
Date: Sun, 24 Nov 2024 01:23:03 -0500
Subject: [PATCH 1/6] Built in tools page and list
---
.../Tool/Cards/AwesomeArcadeToolCard.tsx | 4 +-
.../Tool/Cards/AwesomeArcadeToolOldCard.tsx | 4 +-
src/components/QuickLinks/QuickLinkCards.tsx | 28 +++---
src/components/QuickLinks/types.tsx | 4 +-
src/pages/tools/built-in/index.tsx | 87 +++++++++++++++++++
src/pages/{tools.tsx => tools/index.tsx} | 38 +++++++-
6 files changed, 142 insertions(+), 23 deletions(-)
create mode 100644 src/pages/tools/built-in/index.tsx
rename src/pages/{tools.tsx => tools/index.tsx} (87%)
diff --git a/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolCard.tsx b/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolCard.tsx
index 2d651ac..32889fd 100644
--- a/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolCard.tsx
+++ b/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolCard.tsx
@@ -149,7 +149,7 @@ export function AwesomeArcadeToolCard({
return (
{t.repo}
@@ -171,7 +171,7 @@ export function AwesomeArcadeToolCard({
return (
{t.repo}
diff --git a/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolOldCard.tsx b/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolOldCard.tsx
index cafe46f..f66c3ab 100644
--- a/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolOldCard.tsx
+++ b/src/components/AwesomeArcadeList/Tool/Cards/AwesomeArcadeToolOldCard.tsx
@@ -124,7 +124,7 @@ export function AwesomeArcadeToolOldCard({
return (
{t.repo}
@@ -146,7 +146,7 @@ export function AwesomeArcadeToolOldCard({
return (
{t.repo}
diff --git a/src/components/QuickLinks/QuickLinkCards.tsx b/src/components/QuickLinks/QuickLinkCards.tsx
index 84ef421..bc671ab 100644
--- a/src/components/QuickLinks/QuickLinkCards.tsx
+++ b/src/components/QuickLinks/QuickLinkCards.tsx
@@ -51,19 +51,21 @@ export default function QuickLinkCards({
-
-
- {quick.linkText}
-
-
+ {quick.link ? (
+
+
+ {quick.linkText}
+
+
+ ) : undefined}
diff --git a/src/components/QuickLinks/types.tsx b/src/components/QuickLinks/types.tsx
index ab21d2c..4b307ed 100644
--- a/src/components/QuickLinks/types.tsx
+++ b/src/components/QuickLinks/types.tsx
@@ -3,8 +3,8 @@ import { StaticImageData } from "next/image";
export type QuickLink = {
name: string;
description: string;
- link: string;
- linkText: string;
+ link?: string | undefined;
+ linkText?: string | undefined;
image?: {
darkTheme: StaticImageData;
lightTheme: StaticImageData;
diff --git a/src/pages/tools/built-in/index.tsx b/src/pages/tools/built-in/index.tsx
new file mode 100644
index 0000000..9f475f3
--- /dev/null
+++ b/src/pages/tools/built-in/index.tsx
@@ -0,0 +1,87 @@
+import React from "react";
+import Layout from "../../../components/Layout";
+import getAppProps, { AppProps } from "../../../components/WithAppProps";
+import { useFeatureValue } from "@growthbook/growthbook-react";
+import { QuickLink } from "@/components/QuickLinks/types";
+import QuickLinkCards from "@/components/QuickLinks/QuickLinkCards";
+
+const pageName = "Built In Tools";
+
+export function About({ appProps }: { appProps: AppProps }): React.ReactNode {
+ const quickLinks: QuickLink[] = [
+ {
+ name: "Image Importer (beta)",
+ description:
+ "Convert your images, including GIFs, into MakeCode Arcade images and animations!",
+ link: "/tools/built-in/image-importer",
+ linkText: "Import images into MakeCode Arcade",
+ },
+ {
+ name: "Image Exporter (coming soon)",
+ description:
+ "Convert your MakeCode Arcade images and animations to downloadable images and GIFs!",
+ // link: "/tools/built-in/image-exporter",
+ // linkText: "Export images from MakeCode Arcade",
+ },
+ {
+ name: "MIDI Importer (coming soon)",
+ description:
+ "Convert your piano songs in the MIDI file format into MakeCode Arcade songs!",
+ // link: "/tools/built-in/midi-importer",
+ // linkText: "Import MIDI songs into MakeCode Arcade",
+ },
+ {
+ name: "MIDI Exporter (coming soon)",
+ description:
+ "Convert your MakeCode Arcade songs to piano songs in the MIDI file format!",
+ // link: "/tools/built-in/midi-exporter",
+ // linkText: "Export MIDI songs from MakeCode Arcade",
+ },
+ ];
+
+ const builtinToolsTag = useFeatureValue("built-in-tools-tag", "");
+ const builtinToolsDesc = useFeatureValue(
+ "built-in-tools-description",
+ "Use Awesome Arcade's tools to make your game development experience even better!",
+ );
+
+ return (
+
+
+ {builtinToolsTag ? (
+ <>
+ {builtinToolsTag} {" "}
+ >
+ ) : (
+ <>>
+ )}
+ Built In Tools
+
+ {builtinToolsDesc}
+
+
+ );
+}
+
+export async function getStaticProps(): Promise<{
+ props: { appProps: AppProps };
+}> {
+ return {
+ props: {
+ appProps: await getAppProps(),
+ },
+ };
+}
+
+export default About;
diff --git a/src/pages/tools.tsx b/src/pages/tools/index.tsx
similarity index 87%
rename from src/pages/tools.tsx
rename to src/pages/tools/index.tsx
index 126e02c..50bf9a6 100644
--- a/src/pages/tools.tsx
+++ b/src/pages/tools/index.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import Layout from "../components/Layout";
-import getAppProps, { AppProps } from "../components/WithAppProps";
+import Layout from "../../components/Layout";
+import getAppProps, { AppProps } from "../../components/WithAppProps";
import Link from "next/link";
import { promises as fs } from "fs";
import { AwesomeArcadeToolsList } from "@/components/AwesomeArcadeList";
@@ -12,6 +12,7 @@ import fetchToolsFromCMS from "@/scripts/FetchListsFromCMS/FetchTools";
import { Tool } from "@/scripts/FetchListsFromCMS/types";
import { stringToBool } from "@/scripts/Utils/StringParsing/FromBool";
import { ToolTableOfContents } from "@/components/AwesomeArcadeList/Tool/toolTableOfContents";
+import { useFeatureIsOn, useFeatureValue } from "@growthbook/growthbook-react";
const pageName = "Tools";
@@ -114,6 +115,17 @@ export function Tools({ appProps, list }: ToolsProps): React.ReactNode {
});
};
+ const showBuiltinTools = useFeatureIsOn("built-in-tools");
+ const builtinToolsTag = useFeatureValue("built-in-tools-tag", "");
+ const builtinToolsDesc = useFeatureValue(
+ "built-in-tools-description",
+ "Use Awesome Arcade's tools to make your game development experience even better!",
+ );
+ const builtinToolsLinkText = useFeatureValue(
+ "built-in-tools-link",
+ "Try them out!",
+ );
+
return (
guide on how to submit a
tool to Awesome Arcade!
+ {showBuiltinTools ? (
+
+ {builtinToolsTag ? (
+ <>
+ {builtinToolsTag} {" "}
+ >
+ ) : (
+ <>>
+ )}
+ {builtinToolsDesc}
+ {" "}
+
+ {builtinToolsLinkText}
+
+
+ ) : (
+ <>>
+ )}
@@ -230,8 +260,8 @@ export function Tools({ appProps, list }: ToolsProps): React.ReactNode {
Looking for Awesome Arcade Extensions? They have been split up into the{" "}
- Extensions page! (Which you can also find in the
- navigation bar!)
+ Extensions page! (Which you can also find in
+ the navigation bar!)
);
From 4b6a70ddeac34c6a489cc4721c92e690b9b5a0d9 Mon Sep 17 00:00:00 2001
From: Cyrus Yiu
Date: Sun, 24 Nov 2024 01:31:48 -0500
Subject: [PATCH 2/6] Oops add description
---
src/pages/tools/built-in/index.tsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/pages/tools/built-in/index.tsx b/src/pages/tools/built-in/index.tsx
index 9f475f3..6216085 100644
--- a/src/pages/tools/built-in/index.tsx
+++ b/src/pages/tools/built-in/index.tsx
@@ -7,7 +7,11 @@ import QuickLinkCards from "@/components/QuickLinks/QuickLinkCards";
const pageName = "Built In Tools";
-export function About({ appProps }: { appProps: AppProps }): React.ReactNode {
+export function BuiltInTools({
+ appProps,
+}: {
+ appProps: AppProps;
+}): React.ReactNode {
const quickLinks: QuickLink[] = [
{
name: "Image Importer (beta)",
@@ -48,6 +52,7 @@ export function About({ appProps }: { appProps: AppProps }): React.ReactNode {
return (
Date: Sun, 24 Nov 2024 18:31:19 -0500
Subject: [PATCH 3/6] Image import file select
---
.../Cards/AwesomeArcadeExtensionCard.tsx | 16 +-
.../Cards/AwesomeArcadeExtensionOldCard.tsx | 16 +-
.../BuiltInTools/ImageImporter/index.tsx | 300 ++++++++++++++++++
src/components/BuiltInTools/ImagePreview.tsx | 57 ++++
src/components/BuiltInTools/PaletteEditor.tsx | 3 +
src/components/ErrorBoundary/boundary.tsx | 18 +-
src/pages/tools/built-in/image-importer.tsx | 44 +++
src/scripts/Utils/Clipboard/clipboard.ts | 113 ++++---
src/scripts/Utils/Clipboard/index.ts | 2 +-
9 files changed, 510 insertions(+), 59 deletions(-)
create mode 100644 src/components/BuiltInTools/ImageImporter/index.tsx
create mode 100644 src/components/BuiltInTools/ImagePreview.tsx
create mode 100644 src/components/BuiltInTools/PaletteEditor.tsx
create mode 100644 src/pages/tools/built-in/image-importer.tsx
diff --git a/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionCard.tsx b/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionCard.tsx
index 6db7762..311531a 100644
--- a/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionCard.tsx
+++ b/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionCard.tsx
@@ -135,13 +135,15 @@ export function AwesomeArcadeExtensionCard({
setTooltip("Click to copy");
}}
onClick={() => {
- if (copyTextToClipboard(ext.url)) {
- setTooltip("Copied!");
- } else {
- setTooltip(
- "Failed to copy - did you give us clipboard permission?",
- );
- }
+ copyTextToClipboard(ext.url)
+ .then(() => {
+ setTooltip("Copied!");
+ })
+ .catch(() => {
+ setTooltip(
+ "Failed to copy - did you give us clipboard permission?",
+ );
+ });
tippyRef.current?.show();
window.document.documentElement.dispatchEvent(
new CustomEvent("clickrepo", {
diff --git a/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionOldCard.tsx b/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionOldCard.tsx
index dfcdceb..0b38d1b 100644
--- a/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionOldCard.tsx
+++ b/src/components/AwesomeArcadeList/Extension/Cards/AwesomeArcadeExtensionOldCard.tsx
@@ -109,13 +109,15 @@ export function AwesomeArcadeExtensionOldCard({
setTooltip("Click to copy");
}}
onClick={() => {
- if (copyTextToClipboard(ext.url)) {
- setTooltip("Copied!");
- } else {
- setTooltip(
- "Failed to copy - did you give us clipboard permission?",
- );
- }
+ copyTextToClipboard(ext.url)
+ .then(() => {
+ setTooltip("Copied!");
+ })
+ .catch(() => {
+ setTooltip(
+ "Failed to copy - did you give us clipboard permission?",
+ );
+ });
tippyRef.current?.show();
window.document.documentElement.dispatchEvent(
new CustomEvent("clickrepo", {
diff --git a/src/components/BuiltInTools/ImageImporter/index.tsx b/src/components/BuiltInTools/ImageImporter/index.tsx
new file mode 100644
index 0000000..f77833c
--- /dev/null
+++ b/src/components/BuiltInTools/ImageImporter/index.tsx
@@ -0,0 +1,300 @@
+import React from "react";
+import AutoLink from "@/components/Linkable/AutoLink";
+import {
+ copyTextToClipboard,
+ readBlobsFromClipboard,
+} from "@/scripts/Utils/Clipboard/clipboard";
+import { NotificationType, notify } from "@/components/Notifications";
+import getElement from "@/scripts/Utils/Element";
+import ImagePreview from "@/components/BuiltInTools/ImagePreview";
+
+export type ImageImporterToolInput = {
+ width?: number | undefined;
+ height?: number | undefined;
+ palette?: boolean | undefined;
+ gif?: boolean | undefined;
+};
+
+export default function ImageImporterTool(): React.ReactNode {
+ const [inputBuf, setInputBuf] = React.useState(null);
+
+ const [outputCode, setOutputCode] = React.useState(null);
+ const [outputBuf, setOutputBuf] = React.useState(null);
+
+ return (
+
+
+
+
+
+
+ Output MakeCode Arcade image code
+
+
+
+
+
+
+ {
+ copyTextToClipboard(outputCode!)
+ .then(() => {
+ notify(
+ "Copied image code to clipboard",
+ NotificationType.Success,
+ );
+ })
+ .catch(() => {
+ notify(
+ "Failed to copy image code to clipboard",
+ NotificationType.Error,
+ );
+ });
+ }}
+ >
+ Copy all to clipboard
+
+
+
+
+
+
+ Output image preview
+
+
+
+ When converting GIF animations, only the first frame is previewed
+ below.
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/BuiltInTools/ImagePreview.tsx b/src/components/BuiltInTools/ImagePreview.tsx
new file mode 100644
index 0000000..f2f04c2
--- /dev/null
+++ b/src/components/BuiltInTools/ImagePreview.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+
+export default function ImagePreview({
+ imageBuf,
+}: {
+ imageBuf?: ArrayBuffer | null;
+}): React.ReactNode {
+ const [imageSrc, setImageSrc] = React.useState(undefined);
+ const [imageDims, setImageDims] = React.useState<{
+ width: number;
+ height: number;
+ } | null>(null);
+
+ React.useEffect(() => {
+ if (imageSrc != null) {
+ URL.revokeObjectURL(imageSrc);
+ }
+ if (!imageBuf) {
+ setImageSrc(undefined);
+ return;
+ }
+ const url = URL.createObjectURL(new Blob([imageBuf]));
+ setImageSrc(url);
+ return () => {
+ URL.revokeObjectURL(url);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [imageBuf]);
+
+ return imageSrc != null ? (
+ <>
+ {
+ setImageDims({
+ width: (e.target as HTMLImageElement).naturalWidth,
+ height: (e.target as HTMLImageElement).naturalHeight,
+ });
+ }}
+ />
+
+
+ {imageDims != null && imageBuf != null
+ ? `${imageDims.width}x${imageDims.height} px, ${new Intl.NumberFormat().format(Math.round(imageBuf.byteLength / 1024))} kb`
+ : undefined}
+
+ >
+ ) : (
+
+ No image yet.
+
+ );
+}
diff --git a/src/components/BuiltInTools/PaletteEditor.tsx b/src/components/BuiltInTools/PaletteEditor.tsx
new file mode 100644
index 0000000..d5046fd
--- /dev/null
+++ b/src/components/BuiltInTools/PaletteEditor.tsx
@@ -0,0 +1,3 @@
+export default function PaletteEditor(): React.ReactNode {
+ return
;
+}
diff --git a/src/components/ErrorBoundary/boundary.tsx b/src/components/ErrorBoundary/boundary.tsx
index 0fb04c6..7c24082 100644
--- a/src/components/ErrorBoundary/boundary.tsx
+++ b/src/components/ErrorBoundary/boundary.tsx
@@ -67,14 +67,16 @@ export class ErrorBoundary extends React.Component {
className="btn btn-primary"
onClick={() => {
if (this.errorStack != undefined) {
- if (copyTextToClipboard(this.errorStack)) {
- notify("Copied to clipboard!", NotificationType.Success);
- } else {
- notify(
- "Unable to copy to clipboard!",
- NotificationType.Error,
- );
- }
+ copyTextToClipboard(this.errorStack)
+ .then(() => {
+ notify("Copied to clipboard!", NotificationType.Success);
+ })
+ .catch(() => {
+ notify(
+ "Unable to copy to clipboard!",
+ NotificationType.Error,
+ );
+ });
} else {
notify(
"Unable to copy to clipboard!",
diff --git a/src/pages/tools/built-in/image-importer.tsx b/src/pages/tools/built-in/image-importer.tsx
new file mode 100644
index 0000000..4298fa6
--- /dev/null
+++ b/src/pages/tools/built-in/image-importer.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import Layout from "@/components/Layout";
+import getAppProps, { AppProps } from "@/components/WithAppProps";
+import ImageImporterTool from "@/components/BuiltInTools/ImageImporter";
+
+const pageName = "Image Importer";
+
+export function ImageImporter({
+ appProps,
+}: {
+ appProps: AppProps;
+}): React.ReactNode {
+ return (
+
+ Image Importer (beta)
+
+ Convert your images, including GIFs, into MakeCode Arcade images and
+ animations!
+
+
+
+ );
+}
+
+export async function getStaticProps(): Promise<{
+ props: { appProps: AppProps };
+}> {
+ return {
+ props: {
+ appProps: await getAppProps(),
+ },
+ };
+}
+
+export default ImageImporter;
diff --git a/src/scripts/Utils/Clipboard/clipboard.ts b/src/scripts/Utils/Clipboard/clipboard.ts
index 5e8bfa4..0644b2b 100644
--- a/src/scripts/Utils/Clipboard/clipboard.ts
+++ b/src/scripts/Utils/Clipboard/clipboard.ts
@@ -18,47 +18,88 @@ function copyToClipboardFallback(text: string): boolean {
return successful;
}
-export function copyTextToClipboard(text: string): boolean {
- if (!navigator.clipboard) {
- return copyToClipboardFallback(text);
- }
- navigator.clipboard.writeText(text).then(
- () => {},
- (err) => {
- console.error("Failed to copy text to clipboard, using fallback: " + err);
- copyToClipboardFallback(text);
+export function copyTextToClipboard(text: string): Promise {
+ return new Promise((resolve, reject) => {
+ if (!navigator.clipboard) {
+ if (copyToClipboardFallback(text)) {
+ resolve();
+ } else {
+ reject();
+ }
+ return;
}
- );
- return true;
+ navigator.clipboard.writeText(text).then(
+ () => {
+ resolve();
+ },
+ (err) => {
+ console.error(
+ "Failed to copy text to clipboard, using fallback: " + err,
+ );
+ if (copyToClipboardFallback(text)) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ );
+ });
}
-export function readTextFromClipboard(
- callback: (_text: string | undefined) => void
-): void {
- if (!navigator.clipboard) {
- return;
- }
- navigator.clipboard.readText().then(
- (text) => {
- callback(text);
- },
- (err) => {
- console.error("Failed to read from clipboard: " + err);
- callback(undefined);
+export function readTextFromClipboard(): Promise {
+ return new Promise((resolve, reject) => {
+ if (!navigator.clipboard) {
+ reject();
}
- );
+ navigator.clipboard.readText().then(
+ (text) => {
+ resolve(text);
+ },
+ (err) => {
+ console.error("Failed to read from clipboard: " + err);
+ reject();
+ },
+ );
+ });
}
// https://stackoverflow.com/a/59162806/10291933
-export function copyPNGImageBlobToClipboard(imgBlob: Blob): boolean {
- if (!navigator.clipboard) {
- return false;
- }
- try {
- navigator.clipboard.write([new ClipboardItem({ "image/png": imgBlob })]);
- return true;
- } catch (err) {
- console.error("Failed to copy image to clipboard");
- return false;
- }
+export function copyBlobsToClipboard(items: ClipboardItems): Promise {
+ return new Promise((resolve, reject) => {
+ if (!navigator.clipboard) {
+ reject();
+ return;
+ }
+ try {
+ navigator.clipboard
+ .write(items)
+ .then(() => {
+ resolve();
+ })
+ .catch(() => {
+ console.error("Failed to copy items to clipboard");
+ reject();
+ });
+ } catch (err) {
+ console.error("Failed to copy items to clipboard");
+ reject();
+ }
+ });
+}
+
+export function readBlobsFromClipboard(): Promise {
+ return new Promise((resolve, reject) => {
+ if (!navigator.clipboard) {
+ reject();
+ }
+ navigator.clipboard
+ .read()
+ .then((clipboardItems) => {
+ resolve(clipboardItems);
+ })
+ .catch(() => {
+ console.error("Failed to read items from clipboard");
+ reject();
+ });
+ });
}
diff --git a/src/scripts/Utils/Clipboard/index.ts b/src/scripts/Utils/Clipboard/index.ts
index c49a43f..f66d818 100644
--- a/src/scripts/Utils/Clipboard/index.ts
+++ b/src/scripts/Utils/Clipboard/index.ts
@@ -1,5 +1,5 @@
export {
copyTextToClipboard,
readTextFromClipboard,
- copyPNGImageBlobToClipboard,
+ copyBlobToClipboard,
} from "./clipboard";
From 9aedac32b4687a6e0a58127fd862e311921e330e Mon Sep 17 00:00:00 2001
From: Cyrus Yiu
Date: Sun, 24 Nov 2024 19:36:25 -0500
Subject: [PATCH 4/6] Input forms all done
---
.../BuiltInTools/ImageImporter/index.tsx | 47 ++-
src/components/BuiltInTools/PaletteEditor.tsx | 137 ++++++++-
src/components/BuiltInTools/palettes.ts | 287 ++++++++++++++++++
src/scripts/Utils/TypeHelp/NullUndefined.ts | 12 +
4 files changed, 480 insertions(+), 3 deletions(-)
create mode 100644 src/components/BuiltInTools/palettes.ts
diff --git a/src/components/BuiltInTools/ImageImporter/index.tsx b/src/components/BuiltInTools/ImageImporter/index.tsx
index f77833c..ed2d480 100644
--- a/src/components/BuiltInTools/ImageImporter/index.tsx
+++ b/src/components/BuiltInTools/ImageImporter/index.tsx
@@ -7,17 +7,26 @@ import {
import { NotificationType, notify } from "@/components/Notifications";
import getElement from "@/scripts/Utils/Element";
import ImagePreview from "@/components/BuiltInTools/ImagePreview";
+import PaletteEditor from "@/components/BuiltInTools/PaletteEditor";
+import { makeNaNUndefined } from "@/scripts/Utils/TypeHelp/NullUndefined";
export type ImageImporterToolInput = {
width?: number | undefined;
height?: number | undefined;
- palette?: boolean | undefined;
+ palette?: string | undefined;
gif?: boolean | undefined;
};
export default function ImageImporterTool(): React.ReactNode {
const [inputBuf, setInputBuf] = React.useState(null);
+ const [options, setOptions] = React.useState({
+ width: undefined,
+ height: undefined,
+ palette: undefined,
+ gif: undefined,
+ });
+
const [outputCode, setOutputCode] = React.useState(null);
const [outputBuf, setOutputBuf] = React.useState(null);
@@ -26,6 +35,11 @@ export default function ImageImporterTool(): React.ReactNode {
@@ -199,6 +220,13 @@ export default function ImageImporterTool(): React.ReactNode {
aria-label="Height"
aria-describedby="height-label"
placeholder="Leave blank to auto-calculate from width and keep aspect ratio"
+ onChange={(e) => {
+ const v = e.target.value.trim();
+ setOptions({
+ ...options,
+ height: makeNaNUndefined(parseInt(e.target.value.trim())),
+ });
+ }}
/>
@@ -211,6 +239,16 @@ export default function ImageImporterTool(): React.ReactNode {
+
+
+
{
+ setOptions({ ...options, palette: p });
+ }}
+ />
+
+
@@ -218,6 +256,12 @@ export default function ImageImporterTool(): React.ReactNode {
type="checkbox"
className="form-check-input"
id="gif-checkbox"
+ onChange={(e) => {
+ setOptions({
+ ...options,
+ gif: e.target.checked ? true : undefined,
+ });
+ }}
/>
Try parsing GIF to image array/animation (experimental)
@@ -226,6 +270,7 @@ export default function ImageImporterTool(): React.ReactNode {
+ {/*{JSON.stringify(options, null, 2)} */}
;
+import React from "react";
+import { AllPalettes } from "@/components/BuiltInTools/palettes";
+
+export default function PaletteEditor({
+ palette,
+ setPalette,
+}: {
+ palette: string | undefined;
+ setPalette: (value: string | undefined) => void;
+}): React.ReactNode {
+ const [paletteName, setPaletteName] = React.useState("Custom");
+
+ React.useEffect(() => {
+ if (palette == null) {
+ setPalette(
+ "#000000,#ffffff,#ff2121,#ff93c4,#ff8135,#fff609,#249ca3,#78dc52,#003fad,#87f2ff,#8e2ec4,#a4839f,#5c406c,#e5cdc4,#91463d,#000000",
+ );
+ }
+ }, [palette, setPalette]);
+
+ React.useEffect(() => {
+ setPaletteName("Custom");
+ if (palette == null) {
+ return;
+ }
+ for (const preset of AllPalettes) {
+ if (preset.colors.join(",").toUpperCase() === palette.toUpperCase()) {
+ setPaletteName(preset.name);
+ break;
+ }
+ }
+ }, [palette]);
+
+ return (
+
+ Palette ({paletteName})
+
+
+ Select palette preset (clicking will overwrite current palette)
+
+ {
+ setPalette(e.target.value);
+ }}
+ >
+ {(() => {
+ return AllPalettes.map((palette) => {
+ return (
+
+ {palette.name}
+
+ );
+ });
+ })()}
+
+
+
+ {palette != null && palette.split(",").length > 0 ? (
+ palette.split(",").map((color, index, colors) => {
+ return (
+
+ {index == 0 ? (
+
+ Reserved for transparency, cannot be changed.
+
+ ) : (
+
+ {
+ const newColors = colors.slice();
+ const temp = newColors[index];
+ newColors[index] = newColors[index - 1];
+ newColors[index - 1] = temp;
+ setPalette(newColors.join(","));
+ }}
+ >
+ Move up
+
+ {
+ const newColors = colors.slice();
+ const temp = newColors[index];
+ newColors[index] = newColors[index + 1];
+ newColors[index + 1] = temp;
+ setPalette(newColors.join(","));
+ }}
+ >
+ Move down
+
+ {
+ const newColors = colors.slice();
+ newColors[index] = e.target.value;
+ setPalette(newColors.join(","));
+ }}
+ />
+ {
+ const newColors = colors.slice();
+ newColors[index] = e.target.value;
+ setPalette(newColors.join(","));
+ }}
+ />
+
+ )}
+
+ );
+ })
+ ) : (
+
+ No palette selected.
+
+ )}
+
+
+ );
}
diff --git a/src/components/BuiltInTools/palettes.ts b/src/components/BuiltInTools/palettes.ts
new file mode 100644
index 0000000..ff35e81
--- /dev/null
+++ b/src/components/BuiltInTools/palettes.ts
@@ -0,0 +1,287 @@
+// https://github.com/microsoft/pxt/blob/master/react-common/components/palette/Palettes.ts
+
+function lf(s: string) {
+ return s;
+}
+
+export interface Palette {
+ name: string;
+ id: string;
+ colors: string[];
+ custom?: boolean;
+}
+
+export const Adafruit: Palette = {
+ id: "Adafruit",
+ name: lf("Adafruit"),
+ colors: [
+ "#000000",
+ "#FFFFFF",
+ "#FF0000",
+ "#FF007D",
+ "#FF7a00",
+ "#e5FF00",
+ "#2D9F00",
+ "#00FF72",
+ "#0034FF",
+ "#17ABFF",
+ "#C600FF",
+ "#636363",
+ "#7400DB",
+ "#00EFFF",
+ "#DF2929",
+ "#000000",
+ ],
+};
+
+// https://lospec.com/palette-list/vanilla-milkshake missing #e9f59d
+export const Pastel: Palette = {
+ id: "Pastel",
+ name: lf("Pastel"),
+ colors: [
+ "#000000",
+ "#fff7e4",
+ "#f98284",
+ "#feaae4",
+ "#ffc384",
+ "#fff7a0",
+ "#87a889",
+ "#b0eb93",
+ "#b0a9e4",
+ "#accce4",
+ "#b3e3da",
+ "#d9c8bf",
+ "#6c5671",
+ "#ffe6c6",
+ "#dea38b",
+ "#28282e",
+ ],
+};
+
+export const Matte: Palette = {
+ id: "Matte",
+ name: lf("Matte"),
+ colors: [
+ "#000000",
+ "#FFF1E8",
+ "#FF004D",
+ "#FF77A8",
+ "#FFA300",
+ "#FFEC27",
+ "#008751",
+ "#00E436",
+ "#29ADFF",
+ "#C2C3C7",
+ "#7E2553",
+ "#83769C",
+ "#5F574F",
+ "#FFCCAA",
+ "#AB5236",
+ "#1D2B53",
+ ],
+};
+
+export const Grayscale: Palette = {
+ id: "Grayscale",
+ name: lf("Grayscale"),
+ colors: [
+ "#000000",
+ "#FFFFFF",
+ "#EDEDED",
+ "#DBDBDB",
+ "#C8C8C8",
+ "#B6B6B6",
+ "#A4A4A4",
+ "#929292",
+ "#808080",
+ "#6D6D6D",
+ "#5B5B5B",
+ "#494949",
+ "#373737",
+ "#242424",
+ "#121212",
+ "#000000",
+ ],
+};
+
+// https://lospec.com/palette-list/poke14 with #b56edd added for purple
+export const Poke: Palette = {
+ id: "Poke",
+ name: lf("Poke"),
+ colors: [
+ "#000000",
+ "#ffffff",
+ "#d45362",
+ "#e8958b",
+ "#cc8945",
+ "#f5dc8c",
+ "#417d53",
+ "#5dd48f",
+ "#5162c2",
+ "#6cadeb",
+ "#b56edd",
+ "#8f3f29",
+ "#612431",
+ "#c0fac2",
+ "#24325e",
+ "#1b1221",
+ ],
+};
+
+// https://lospec.com/palette-list/warioware-diy
+export const DIY: Palette = {
+ id: "DIY",
+ name: lf("DIY"),
+ colors: [
+ "#000000",
+ "#f8f8f8",
+ "#f80000",
+ "#FF93C4",
+ "#f8a830",
+ "#f8f858",
+ "#089050",
+ "#70d038",
+ "#2868c0",
+ "#10c0c8",
+ "#c868e8",
+ "#c0c0c0",
+ "#787878",
+ "#f8d898",
+ "#c04800",
+ "#000000",
+ ],
+};
+
+// https://lospec.com/palette-list/still-life
+export const StillLife: Palette = {
+ id: "StillLife",
+ name: lf("Still Life"),
+ colors: [
+ "#000000",
+ "#a8e4d4",
+ "#d13b27",
+ "#e07f8a",
+ "#cc8218",
+ "#b3e868",
+ "#5d853a",
+ "#68c127",
+ "#286fb8",
+ "#9b8bff",
+ "#3f2811",
+ "#513155",
+ "#122615",
+ "#c7b581",
+ "#7a2222",
+ "#000000",
+ ],
+};
+
+// https://lospec.com/palette-list/steam-lords, missing 0xa0b9ba
+export const SteamPunk: Palette = {
+ id: "SteamPunk",
+ name: lf("Steam Punk"),
+ colors: [
+ "#000000",
+ "#c0d1cc",
+ "#603b3a",
+ "#170e19",
+ "#775c4f",
+ "#77744f",
+ "#4f7754",
+ "#a19f7c",
+ "#4f5277",
+ "#65738c",
+ "#3a604a",
+ "#213b25",
+ "#433a60",
+ "#7c94a1",
+ "#3b2137",
+ "#2f213b",
+ ],
+};
+
+// https://lospec.com/palette-list/sweetie-16, missing 0x73eff7
+export const Sweet: Palette = {
+ id: "Sweet",
+ name: lf("Sweet"),
+ colors: [
+ "#000000",
+ "#f4f4f4",
+ "#b13e53",
+ "#a7f070",
+ "#ef7d57",
+ "#ffcd75",
+ "#257179",
+ "#38b764",
+ "#29366f",
+ "#3b5dc9",
+ "#41a6f6",
+ "#566c86",
+ "#333c57",
+ "#94b0c2",
+ "#5d275d",
+ "#1a1c2c",
+ ],
+};
+
+// https://lospec.com/palette-list/na16, missing 0x70377f
+export const Adventure: Palette = {
+ id: "Adventure",
+ name: lf("Adventure"),
+ colors: [
+ "#000000",
+ "#f5edba",
+ "#9d303b",
+ "#d26471",
+ "#e4943a",
+ "#c0c741",
+ "#647d34",
+ "#34859d",
+ "#17434b",
+ "#7ec4c1",
+ "#584563",
+ "#8c8fae",
+ "#3e2137",
+ "#d79b7d",
+ "#9a6348",
+ "#1f0e1c",
+ ],
+};
+
+//% fixedInstance whenUsed block="arcade"
+export const Arcade: Palette = {
+ id: "Arcade",
+ name: lf("Arcade"),
+ colors: [
+ "#000000",
+ "#FFFFFF",
+ "#FF2121",
+ "#FF93C4",
+ "#FF8135",
+ "#FFF609",
+ "#249CA3",
+ "#78DC52",
+ "#003FAD",
+ "#87F2FF",
+ "#8E2EC4",
+ "#A4839F",
+ "#5C406c",
+ "#E5CDC4",
+ "#91463d",
+ "#000000",
+ ],
+};
+
+export const AllPalettes = [
+ Arcade,
+ Matte,
+ Pastel,
+ Sweet,
+ Poke,
+ Adventure,
+ DIY,
+ Adafruit,
+ StillLife,
+ SteamPunk,
+ Grayscale,
+];
diff --git a/src/scripts/Utils/TypeHelp/NullUndefined.ts b/src/scripts/Utils/TypeHelp/NullUndefined.ts
index e73ae72..74a671c 100644
--- a/src/scripts/Utils/TypeHelp/NullUndefined.ts
+++ b/src/scripts/Utils/TypeHelp/NullUndefined.ts
@@ -13,3 +13,15 @@ export function makeNullUndefined(
}
return value;
}
+
+export function makeNaNUndefined(
+ value: number | undefined,
+): number | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (isNaN(value)) {
+ return undefined;
+ }
+ return value;
+}
From cae98330fa0d927f1f2f7fa3bcacfbd569071a5d Mon Sep 17 00:00:00 2001
From: Cyrus Yiu
Date: Mon, 25 Nov 2024 01:36:14 -0500
Subject: [PATCH 5/6] Basic image import works
---
.../BuiltInTools/ImageImporter/index.tsx | 87 ++++++++++++++++++-
src/components/BuiltInTools/ImagePreview.tsx | 6 +-
yarn.lock | 8 +-
3 files changed, 93 insertions(+), 8 deletions(-)
diff --git a/src/components/BuiltInTools/ImageImporter/index.tsx b/src/components/BuiltInTools/ImageImporter/index.tsx
index ed2d480..2612a99 100644
--- a/src/components/BuiltInTools/ImageImporter/index.tsx
+++ b/src/components/BuiltInTools/ImageImporter/index.tsx
@@ -17,6 +17,8 @@ export type ImageImporterToolInput = {
gif?: boolean | undefined;
};
+// TODO: FIX GIFS
+// https://pyscript.com/@ckyiu/image-to-makecode-arcade/latest?files=main.py,index.html
export default function ImageImporterTool(): React.ReactNode {
const [inputBuf, setInputBuf] = React.useState(null);
@@ -30,6 +32,36 @@ export default function ImageImporterTool(): React.ReactNode {
const [outputCode, setOutputCode] = React.useState(null);
const [outputBuf, setOutputBuf] = React.useState(null);
+ const [iframeReady, setIframeReady] = React.useState(false);
+
+ const handleMessage = React.useCallback((e: MessageEvent) => {
+ let data = e.data;
+ console.log("Received message from iframe");
+ if (e.origin !== "https://ckyiu.pyscriptapps.com") {
+ console.warn("Received message from unknown origin", e.origin);
+ return;
+ }
+ if (data === "ready") {
+ setIframeReady(true);
+ return;
+ }
+ try {
+ data = JSON.parse(data);
+ setOutputCode(data.output_image_code);
+ setOutputBuf(Buffer.from(data.output_preview_img, "base64"));
+ setIframeReady(true);
+ } catch (e) {
+ console.warn(e);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ window.addEventListener("message", handleMessage);
+ return () => {
+ window.removeEventListener("message", handleMessage);
+ };
+ }, [handleMessage]);
+
return (
);
}
diff --git a/src/components/BuiltInTools/ImagePreview.tsx b/src/components/BuiltInTools/ImagePreview.tsx
index f2f04c2..25c300b 100644
--- a/src/components/BuiltInTools/ImagePreview.tsx
+++ b/src/components/BuiltInTools/ImagePreview.tsx
@@ -34,7 +34,11 @@ export default function ImagePreview({
className="img-fluid"
src={imageSrc}
alt="Preview"
- style={{ maxWidth: "50vw", maxHeight: "50vh" }}
+ style={{
+ maxWidth: "50vw",
+ maxHeight: "50vh",
+ imageRendering: "pixelated",
+ }}
onLoad={(e) => {
setImageDims({
width: (e.target as HTMLImageElement).naturalWidth,
diff --git a/yarn.lock b/yarn.lock
index b1c8e2a..a6853a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2709,10 +2709,10 @@
dependencies:
undici-types "~5.26.4"
-"@types/node@^21.6.1":
- version "20.16.11"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.11.tgz#9b544c3e716b1577ac12e70f9145193f32750b33"
- integrity sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==
+"@types/node@^20.16.11":
+ version "20.17.7"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.7.tgz#790151a28c5a172773d95d53a0c23d3c59a883c4"
+ integrity sha512-sZXXnpBFMKbao30dUAvzKbdwA2JM1fwUtVEq/kxKuPI5mMwZiRElCpTXb0Biq/LMEVpXDZL5G5V0RPnxKeyaYg==
dependencies:
undici-types "~6.19.2"
From 0737d02103a4689ce9cf1edcda4dd6146b36c767 Mon Sep 17 00:00:00 2001
From: Cyrus Yiu
Date: Mon, 25 Nov 2024 01:50:58 -0500
Subject: [PATCH 6/6] Notification
---
.../BuiltInTools/ImageImporter/index.tsx | 19 +++++++++++++++----
.../Notifications/notifications.tsx | 6 +++---
src/scripts/Utils/Clipboard/index.ts | 3 ++-
3 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/src/components/BuiltInTools/ImageImporter/index.tsx b/src/components/BuiltInTools/ImageImporter/index.tsx
index 2612a99..874b8b8 100644
--- a/src/components/BuiltInTools/ImageImporter/index.tsx
+++ b/src/components/BuiltInTools/ImageImporter/index.tsx
@@ -3,12 +3,17 @@ import AutoLink from "@/components/Linkable/AutoLink";
import {
copyTextToClipboard,
readBlobsFromClipboard,
-} from "@/scripts/Utils/Clipboard/clipboard";
-import { NotificationType, notify } from "@/components/Notifications";
+} from "@/scripts/Utils/Clipboard";
+import {
+ loadingNotify,
+ NotificationType,
+ notify,
+} from "@/components/Notifications";
import getElement from "@/scripts/Utils/Element";
import ImagePreview from "@/components/BuiltInTools/ImagePreview";
import PaletteEditor from "@/components/BuiltInTools/PaletteEditor";
import { makeNaNUndefined } from "@/scripts/Utils/TypeHelp/NullUndefined";
+import { LoadingNotifyReturn } from "@/components/Notifications/notifications";
export type ImageImporterToolInput = {
width?: number | undefined;
@@ -33,6 +38,7 @@ export default function ImageImporterTool(): React.ReactNode {
const [outputBuf, setOutputBuf] = React.useState(null);
const [iframeReady, setIframeReady] = React.useState(false);
+ const notifyCbs = React.useRef();
const handleMessage = React.useCallback((e: MessageEvent) => {
let data = e.data;
@@ -50,6 +56,7 @@ export default function ImageImporterTool(): React.ReactNode {
setOutputCode(data.output_image_code);
setOutputBuf(Buffer.from(data.output_preview_img, "base64"));
setIframeReady(true);
+ notifyCbs.current?.successCallback();
} catch (e) {
console.warn(e);
}
@@ -78,6 +85,12 @@ export default function ImageImporterTool(): React.ReactNode {
setIframeReady(false);
setOutputCode(null);
setOutputBuf(null);
+ notifyCbs.current = loadingNotify(
+ "Converting image...",
+ "Conversion complete!",
+ "Failed to convert!",
+ "Canceled conversion.",
+ );
setTimeout(() => {
iframe.contentWindow!.postMessage(
JSON.stringify({
@@ -247,7 +260,6 @@ export default function ImageImporterTool(): React.ReactNode {
aria-describedby="width-label"
placeholder="Leave blank to auto-calculate from height and keep aspect ratio"
onChange={(e) => {
- const v = e.target.value.trim();
setOptions({
...options,
width: makeNaNUndefined(parseInt(e.target.value.trim())),
@@ -272,7 +284,6 @@ export default function ImageImporterTool(): React.ReactNode {
aria-describedby="height-label"
placeholder="Leave blank to auto-calculate from width and keep aspect ratio"
onChange={(e) => {
- const v = e.target.value.trim();
setOptions({
...options,
height: makeNaNUndefined(parseInt(e.target.value.trim())),
diff --git a/src/components/Notifications/notifications.tsx b/src/components/Notifications/notifications.tsx
index a798005..93bd6ce 100644
--- a/src/components/Notifications/notifications.tsx
+++ b/src/components/Notifications/notifications.tsx
@@ -93,10 +93,10 @@ export function promiseNotify(
);
}
-type LoadingNotifyReturn = {
+export type LoadingNotifyReturn = {
successCallback: () => void;
errorCallback: () => void;
- canceledCallback: () => void;
+ cancelCallback: () => void;
};
export function loadingNotify(
@@ -123,7 +123,7 @@ export function loadingNotify(
autoClose: 5000,
});
},
- canceledCallback: () => {
+ cancelCallback: () => {
toast.update(id, {
render: canceledText,
type: "default",
diff --git a/src/scripts/Utils/Clipboard/index.ts b/src/scripts/Utils/Clipboard/index.ts
index f66d818..f856010 100644
--- a/src/scripts/Utils/Clipboard/index.ts
+++ b/src/scripts/Utils/Clipboard/index.ts
@@ -1,5 +1,6 @@
export {
copyTextToClipboard,
readTextFromClipboard,
- copyBlobToClipboard,
+ copyBlobsToClipboard,
+ readBlobsFromClipboard,
} from "./clipboard";