mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 22:01:59 +00:00
add settings page with install from URL option
This commit is contained in:
+6
-7
@@ -31,7 +31,7 @@ class PluginBrowser:
|
|||||||
|
|
||||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||||
zip_hash = sha256(zip.getbuffer()).hexdigest()
|
zip_hash = sha256(zip.getbuffer()).hexdigest()
|
||||||
if zip_hash != hash:
|
if hash and (zip_hash != hash):
|
||||||
return False
|
return False
|
||||||
zip_file = ZipFile(zip)
|
zip_file = ZipFile(zip)
|
||||||
zip_file.extractall(self.plugin_path)
|
zip_file.extractall(self.plugin_path)
|
||||||
@@ -45,9 +45,8 @@ class PluginBrowser:
|
|||||||
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
|
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
|
||||||
self.log.info(f"Installing {artifact} (Version: {version})")
|
self.log.info(f"Installing {artifact} (Version: {version})")
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
url = f"https://github.com/{artifact}/archive/refs/tags/{version}.zip"
|
self.log.debug(f"Fetching {artifact}")
|
||||||
self.log.debug(f"Fetching {url}")
|
res = await client.get(artifact)
|
||||||
res = await client.get(url)
|
|
||||||
if res.status == 200:
|
if res.status == 200:
|
||||||
self.log.debug("Got 200. Reading...")
|
self.log.debug("Got 200. Reading...")
|
||||||
data = await res.read()
|
data = await res.read()
|
||||||
@@ -67,14 +66,14 @@ class PluginBrowser:
|
|||||||
else:
|
else:
|
||||||
self.log.fatal(f"SHA-256 Mismatch!!!! {artifact} (Version: {version})")
|
self.log.fatal(f"SHA-256 Mismatch!!!! {artifact} (Version: {version})")
|
||||||
else:
|
else:
|
||||||
self.log.fatal(f"Could not fetch from github. {await res.text()}")
|
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||||
|
|
||||||
async def redirect_to_store(self, request):
|
async def redirect_to_store(self, request):
|
||||||
return web.Response(status=302, headers={"Location": self.store_url})
|
return web.Response(status=302, headers={"Location": self.store_url})
|
||||||
|
|
||||||
async def install_plugin(self, request):
|
async def install_plugin(self, request):
|
||||||
data = await request.post()
|
data = await request.post()
|
||||||
get_event_loop().create_task(self.request_plugin_install(data["artifact"], data["version"], data["hash"]))
|
get_event_loop().create_task(self.request_plugin_install(data["artifact"], data.get("version", "dev"), data.get("hash", False)))
|
||||||
return web.Response(text="Requested plugin install")
|
return web.Response(text="Requested plugin install")
|
||||||
|
|
||||||
async def request_plugin_install(self, artifact, version, hash):
|
async def request_plugin_install(self, artifact, version, hash):
|
||||||
@@ -82,7 +81,7 @@ class PluginBrowser:
|
|||||||
self.install_requests[request_id] = PluginInstallContext(artifact, version, hash)
|
self.install_requests[request_id] = PluginInstallContext(artifact, version, hash)
|
||||||
tab = await get_tab("SP")
|
tab = await get_tab("SP")
|
||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{artifact}', '{version}', '{request_id}')")
|
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{artifact}', '{version}', '{request_id}', '{hash}')")
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id):
|
async def confirm_plugin_install(self, request_id):
|
||||||
request = self.install_requests.pop(request_id)
|
request = self.install_requests.pop(request_id)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decky-frontend-lib": "^0.11.0",
|
"decky-frontend-lib": "^1.0.0",
|
||||||
"react-icons": "^4.4.0"
|
"react-icons": "^4.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -9,7 +9,7 @@ specifiers:
|
|||||||
'@types/react': 16.14.0
|
'@types/react': 16.14.0
|
||||||
'@types/react-router': 5.1.18
|
'@types/react-router': 5.1.18
|
||||||
'@types/webpack': ^5.28.0
|
'@types/webpack': ^5.28.0
|
||||||
decky-frontend-lib: ^0.11.0
|
decky-frontend-lib: ^1.0.0
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
import-sort-style-module: ^6.0.0
|
import-sort-style-module: ^6.0.0
|
||||||
inquirer: ^8.2.4
|
inquirer: ^8.2.4
|
||||||
@@ -23,7 +23,7 @@ specifiers:
|
|||||||
typescript: ^4.7.3
|
typescript: ^4.7.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
decky-frontend-lib: 0.11.0
|
decky-frontend-lib: 1.0.0
|
||||||
react-icons: 4.4.0_react@16.14.0
|
react-icons: 4.4.0_react@16.14.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -803,8 +803,8 @@ packages:
|
|||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/decky-frontend-lib/0.11.0:
|
/decky-frontend-lib/1.0.0:
|
||||||
resolution: {integrity: sha512-pqBW5SQseKIvq59cvEztn6zzI4rGbd+kMx/4utzqun8lbUALODh21BU3NRsBId9TSEcRwPNl1na/QYLRsF9v9A==}
|
resolution: {integrity: sha512-ebBLyZEv0z51UmzhUNvULwmZfXsknLIelj1iQeGxfFOEI6JXrrjztcF3PsZVv3rVTTgqRfIQnXqyaaUdaeOUxA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/deepmerge/4.2.2:
|
/deepmerge/4.2.2:
|
||||||
|
|||||||
@@ -1,37 +1,17 @@
|
|||||||
import { ButtonItem, DialogButton, PanelSection, PanelSectionRow, Router } from 'decky-frontend-lib';
|
import { ButtonItem, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
|
||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
|
||||||
|
|
||||||
import { useDeckyState } from './DeckyState';
|
import { useDeckyState } from './DeckyState';
|
||||||
|
|
||||||
const PluginView: VFC = () => {
|
const PluginView: VFC = () => {
|
||||||
const { plugins, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState();
|
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
|
||||||
|
|
||||||
const onStoreClick = () => {
|
|
||||||
Router.CloseSideMenus();
|
|
||||||
Router.Navigate('/decky/store');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activePlugin) {
|
if (activePlugin) {
|
||||||
return (
|
return <div style={{ height: '100%' }}>{activePlugin.content}</div>;
|
||||||
<div style={{ height: '100%' }}>
|
|
||||||
<div style={{ position: 'absolute', top: '3px', left: '16px', zIndex: 20 }}>
|
|
||||||
<DialogButton style={{ minWidth: 0, padding: '10px 12px' }} onClick={closeActivePlugin}>
|
|
||||||
<FaArrowLeft style={{ display: 'block' }} />
|
|
||||||
</DialogButton>
|
|
||||||
</div>
|
|
||||||
{activePlugin.content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection>
|
<PanelSection>
|
||||||
<div style={{ position: 'absolute', top: '3px', right: '16px', zIndex: 20 }}>
|
|
||||||
<DialogButton style={{ minWidth: 0, padding: '10px 12px' }} onClick={onStoreClick}>
|
|
||||||
<FaStore style={{ display: 'block' }} />
|
|
||||||
</DialogButton>
|
|
||||||
</div>
|
|
||||||
{plugins.map(({ name, icon }) => (
|
{plugins.map(({ name, icon }) => (
|
||||||
<PanelSectionRow key={name}>
|
<PanelSectionRow key={name}>
|
||||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||||
|
|||||||
@@ -1,18 +1,59 @@
|
|||||||
import { staticClasses } from 'decky-frontend-lib';
|
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||||
import { VFC } from 'react';
|
import { CSSProperties, VFC } from 'react';
|
||||||
|
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
|
||||||
|
|
||||||
import { useDeckyState } from './DeckyState';
|
import { useDeckyState } from './DeckyState';
|
||||||
|
|
||||||
|
const titleStyles: CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
paddingTop: '3px',
|
||||||
|
paddingBottom: '14px',
|
||||||
|
paddingRight: '16px',
|
||||||
|
boxShadow: 'unset',
|
||||||
|
};
|
||||||
|
|
||||||
const TitleView: VFC = () => {
|
const TitleView: VFC = () => {
|
||||||
const { activePlugin } = useDeckyState();
|
const { activePlugin, closeActivePlugin } = useDeckyState();
|
||||||
|
|
||||||
|
const onSettingsClick = () => {
|
||||||
|
Router.CloseSideMenus();
|
||||||
|
Router.Navigate('/decky/settings');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStoreClick = () => {
|
||||||
|
Router.CloseSideMenus();
|
||||||
|
Router.Navigate('/decky/store');
|
||||||
|
};
|
||||||
|
|
||||||
if (activePlugin === null) {
|
if (activePlugin === null) {
|
||||||
return <div className={staticClasses.Title}>Decky</div>;
|
return (
|
||||||
|
<Focusable style={titleStyles} className={staticClasses.Title}>
|
||||||
|
<DialogButton
|
||||||
|
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||||
|
onClick={onSettingsClick}
|
||||||
|
>
|
||||||
|
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
|
||||||
|
</DialogButton>
|
||||||
|
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
||||||
|
<DialogButton
|
||||||
|
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||||
|
onClick={onStoreClick}
|
||||||
|
>
|
||||||
|
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||||
|
</DialogButton>
|
||||||
|
</Focusable>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={staticClasses.Title} style={{ paddingLeft: '60px' }}>
|
<div className={staticClasses.Title} style={titleStyles}>
|
||||||
{activePlugin.name}
|
<DialogButton
|
||||||
|
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||||
|
onClick={closeActivePlugin}
|
||||||
|
>
|
||||||
|
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
|
||||||
|
</DialogButton>
|
||||||
|
<div style={{ flex: 0.9 }}>{activePlugin.name}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||||
|
|
||||||
|
import GeneralSettings from './pages/GeneralSettings';
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
return (
|
||||||
|
<SidebarNavigation
|
||||||
|
title="Decky Settings"
|
||||||
|
showTitle
|
||||||
|
pages={[
|
||||||
|
{
|
||||||
|
title: 'General',
|
||||||
|
content: <GeneralSettings />,
|
||||||
|
route: '/decky/settings/general',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { DialogButton, Field, TextField } from 'decky-frontend-lib';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FaShapes } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { installFromURL } from '../../store/Store';
|
||||||
|
|
||||||
|
export default function GeneralSettings() {
|
||||||
|
const [pluginURL, setPluginURL] = useState('');
|
||||||
|
// const [checked, setChecked] = useState(false); // store these in some kind of State instead
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* <Field
|
||||||
|
label="A Toggle with an icon"
|
||||||
|
icon={<FaShapes style={{ display: 'block' }} />}
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
value={checked}
|
||||||
|
onChange={(e) => setChecked(e)}
|
||||||
|
/>
|
||||||
|
</Field> */}
|
||||||
|
<Field
|
||||||
|
label="Manual plugin install"
|
||||||
|
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||||
|
icon={<FaShapes style={{ display: 'block' }} />}
|
||||||
|
>
|
||||||
|
<DialogButton onClick={() => installFromURL(pluginURL)}>Install</DialogButton>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
DialogButton,
|
DialogButton,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Focusable,
|
Focusable,
|
||||||
|
QuickAccessTab,
|
||||||
Router,
|
Router,
|
||||||
SingleDropdownOption,
|
SingleDropdownOption,
|
||||||
SuspensefulImage,
|
SuspensefulImage,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
} from 'decky-frontend-lib';
|
} from 'decky-frontend-lib';
|
||||||
import { FC, useRef, useState } from 'react';
|
import { FC, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { StorePlugin } from './Store';
|
import { StorePlugin, requestPluginInstall } from './Store';
|
||||||
|
|
||||||
interface PluginCardProps {
|
interface PluginCardProps {
|
||||||
plugin: StorePlugin;
|
plugin: StorePlugin;
|
||||||
@@ -19,17 +20,6 @@ const classNames = (...classes: string[]) => {
|
|||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function requestPluginInstall(plugin: StorePlugin, selectedVer: string) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('artifact', plugin.artifact);
|
|
||||||
formData.append('version', selectedVer);
|
|
||||||
formData.append('hash', plugin.versions[selectedVer]);
|
|
||||||
await fetch('http://localhost:1337/browser/install_plugin', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -50,9 +40,12 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
buttonRef.current!.focus();
|
buttonRef.current!.focus();
|
||||||
}}
|
}}
|
||||||
onCancel={(e: CustomEvent) => {
|
onCancel={(e: CustomEvent) => {
|
||||||
containerRef.current!.querySelectorAll('* :focus').length === 0
|
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
||||||
? Router.NavigateBackOrOpenMenu()
|
Router.NavigateBackOrOpenMenu();
|
||||||
: containerRef.current!.focus();
|
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||||
|
} else {
|
||||||
|
containerRef.current!.focus();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -13,6 +13,26 @@ export interface StorePlugin {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installFromURL(url: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('artifact', url);
|
||||||
|
await fetch('http://localhost:1337/browser/install_plugin', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
|
||||||
|
formData.append('version', selectedVer);
|
||||||
|
formData.append('hash', plugin.versions[selectedVer]);
|
||||||
|
await fetch('http://localhost:1337/browser/install_plugin', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const StorePage: FC<{}> = () => {
|
const StorePage: FC<{}> = () => {
|
||||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
|
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib';
|
||||||
import { FaPlug } from 'react-icons/fa';
|
import { FaPlug } from 'react-icons/fa';
|
||||||
|
|
||||||
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
|
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
|
||||||
import LegacyPlugin from './components/LegacyPlugin';
|
import LegacyPlugin from './components/LegacyPlugin';
|
||||||
import PluginView from './components/PluginView';
|
import PluginView from './components/PluginView';
|
||||||
|
import SettingsPage from './components/settings';
|
||||||
import StorePage from './components/store/Store';
|
import StorePage from './components/store/Store';
|
||||||
import TitleView from './components/TitleView';
|
import TitleView from './components/TitleView';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
@@ -31,14 +32,11 @@ class PluginLoader extends Logger {
|
|||||||
this.log('Initialized');
|
this.log('Initialized');
|
||||||
|
|
||||||
this.tabsHook.add({
|
this.tabsHook.add({
|
||||||
id: 'main',
|
id: QuickAccessTab.Decky,
|
||||||
title: (
|
title: null,
|
||||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
|
||||||
<TitleView />
|
|
||||||
</DeckyStateContextProvider>
|
|
||||||
),
|
|
||||||
content: (
|
content: (
|
||||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||||
|
<TitleView />
|
||||||
<PluginView />
|
<PluginView />
|
||||||
</DeckyStateContextProvider>
|
</DeckyStateContextProvider>
|
||||||
),
|
),
|
||||||
@@ -46,22 +44,23 @@ class PluginLoader extends Logger {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.routerHook.addRoute('/decky/store', () => <StorePage />);
|
this.routerHook.addRoute('/decky/store', () => <StorePage />);
|
||||||
|
this.routerHook.addRoute('/decky/settings', () => <SettingsPage />);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addPluginInstallPrompt(artifact: string, version: string, request_id: string) {
|
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||||
showModal(
|
showModal(
|
||||||
<ModalRoot
|
<ModalRoot
|
||||||
onOK={() => {
|
onOK={() => {
|
||||||
console.log('ok');
|
|
||||||
this.callServerMethod('confirm_plugin_install', { request_id });
|
this.callServerMethod('confirm_plugin_install', { request_id });
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
console.log('nope');
|
|
||||||
this.callServerMethod('cancel_plugin_install', { request_id });
|
this.callServerMethod('cancel_plugin_install', { request_id });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={staticClasses.Title}>
|
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||||
Install {artifact} version {version}?
|
{hash == 'False' ? <h1 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h1> : null}
|
||||||
|
Install {artifact}
|
||||||
|
{version ? ' version ' + version : null}?
|
||||||
</div>
|
</div>
|
||||||
</ModalRoot>,
|
</ModalRoot>,
|
||||||
);
|
);
|
||||||
@@ -76,6 +75,7 @@ class PluginLoader extends Logger {
|
|||||||
|
|
||||||
public deinit() {
|
public deinit() {
|
||||||
this.routerHook.removeRoute('/decky/store');
|
this.routerHook.removeRoute('/decky/store');
|
||||||
|
this.routerHook.removeRoute('/decky/settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importPlugin(name: string) {
|
public async importPlugin(name: string) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterPatch, sleep, unpatch } from 'decky-frontend-lib';
|
import { QuickAccessTab, afterPatch, sleep, unpatch } from 'decky-frontend-lib';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
@@ -18,7 +18,7 @@ const isTabsArray = (tabs: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string;
|
id: QuickAccessTab | number;
|
||||||
title: any;
|
title: any;
|
||||||
content: any;
|
content: any;
|
||||||
icon: any;
|
icon: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user