Skip to content

Commit

Permalink
feat(web): add ability to share plugin in plugin-playground (#1305)
Browse files Browse the repository at this point in the history
  • Loading branch information
mulengawilfred authored Dec 12, 2024
1 parent c1589da commit 99c8f15
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 6 deletions.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"localforage": "1.10.0",
"lodash-es": "4.17.21",
"lucide-react": "0.462.0",
"lz-string": "^1.5.0",
"quickjs-emscripten": "0.24.0",
"quickjs-emscripten-sync": "1.5.2",
"rc-slider": "9.7.5",
Expand Down
39 changes: 38 additions & 1 deletion web/src/beta/features/PluginPlayground/Plugins/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import usePlugins from "./usePlugins";

type UsePluginsReturn = Pick<
ReturnType<typeof usePlugins>,
| "encodeAndSharePlugin"
| "presetPlugins"
| "selectPlugin"
| "selectedPlugin"
Expand All @@ -17,11 +18,13 @@ type UsePluginsReturn = Pick<
| "updateFileTitle"
| "deleteFile"
| "handleFileUpload"
| "sharedPlugins"
>;

type Props = UsePluginsReturn;

const Plugins: FC<Props> = ({
encodeAndSharePlugin,
presetPlugins,
selectedPlugin,
selectPlugin,
Expand All @@ -30,10 +33,15 @@ const Plugins: FC<Props> = ({
addFile,
updateFileTitle,
deleteFile,
handleFileUpload
handleFileUpload,
sharedPlugins
}) => {
const [isAddingNewFile, setIsAddingNewFile] = useState(false);

const handleShareIconClicked = (pluginId: string): void => {
encodeAndSharePlugin(pluginId);
};

return (
<Wrapper>
<PluginList>
Expand All @@ -46,10 +54,39 @@ const Plugins: FC<Props> = ({
highlighted={selectedPlugin.id === plugin.id}
onClick={() => selectPlugin(plugin.id)}
title={plugin.title}
optionsMenu={[
{
id: "0",
title: "share",
icon: "paperPlaneTilt",
onClick: () => handleShareIconClicked(plugin.id)
}
]}
optionsMenuWidth={100}
/>
))}
</div>
))}
<div>
<CategoryTitle>Shared</CategoryTitle>
{sharedPlugins.map((plugin) => (
<EntryItem
key={plugin.id}
highlighted={selectedPlugin.id === plugin.id}
onClick={() => selectPlugin(plugin.id)}
title={plugin.title}
optionsMenu={[
{
id: "0",
title: "share",
icon: "paperPlaneTilt",
onClick: () => handleShareIconClicked(plugin.id)
}
]}
optionsMenuWidth={100}
/>
))}
</div>
</PluginList>
<FileListWrapper>
<ButtonsWrapper>
Expand Down
91 changes: 88 additions & 3 deletions web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useNotification } from "@reearth/services/state";
import JSZip from "jszip";
import { useCallback, useMemo, useState } from "react";
import LZString from "lz-string";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import useFileInput from "use-file-input";
import { v4 as uuidv4 } from "uuid";

Expand All @@ -9,8 +11,53 @@ import { presetPlugins } from "./presets";
import { validateFileTitle } from "./utils";

