diff options
| author | AAGaming <aagaming00@protonmail.com> | 2022-06-17 18:43:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-17 18:43:53 -0400 |
| commit | 99b4b939bdd2140aecf19ddb09a59b44e9cd117d (patch) | |
| tree | d1a4c154101cb43b34c782a310e9c0699c9cf005 /frontend/src | |
| parent | a95bf94d878f61869895bb22cbff1b4f524c5dca (diff) | |
| download | decky-loader-99b4b939bdd2140aecf19ddb09a59b44e9cd117d.tar.gz decky-loader-99b4b939bdd2140aecf19ddb09a59b44e9cd117d.zip | |
Implement React-based plugin store (#81)
Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
Co-authored-by: WerWolv <werwolv98@gmail.com>
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/PluginView.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/store/PluginCard.tsx | 172 | ||||
| -rw-r--r-- | frontend/src/components/store/Store.tsx | 55 | ||||
| -rw-r--r-- | frontend/src/index.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/router-hook.tsx | 4 |
6 files changed, 240 insertions, 1 deletions
diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx index 78bb22c2..92650fec 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -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) { diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx new file mode 100644 index 00000000..7816d1bb --- /dev/null +++ b/frontend/src/components/store/PluginCard.tsx @@ -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; diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx new file mode 100644 index 00000000..ebb2bb8e --- /dev/null +++ b/frontend/src/components/store/Store.tsx @@ -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; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 89194777..5cf2ed14 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -9,6 +9,7 @@ declare global { } window.DeckyPluginLoader?.dismountAll(); +window.DeckyPluginLoader?.deinit(); window.DeckyPluginLoader = new PluginLoader(); window.importDeckyPlugin = function (name: string) { diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 73f65415..eb3344a3 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -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) { diff --git a/frontend/src/router-hook.tsx b/frontend/src/router-hook.tsx index 83d23bf1..ce0c553f 100644 --- a/frontend/src/router-hook.tsx +++ b/frontend/src/router-hook.tsx @@ -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'); |
