Implement React-based plugin store (#81)

Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
Co-authored-by: WerWolv <werwolv98@gmail.com>
This commit is contained in:
AAGaming
2022-06-17 18:43:53 -04:00
committed by GitHub
parent a95bf94d87
commit 99b4b939bd
15 changed files with 2178 additions and 3915 deletions
-3881
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -13,7 +13,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.2.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.3.2",
"@types/react": "16.14.0",
@@ -21,13 +21,14 @@
"@types/webpack": "^5.28.0",
"husky": "^8.0.1",
"import-sort-style-module": "^6.0.0",
"inquirer": "^8.2.4",
"prettier": "^2.6.2",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.70.2",
"rollup": "^2.75.6",
"tslib": "^2.4.0",
"typescript": "^4.7.2"
"typescript": "^4.7.3"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
@@ -36,7 +37,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^0.0.6",
"react-icons": "^4.3.1"
"decky-frontend-lib": "^0.10.2",
"react-icons": "^4.4.0"
}
}
+1724
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ const PluginView: VFC = () => {
const onStoreClick = () => {
Router.CloseSideMenus();
Router.NavigateToExternalWeb('http://127.0.0.1:1337/browser/redirect');
Router.Navigate('/decky/store');
};
if (activePlugin) {
@@ -0,0 +1,172 @@
import {
DialogButton,
Dropdown,
Focusable,
Router,
SingleDropdownOption,
SuspensefulImage,
staticClasses,
} from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react';
import { StorePlugin } from './Store';
interface PluginCardProps {
plugin: StorePlugin;
}
const classNames = (...classes: string[]) => {
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 [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
return (
<div
style={{
padding: '30px',
paddingTop: '10px',
paddingBottom: '10px',
}}
>
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
<Focusable
// className="Panel Focusable"
ref={containerRef}
onActivate={(e: CustomEvent) => {
buttonRef.current!.focus();
}}
onCancel={(e: CustomEvent) => {
containerRef.current!.querySelectorAll('* :focus').length === 0
? Router.NavigateBackOrOpenMenu()
: containerRef.current!.focus();
}}
style={{
display: 'flex',
flexDirection: 'column',
background: '#ACB2C924',
height: 'unset',
marginBottom: 'unset',
// boxShadow: var(--gpShadow-Medium);
scrollSnapAlign: 'start',
boxSizing: 'border-box',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<a
style={{ fontSize: '18pt', padding: '10px' }}
className={classNames(staticClasses.Text)}
onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
{plugin.artifact.split('/')[1]}
</a>
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
}}
>
<SuspensefulImage
suspenseWidth="256px"
style={{
width: 'auto',
height: '160px',
}}
src={`https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace(
'/',
'_',
)}.png`}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<p className={classNames(staticClasses.PanelSectionRow)}>
<span>Author: {plugin.author}</span>
</p>
<p className={classNames(staticClasses.PanelSectionRow)}>
<span>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
style={{
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: tag == 'root' ? '#842029' : '#ACB2C947',
}}
>
{tag == 'root' ? 'Requires root' : tag}
</span>
))}
</p>
</div>
</div>
<div
style={{
width: '100%',
alignSelf: 'flex-end',
display: 'flex',
flexDirection: 'row',
}}
>
<Focusable
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
}}
>
<div
style={{
flex: '1',
}}
>
<DialogButton
ref={buttonRef}
onClick={() => requestPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])}
>
Install
</DialogButton>
</div>
<div
style={{
flex: '0.2',
}}
>
<Dropdown
rgOptions={
Object.keys(plugin.versions).map((v, k) => ({
data: k,
label: v,
})) as SingleDropdownOption[]
}
strDefaultLabel={'Select a version'}
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
</div>
</Focusable>
</div>
</Focusable>
</div>
);
};
export default PluginCard;
+55
View File
@@ -0,0 +1,55 @@
import { SteamSpinner } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import PluginCard from './PluginCard';
export interface StorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
const StorePage: FC<{}> = () => {
const [data, setData] = useState<StorePlugin[] | null>(null);
useEffect(() => {
(async () => {
const res = await fetch('https://beta.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
console.log(res);
setData(res);
})();
}, []);
return (
<div
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<div
style={{
display: 'flex',
flexWrap: 'nowrap',
flexDirection: 'column',
height: '100%',
}}
>
{data === null ? (
<div style={{ height: '100%' }}>
<SteamSpinner />
</div>
) : (
data.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />)
)}
</div>
</div>
);
};
export default StorePage;
+1
View File
@@ -9,6 +9,7 @@ declare global {
}
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
+7
View File
@@ -4,6 +4,7 @@ import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import PluginView from './components/PluginView';
import StorePage from './components/store/Store';
import TitleView from './components/TitleView';
import Logger from './logger';
import { Plugin } from './plugin';
@@ -43,6 +44,8 @@ class PluginLoader extends Logger {
),
icon: <FaPlug />,
});
this.routerHook.addRoute('/decky/store', () => <StorePage />);
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string) {
@@ -71,6 +74,10 @@ class PluginLoader extends Logger {
}
}
public deinit() {
this.routerHook.removeRoute('/decky/store');
}
public async importPlugin(name: string) {
try {
if (this.reloadLock) {
+4
View File
@@ -92,6 +92,10 @@ class RouterHook extends Logger {
this.routerState.addRoute(path, component, props);
}
removeRoute(path: string) {
this.routerState.removeRoute(path);
}
deinit() {
unpatch(this.gamepadWrapper, 'render');
this.router && unpatch(this.router, 'type');