diff --git a/packages/app/src/settings/extensions/components/ExtensionItem.jsx b/packages/app/src/settings/extensions/components/ExtensionItem.jsx new file mode 100644 index 00000000..5a31b908 --- /dev/null +++ b/packages/app/src/settings/extensions/components/ExtensionItem.jsx @@ -0,0 +1,72 @@ +import React from "react" +import { Tag, Switch, Button } from "antd" +import { Icons } from "@components/Icons" +import Image from "@components/Image" + +const ExtensionItem = ({ extension, onClickUninstall, onSwitchEnable }) => { + return ( +
+ {extension.manifest.icon && ( +
+ {extension.manifest.name} +
+ )} + + {!extension.manifest.icon && ( +
+ +
+ )} + +
+

+ {extension.manifest.name} [{extension.id}] +

+

+ {extension.manifest.description} +

+
+ }> + v{extension.manifest.version} + + }> + Load {extension.loadDuration.toFixed(2)}ms + + {extension.manifest.author && ( + }> + {extension.manifest.author} + + )} + {extension.manifest.license && ( + }> + {extension.manifest.license} + + )} + + {extension.manifest.homepage && ( + }> + {extension.manifest.homepage} + + )} +
+
+ +
+ onSwitchEnable(extension, checked)} + /> + +
+
+ ) +} + +export default ExtensionItem diff --git a/packages/app/src/settings/extensions/components/InstallCustom.jsx b/packages/app/src/settings/extensions/components/InstallCustom.jsx new file mode 100644 index 00000000..2ba95e37 --- /dev/null +++ b/packages/app/src/settings/extensions/components/InstallCustom.jsx @@ -0,0 +1,137 @@ +import React from "react" +import { Input, Alert, Button } from "antd" + +const confirmInstall = async (url, data) => { + return new Promise((resolve, reject) => { + app.layout.modal.confirm({ + headerText: "Confirm Installation", + descriptionText: + "Check and verify the details of the extension before installing.", + onConfirm: async () => { + app.extensions + .install(url) + .then(() => { + app.message.success("Extension installed successfully") + resolve() + }) + .catch((error) => { + console.error("Error installing extension:", error) + reject(error) + }) + }, + onCancel: () => { + resolve() + }, + render: () => { + return ( + + Name: {data.name} + Version: {data.version} + Description: {data.description} + Author: {data.author} + License: {data.license} + Homepage: {data.homepage} + + ) + }, + }) + }) +} + +const InstallCustom = (props) => { + const [url, setUrl] = React.useState("") + const [error, setError] = React.useState("") + const [installing, setInstalling] = React.useState(false) + + const handleInputChange = (event) => { + setUrl(event.target.value) + } + + const handleInstallClick = async () => { + setError(null) + setInstalling(true) + + if (!url) { + setError("Please enter a valid URL") + setInstalling(false) + return false + } + + let data = await fetch(url).catch((error) => { + return null + }) + + if ( + !data || + data.status !== 200 || + !data.headers.get("content-type").includes("application/json") + ) { + setError("Failed to fetch extension data") + setInstalling(false) + return false + } + + try { + data = await data.json() + } catch (error) { + setError("Failed to parse extension data") + setInstalling(false) + return false + } + + await confirmInstall(url, data) + + setInstalling(false) + + if (props.close) { + props.close() + } + } + + return ( +
+
+

Install Custom Extension

+

+ Please enter the URL of the extension you want to install. +

+
+ + + + + {error && } + + +
+ ) +} + +export default InstallCustom diff --git a/packages/app/src/settings/extensions/index.jsx b/packages/app/src/settings/extensions/index.jsx index 6b8b5fbb..cb2664a6 100755 --- a/packages/app/src/settings/extensions/index.jsx +++ b/packages/app/src/settings/extensions/index.jsx @@ -2,11 +2,12 @@ import React from "react" import loadable from "@loadable/component" export default { - id: "extensions", - icon: "MdOutlineCode", - label: "Extensions", - group: "advanced", - settings: [ - - ] -} \ No newline at end of file + id: "extensions", + icon: "MdOutlineCode", + label: "Extensions", + group: "advanced", + render: () => { + const ExtensionsPage = loadable(() => import("./page")) + return + }, +} diff --git a/packages/app/src/settings/extensions/index.less b/packages/app/src/settings/extensions/index.less new file mode 100644 index 00000000..08cc1343 --- /dev/null +++ b/packages/app/src/settings/extensions/index.less @@ -0,0 +1,117 @@ +.extensions-page { + display: flex; + flex-direction: column; + + gap: 20px; + + .extensions-page-header { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: space-between; + + .extensions-page-header-text { + display: flex; + flex-direction: column; + } + + .extensions-page-header-actions { + display: flex; + flex-direction: row; + gap: 10px; + } + } + + .extensions-list { + display: flex; + flex-direction: column; + + gap: 10px; + } +} + +.extension-item { + position: relative; + + display: flex; + flex-direction: row; + + align-items: center; + + gap: 10px; + padding: 10px; + + border-radius: 12px; + background-color: var(--background-color-accent); + + .extension-item-icon { + width: 50px; + height: 50px; + + overflow: hidden; + + border-radius: 12px; + + svg { + width: 100%; + height: 100%; + } + + img { + width: 100%; + height: 100%; + + object-fit: cover; + } + } + + .extension-item-details { + display: flex; + flex-direction: column; + + gap: 5px; + + .ant-tag { + display: inline-flex; + flex-direction: row; + gap: 5px; + + align-items: center; + + width: fit-content; + } + + .extension-item-name { + font-family: "DM Mono", monospace; + font-size: 0.9rem; + font-weight: 500; + } + } + + .extension-item-actions { + position: absolute; + right: 0; + top: 0; + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + gap: 10px; + height: 100%; + padding: 10px; + } +} + +.install-custom-extension { + display: flex; + flex-direction: column; + + width: 100%; + gap: 10px; + + font-size: 0.9rem; +} diff --git a/packages/app/src/settings/extensions/page.jsx b/packages/app/src/settings/extensions/page.jsx new file mode 100644 index 00000000..50ca4104 --- /dev/null +++ b/packages/app/src/settings/extensions/page.jsx @@ -0,0 +1,104 @@ +import React from "react" +import { Tag, Switch, Button } from "antd" +import { Icons } from "@components/Icons" +import Image from "@components/Image" + +import ExtensionItem from "./components/ExtensionItem" +import InstallCustom from "./components/InstallCustom" + +import "./index.less" + +function getInstalledExtensions() { + let extensions = [] + + for (let extension of app.extensions.extensions.values()) { + extensions.push(extension) + } + + return extensions +} + +const ExtensionsPage = () => { + const [loading, setLoading] = React.useState(false) + const [extensions, setExtensions] = React.useState([]) + + const events = { + "extension:installed": () => { + setExtensions(getInstalledExtensions()) + }, + "extension:uninstalled": () => { + setExtensions(getInstalledExtensions()) + }, + } + + const onSwitchEnable = (extension, checked) => { + app.extensions.toggleExtension(extension.id, checked) + return !checked + } + + const onClickUninstall = (extension) => { + app.layout.modal.confirm({ + headerText: "Uninstall Extension", + descriptionText: + "Are you sure you want to uninstall this extension?", + onConfirm: async () => { + await app.extensions.uninstall(extension.id) + app.message.success("Extension uninstalled") + }, + }) + } + + React.useEffect(() => { + setLoading(true) + setExtensions(getInstalledExtensions()) + setLoading(false) + }, []) + + React.useEffect(() => { + for (const event in events) { + app.eventBus.on(event, events[event]) + } + + return () => { + for (const event in events) { + app.eventBus.off(event, events[event]) + } + } + }, []) + + return ( +
+
+
+

Extensions

+

Manage your extensions here.

+
+
+
+
+ +
+ {extensions.map((extension) => ( + + ))} +
+
+ ) +} + +export default ExtensionsPage