feat: Experimental Feature Toggle (#525)
* feat: Experimental Feature Toggle * chore: add open app directory action * chore: disable experimental feature test case
This commit is contained in:
parent
6f3b17bed8
commit
0d13756a86
@ -212,6 +212,15 @@ function handleIPCs() {
|
|||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
||||||
|
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("openAppDirectory", async (_event) => {
|
||||||
|
shell.openPath(app.getPath("userData"));
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a URL in the user's default browser.
|
* Opens a URL in the user's default browser.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
|
|||||||
@ -28,6 +28,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
|
|
||||||
relaunch: () => ipcRenderer.invoke("relaunch"),
|
relaunch: () => ipcRenderer.invoke("relaunch"),
|
||||||
|
|
||||||
|
openAppDirectory: () => ipcRenderer.invoke("openAppDirectory"),
|
||||||
|
|
||||||
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
||||||
|
|
||||||
installRemotePlugin: (pluginName: string) =>
|
installRemotePlugin: (pluginName: string) =>
|
||||||
|
|||||||
@ -40,8 +40,12 @@ test("renders left navigation panel", async () => {
|
|||||||
expect(chatSection).toBe(false);
|
expect(chatSection).toBe(false);
|
||||||
|
|
||||||
// Home actions
|
// Home actions
|
||||||
const botBtn = await page.getByTestId("Bot").first().isEnabled();
|
/* Disable unstable feature tests
|
||||||
|
** const botBtn = await page.getByTestId("Bot").first().isEnabled();
|
||||||
|
** Enable back when it is whitelisted
|
||||||
|
*/
|
||||||
|
|
||||||
const myModelsBtn = await page.getByTestId("My Models").first().isEnabled();
|
const myModelsBtn = await page.getByTestId("My Models").first().isEnabled();
|
||||||
const settingsBtn = await page.getByTestId("Settings").first().isEnabled();
|
const settingsBtn = await page.getByTestId("Settings").first().isEnabled();
|
||||||
expect([botBtn, myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0);
|
expect([myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import SecondaryButton from '../SecondaryButton'
|
import SecondaryButton from '../SecondaryButton'
|
||||||
import { useSetAtom, useAtomValue } from 'jotai'
|
import { useSetAtom, useAtomValue } from 'jotai'
|
||||||
import {
|
import {
|
||||||
@ -13,6 +13,9 @@ import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|||||||
import { Button } from '@uikit'
|
import { Button } from '@uikit'
|
||||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||||
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
||||||
|
import {
|
||||||
|
FeatureToggleContext,
|
||||||
|
} from '@helpers/FeatureToggleWrapper'
|
||||||
|
|
||||||
const LeftHeaderAction: React.FC = () => {
|
const LeftHeaderAction: React.FC = () => {
|
||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
const setMainView = useSetAtom(setMainViewStateAtom)
|
||||||
@ -20,6 +23,7 @@ const LeftHeaderAction: React.FC = () => {
|
|||||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
||||||
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
|
|
||||||
const onExploreClick = () => {
|
const onExploreClick = () => {
|
||||||
setMainView(MainViewState.ExploreModel)
|
setMainView(MainViewState.ExploreModel)
|
||||||
@ -50,12 +54,14 @@ const LeftHeaderAction: React.FC = () => {
|
|||||||
className="w-full flex-1"
|
className="w-full flex-1"
|
||||||
icon={<MagnifyingGlassIcon width={16} height={16} />}
|
icon={<MagnifyingGlassIcon width={16} height={16} />}
|
||||||
/>
|
/>
|
||||||
|
{experimentalFeatureEnabed && (
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
title={'Create bot'}
|
title={'Create bot'}
|
||||||
onClick={onCreateBotClicked}
|
onClick={onCreateBotClicked}
|
||||||
className="w-full flex-1"
|
className="w-full flex-1"
|
||||||
icon={<PlusIcon width={16} height={16} />}
|
icon={<PlusIcon width={16} height={16} />}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={onNewConversationClick}
|
onClick={onNewConversationClick}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
isCorePluginInstalled,
|
isCorePluginInstalled,
|
||||||
setupBasePlugins,
|
setupBasePlugins,
|
||||||
} from '@services/pluginService'
|
} from '@services/pluginService'
|
||||||
|
import { FeatureToggleWrapper } from '@helpers/FeatureToggleWrapper'
|
||||||
|
|
||||||
const Providers = (props: PropsWithChildren) => {
|
const Providers = (props: PropsWithChildren) => {
|
||||||
const [setupCore, setSetupCore] = useState(false)
|
const [setupCore, setSetupCore] = useState(false)
|
||||||
@ -70,9 +71,11 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
{setupCore && (
|
{setupCore && (
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
{activated ? (
|
{activated ? (
|
||||||
|
<FeatureToggleWrapper>
|
||||||
<EventListenerWrapper>
|
<EventListenerWrapper>
|
||||||
<ModalWrapper>{children}</ModalWrapper>
|
<ModalWrapper>{children}</ModalWrapper>
|
||||||
</EventListenerWrapper>
|
</EventListenerWrapper>
|
||||||
|
</FeatureToggleWrapper>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||||
<CompactLogo width={56} height={56} />
|
<CompactLogo width={56} height={56} />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import {
|
import {
|
||||||
MainViewState,
|
MainViewState,
|
||||||
@ -20,6 +20,9 @@ import { twMerge } from 'tailwind-merge'
|
|||||||
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
|
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
|
||||||
import useGetBots from '@hooks/useGetBots'
|
import useGetBots from '@hooks/useGetBots'
|
||||||
import { useUserConfigs } from '@hooks/useUserConfigs'
|
import { useUserConfigs } from '@hooks/useUserConfigs'
|
||||||
|
import {
|
||||||
|
FeatureToggleContext,
|
||||||
|
} from '@helpers/FeatureToggleWrapper'
|
||||||
|
|
||||||
export const SidebarLeft = () => {
|
export const SidebarLeft = () => {
|
||||||
const [config] = useUserConfigs()
|
const [config] = useUserConfigs()
|
||||||
@ -28,6 +31,7 @@ export const SidebarLeft = () => {
|
|||||||
const setBotListModal = useSetAtom(showingBotListModalAtom)
|
const setBotListModal = useSetAtom(showingBotListModalAtom)
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
const { getAllBots } = useGetBots()
|
const { getAllBots } = useGetBots()
|
||||||
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
|
|
||||||
const onMenuClick = (mainViewState: MainViewState) => {
|
const onMenuClick = (mainViewState: MainViewState) => {
|
||||||
if (currentState === mainViewState) return
|
if (currentState === mainViewState) return
|
||||||
@ -88,18 +92,21 @@ export const SidebarLeft = () => {
|
|||||||
icon: <LayoutGrid size={20} className="flex-shrink-0" />,
|
icon: <LayoutGrid size={20} className="flex-shrink-0" />,
|
||||||
state: MainViewState.MyModel,
|
state: MainViewState.MyModel,
|
||||||
},
|
},
|
||||||
|
...(experimentalFeatureEnabed
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Bot',
|
name: 'Bot',
|
||||||
icon: <Bot size={20} className="flex-shrink-0" />,
|
icon: <Bot size={20} className="flex-shrink-0" />,
|
||||||
state: MainViewState.CreateBot,
|
state: MainViewState.CreateBot,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
icon: <Settings size={20} className="flex-shrink-0" />,
|
icon: <Settings size={20} className="flex-shrink-0" />,
|
||||||
state: MainViewState.Setting,
|
state: MainViewState.Setting,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<m.div
|
<m.div
|
||||||
initial={false}
|
initial={false}
|
||||||
@ -124,7 +131,9 @@ export const SidebarLeft = () => {
|
|||||||
config.sidebarLeftExpand ? 'items-start' : 'items-center'
|
config.sidebarLeftExpand ? 'items-start' : 'items-center'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{menus.map((menu, i) => {
|
{menus
|
||||||
|
.filter((menu) => !!menu)
|
||||||
|
.map((menu, i) => {
|
||||||
const isActive = currentState === menu.state
|
const isActive = currentState === menu.state
|
||||||
const isBotMenu = menu.name === 'Bot'
|
const isBotMenu = menu.name === 'Bot'
|
||||||
return (
|
return (
|
||||||
@ -170,9 +179,7 @@ export const SidebarLeft = () => {
|
|||||||
<div className="space-y-2 rounded-lg border border-border bg-background/50 p-3">
|
<div className="space-y-2 rounded-lg border border-border bg-background/50 p-3">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.coreAPI?.openExternalUrl(
|
window.coreAPI?.openExternalUrl('https://discord.gg/AsJ8krTT3N')
|
||||||
'https://discord.gg/AsJ8krTT3N'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="block text-xs font-semibold text-muted-foreground"
|
className="block text-xs font-semibold text-muted-foreground"
|
||||||
>
|
>
|
||||||
@ -180,9 +187,7 @@ export const SidebarLeft = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.coreAPI?.openExternalUrl(
|
window.coreAPI?.openExternalUrl('https://twitter.com/janhq_')
|
||||||
'https://twitter.com/janhq_'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="block text-xs font-semibold text-muted-foreground"
|
className="block text-xs font-semibold text-muted-foreground"
|
||||||
>
|
>
|
||||||
|
|||||||
43
web/helpers/FeatureToggleWrapper.tsx
Normal file
43
web/helpers/FeatureToggleWrapper.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface FeatureToggleContextType {
|
||||||
|
experimentalFeatureEnabed: boolean
|
||||||
|
setExperimentalFeatureEnabled: (on: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialContext: FeatureToggleContextType = {
|
||||||
|
experimentalFeatureEnabed: false,
|
||||||
|
setExperimentalFeatureEnabled: (boolean) => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureToggleContext =
|
||||||
|
React.createContext<FeatureToggleContextType>(initialContext)
|
||||||
|
|
||||||
|
export function FeatureToggleWrapper({ children }: { children: ReactNode }) {
|
||||||
|
const EXPERIMENTAL_FEATURE_ENABLED = 'expermientalFeatureEnabled'
|
||||||
|
const [experimentalEnabed, setExperimentalEnabled] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Global experimental feature is disabled
|
||||||
|
let globalFeatureEnabled = false
|
||||||
|
if (localStorage.getItem(EXPERIMENTAL_FEATURE_ENABLED) !== 'true') {
|
||||||
|
globalFeatureEnabled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setExperimentalFeature = (on: boolean) => {
|
||||||
|
localStorage.setItem(EXPERIMENTAL_FEATURE_ENABLED, on ? 'true' : 'false')
|
||||||
|
setExperimentalEnabled(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureToggleContext.Provider
|
||||||
|
value={{
|
||||||
|
experimentalFeatureEnabed: experimentalEnabed,
|
||||||
|
setExperimentalFeatureEnabled: setExperimentalFeature,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FeatureToggleContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
63
web/screens/Settings/Advanced/index.tsx
Normal file
63
web/screens/Settings/Advanced/index.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useContext, useRef } from 'react'
|
||||||
|
import { Button, Switch } from '@uikit'
|
||||||
|
import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper'
|
||||||
|
|
||||||
|
const Advanced = () => {
|
||||||
|
const { experimentalFeatureEnabed, setExperimentalFeatureEnabled } =
|
||||||
|
useContext(FeatureToggleContext)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
return (
|
||||||
|
<div className="block w-full">
|
||||||
|
<div className="flex w-full items-start justify-between border-b border-gray-200 py-4 first:pt-0 last:border-none dark:border-gray-800">
|
||||||
|
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<h6 className="text-sm font-semibold capitalize">
|
||||||
|
Experimental Mode
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
|
||||||
|
Enable experimental features that may be unstable or not fully
|
||||||
|
tested.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={experimentalFeatureEnabed}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (e === true) {
|
||||||
|
setExperimentalFeatureEnabled(true)
|
||||||
|
} else {
|
||||||
|
setExperimentalFeatureEnabled(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{window.electronAPI && (
|
||||||
|
<div className="flex w-full items-start justify-between border-b border-gray-200 py-4 first:pt-0 last:border-none dark:border-gray-800">
|
||||||
|
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<h6 className="text-sm font-semibold capitalize">
|
||||||
|
Open App Directory
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<p className="whitespace-pre-wrap leading-relaxed text-gray-600 dark:text-gray-400">
|
||||||
|
Open the directory where the app data is located.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
themes="outline"
|
||||||
|
onClick={() => window.electronAPI.openAppDirectory()}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Advanced
|
||||||
@ -1,24 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef, useContext } from 'react'
|
||||||
import { Button, Switch } from '@uikit'
|
import { Button, Switch } from '@uikit'
|
||||||
import Loader from '@containers/Loader'
|
import Loader from '@containers/Loader'
|
||||||
import { formatPluginsName } from '@utils/converter'
|
import { formatPluginsName } from '@utils/converter'
|
||||||
|
|
||||||
import { plugins, extensionPoints } from '@plugin'
|
import { plugins, extensionPoints } from '@plugin'
|
||||||
import { executeSerial } from '@services/pluginService'
|
|
||||||
import { DataService } from '@janhq/core'
|
|
||||||
import useGetAppVersion from '@hooks/useGetAppVersion'
|
import useGetAppVersion from '@hooks/useGetAppVersion'
|
||||||
|
import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper'
|
||||||
|
|
||||||
const PluginCatalog = () => {
|
const PluginCatalog = () => {
|
||||||
// const [search, setSearch] = useState<string>('')
|
|
||||||
// const [fileName, setFileName] = useState('')
|
|
||||||
const [activePlugins, setActivePlugins] = useState<any[]>([])
|
const [activePlugins, setActivePlugins] = useState<any[]>([])
|
||||||
const [pluginCatalog, setPluginCatalog] = useState<any[]>([])
|
const [pluginCatalog, setPluginCatalog] = useState<any[]>([])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
const experimentRef = useRef(null)
|
const experimentRef = useRef(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const { version } = useGetAppVersion()
|
const { version } = useGetAppVersion()
|
||||||
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
/**
|
/**
|
||||||
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
||||||
*/
|
*/
|
||||||
@ -28,33 +26,13 @@ const PluginCatalog = () => {
|
|||||||
}
|
}
|
||||||
if (!version) return
|
if (!version) return
|
||||||
|
|
||||||
// Load plugin manifest from plugin if any
|
// Get plugin manifest
|
||||||
if (extensionPoints.get(DataService.GetPluginManifest)) {
|
import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then(
|
||||||
executeSerial(DataService.GetPluginManifest).then((data: any) => {
|
(data) => {
|
||||||
setPluginCatalog(
|
if (Array.isArray(data.default) && experimentalFeatureEnabed)
|
||||||
data.filter(
|
setPluginCatalog(data.default)
|
||||||
(e: any) =>
|
|
||||||
!e.requiredVersion ||
|
|
||||||
e.requiredVersion.replace(/[.^]/g, '') <=
|
|
||||||
version.replaceAll('.', '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Fallback to app default manifest
|
|
||||||
import(
|
|
||||||
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
|
|
||||||
).then((data) =>
|
|
||||||
setPluginCatalog(
|
|
||||||
data.default.filter(
|
|
||||||
(e: any) =>
|
|
||||||
!e.requiredVersion ||
|
|
||||||
e.requiredVersion.replace(/[.^]/g, '') <=
|
|
||||||
version.replaceAll('.', '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}, [version])
|
}, [version])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +45,7 @@ const PluginCatalog = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getActivePlugins = async () => {
|
const getActivePlugins = async () => {
|
||||||
const plgs = await plugins.getActive()
|
const plgs = await plugins.getActive()
|
||||||
setActivePlugins(plgs)
|
if (Array.isArray(plgs)) setActivePlugins(plgs)
|
||||||
|
|
||||||
if (extensionPoints.get('experimentComponent')) {
|
if (extensionPoints.get('experimentComponent')) {
|
||||||
const components = await Promise.all(
|
const components = await Promise.all(
|
||||||
@ -111,20 +89,6 @@ const PluginCatalog = () => {
|
|||||||
if (res) window.coreAPI?.relaunch()
|
if (res) window.coreAPI?.relaunch()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a plugin by calling the `window.pluggableElectronIpc.update` function with the plugin name.
|
|
||||||
* If the update is successful, the application is relaunched using the `window.coreAPI.relaunch` function.
|
|
||||||
* TODO: should update using window.coreAPI rather than pluggableElectronIpc (Plugin Manager Facades)
|
|
||||||
* @param plugin - The name of the plugin to update.
|
|
||||||
*/
|
|
||||||
const update = async (plugin: string) => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// @ts-ignore
|
|
||||||
await window.pluggableElectronIpc.update([plugin], true)
|
|
||||||
window.coreAPI?.relaunch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a remote plugin tarball and installs it using the `plugins.install` function.
|
* Downloads a remote plugin tarball and installs it using the `plugins.install` function.
|
||||||
* If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function.
|
* If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function.
|
||||||
@ -152,9 +116,9 @@ const PluginCatalog = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
{(pluginCatalog ?? [])
|
{pluginCatalog
|
||||||
.concat(
|
.concat(
|
||||||
activePlugins?.filter(
|
activePlugins.filter(
|
||||||
(e) => !(pluginCatalog ?? []).some((p) => p.name === e.name)
|
(e) => !(pluginCatalog ?? []).some((p) => p.name === e.name)
|
||||||
) ?? []
|
) ?? []
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,17 +14,23 @@ import { twMerge } from 'tailwind-merge'
|
|||||||
import { formatPluginsName } from '@utils/converter'
|
import { formatPluginsName } from '@utils/converter'
|
||||||
|
|
||||||
import { extensionPoints } from '@plugin'
|
import { extensionPoints } from '@plugin'
|
||||||
|
import Advanced from './Advanced'
|
||||||
const staticMenu = ['Appearance']
|
|
||||||
|
|
||||||
if (typeof window !== "undefined" && window.electronAPI) {
|
|
||||||
staticMenu.push('Core Plugins');
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsScreen = () => {
|
const SettingsScreen = () => {
|
||||||
const [activeStaticMenu, setActiveStaticMenu] = useState('Appearance')
|
const [activeStaticMenu, setActiveStaticMenu] = useState('Appearance')
|
||||||
const [preferenceItems, setPreferenceItems] = useState<any[]>([])
|
const [preferenceItems, setPreferenceItems] = useState<any[]>([])
|
||||||
const [preferenceValues, setPreferenceValues] = useState<any[]>([])
|
const [preferenceValues, setPreferenceValues] = useState<any[]>([])
|
||||||
|
const [menus, setMenus] = useState<any[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const menu = ['Appearance']
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && window.electronAPI) {
|
||||||
|
menu.push('Core Plugins')
|
||||||
|
}
|
||||||
|
menu.push('Advanced')
|
||||||
|
setMenus(menu)
|
||||||
|
}, [])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the active plugins and their preferences from the `plugins` and `preferences` modules.
|
* Fetches the active plugins and their preferences from the `plugins` and `preferences` modules.
|
||||||
@ -72,6 +78,9 @@ const SettingsScreen = () => {
|
|||||||
case 'Appearance':
|
case 'Appearance':
|
||||||
return <AppearanceOptions />
|
return <AppearanceOptions />
|
||||||
|
|
||||||
|
case 'Advanced':
|
||||||
|
return <Advanced />
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<PreferencePlugins
|
<PreferencePlugins
|
||||||
@ -99,7 +108,7 @@ const SettingsScreen = () => {
|
|||||||
Options
|
Options
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 font-semibold">
|
<div className="mt-1 font-semibold">
|
||||||
{staticMenu.map((menu, i) => {
|
{menus.map((menu, i) => {
|
||||||
const isActive = activeStaticMenu === menu
|
const isActive = activeStaticMenu === menu
|
||||||
return (
|
return (
|
||||||
<div key={i} className="relative block py-2">
|
<div key={i} className="relative block py-2">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user