mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
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:
Generated
-3881
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1724
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
@@ -9,6 +9,7 @@ declare global {
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
window.DeckyPluginLoader?.deinit();
|
||||
|
||||
window.DeckyPluginLoader = new PluginLoader();
|
||||
window.importDeckyPlugin = function (name: string) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user