added extensions settings

This commit is contained in:
SrGooglo 2025-03-13 23:36:51 +00:00
parent 64fd80c0aa
commit 44ce0a0d1c
5 changed files with 439 additions and 8 deletions

View File

@ -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 (
<div key={extension.id} className="extension-item">
{extension.manifest.icon && (
<div className="extension-item-icon">
<Image
src={extension.manifest.icon}
alt={extension.manifest.name}
/>
</div>
)}
{!extension.manifest.icon && (
<div className="extension-item-icon">
<Icons.FiBox />
</div>
)}
<div className="extension-item-details">
<p className="extension-item-name">
{extension.manifest.name} [{extension.id}]
</p>
<p className="extension-item-description">
{extension.manifest.description}
</p>
<div className="extension-item-indicators">
<Tag color="blue" icon={<Icons.FiTag />}>
v{extension.manifest.version}
</Tag>
<Tag color="green" icon={<Icons.FiClock />}>
Load {extension.loadDuration.toFixed(2)}ms
</Tag>
{extension.manifest.author && (
<Tag icon={<Icons.FiUser />}>
{extension.manifest.author}
</Tag>
)}
{extension.manifest.license && (
<Tag icon={<Icons.FiLock />}>
{extension.manifest.license}
</Tag>
)}
{extension.manifest.homepage && (
<Tag icon={<Icons.FiExternalLink />}>
{extension.manifest.homepage}
</Tag>
)}
</div>
</div>
<div className="extension-item-actions">
<Switch
defaultChecked={extension.enabled}
onChange={(checked) => onSwitchEnable(extension, checked)}
/>
<Button
icon={<Icons.FiTrash />}
onClick={() => onClickUninstall(extension)}
/>
</div>
</div>
)
}
export default ExtensionItem

View File

@ -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 (
<code
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
fontSize: "14px",
backgroundColor: "var(--background-color-primary)",
padding: "10px",
borderRadius: "12px",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
<span>Name: {data.name}</span>
<span>Version: {data.version}</span>
<span>Description: {data.description}</span>
<span>Author: {data.author}</span>
<span>License: {data.license}</span>
<span>Homepage: {data.homepage}</span>
</code>
)
},
})
})
}
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 (
<div className="install-custom-extension">
<div className="install-custom-extension-header">
<h2>Install Custom Extension</h2>
<p>
Please enter the URL of the extension you want to install.
</p>
</div>
<Input
placeholder="https://example.com/extension/package.json"
onChange={handleInputChange}
/>
<Button
size="small"
type="primary"
onClick={handleInstallClick}
disabled={installing}
loading={installing}
>
Install
</Button>
{error && <Alert message={error} type="error" />}
<Alert
message="Be aware installing custom extensions may pose security risks. Only install extensions from trusted sources."
type="warning"
/>
</div>
)
}
export default InstallCustom

View File

@ -2,11 +2,12 @@ import React from "react"
import loadable from "@loadable/component" import loadable from "@loadable/component"
export default { export default {
id: "extensions", id: "extensions",
icon: "MdOutlineCode", icon: "MdOutlineCode",
label: "Extensions", label: "Extensions",
group: "advanced", group: "advanced",
settings: [ render: () => {
const ExtensionsPage = loadable(() => import("./page"))
] return <ExtensionsPage />
},
} }

View File

@ -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;
}

View File

@ -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 (
<div className="extensions-page">
<div className="extensions-page-header">
<div className="extensions-page-header-text">
<h1>Extensions</h1>
<p>Manage your extensions here.</p>
</div>
<div className="extensions-page-header-actions">
<Button
type="primary"
icon={<Icons.FiPlus />}
onClick={() =>
app.layout.modal.open(
"install_custom",
InstallCustom,
)
}
/>
</div>
</div>
<div className="extensions-list">
{extensions.map((extension) => (
<ExtensionItem
key={extension.id}
extension={extension}
onSwitchEnable={onSwitchEnable}
onClickUninstall={onClickUninstall}
/>
))}
</div>
</div>
)
}
export default ExtensionsPage