mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 04:05:04 +03:00
Work on react frontend loader
This commit is contained in:
@@ -19,20 +19,31 @@ jobs:
|
||||
- name: 🧰 Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 💎 Set up NodeJS 17
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 17
|
||||
|
||||
- name: 🐍 Set up Python 3.10
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: ⬇️ Install dependencies
|
||||
- name: ⬇️ Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: ⬇️ Install NodeJS dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
- name: 🛠️ Build
|
||||
run: |
|
||||
pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./plugin_loader/static:/static --add-data ./plugin_loader/templates:/templates ./plugin_loader/*.py
|
||||
pyinstaller --noconfirm --onefile --name "Decky" --add-data ./backend/static:/static ./backend/*.py
|
||||
|
||||
- name: ⬆️ Upload package
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
@@ -150,3 +150,6 @@ dmypy.json
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# static files are built
|
||||
backend/static
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from logging import debug, getLogger
|
||||
from asyncio import sleep
|
||||
from logging import debug, getLogger
|
||||
from traceback import format_exc
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
@@ -43,13 +44,10 @@ class Tab:
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
})
|
||||
|
||||
await self.client.close()
|
||||
return res
|
||||
|
||||
async def get_steam_resource(self, url):
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
def __repr__(self):
|
||||
return self.title
|
||||
|
||||
@@ -84,12 +82,12 @@ async def inject_to_tab(tab_name, js, run_async=False):
|
||||
|
||||
return await tab.evaluate_js(js, run_async)
|
||||
|
||||
async def tab_has_element(tab_name, element_name):
|
||||
async def tab_has_global_var(tab_name, var_name):
|
||||
try:
|
||||
tab = await get_tab(tab_name)
|
||||
except ValueError:
|
||||
return False
|
||||
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null", False)
|
||||
res = await tab.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False)
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
@@ -0,0 +1,162 @@
|
||||
from asyncio import Queue
|
||||
from logging import getLogger
|
||||
from os import listdir, path
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from traceback import print_exc
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_jinja2 import template
|
||||
from genericpath import exists
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers.polling import PollingObserver as Observer
|
||||
|
||||
from injector import inject_to_tab
|
||||
from plugin import PluginWrapper
|
||||
|
||||
|
||||
class FileChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, queue, plugin_path) -> None:
|
||||
super().__init__()
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
self.logger.info(path.join(self.plugin_path, plugin_dir, "plugin.json"))
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.import_plugins()
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/plugins", self.handle_plugins),
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
|
||||
web.post("/methods/{method_name}", self.handle_server_method_call)
|
||||
])
|
||||
|
||||
def handle_plugins(self, request):
|
||||
plugins = list(map(lambda kv: dict([("name", kv[0])]), self.plugins.items()))
|
||||
return web.json_response(plugins)
|
||||
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.frontend_bundle), 'r') as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if refresh:
|
||||
self.loop.create_task(self.reload_frontend_plugin(plugin.name))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def reload_frontend_plugin(self, name):
|
||||
await inject_to_tab("SP", f"window.DeckyPluginLoader?.loadPlugin('{name}')")
|
||||
|
||||
def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
self.import_plugin(*args)
|
||||
|
||||
async def handle_server_method_call(self, request):
|
||||
method_name = request.match_info["method_name"]
|
||||
method_info = await request.json()
|
||||
args = method_info["args"]
|
||||
|
||||
res = {}
|
||||
try:
|
||||
r = await self.utilities.util_methods[method_name](**args)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
|
||||
return web.json_response(res)
|
||||
|
||||
async def handle_plugin_method_call(self, request):
|
||||
res = {}
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
method_name = request.match_info["method_name"]
|
||||
|
||||
method_info = await request.json()
|
||||
args = method_info["args"]
|
||||
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
|
||||
res["result"] = await plugin.execute_method(method_name, args)
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
|
||||
return web.json_response(res)
|
||||
@@ -0,0 +1,85 @@
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv
|
||||
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
|
||||
"store_url": getenv("STORE_URL", "https://sdh.tzatzi.me/browse")
|
||||
}
|
||||
|
||||
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
||||
|
||||
from asyncio import get_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from os import path
|
||||
from subprocess import Popen
|
||||
|
||||
import aiohttp_cors
|
||||
from aiohttp.web import Application, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
from jinja2 import FileSystemLoader
|
||||
|
||||
from browser import PluginBrowser
|
||||
from injector import get_tab, inject_to_tab, tab_has_global_var
|
||||
from loader import Loader
|
||||
from utilities import Utilities
|
||||
|
||||
logger = getLogger("Main")
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
|
||||
Popen(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self.loop = get_event_loop()
|
||||
self.web_app = Application()
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*",
|
||||
allow_headers="*")
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"])
|
||||
self.utilities = Utilities(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())
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route)
|
||||
|
||||
def exception_handler(self, loop, context):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
await sleep(1)
|
||||
if not await tab_has_global_var("SP", "DeckyPluginLoader"):
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
|
||||
async def inject_javascript(self, request=None):
|
||||
try:
|
||||
await inject_to_tab("SP", open(path.join(path.dirname(__file__), "./static/plugin-loader.iife.js"), "r").read(), True)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab")
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
PluginManager().run()
|
||||
@@ -1,11 +1,17 @@
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
from asyncio import get_event_loop, new_event_loop, set_event_loop, start_unix_server, open_unix_connection, sleep, Lock
|
||||
from os import path, setuid
|
||||
from json import loads, dumps, load
|
||||
from time import time
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
open_unix_connection, set_event_loop, sleep,
|
||||
start_unix_server)
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from multiprocessing import Process
|
||||
from signal import signal, SIGINT
|
||||
from os import path, setuid
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
from time import time
|
||||
|
||||
from injector import inject_to_tab
|
||||
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
@@ -20,8 +26,7 @@ class PluginWrapper:
|
||||
|
||||
self.name = json["name"]
|
||||
self.author = json["author"]
|
||||
self.main_view_html = json["main_view_html"]
|
||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||
self.frontend_bundle = json["frontend_bundle"]
|
||||
self.flags = json["flags"]
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && npm run lint
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
endOfLine: 'auto',
|
||||
plugins: [require('prettier-plugin-import-sort')],
|
||||
};
|
||||
Generated
+2323
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,191 +0,0 @@
|
||||
from aiohttp import web
|
||||
from aiohttp_jinja2 import template
|
||||
from watchdog.observers.polling import PollingObserver as Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from asyncio import Queue
|
||||
from os import path, listdir
|
||||
from logging import getLogger
|
||||
from time import time
|
||||
|
||||
from injector import get_tabs, get_tab
|
||||
from plugin import PluginWrapper
|
||||
from traceback import print_exc
|
||||
|
||||
class FileChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, queue, plugin_path) -> None:
|
||||
super().__init__()
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path]))
|
||||
plugin_dir = path.split(rel_path)[0]
|
||||
main_file_path = path.join(self.plugin_path, plugin_dir, "main.py")
|
||||
self.queue.put_nowait((main_file_path, plugin_dir, True))
|
||||
|
||||
def on_modified(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
plugin_dir = path.split(path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])))[0]
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.callsigns = {}
|
||||
self.callsign_matches = {}
|
||||
self.import_plugins()
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/plugins/iframe", self.plugin_iframe_route),
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
||||
web.get("/plugins/load_tile/{name}", self.load_plugin_tile_view),
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
||||
])
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
self.callsigns.pop(self.callsign_matches[file], None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
callsign = str(time())
|
||||
plugin.callsign = callsign
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.callsigns[callsign] = plugin
|
||||
self.callsign_matches[file] = callsign
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
finally:
|
||||
if refresh:
|
||||
self.loop.create_task(self.refresh_iframe())
|
||||
|
||||
def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
self.import_plugin(*args)
|
||||
|
||||
async def handle_plugin_method_call(self, callsign, method_name, **kwargs):
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
return await self.callsigns[callsign].execute_method(method_name, kwargs)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = (await get_tabs())[0]
|
||||
try:
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
except Exception as e:
|
||||
return web.Response(text=str(e), status=400)
|
||||
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.callsigns[request.match_info["name"]]
|
||||
|
||||
# open up the main template
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||
template_data = template.read()
|
||||
# setup the main script, plugin, and pull in the template
|
||||
ret = f"""
|
||||
<script src="/static/library.js"></script>
|
||||
<script>const plugin_name = '{plugin.callsign}' </script>
|
||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.callsign}/">
|
||||
{template_data}
|
||||
"""
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
async def handle_sub_route(self, request):
|
||||
plugin = self.callsigns[request.match_info["name"]]
|
||||
route_path = request.match_info["path"]
|
||||
self.logger.info(path)
|
||||
|
||||
ret = ""
|
||||
|
||||
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
|
||||
with open(file_path, 'r') as resource_data:
|
||||
ret = resource_data.read()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def load_plugin_tile_view(self, request):
|
||||
plugin = self.callsigns[request.match_info["name"]]
|
||||
|
||||
inner_content = ""
|
||||
|
||||
# open up the tile template (if we have one defined)
|
||||
if hasattr(plugin, "tile_view_html"):
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.tile_view_html), 'r') as template:
|
||||
template_data = template.read()
|
||||
inner_content = template_data
|
||||
|
||||
# setup the default template
|
||||
ret = f"""
|
||||
<html style="height: fit-content;">
|
||||
<head>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script src="/static/library.js"></script>
|
||||
<script>const plugin_name = '{plugin.callsign}';</script>
|
||||
</head>
|
||||
<body style="height: fit-content; display: block;">
|
||||
{inner_content}
|
||||
</body>
|
||||
<html>
|
||||
"""
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
@template('plugin_view.html')
|
||||
async def plugin_iframe_route(self, request):
|
||||
return {"plugins": self.plugins.values()}
|
||||
|
||||
async def refresh_iframe(self):
|
||||
tab = await get_tab("QuickAccess")
|
||||
await tab.open_websocket()
|
||||
return await tab.evaluate_js("reloadIframe()", False)
|
||||
@@ -1,144 +0,0 @@
|
||||
from logging import getLogger, basicConfig, INFO, DEBUG, Filter, root
|
||||
from os import getenv
|
||||
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"),
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
|
||||
"store_url": getenv("STORE_URL", "https://plugins.deckbrew.xyz"),
|
||||
"log_base_events": getenv("LOG_BASE_EVENTS", "0")=="1"
|
||||
}
|
||||
|
||||
class NoBaseEvents(Filter):
|
||||
def filter(self, record):
|
||||
return not "asyncio" in record.name
|
||||
|
||||
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
||||
for handler in root.handlers:
|
||||
if not CONFIG["log_base_events"]:
|
||||
handler.addFilter(NoBaseEvents())
|
||||
|
||||
from aiohttp.web import Application, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
from jinja2 import FileSystemLoader
|
||||
from os import path
|
||||
from asyncio import get_event_loop, sleep
|
||||
from json import loads, dumps
|
||||
from subprocess import Popen
|
||||
|
||||
from loader import Loader
|
||||
from injector import inject_to_tab, get_tab, tab_has_element
|
||||
from utilities import Utilities
|
||||
from browser import PluginBrowser
|
||||
|
||||
logger = getLogger("Main")
|
||||
from traceback import print_exc
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
|
||||
Popen(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self.loop = get_event_loop()
|
||||
self.web_app = Application()
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"])
|
||||
self.utilities = Utilities(self)
|
||||
|
||||
jinja_setup(self.web_app, loader=FileSystemLoader(path.join(path.dirname(__file__), 'templates')))
|
||||
self.web_app.on_startup.append(self.inject_javascript)
|
||||
self.web_app.on_startup.append(chown_plugin_dir)
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
self.loop.create_task(self.method_call_listener())
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
|
||||
def exception_handler(self, loop, context):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def loader_reinjector(self):
|
||||
finished_reinjection = False
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
while True:
|
||||
await sleep(1)
|
||||
if not await tab_has_element("QuickAccess", "plugin_iframe"):
|
||||
logger.debug("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
finished_reinjection = True
|
||||
elif finished_reinjection:
|
||||
finished_reinjection = False
|
||||
logger.info("Reinjecting successful!")
|
||||
|
||||
self.loop.create_task(self.method_call_listener())
|
||||
|
||||
async def inject_javascript(self, request=None):
|
||||
try:
|
||||
await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/library.js"), "r").read())
|
||||
await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/plugin_page.js"), "r").read())
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab")
|
||||
pass
|
||||
|
||||
async def resolve_method_call(self, tab, call_id, response):
|
||||
try:
|
||||
r = dumps(response)
|
||||
except Exception as e:
|
||||
logger.error(response["result"])
|
||||
response["result"] = str(response["result"])
|
||||
r = response
|
||||
await tab._send_devtools_cmd({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": f"resolveMethodCall({call_id}, {r})",
|
||||
"userGesture": True
|
||||
}
|
||||
}, receive=False)
|
||||
|
||||
async def handle_method_call(self, method, tab):
|
||||
res = {}
|
||||
try:
|
||||
if method["method"] == "plugin_method":
|
||||
res["result"] = await self.plugin_loader.handle_plugin_method_call(
|
||||
method["args"]["plugin_name"],
|
||||
method["args"]["method_name"],
|
||||
**method["args"]["args"]
|
||||
)
|
||||
res["success"] = True
|
||||
else:
|
||||
r = await self.utilities.util_methods[method["method"]](**method["args"])
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
finally:
|
||||
await self.resolve_method_call(tab, method["id"], res)
|
||||
|
||||
async def method_call_listener(self):
|
||||
while True:
|
||||
try:
|
||||
tab = await get_tab("QuickAccess")
|
||||
break
|
||||
except:
|
||||
await sleep(1)
|
||||
await tab.open_websocket()
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"})
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"})
|
||||
async for message in tab.listen_for_message():
|
||||
data = message.json()
|
||||
if not "id" in data and data["method"] == "Runtime.consoleAPICalled" and data["params"]["type"] == "debug":
|
||||
method = loads(data["params"]["args"][0]["value"])
|
||||
self.loop.create_task(self.handle_method_call(method, tab))
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
PluginManager().run()
|
||||
@@ -1,71 +0,0 @@
|
||||
class PluginEventTarget extends EventTarget { }
|
||||
method_call_ev_target = new PluginEventTarget();
|
||||
|
||||
window.addEventListener("message", function(evt) {
|
||||
let ev = new Event(evt.data.call_id);
|
||||
ev.data = evt.data.result;
|
||||
method_call_ev_target.dispatchEvent(ev);
|
||||
}, false);
|
||||
|
||||
async function call_server_method(method_name, arg_object={}) {
|
||||
let id = `${uuidv4()}`;
|
||||
console.debug(JSON.stringify({
|
||||
"id": id,
|
||||
"method": method_name,
|
||||
"args": arg_object
|
||||
}));
|
||||
return new Promise((resolve, reject) => {
|
||||
method_call_ev_target.addEventListener(`${id}`, function (event) {
|
||||
if (event.data.success) resolve(event.data.result);
|
||||
else reject(event.data.result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/2117523 Thanks!
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
async function fetch_nocors(url, request={}) {
|
||||
let args = { method: "POST", headers: {}, body: "" };
|
||||
request = {...args, ...request};
|
||||
request.url = url;
|
||||
request.data = request.body;
|
||||
delete request.body; //maintain api-compatibility with fetch
|
||||
return await call_server_method("http_request", request);
|
||||
}
|
||||
|
||||
async function call_plugin_method(method_name, arg_object={}) {
|
||||
if (plugin_name == undefined)
|
||||
throw new Error("Plugin methods can only be called from inside plugins (duh)");
|
||||
return await call_server_method("plugin_method", {
|
||||
'plugin_name': plugin_name,
|
||||
'method_name': method_name,
|
||||
'args': arg_object
|
||||
});
|
||||
}
|
||||
|
||||
async function execute_in_tab(tab, run_async, code) {
|
||||
return await call_server_method("execute_in_tab", {
|
||||
'tab': tab,
|
||||
'run_async': run_async,
|
||||
'code': code
|
||||
});
|
||||
}
|
||||
|
||||
async function inject_css_into_tab(tab, style) {
|
||||
return await call_server_method("inject_css_into_tab", {
|
||||
'tab': tab,
|
||||
'style': style
|
||||
});
|
||||
}
|
||||
|
||||
async function remove_css_from_tab(tab, css_id) {
|
||||
return await call_server_method("remove_css_from_tab", {
|
||||
'tab': tab,
|
||||
'css_id': css_id
|
||||
});
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
function reloadIframe() {
|
||||
document.getElementById("plugin_iframe").contentWindow.location.href = "http://127.0.0.1:1337/plugins/iframe";
|
||||
}
|
||||
|
||||
function resolveMethodCall(call_id, result) {
|
||||
let iframe = document.getElementById("plugin_iframe").contentWindow;
|
||||
iframe.postMessage({'call_id': call_id, 'result': result}, "http://127.0.0.1:1337");
|
||||
}
|
||||
|
||||
function installPlugin(request_id) {
|
||||
let id = `${new Date().getTime()}`;
|
||||
console.debug(JSON.stringify({
|
||||
"id": id,
|
||||
"method": "confirm_plugin_install",
|
||||
"args": {"request_id": request_id}
|
||||
}));
|
||||
document.getElementById('plugin_install_list').removeChild(document.getElementById(`plugin_install_prompt_${request_id}`));
|
||||
}
|
||||
|
||||
function addPluginInstallPrompt(artifact, version, request_id) {
|
||||
let text = `
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
|
||||
<div id="plugin_install_prompt_${request_id}" style="background-color: #0c131b; display: block; border: 1px solid #22262f; box-shadow: 0px 0px 8px #202020; width: calc(100% - 50px); padding: 0px 10px 10px 10px;">
|
||||
<h3>Install Plugin?</h3>
|
||||
<p style="font-size: 12px;">
|
||||
${artifact}
|
||||
Version: ${version}
|
||||
</p>
|
||||
<button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
|
||||
onclick="installPlugin('${request_id}')">
|
||||
Install
|
||||
</button>
|
||||
<p style="margin: 2px;"></p>
|
||||
<button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
|
||||
onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('plugin_install_list').innerHTML = text;
|
||||
|
||||
execute_in_tab('SP', false, 'FocusNavController.DispatchVirtualButtonClick(28)')
|
||||
}
|
||||
|
||||
(function () {
|
||||
const PLUGIN_ICON = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plugin" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 1 2.898 5.673c-.167-.121-.216-.406-.002-.62l1.8-1.8a3.5 3.5 0 0 0
|
||||
4.572-.328l1.414-1.415a.5.5 0 0 0 0-.707l-.707-.707 1.559-1.563a.5.5 0 1 0-.708-.706l-1.559 1.562-1.414-1.414
|
||||
1.56-1.562a.5.5 0 1 0-.707-.706l-1.56 1.56-.707-.706a.5.5 0 0 0-.707 0L5.318 5.975a3.5 3.5 0 0 0-.328
|
||||
4.571l-1.8 1.8c-.58.58-.62 1.6.121 2.137A8 8 0 1 0 0 8a.5.5 0 0 0 1 0Z"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
function createTitle(text) {
|
||||
return `<div id="plugin_title" class="quickaccessmenu_Title_34nl5">${text}</div>`;
|
||||
}
|
||||
|
||||
function createPluginList() {
|
||||
let pages = document.getElementsByClassName("quickaccessmenu_AllTabContents_2yKG4 quickaccessmenu_Down_3rR0o")[0];
|
||||
let pluginPage = pages.children[pages.children.length - 1];
|
||||
pluginPage.innerHTML = createTitle("Plugins");
|
||||
|
||||
pluginPage.innerHTML += `<div id="plugin_install_list" style="position: fixed; height: 100%; z-index: 99; transform: translate(5%, 0);"></div>`
|
||||
|
||||
pluginPage.innerHTML += `<iframe id="plugin_iframe" style="border: none; width: 100%; height: 100%;" src="http://127.0.0.1:1337/plugins/iframe"></iframe>`;
|
||||
}
|
||||
|
||||
function inject() {
|
||||
let tabs = document.getElementsByClassName("quickaccessmenu_TabContentColumn_2z5NL Panel Focusable")[0];
|
||||
tabs.children[tabs.children.length - 1].innerHTML = PLUGIN_ICON;
|
||||
|
||||
createPluginList();
|
||||
}
|
||||
|
||||
let injector = setInterval(function () {
|
||||
if (document.hasFocus()) {
|
||||
inject();
|
||||
document.getElementById("plugin_title").onclick = function() {
|
||||
reloadIframe();
|
||||
document.getElementById("plugin_title").innerText = "Plugins";
|
||||
}
|
||||
window.onmessage = function(ev) {
|
||||
let title = ev.data;
|
||||
if (title.startsWith("PLUGIN_LOADER__")) {
|
||||
document.getElementById("plugin_title").innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1z"/>
|
||||
</svg>
|
||||
${title.replace("PLUGIN_LOADER__", "")}
|
||||
`;
|
||||
}
|
||||
}
|
||||
clearInterval(injector);
|
||||
}
|
||||
}, 100);
|
||||
})();
|
||||
@@ -1,3 +0,0 @@
|
||||
@import url("/steam_resource/css/2.css");
|
||||
@import url("/steam_resource/css/39.css");
|
||||
@import url("/steam_resource/css/library.css");
|
||||
@@ -1,76 +0,0 @@
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script>
|
||||
const tile_iframes = [];
|
||||
window.addEventListener("message", function (evt) {
|
||||
tile_iframes.forEach(iframe => {
|
||||
iframe.contentWindow.postMessage(evt.data, "http://127.0.0.1:1337");
|
||||
});
|
||||
}, false);
|
||||
|
||||
function loadPlugin(callsign, name) {
|
||||
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
|
||||
location.href = `/plugins/load_main/${callsign}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{% if not plugins|length %}
|
||||
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||
<div class="quickaccesscontrols_EmptyNotifications_3ZjbM" style="padding-top:7px;">
|
||||
No plugins installed
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||
{% for plugin in plugins %}
|
||||
{% if plugin.tile_view_html|length %}
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')"
|
||||
class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
|
||||
style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;">
|
||||
<iframe id="tile_view_iframe_{{ plugin.callsign }}"
|
||||
scrolling="no" marginwidth="0" marginheight="0"
|
||||
hspace="0" vspace="0" frameborder="0"
|
||||
style="border-radius: 2px;"
|
||||
src="/plugins/load_tile/{{ plugin.callsign }}">
|
||||
</iframe>
|
||||
<script>
|
||||
(function() {
|
||||
let iframe = document.getElementById("tile_view_iframe_{{ plugin.callsign }}");
|
||||
tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.callsign }}"));
|
||||
|
||||
iframe.onload = function() {
|
||||
let html = iframe.contentWindow.document.children[0];
|
||||
let last_height = 0;
|
||||
|
||||
setInterval(function() {
|
||||
let height = iframe.contentWindow.document.children[0].scrollHeight;
|
||||
if (height != last_height) {
|
||||
iframe.height = height + "px";
|
||||
last_height = height;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
iframe.contentWindow.document.body.onclick = function () {
|
||||
loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}');
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')"
|
||||
class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
|
||||
style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;">
|
||||
<div class="basicdialog_FieldChildren_279n8">
|
||||
<button type="button" tabindex="0"
|
||||
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -1,3 +1,4 @@
|
||||
aiohttp==3.8.1
|
||||
aiohttp-jinja2==1.5.0
|
||||
aiohttp_cors==0.7.0
|
||||
watchdog==2.1.7
|
||||
Reference in New Issue
Block a user