export default () => {
const [searchParams] = useSearchParams();
const sharedPluginUrl = searchParams.get("plugin");

const decodePluginURL = useCallback((encoded: string) => {
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
// Decompress and parse
const decompressed = LZString.decompressFromBase64(base64);

return JSON.parse(decompressed);
}, []);

const sharedPlugin = sharedPluginUrl
? decodePluginURL(sharedPluginUrl)
: null;

const locallyStoredSharedPlugins = localStorage.getItem("SHARED_PLUGINS");
const [sharedPlugins, setSharedPlugins] = useState<PluginType[]>(
JSON.parse(locallyStoredSharedPlugins ?? "[]")
);

useEffect(() => {
if (sharedPlugin) {
setSharedPlugins((prevSharedPlugins) => {
const doesSharedPluginExist = prevSharedPlugins.some(
(element) => element.id === sharedPlugin.id
);
const tempSharedPlugins = doesSharedPluginExist
? prevSharedPlugins
: [...prevSharedPlugins, sharedPlugin];
localStorage.setItem(
"SHARED_PLUGINS",
JSON.stringify(tempSharedPlugins)
);
return tempSharedPlugins;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const presetPluginsArray = presetPlugins
.map((category) => category.plugins)
.flat();

const [plugins, setPlugins] = useState<PluginType[]>(
presetPlugins.map((category) => category.plugins).flat()
locallyStoredSharedPlugins
? [...JSON.parse(locallyStoredSharedPlugins), ...presetPluginsArray]
: presetPluginsArray
);

const [selectedPluginId, setSelectedPluginId] = useState(plugins[0].id);
Expand Down Expand Up @@ -199,7 +246,44 @@ export default () => {
}
}, [selectedPlugin, setNotification]);

const encodeAndSharePlugin = useCallback(
(pluginId: string): string | undefined => {
selectPlugin(pluginId);
// Need to do a find here as the selectedPlugin does not get updated immediately
const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);

// Note: We can't use the same id for a shared plugin
const selectedPluginCopy = { ...sharedPlugin, id: uuidv4() };

// First compress the code
try {
const compressed = LZString.compressToBase64(
JSON.stringify(selectedPluginCopy)
)
.replace(/\+/g, "-") // Convert + to -
.replace(/\//g, "_") // Convert / to _
.replace(/=/g, ""); // Remove padding =

const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
navigator.clipboard.writeText(shareUrl);

setNotification({
type: "success",
text: "Plugin link copied to clipboard"
});
return compressed;
} catch (error) {
if (error instanceof Error) {
setNotification({ type: "error", text: error.message });
}
return;
}
},
[plugins, selectPlugin, setNotification]
);

return {
encodeAndSharePlugin,
presetPlugins,
selectPlugin,
selectedPlugin,
Expand All @@ -210,6 +294,7 @@ export default () => {
updateFileSourceCode,
deleteFile,
handleFileUpload,
handlePluginDownload
handlePluginDownload,
sharedPlugins
};
};
10 changes: 8 additions & 2 deletions web/src/beta/features/PluginPlayground/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default () => {
updateFileSourceCode,
deleteFile,
handleFileUpload,
handlePluginDownload
handlePluginDownload,
encodeAndSharePlugin,
sharedPlugins
} = usePlugins();

const { widgets, executeCode, fileOutputs } = useCode({
Expand Down Expand Up @@ -59,6 +61,7 @@ export default () => {
name: "Plugins",
children: (
<Plugins
encodeAndSharePlugin={encodeAndSharePlugin}
presetPlugins={presetPlugins}
selectedPlugin={selectedPlugin}
selectPlugin={selectPlugin}
Expand All @@ -68,11 +71,13 @@ export default () => {
updateFileTitle={updateFileTitle}
deleteFile={deleteFile}
handleFileUpload={handleFileUpload}
sharedPlugins={sharedPlugins}
/>
)
}
],
[
encodeAndSharePlugin,
presetPlugins,
selectedPlugin,
selectPlugin,
Expand All @@ -81,7 +86,8 @@ export default () => {
addFile,
updateFileTitle,
deleteFile,
handleFileUpload
handleFileUpload,
sharedPlugins
]
);

Expand Down
1 change: 1 addition & 0 deletions web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8321,6 +8321,7 @@ __metadata:
localforage: "npm:1.10.0"
lodash-es: "npm:4.17.21"
lucide-react: "npm:0.462.0"
lz-string: "npm:^1.5.0"
npm-run-all: "npm:4.1.5"
postcss: "npm:8.4.49"
prettier: "npm:3.3.3"
Expand Down

0 comments on commit 99c8f15

Please sign in to comment.