Work on react frontend loader

This commit is contained in:
Jonas Dellinger
2022-05-13 19:14:47 +02:00
parent 945db5de47
commit 74438a3145
25 changed files with 2921 additions and 608 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd frontend && npm run lint
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
endOfLine: 'auto',
plugins: [require('prettier-plugin-import-sort')],
};
+2323
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "decky_frontend",
"version": "0.0.1",
"private": true,
"license": "GPLV2",
"scripts": {
"prepare": "cd .. && husky install frontend/.husky",
"build": "rollup -c",
"lint": "prettier -c src",
"format": "prettier -c src -w"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.3.2",
"husky": "^8.0.1",
"import-sort-style-module": "^6.0.0",
"prettier": "^2.6.2",
"prettier-plugin-import-sort": "^0.0.7",
"rollup": "^2.71.1"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
"style": "module"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
/** @type {import('rollup').RollupOptions} */
const options = {
input: 'src/index.ts',
output: {
file: '../backend/static/plugin-loader.iife.js',
format: 'iife',
},
plugins: [commonjs(), resolve(), typescript()]
}
export default options
+16
View File
@@ -0,0 +1,16 @@
import PluginLoader from './plugin-loader';
declare global {
interface Window {
DeckyPluginLoader?: PluginLoader;
}
}
if (window.DeckyPluginLoader) {
window.DeckyPluginLoader?.dismountAll();
}
window.DeckyPluginLoader = new PluginLoader();
setTimeout(async () => {
window.DeckyPluginLoader?.loadAllPlugins();
}, 5000);
+35
View File
@@ -0,0 +1,35 @@
export const log = (name: string, ...args: any[]) => {
console.log(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #1abc9c; color: black;',
'background: transparent;',
...args,
);
};
export const error = (name: string, ...args: any[]) => {
console.log(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #FF0000;',
'background: transparent;',
...args,
);
};
class Logger {
constructor(private name: string) {
this.name = name;
}
log(...args: any[]) {
log(this.name, ...args);
}
debug(...args: any[]) {
log(this.name, ...args);
}
}
export default Logger;
+131
View File
@@ -0,0 +1,131 @@
import Logger from './logger';
import TabsHook from './tabs-hook';
interface Plugin {
title: any;
content: any;
icon: any;
onDismount?(): void;
}
class PluginLoader extends Logger {
private pluginInstances: Record<string, Plugin> = {};
private tabsHook: TabsHook;
private lock = 0;
constructor() {
super(PluginLoader.name);
this.log('Initialized');
this.tabsHook = new TabsHook();
}
dismountPlugin(name: string) {
this.log(`Dismounting ${name}`);
this.pluginInstances[name]?.onDismount?.();
delete this.pluginInstances[name];
this.tabsHook.removeById(name);
}
async loadAllPlugins() {
this.log('Loading all plugins');
const plugins = await (await fetch(`http://127.0.0.1:1337/plugins`)).json();
this.log('Received:', plugins);
return Promise.all(plugins.map((plugin) => this.loadPlugin(plugin.name)));
}
async loadPlugin(name) {
this.log('Loading Plugin:', name);
try {
while (this.lock === 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
this.lock = 1;
if (this.pluginInstances[name]) {
this.dismountPlugin(name);
}
const response = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
const code = await response.text();
const pluginAPI = PluginLoader.createPluginAPI(name);
this.pluginInstances[name] = await eval(code)(pluginAPI);
const { title, icon, content } = this.pluginInstances[name];
this.tabsHook.add({
id: name,
title,
icon,
content,
});
} catch (e) {
console.error(e);
} finally {
this.lock = 0;
}
}
dismountAll() {
for (const name of Object.keys(this.pluginInstances)) {
this.dismountPlugin(name);
}
}
static createPluginAPI(pluginName) {
return {
async callServerMethod(methodName, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
return response.json();
},
async callPluginMethod(methodName, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
args,
}),
});
return response.json();
},
fetchNoCors(url, request: any = {}) {
let args = { method: 'POST', headers: {}, body: '' };
const req = { ...args, ...request, url, data: request.body };
return this.callServerMethod('http_request', req);
},
executeInTab(tab, runAsync, code) {
return this.callServerMethod('execute_in_tab', {
tab,
run_async: runAsync,
code,
});
},
injectCssIntoTab(tab, style) {
return this.callServerMethod('inject_css_into_tab', {
tab,
style,
});
},
removeCssFromTab(tab, cssId) {
return this.callServerMethod('remove_css_from_tab', {
tab,
css_id: cssId,
});
},
};
}
}
export default PluginLoader;
+69
View File
@@ -0,0 +1,69 @@
import Logger from './logger';
declare global {
interface Window {
__TABS_HOOK_INSTANCE: any;
}
interface Array<T> {
__filter: any;
}
}
const isTabsArray = (tabs) => {
const length = tabs.length;
return length === 7 && tabs[length - 1]?.key === 6 && tabs[length - 1]?.tab;
};
interface Tab {
id: string;
title: any;
content: any;
icon: any;
}
class TabsHook extends Logger {
// private keys = 7;
tabs: Tab[] = [];
constructor() {
super('TabsHook');
this.log('Initialized');
window.__TABS_HOOK_INSTANCE = this;
const self = this;
const filter = Array.prototype.__filter ?? Array.prototype.filter;
Array.prototype.__filter = filter;
Array.prototype.filter = function (...args) {
if (isTabsArray(this)) {
self.render(this);
}
// @ts-ignore
return filter.call(this, ...args);
};
}
add(tab: Tab) {
this.log('Adding tab', tab.id, 'to render array');
this.tabs.push(tab);
}
removeById(id: string) {
this.log('Removing tab', id);
this.tabs = this.tabs.filter((tab) => tab.id !== id);
}
render(existingTabs: any[]) {
for (const { title, icon, content, id } of this.tabs) {
existingTabs.push({
key: id,
title,
tab: icon,
panel: content,
});
}
}
}
export default TabsHook;