mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
backwards-compatible plugin store, legacy plugin library
This commit is contained in:
+12
-16
@@ -12,21 +12,20 @@ from hashlib import sha256
|
|||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
class PluginInstallContext:
|
class PluginInstallContext:
|
||||||
def __init__(self, gh_url, version, hash) -> None:
|
def __init__(self, artifact, name, version, hash) -> None:
|
||||||
self.gh_url = gh_url
|
self.artifact = artifact
|
||||||
|
self.name = name
|
||||||
self.version = version
|
self.version = version
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
|
|
||||||
class PluginBrowser:
|
class PluginBrowser:
|
||||||
def __init__(self, plugin_path, server_instance, store_url) -> None:
|
def __init__(self, plugin_path, server_instance) -> None:
|
||||||
self.log = getLogger("browser")
|
self.log = getLogger("browser")
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
self.store_url = store_url
|
|
||||||
self.install_requests = {}
|
self.install_requests = {}
|
||||||
|
|
||||||
server_instance.add_routes([
|
server_instance.add_routes([
|
||||||
web.post("/browser/install_plugin", self.install_plugin),
|
web.post("/browser/install_plugin", self.install_plugin)
|
||||||
web.get("/browser/redirect", self.redirect_to_store)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||||
@@ -40,12 +39,12 @@ class PluginBrowser:
|
|||||||
Popen(["chmod", "-R", "555", self.plugin_path])
|
Popen(["chmod", "-R", "555", self.plugin_path])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _install(self, name, version, hash):
|
async def _install(self, artifact, name, version, hash):
|
||||||
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 {name} (Version: {version})")
|
self.log.info(f"Installing {name} (Version: {version})")
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
self.log.debug(f"Fetching {name}")
|
self.log.debug(f"Fetching {artifact}")
|
||||||
res = await client.get(name)
|
res = await client.get(artifact)
|
||||||
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,24 +66,21 @@ class PluginBrowser:
|
|||||||
else:
|
else:
|
||||||
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
|
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||||
|
|
||||||
async def redirect_to_store(self, request):
|
|
||||||
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.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
|
get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), 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, name, version, hash):
|
async def request_plugin_install(self, artifact, name, version, hash):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
self.install_requests[request_id] = PluginInstallContext(name, version, hash)
|
self.install_requests[request_id] = PluginInstallContext(artifact, name, 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('{name}', '{version}', '{request_id}', '{hash}')")
|
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{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)
|
||||||
await self._install(request.gh_url, request.version, request.hash)
|
await self._install(request.artifact, request.name, request.version, request.hash)
|
||||||
|
|
||||||
def cancel_plugin_install(self, request_id):
|
def cancel_plugin_install(self, request_id):
|
||||||
self.install_requests.pop(request_id)
|
self.install_requests.pop(request_id)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
class PluginEventTarget extends EventTarget { }
|
||||||
|
method_call_ev_target = new PluginEventTarget();
|
||||||
|
|
||||||
|
window.addEventListener("message", function(evt) {
|
||||||
|
let ev = new Event(evt.data.call_id);
|
||||||
|
ev.data = evt.data.result;
|
||||||
|
method_call_ev_target.dispatchEvent(ev);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
async function call_server_method(method_name, arg_object={}) {
|
||||||
|
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(arg_object),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dta = await response.json();
|
||||||
|
if (!dta.success) throw dta.result;
|
||||||
|
return dta.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source: https://stackoverflow.com/a/2117523 Thanks!
|
||||||
|
function uuidv4() {
|
||||||
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||||
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch_nocors(url, request={}) {
|
||||||
|
let args = { method: "POST", headers: {}, body: "" };
|
||||||
|
request = {...args, ...request};
|
||||||
|
request.url = url;
|
||||||
|
request.data = request.body;
|
||||||
|
delete request.body; //maintain api-compatibility with fetch
|
||||||
|
return await call_server_method("http_request", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function call_plugin_method(method_name, arg_object={}) {
|
||||||
|
if (plugin_name == undefined)
|
||||||
|
throw new Error("Plugin methods can only be called from inside plugins (duh)");
|
||||||
|
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
args: arg_object,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dta = await response.json();
|
||||||
|
if (!dta.success) throw dta.result;
|
||||||
|
return dta.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute_in_tab(tab, run_async, code) {
|
||||||
|
return await call_server_method("execute_in_tab", {
|
||||||
|
'tab': tab,
|
||||||
|
'run_async': run_async,
|
||||||
|
'code': code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inject_css_into_tab(tab, style) {
|
||||||
|
return await call_server_method("inject_css_into_tab", {
|
||||||
|
'tab': tab,
|
||||||
|
'style': style
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove_css_from_tab(tab, css_id) {
|
||||||
|
return await call_server_method("remove_css_from_tab", {
|
||||||
|
'tab': tab,
|
||||||
|
'css_id': css_id
|
||||||
|
});
|
||||||
|
}
|
||||||
+3
-3
@@ -116,7 +116,7 @@ class Loader:
|
|||||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||||
self.plugins[plugin.name] = plugin.start()
|
self.plugins[plugin.name] = plugin.start()
|
||||||
self.logger.info(f"Loaded {plugin.name}")
|
self.logger.info(f"Loaded {plugin.name}")
|
||||||
self.loop.create_task(self.dispatch_plugin(plugin.name))
|
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Could not load {file}. {e}")
|
self.logger.error(f"Could not load {file}. {e}")
|
||||||
print_exc()
|
print_exc()
|
||||||
@@ -168,8 +168,8 @@ class Loader:
|
|||||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||||
template_data = template.read()
|
template_data = template.read()
|
||||||
ret = f"""
|
ret = f"""
|
||||||
<script src="/static/legacy-library.js"></script>
|
<script src="/legacy/library.js"></script>
|
||||||
<script>const plugin_name = '{plugin.name}' </script>
|
<script>window.plugin_name = '{plugin.name}' </script>
|
||||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
|
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
|
||||||
{template_data}
|
{template_data}
|
||||||
"""
|
"""
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,7 @@ CONFIG = {
|
|||||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
|
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")]
|
||||||
"store_url": getenv("STORE_URL", "https://beta.deckbrew.xyz")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
||||||
@@ -44,7 +43,7 @@ class PluginManager:
|
|||||||
allow_headers="*")
|
allow_headers="*")
|
||||||
})
|
})
|
||||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"])
|
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app)
|
||||||
self.utilities = Utilities(self)
|
self.utilities = Utilities(self)
|
||||||
|
|
||||||
jinja_setup(self.web_app)
|
jinja_setup(self.web_app)
|
||||||
@@ -57,6 +56,7 @@ class PluginManager:
|
|||||||
for route in list(self.web_app.router.routes()):
|
for route in list(self.web_app.router.routes()):
|
||||||
self.cors.add(route)
|
self.cors.add(route)
|
||||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||||
|
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||||
|
|
||||||
def exception_handler(self, loop, context):
|
def exception_handler(self, loop, context):
|
||||||
if context["message"] == "Unclosed connection":
|
if context["message"] == "Unclosed connection":
|
||||||
|
|||||||
@@ -10,16 +10,26 @@ import {
|
|||||||
} from 'decky-frontend-lib';
|
} from 'decky-frontend-lib';
|
||||||
import { FC, useRef, useState } from 'react';
|
import { FC, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { StorePlugin, StorePluginVersion, requestPluginInstall } from './Store';
|
import {
|
||||||
|
LegacyStorePlugin,
|
||||||
|
StorePlugin,
|
||||||
|
StorePluginVersion,
|
||||||
|
requestLegacyPluginInstall,
|
||||||
|
requestPluginInstall,
|
||||||
|
} from './Store';
|
||||||
|
|
||||||
interface PluginCardProps {
|
interface PluginCardProps {
|
||||||
plugin: StorePlugin;
|
plugin: StorePlugin | LegacyStorePlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classNames = (...classes: string[]) => {
|
const classNames = (...classes: string[]) => {
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
|
||||||
|
return 'artifact' in plugin;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -36,10 +46,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
<Focusable
|
<Focusable
|
||||||
// className="Panel Focusable"
|
// className="Panel Focusable"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onActivate={(e: CustomEvent) => {
|
onActivate={(_: CustomEvent) => {
|
||||||
buttonRef.current!.focus();
|
buttonRef.current!.focus();
|
||||||
}}
|
}}
|
||||||
onCancel={(e: CustomEvent) => {
|
onCancel={(_: CustomEvent) => {
|
||||||
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
||||||
Router.NavigateBackOrOpenMenu();
|
Router.NavigateBackOrOpenMenu();
|
||||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||||
@@ -64,7 +74,14 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
className={classNames(staticClasses.Text)}
|
className={classNames(staticClasses.Text)}
|
||||||
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
||||||
>
|
>
|
||||||
{plugin.name}
|
{isLegacyPlugin(plugin) ? (
|
||||||
|
<div>
|
||||||
|
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
|
||||||
|
{plugin.artifact.split('/')[1]}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
plugin.name
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -79,10 +96,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: '160px',
|
height: '160px',
|
||||||
}}
|
}}
|
||||||
src={`https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
|
src={
|
||||||
'/',
|
isLegacyPlugin(plugin)
|
||||||
'_',
|
? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace(
|
||||||
)}.png`}
|
'/',
|
||||||
|
'_',
|
||||||
|
)}.png`
|
||||||
|
: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
|
||||||
|
'/',
|
||||||
|
'_',
|
||||||
|
)}.png`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -107,6 +131,18 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
{tag == 'root' ? 'Requires root' : tag}
|
{tag == 'root' ? 'Requires root' : tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
{isLegacyPlugin(plugin) && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: '5px',
|
||||||
|
marginRight: '10px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
background: '#ACB2C947',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
legacy
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +168,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
>
|
>
|
||||||
<DialogButton
|
<DialogButton
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={() => requestPluginInstall(plugin, plugin.versions[selectedOption])}
|
onClick={() =>
|
||||||
|
isLegacyPlugin(plugin)
|
||||||
|
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
|
||||||
|
: requestPluginInstall(plugin, plugin.versions[selectedOption])
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Install
|
Install
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
@@ -144,10 +184,15 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
rgOptions={
|
rgOptions={
|
||||||
plugin.versions.map((version: StorePluginVersion, index) => ({
|
(isLegacyPlugin(plugin)
|
||||||
data: index,
|
? Object.keys(plugin.versions).map((v, k) => ({
|
||||||
label: version.name,
|
data: k,
|
||||||
})) as SingleDropdownOption[]
|
label: v,
|
||||||
|
}))
|
||||||
|
: plugin.versions.map((version: StorePluginVersion, index) => ({
|
||||||
|
data: index,
|
||||||
|
label: version.name,
|
||||||
|
}))) as SingleDropdownOption[]
|
||||||
}
|
}
|
||||||
strDefaultLabel={'Select a version'}
|
strDefaultLabel={'Select a version'}
|
||||||
selectedOption={selectedOption}
|
selectedOption={selectedOption}
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export interface StorePlugin {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LegacyStorePlugin {
|
||||||
|
artifact: string;
|
||||||
|
versions: {
|
||||||
|
[version: string]: string;
|
||||||
|
};
|
||||||
|
author: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export async function installFromURL(url: string) {
|
export async function installFromURL(url: string) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const splitURL = url.split('/');
|
const splitURL = url.split('/');
|
||||||
@@ -28,6 +38,18 @@ export async function installFromURL(url: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', plugin.artifact);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
|
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', plugin.name);
|
formData.append('name', plugin.name);
|
||||||
@@ -42,6 +64,7 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto
|
|||||||
|
|
||||||
const StorePage: FC<{}> = () => {
|
const StorePage: FC<{}> = () => {
|
||||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||||
|
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -49,6 +72,11 @@ const StorePage: FC<{}> = () => {
|
|||||||
console.log(res);
|
console.log(res);
|
||||||
setData(res);
|
setData(res);
|
||||||
})();
|
})();
|
||||||
|
(async () => {
|
||||||
|
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
|
||||||
|
console.log(res);
|
||||||
|
setLegacyData(res);
|
||||||
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,12 +95,21 @@ const StorePage: FC<{}> = () => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data === null ? (
|
{!data ? (
|
||||||
<div style={{ height: '100%' }}>
|
<div style={{ height: '100%' }}>
|
||||||
<SteamSpinner />
|
<SteamSpinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />)
|
<div>
|
||||||
|
{data.map((plugin: StorePlugin) => (
|
||||||
|
<PluginCard plugin={plugin} />
|
||||||
|
))}
|
||||||
|
{!legacyData ? (
|
||||||
|
<SteamSpinner />
|
||||||
|
) : (
|
||||||
|
legacyData.map((plugin: LegacyStorePlugin) => <PluginCard plugin={plugin} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user