mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Implement legacy & modern plugin method calls over WS
This version builds fine and runs all of the 14 plugins I have installed perfectly, so we're really close to having this done.
This commit is contained in:
@@ -35,6 +35,8 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
|
||||
|
||||
type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
|
||||
|
||||
const reloadPluginBackend = window.DeckyBackend.callable<[pluginName: string], void>('loader/reload_plugin');
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -49,15 +51,9 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
showContextMenu(
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
<MenuItem
|
||||
onSelected={() => {
|
||||
onSelected={async () => {
|
||||
try {
|
||||
fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
await reloadPluginBackend(name);
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@ export const debug = (name: string, ...args: any[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const warn = (name: string, ...args: any[]) => {
|
||||
console.warn(
|
||||
`%c Decky %c ${name} %c`,
|
||||
'background: #16a085; color: black;',
|
||||
'background: #ffbb00; color: black;',
|
||||
'color: blue;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const error = (name: string, ...args: any[]) => {
|
||||
console.error(
|
||||
`%c Decky %c ${name} %c`,
|
||||
@@ -41,6 +51,10 @@ class Logger {
|
||||
debug(this.name, ...args);
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
warn(this.name, ...args);
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
error(this.name, ...args);
|
||||
}
|
||||
|
||||
+100
-50
@@ -5,6 +5,7 @@ import {
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
findSP,
|
||||
quickAccessMenuClasses,
|
||||
showModal,
|
||||
sleep,
|
||||
@@ -60,7 +61,6 @@ class PluginLoader extends Logger {
|
||||
constructor() {
|
||||
super(PluginLoader.name);
|
||||
this.tabsHook.init();
|
||||
this.log('Initialized');
|
||||
|
||||
const TabBadge = () => {
|
||||
const { updates, hasLoaderUpdate } = useDeckyState();
|
||||
@@ -102,9 +102,32 @@ class PluginLoader extends Logger {
|
||||
|
||||
initFilepickerPatches();
|
||||
|
||||
this.getUserInfo();
|
||||
Promise.all([this.getUserInfo(), this.updateVersion()])
|
||||
.then(() => this.loadPlugins())
|
||||
.then(() => this.checkPluginUpdates())
|
||||
.then(() => this.log('Initialized'));
|
||||
}
|
||||
|
||||
this.updateVersion();
|
||||
private getPluginsFromBackend = window.DeckyBackend.callable<[], { name: string; version: string }[]>(
|
||||
'loader/get_plugins',
|
||||
);
|
||||
|
||||
private async loadPlugins() {
|
||||
// wait for SP window to exist before loading plugins
|
||||
while (!findSP()) {
|
||||
await sleep(100);
|
||||
}
|
||||
const plugins = await this.getPluginsFromBackend();
|
||||
const pluginLoadPromises = [];
|
||||
const loadStart = performance.now();
|
||||
for (const plugin of plugins) {
|
||||
if (!this.hasPlugin(plugin.name)) pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, false));
|
||||
}
|
||||
await Promise.all(pluginLoadPromises);
|
||||
const loadEnd = performance.now();
|
||||
this.log(`Loaded ${plugins.length} plugins in ${loadEnd - loadStart}ms`);
|
||||
|
||||
this.checkPluginUpdates();
|
||||
}
|
||||
|
||||
public async getUserInfo() {
|
||||
@@ -217,9 +240,9 @@ class PluginLoader extends Logger {
|
||||
if (val) import('./developer').then((developer) => developer.startup());
|
||||
});
|
||||
|
||||
//* Grab and set plugin order
|
||||
// Grab and set plugin order
|
||||
getSetting<string[]>('pluginOrder', []).then((pluginOrder) => {
|
||||
console.log(pluginOrder);
|
||||
this.debug('pluginOrder: ', pluginOrder);
|
||||
this.deckyState.setPluginOrder(pluginOrder);
|
||||
});
|
||||
|
||||
@@ -236,15 +259,14 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
console.log('Plugin List: ', this.plugins);
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name);
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
}
|
||||
|
||||
public async importPlugin(name: string, version?: string | undefined) {
|
||||
if (this.reloadLock) {
|
||||
public async importPlugin(name: string, version?: string | undefined, useQueue: boolean = true) {
|
||||
if (useQueue && this.reloadLock) {
|
||||
this.log('Reload currently in progress, adding to queue', name);
|
||||
this.pluginReloadQueue.push({ name, version: version });
|
||||
return;
|
||||
@@ -255,17 +277,21 @@ class PluginLoader extends Logger {
|
||||
this.log(`Trying to load ${name}`);
|
||||
|
||||
this.unloadPlugin(name);
|
||||
const startTime = performance.now();
|
||||
await this.importReactPlugin(name, version);
|
||||
const endTime = performance.now();
|
||||
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
this.log(`Loaded ${name}`);
|
||||
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.reloadLock = false;
|
||||
const nextPlugin = this.pluginReloadQueue.shift();
|
||||
if (nextPlugin) {
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version);
|
||||
if (useQueue) {
|
||||
this.reloadLock = false;
|
||||
const nextPlugin = this.pluginReloadQueue.shift();
|
||||
if (nextPlugin) {
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,17 +363,14 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
async callServerMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
this.warn(
|
||||
`Calling ${methodName} via callServerMethod, which is deprecated and will be removed in a future release. Please switch to the backend API.`,
|
||||
);
|
||||
return await window.DeckyBackend.call<[methodName: string, kwargs: any], any>(
|
||||
'utilities/_call_legacy_utility',
|
||||
methodName,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
openFilePicker(
|
||||
@@ -355,7 +378,7 @@ class PluginLoader extends Logger {
|
||||
selectFiles?: boolean,
|
||||
regex?: RegExp,
|
||||
): Promise<{ path: string; realpath: string }> {
|
||||
console.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
|
||||
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
|
||||
if (selectFiles) {
|
||||
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
|
||||
} else {
|
||||
@@ -405,45 +428,72 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
createPluginAPI(pluginName: string) {
|
||||
return {
|
||||
const pluginAPI = {
|
||||
backend: {
|
||||
call<Args extends any[] = any[], Return = void>(method: string, ...args: Args): Promise<Return> {
|
||||
return window.DeckyBackend.call<[pluginName: string, method: string, ...args: Args], Return>(
|
||||
'loader/call_plugin_method',
|
||||
pluginName,
|
||||
method,
|
||||
...args,
|
||||
);
|
||||
},
|
||||
callable<Args extends any[] = any[], Return = void>(method: string): (...args: Args) => Promise<Return> {
|
||||
return (...args) => pluginAPI.backend.call<Args, Return>(method, ...args);
|
||||
},
|
||||
},
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
// Legacy
|
||||
callServerMethod: this.callServerMethod,
|
||||
openFilePicker: this.openFilePicker,
|
||||
openFilePickerV2: this.openFilePickerV2,
|
||||
// Legacy
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
return window.DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
|
||||
'loader/call_legacy_plugin_method',
|
||||
pluginName,
|
||||
methodName,
|
||||
args,
|
||||
);
|
||||
},
|
||||
fetchNoCors(url: string, request: any = {}) {
|
||||
let args = { method: 'POST', headers: {} };
|
||||
const req = { ...args, ...request, url, data: request.body };
|
||||
/* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
|
||||
frontend --request URL only--> backend (ws method)
|
||||
backend --new temporary backend URL--> frontend (ws response)
|
||||
frontend <--> backend <--> target URL (over http!)
|
||||
*/
|
||||
async fetchNoCors(url: string, request: any = {}) {
|
||||
let method: string;
|
||||
const req = { headers: {}, ...request, data: request.body };
|
||||
req?.body && delete req.body;
|
||||
return this.callServerMethod('http_request', req);
|
||||
},
|
||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||
return this.callServerMethod('execute_in_tab', {
|
||||
tab,
|
||||
run_async: runAsync,
|
||||
code,
|
||||
});
|
||||
if (!request.method) {
|
||||
method = 'POST';
|
||||
} else {
|
||||
method = request.method;
|
||||
delete req.method;
|
||||
}
|
||||
// this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
|
||||
try {
|
||||
const ret = await window.DeckyBackend.call<
|
||||
[method: string, url: string, extra_opts?: any],
|
||||
{ status: number; headers: { [key: string]: string }; body: string }
|
||||
>('utilities/http_request', method, url, req);
|
||||
return { success: true, result: ret };
|
||||
} catch (e) {
|
||||
return { success: false, result: e?.toString() };
|
||||
}
|
||||
},
|
||||
executeInTab: window.DeckyBackend.callable<
|
||||
[tab: String, runAsync: Boolean, code: string],
|
||||
{ success: boolean; result: any }
|
||||
>('utilities/execute_in_tab'),
|
||||
injectCssIntoTab: window.DeckyBackend.callable<[tab: string, style: string], string>(
|
||||
'utilities/inject_css_into_tab',
|
||||
),
|
||||
removeCssFromTab: window.DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
|
||||
};
|
||||
|
||||
return pluginAPI;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ declare global {
|
||||
DeckyPluginLoader: PluginLoader;
|
||||
DeckyUpdater?: DeckyUpdater;
|
||||
importDeckyPlugin: Function;
|
||||
syncDeckyPlugins: Function;
|
||||
deckyHasLoaded: boolean;
|
||||
deckyHasConnectedRDT?: boolean;
|
||||
deckyAuthToken: string;
|
||||
@@ -53,23 +52,6 @@ declare global {
|
||||
window.importDeckyPlugin = function (name: string, version: string) {
|
||||
window.DeckyPluginLoader?.importPlugin(name, version);
|
||||
};
|
||||
|
||||
window.syncDeckyPlugins = async function () {
|
||||
const plugins = await (
|
||||
await fetch('http://127.0.0.1:1337/plugins', {
|
||||
credentials: 'include',
|
||||
headers: { Authentication: window.deckyAuthToken },
|
||||
})
|
||||
).json();
|
||||
for (const plugin of plugins) {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
|
||||
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
})();
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from './logger';
|
||||
|
||||
declare global {
|
||||
@@ -161,6 +163,8 @@ export class WSRouter extends Logger {
|
||||
|
||||
async onError(error: any) {
|
||||
this.error('WS DISCONNECTED', error);
|
||||
// TODO queue up lost messages and send them once we connect again
|
||||
await sleep(5000);
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user