mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59038f65ac | |||
| 5960c11d60 | |||
| 8d065eab1f | |||
| 3b1b6d28d6 | |||
| 0a735886c9 | |||
| c9430f5be4 | |||
| a4e2237fc0 | |||
| 85d0398e62 | |||
| 30a538e85e | |||
| 84a19203c5 | |||
| 99cda2907d | |||
| a38582d158 | |||
| 9556994e14 | |||
| dee2cfa47b | |||
| 463403be23 | |||
| b68eaca55d | |||
| 114c54c9b0 | |||
| 47e0661773 | |||
| 6c48dfe7f6 | |||
| ed0ae7c9e2 |
+12
-14
@@ -117,29 +117,27 @@ jobs:
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.old_tag.outputs.tag }}
|
||||
export SEMVER=$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)
|
||||
echo "VERS: $VERSION"
|
||||
echo "TO SEMVER: $SEMVER"
|
||||
OUT=$(semver bump prerel "$SEMVER")-pre
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
echo "OUT: $OUT"
|
||||
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-pre
|
||||
echo ::set-output name=tag_name::v$OUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Nightly ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
# - name: Release 📦
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
# with:
|
||||
# name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
# tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
# files: ./dist/PluginLoader
|
||||
# prerelease: true
|
||||
# generate_release_notes: true
|
||||
- name: Release 📦
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
with:
|
||||
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
|
||||
# nightly:
|
||||
# name: Release the nightly version of the package
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import certifi
|
||||
import ssl
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from aiohttp.web import middleware, Response
|
||||
from subprocess import check_output
|
||||
@@ -12,6 +13,8 @@ ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
user = None
|
||||
group = None
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
|
||||
@@ -20,7 +23,7 @@ def get_csrf_token():
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/"):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
|
||||
+5
-2
@@ -5,6 +5,7 @@ from logging import debug, getLogger
|
||||
from traceback import format_exc
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
@@ -65,11 +66,13 @@ async def get_tabs():
|
||||
while True:
|
||||
try:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json")
|
||||
break
|
||||
except:
|
||||
except ClientConnectorError:
|
||||
logger.debug("ClientConnectorError excepted.")
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
logger.debug(format_exc())
|
||||
await sleep(5)
|
||||
else:
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
|
||||
+2
-1
@@ -62,7 +62,6 @@ class PluginManager:
|
||||
self.updater = Updater(self)
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
self.web_app.on_startup.append(self.inject_javascript)
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
self.web_app.on_startup.append(chown_plugin_dir)
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
@@ -98,6 +97,8 @@ class PluginManager:
|
||||
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
await sleep(2)
|
||||
await self.inject_javascript()
|
||||
while True:
|
||||
await sleep(5)
|
||||
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ class Updater:
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_tab("SP")
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^1.5.1",
|
||||
"decky-frontend-lib": "^1.6.2",
|
||||
"react-icons": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -9,7 +9,7 @@ specifiers:
|
||||
'@types/react': 16.14.0
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^1.5.1
|
||||
decky-frontend-lib: ^1.6.2
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -23,7 +23,7 @@ specifiers:
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 1.5.1
|
||||
decky-frontend-lib: 1.6.2
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
|
||||
devDependencies:
|
||||
@@ -806,8 +806,8 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decky-frontend-lib/1.5.1:
|
||||
resolution: {integrity: sha512-XrcMNxqdXJFyuJYJX4Wmo7DvFVkwBsl8aWU5wfLdPQmcPz3drafyEgqIdO4AxpFuTsHTf1qaHBiYYw349vYpgw==}
|
||||
/decky-frontend-lib/1.6.2:
|
||||
resolution: {integrity: sha512-O34rHg6BWYP99jxlosAl0hUfo+SmOyjN+TtcyutdIjvPkg/FgHwahTd+SMujTpZemV31c4JX8hdGnN/Z7tjL9g==}
|
||||
dependencies:
|
||||
minimist: 1.2.6
|
||||
dev: false
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ButtonItem, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
|
||||
import {
|
||||
ButtonItem,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
joinClassNames,
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { VFC } from 'react';
|
||||
|
||||
import { useDeckyState } from './DeckyState';
|
||||
@@ -7,24 +14,33 @@ const PluginView: VFC = () => {
|
||||
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
|
||||
|
||||
if (activePlugin) {
|
||||
return <div style={{ height: '100%' }}>{activePlugin.content}</div>;
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{activePlugin.content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
{plugins
|
||||
.filter((p) => p.content)
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>{icon}</div>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
</PanelSection>
|
||||
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
|
||||
<PanelSection>
|
||||
{plugins
|
||||
.filter((p) => p.content)
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>{icon}</div>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useDeckyState } from './DeckyState';
|
||||
const titleStyles: CSSProperties = {
|
||||
display: 'flex',
|
||||
paddingTop: '3px',
|
||||
paddingBottom: '14px',
|
||||
paddingRight: '16px',
|
||||
boxShadow: 'unset',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DialogButton, staticClasses } from 'decky-frontend-lib';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { DialogButton, Menu, MenuItem, showContextMenu, staticClasses } from 'decky-frontend-lib';
|
||||
import { FaEllipsisH } from 'react-icons/fa';
|
||||
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
|
||||
@@ -22,9 +22,17 @@ export default function PluginList() {
|
||||
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
|
||||
onClick={() => window.DeckyPluginLoader.uninstallPlugin(name)}
|
||||
onClick={(e: MouseEvent) =>
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name)}>Reload</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaTrash />
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Router,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
joinClassNames,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useRef, useState } from 'react';
|
||||
@@ -22,10 +23,6 @@ interface PluginCardProps {
|
||||
plugin: StorePlugin | LegacyStorePlugin;
|
||||
}
|
||||
|
||||
const classNames = (...classes: string[]) => {
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
|
||||
return 'artifact' in plugin;
|
||||
}
|
||||
@@ -44,7 +41,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
>
|
||||
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
|
||||
<Focusable
|
||||
// className="Panel Focusable"
|
||||
className="deckyStoreCard"
|
||||
ref={containerRef}
|
||||
onActivate={(_: CustomEvent) => {
|
||||
buttonRef.current!.focus();
|
||||
@@ -68,15 +65,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<a
|
||||
style={{ fontSize: '18pt', padding: '10px' }}
|
||||
className={classNames(staticClasses.Text)}
|
||||
className={joinClassNames(staticClasses.Text)}
|
||||
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
||||
>
|
||||
{isLegacyPlugin(plugin) ? (
|
||||
<div>
|
||||
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
|
||||
<div className="deckyStoreCardNameContainer">
|
||||
<span className="deckyStoreCardLegacyRepoOwner" style={{ color: 'grey' }}>
|
||||
{plugin.artifact.split('/')[0]}/
|
||||
</span>
|
||||
{plugin.artifact.split('/')[1]}
|
||||
</div>
|
||||
) : (
|
||||
@@ -89,8 +88,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
className="deckyStoreCardBody"
|
||||
>
|
||||
<SuspensefulImage
|
||||
className="deckyStoreCardImage"
|
||||
suspenseWidth="256px"
|
||||
style={{
|
||||
width: 'auto',
|
||||
@@ -113,14 +114,16 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
className="deckyStoreCardInfo"
|
||||
>
|
||||
<p className={classNames(staticClasses.PanelSectionRow)}>
|
||||
<p className={joinClassNames(staticClasses.PanelSectionRow)}>
|
||||
<span>Author: {plugin.author}</span>
|
||||
</p>
|
||||
<p className={classNames(staticClasses.PanelSectionRow)}>
|
||||
<p className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}>
|
||||
<span>Tags:</span>
|
||||
{plugin.tags.map((tag: string) => (
|
||||
<span
|
||||
className="deckyStoreCardTag"
|
||||
style={{
|
||||
padding: '5px',
|
||||
marginRight: '10px',
|
||||
@@ -133,6 +136,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
))}
|
||||
{isLegacyPlugin(plugin) && (
|
||||
<span
|
||||
className="deckyStoreCardTag deckyStoreCardLegacyTag"
|
||||
style={{
|
||||
color: '#232120',
|
||||
padding: '5px',
|
||||
@@ -148,6 +152,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardActionsContainer"
|
||||
style={{
|
||||
width: '100%',
|
||||
alignSelf: 'flex-end',
|
||||
@@ -156,6 +161,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
>
|
||||
<Focusable
|
||||
className="deckyStoreCardActions"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@@ -163,11 +169,13 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="deckyStoreCardInstallButtonContainer"
|
||||
style={{
|
||||
flex: '1',
|
||||
}}
|
||||
>
|
||||
<DialogButton
|
||||
className="deckyStoreCardInstallButton"
|
||||
ref={buttonRef}
|
||||
onClick={() =>
|
||||
isLegacyPlugin(plugin)
|
||||
@@ -179,6 +187,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
</DialogButton>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardVersionDropdownContainer"
|
||||
style={{
|
||||
flex: '0.2',
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sleep } from 'decky-frontend-lib';
|
||||
|
||||
import PluginLoader from './plugin-loader';
|
||||
import { DeckyUpdater } from './updater';
|
||||
|
||||
@@ -14,7 +12,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
(async () => {
|
||||
await sleep(1000);
|
||||
window.deckyHasLoaded = true;
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
@@ -38,6 +36,4 @@ declare global {
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
|
||||
window.deckyHasLoaded = true;
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
|
||||
import { ReactElement, createElement, memo } from 'react';
|
||||
import type { Route, RouteProps } from 'react-router';
|
||||
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
DeckyRouterState,
|
||||
@@ -40,7 +40,7 @@ class RouterHook extends Logger {
|
||||
|
||||
let Route: new () => Route;
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
let toReplace = new Map<string, Route>();
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
|
||||
@@ -62,17 +62,24 @@ class RouterHook extends Logger {
|
||||
routeList.forEach((route: Route, index: number) => {
|
||||
const replaced = toReplace.get(route?.props?.path as string);
|
||||
if (replaced) {
|
||||
routeList[index] = replaced;
|
||||
routeList[index].props.children = replaced;
|
||||
toReplace.delete(route?.props?.path as string);
|
||||
}
|
||||
if (route?.props?.path && routePatches.has(route.props.path as string)) {
|
||||
toReplace.set(
|
||||
route?.props?.path as string,
|
||||
// @ts-ignore
|
||||
createElement(routeList[index].type, routeList[index].props, routeList[index].props.children),
|
||||
routeList[index].props.children,
|
||||
);
|
||||
routePatches.get(route.props.path as string)?.forEach((patch) => {
|
||||
routeList[index].props = patch(routeList[index].props);
|
||||
const oType = routeList[index].props.children.type;
|
||||
routeList[index].props.children = patch({
|
||||
...routeList[index].props,
|
||||
children: {
|
||||
...cloneElement(routeList[index].props.children),
|
||||
type: (props) => createElement(oType, props),
|
||||
},
|
||||
}).children;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,7 +103,7 @@ class TabsHook extends Logger {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
unpatch(this.cNode.stateNode, 'render');
|
||||
if (this.cNode) unpatch(this.cNode.stateNode, 'render');
|
||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
||||
|
||||
@@ -35,7 +35,7 @@ class Toaster extends Logger {
|
||||
while (true) {
|
||||
instance = findInReactTree(
|
||||
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||
(x) => x?.memoizedProps?.className?.startsWith('toastmanager_ToastPlaceholder'),
|
||||
(x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
|
||||
);
|
||||
if (instance) break;
|
||||
this.debug('finding instance');
|
||||
@@ -84,9 +84,9 @@ class Toaster extends Logger {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
unpatch(this.instanceRet, 'type');
|
||||
delete this.node.stateNode.render;
|
||||
this.node.stateNode.forceUpdate();
|
||||
this.instanceRet && unpatch(this.instanceRet, 'type');
|
||||
this.node && delete this.node.stateNode.render;
|
||||
this.node && this.node.stateNode.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user