mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-03 00:09:54 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2045bedee3 | |||
| e35dd5a028 | |||
| c65427e693 | |||
| 0f14f2707b | |||
| 2188aa0343 | |||
| 3dec82672a | |||
| a3619d1d3a | |||
| 3a39c88144 | |||
| 8c0bb20d05 | |||
| 90bd7df52c | |||
| 6644827094 | |||
| fe53bcf127 | |||
| a6943dd7a2 | |||
| 85e5554c05 | |||
| 8e315fd24d | |||
| 3d0c3ef86f | |||
| 30325397d0 | |||
| 8d0fe5c45a | |||
| 1d4100fabb | |||
| 0c58ca50fa |
@@ -10,7 +10,6 @@ __pycache__/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||

|
||||
|
||||
## Installation
|
||||
- Go into the Steam Deck Settings
|
||||
- Under System -> System Settings toggle `Enable Developer Mode`
|
||||
- Scroll the sidebar all the way down and click on `Developer`
|
||||
- Under Miscellaneous, enable `CEF Remote Debugging`
|
||||
- Place the executable under `~/homebrew/services/plugin_loader`. Do not change the name of the file.
|
||||
- Place the plugin_manager.service file under `/etc/systemd/system`
|
||||
- Open a Terminal and type `systemctl --now --user enable plugin_manager`
|
||||
1. Go into the Steam Deck Settings
|
||||
2. Under System -> System Settings toggle `Enable Developer Mode`
|
||||
3. Scroll the sidebar all the way down and click on `Developer`
|
||||
4. Under Miscellaneous, enable `CEF Remote Debugging`
|
||||
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
6. Open a terminal and paste the following command into it:
|
||||
- For users:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_release.sh | sh`
|
||||
- For developers:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`
|
||||
8. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
|
||||
### Install Plugins
|
||||
- Simply copy the plugin's .py file into `~/homebrew/plugins`
|
||||
- Simply copy the plugin's folder into `~/homebrew/plugins`
|
||||
|
||||
### Developing plugins
|
||||
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/Plugin-Template) repository
|
||||
|
||||
## Features
|
||||
- Clean injecting and loading of one or more plugins
|
||||
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader nightly..."
|
||||
|
||||
HOMEBREW_FOLDER=/home/deck/homebrew
|
||||
|
||||
# Create folder structure
|
||||
rm -rf ${HOMEBREW_FOLDER}/services
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services
|
||||
mkdir -p ${HOMEBREW_FOLDER}/plugins
|
||||
|
||||
# Download latest nightly build and install it
|
||||
rm -rf /tmp/plugin_loader
|
||||
mkdir -p /tmp/plugin_loader
|
||||
curl -L https://nightly.link/SteamDeckHomebrew/PluginLoader/workflows/build/main/Plugin%20Loader.zip --output /tmp/plugin_loader/PluginLoader.zip
|
||||
unzip /tmp/plugin_loader/PluginLoader.zip -d /tmp/plugin_loader
|
||||
cp /tmp/plugin_loader/PluginLoader ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
rm -rf /tmp/plugin_loader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
rm -f /home/deck/.config/systemd/user/plugin_loader.service
|
||||
cat > /home/deck/.config/systemd/user/plugin_loader.service <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
||||
WorkingDirectory=/home/deck/homebrew/services
|
||||
|
||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOM
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start plugin_loader
|
||||
systemctl --user enable plugin_loader
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader release..."
|
||||
|
||||
HOMEBREW_FOLDER=/home/deck/homebrew
|
||||
|
||||
# Create folder structure
|
||||
rm -rf ${HOMEBREW_FOLDER}/services
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services
|
||||
mkdir -p ${HOMEBREW_FOLDER}/plugins
|
||||
|
||||
# Download latest release and install it
|
||||
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
rm -f /home/deck/.config/systemd/user/plugin_loader.service
|
||||
cat > /home/deck/.config/systemd/user/plugin_loader.service <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
||||
WorkingDirectory=/home/deck/homebrew/services
|
||||
|
||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOM
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start plugin_loader
|
||||
systemctl --user enable plugin_loader
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run this script as root"
|
||||
exit
|
||||
fi
|
||||
|
||||
HOMEBREW_FOLDER=/home/deck/homebrew
|
||||
LOADER_FOLDER=$(realpath $(dirname "$0"))
|
||||
|
||||
# Create folder structure
|
||||
rm -rf ${HOMEBREW_FOLDER}/services/plugin_loader
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/plugin_loader
|
||||
mkdir -p ${HOMEBREW_FOLDER}/plugins
|
||||
chown -R deck ${HOMEBREW_FOLDER}
|
||||
|
||||
# Install our files
|
||||
cp -a ${LOADER_FOLDER}/plugin_loader/. /home/deck/homebrew/services/plugin_loader/
|
||||
|
||||
# Install pip if it's not installed yet
|
||||
python -m pip &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
curl https://bootstrap.pypa.io/get-pip.py --output /tmp/get-pip.py
|
||||
python /tmp/get-pip.py
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
python -m pip install -r requirements.txt
|
||||
|
||||
# Create a service
|
||||
systemctl stop plugin_loader
|
||||
|
||||
cp ./plugin_loader.service /etc/systemd/system/plugin_loader.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable plugin_loader
|
||||
systemctl start plugin_loader
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Manager
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
ExecStart=/usr/bin/python3 /home/deck/homebrew/services/plugin_loader/main.py
|
||||
WorkingDirectory=/home/deck/homebrew/services/plugin_loader
|
||||
|
||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,76 @@
|
||||
from injector import get_tab
|
||||
from logging import getLogger
|
||||
from os import path, rename
|
||||
from shutil import rmtree
|
||||
from aiohttp import ClientSession, web
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from asyncio import get_event_loop
|
||||
from time import time
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, gh_url, version) -> None:
|
||||
self.gh_url = gh_url
|
||||
self.version = version
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, server_instance, store_url) -> None:
|
||||
self.log = getLogger("browser")
|
||||
self.plugin_path = plugin_path
|
||||
self.store_url = store_url
|
||||
self.install_requests = {}
|
||||
|
||||
server_instance.add_routes([
|
||||
web.post("/browser/install_plugin", self.install_plugin),
|
||||
web.get("/browser/iframe", self.redirect_to_store)
|
||||
])
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name):
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
(rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name)))
|
||||
|
||||
async def _install(self, artifact, version):
|
||||
name = artifact.split("/")[-1]
|
||||
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
|
||||
self.log.info("Installing {} (Version: {})".format(artifact, version))
|
||||
async with ClientSession() as client:
|
||||
url = "https://github.com/{}/archive/refs/tags/{}.zip".format(artifact, version)
|
||||
self.log.debug("Fetching {}".format(url))
|
||||
res = await client.get(url)
|
||||
if res.status == 200:
|
||||
self.log.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
self.log.debug("Read {} bytes".format(len(data)))
|
||||
res_zip = BytesIO(data)
|
||||
with ProcessPoolExecutor() as executor:
|
||||
self.log.debug("Unzipping...")
|
||||
await get_event_loop().run_in_executor(
|
||||
executor,
|
||||
self._unzip_to_plugin_dir,
|
||||
res_zip,
|
||||
name
|
||||
)
|
||||
self.log.info("Installed {} (Version: {})".format(artifact, version))
|
||||
else:
|
||||
self.log.fatal("Could not fetch from github. {}".format(await res.text()))
|
||||
|
||||
async def redirect_to_store(self, request):
|
||||
return web.Response(status=302, headers={"Location": self.store_url})
|
||||
|
||||
async def install_plugin(self, request):
|
||||
data = await request.post()
|
||||
get_event_loop().create_task(self.request_plugin_install(data["artifact"], data["version"]))
|
||||
return web.Response(text="Requested plugin install")
|
||||
|
||||
async def request_plugin_install(self, artifact, version):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, version)
|
||||
tab = await get_tab("QuickAccess")
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js("addPluginInstallPrompt('{}', '{}', '{}')".format(artifact, version, request_id))
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
request = self.install_requests.pop(request_id)
|
||||
await self._install(request.gh_url, request.version)
|
||||
@@ -1,11 +1,13 @@
|
||||
#Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from logging import info
|
||||
from logging import debug, getLogger
|
||||
from asyncio import sleep
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
|
||||
class Tab:
|
||||
def __init__(self, res) -> None:
|
||||
self.title = res["title"]
|
||||
@@ -68,7 +70,7 @@ async def get_tabs():
|
||||
res = await web.get("{}/json".format(BASE_ADDRESS))
|
||||
break
|
||||
except:
|
||||
print("Steam isn't available yet. Wait for a moment...")
|
||||
logger.info("Steam isn't available yet. Wait for a moment...")
|
||||
await sleep(5)
|
||||
|
||||
if res.status == 200:
|
||||
@@ -77,9 +79,25 @@ async def get_tabs():
|
||||
else:
|
||||
raise Exception("/json did not return 200. {}".format(await res.text()))
|
||||
|
||||
async def inject_to_tab(tab_name, js):
|
||||
async def get_tab(tab_name):
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||
if not tab:
|
||||
raise ValueError("Tab {} not found in running tabs".format(tab_name))
|
||||
info(await tab.evaluate_js(js))
|
||||
raise ValueError("Tab {} not found".format(tab_name))
|
||||
return tab
|
||||
|
||||
async def inject_to_tab(tab_name, js):
|
||||
tab = await get_tab(tab_name)
|
||||
logger.debug(f"Injected JavaScript Result: {await tab.evaluate_js(js)}")
|
||||
|
||||
async def tab_has_element(tab_name, element_name):
|
||||
try:
|
||||
tab = await get_tab(tab_name)
|
||||
except ValueError:
|
||||
return False
|
||||
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null")
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
+110
-20
@@ -7,57 +7,89 @@ from os import path, listdir
|
||||
from importlib.util import spec_from_file_location, module_from_spec
|
||||
from logging import getLogger
|
||||
|
||||
from injector import get_tabs
|
||||
from injector import get_tabs, get_tab
|
||||
|
||||
class FileChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, loader) -> None:
|
||||
def __init__(self, loader, plugin_path) -> None:
|
||||
super().__init__()
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.loader : Loader = loader
|
||||
self.plugin_path = plugin_path
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
self.loader.import_plugin(src_path)
|
||||
|
||||
# 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")
|
||||
if not path.isfile(main_file_path):
|
||||
return
|
||||
self.loader.import_plugin(main_file_path, plugin_dir, refresh=True)
|
||||
|
||||
def on_modified(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
self.loader.import_plugin(src_path)
|
||||
|
||||
# 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.loader.import_plugin(path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, refresh=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.import_plugins()
|
||||
|
||||
if live_reload:
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(FileChangeHandler(self), self.plugin_path)
|
||||
self.observer.schedule(FileChangeHandler(self, plugin_path), self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/plugins/iframe", self.plugin_iframe_route),
|
||||
web.get("/plugins/reload", self.reload_plugins),
|
||||
web.post("/plugins/method_call", self.handle_plugin_method_call),
|
||||
web.get("/plugins/load/{name}", self.load_plugin),
|
||||
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):
|
||||
def import_plugin(self, file, plugin_directory, refresh=False):
|
||||
try:
|
||||
spec = spec_from_file_location("_", file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# add member for what directory the given plugin lives under
|
||||
module.Plugin._plugin_directory = plugin_directory
|
||||
|
||||
if not hasattr(module.Plugin, "name"):
|
||||
raise KeyError("Plugin {} has not defined a name".format(file))
|
||||
if module.Plugin.name in self.plugins:
|
||||
if hasattr(module.Plugin, "hot_reload") and not module.Plugin.hot_reload:
|
||||
if hasattr(module.Plugin, "hot_reload") and not module.Plugin.hot_reload and refresh:
|
||||
self.logger.info("Plugin {} is already loaded and has requested to not be re-loaded"
|
||||
.format(module.Plugin.name))
|
||||
return
|
||||
else:
|
||||
if hasattr(self.plugins[module.Plugin.name], "task"):
|
||||
self.plugins[module.Plugin.name].task.cancel()
|
||||
@@ -69,14 +101,17 @@ class Loader:
|
||||
self.logger.info("Loaded {}".format(module.Plugin.name))
|
||||
except Exception as e:
|
||||
self.logger.error("Could not load {}. {}".format(file, e))
|
||||
finally:
|
||||
if refresh:
|
||||
self.loop.create_task(self.refresh_iframe())
|
||||
|
||||
def import_plugins(self):
|
||||
files = [i for i in listdir(self.plugin_path) if i.endswith(".py")]
|
||||
for file in files:
|
||||
self.import_plugin(path.join(self.plugin_path, file))
|
||||
|
||||
async def watch_for_file_change(self):
|
||||
pass
|
||||
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, "main.py"))]
|
||||
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 reload_plugins(self, request=None):
|
||||
self.logger.info("Re-importing plugins.")
|
||||
@@ -89,17 +124,72 @@ class Loader:
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = (await get_tabs())[0]
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
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(self, request):
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.plugins[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 = """
|
||||
<script src="/static/library.js"></script>
|
||||
<script>const plugin_name = '{}' </script>
|
||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{}/">
|
||||
{}
|
||||
""".format(plugin.name, plugin.name, template_data)
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
async def handle_sub_route(self, request):
|
||||
plugin = self.plugins[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.plugins[request.match_info["name"]]
|
||||
|
||||
inner_content = ""
|
||||
|
||||
# open up the tile template (if we have one defined)
|
||||
if len(plugin.tile_view_html) > 0:
|
||||
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 = """
|
||||
<script src="/static/library.js"></script>
|
||||
<script>const plugin_name = '{}' </script>
|
||||
{}
|
||||
""".format(plugin.name, plugin.main_view_html)
|
||||
<html style="height: fit-content;">
|
||||
<head>
|
||||
<link rel="stylesheet" href="/steam_resource/css/2.css">
|
||||
<link rel="stylesheet" href="/steam_resource/css/39.css">
|
||||
<link rel="stylesheet" href="/steam_resource/css/library.css">
|
||||
<script src="/static/library.js"></script>
|
||||
<script>const plugin_name = '{name}';</script>
|
||||
</head>
|
||||
<body style="height: fit-content; display: block;">
|
||||
{content}
|
||||
</body>
|
||||
<html>
|
||||
""".format(name=plugin.name, content=inner_content)
|
||||
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()")
|
||||
+47
-15
@@ -1,31 +1,52 @@
|
||||
from aiohttp.web import Application, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
from jinja2 import FileSystemLoader
|
||||
from os import getenv, path
|
||||
from asyncio import get_event_loop
|
||||
from json import loads, dumps
|
||||
|
||||
from loader import Loader
|
||||
from injector import inject_to_tab, get_tabs
|
||||
from utilities import util_methods
|
||||
from logging import getLogger, basicConfig, INFO, DEBUG
|
||||
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"
|
||||
"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 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 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")
|
||||
|
||||
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.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 resolve_method_call(self, tab, call_id, response):
|
||||
await tab._send_devtools_cmd({
|
||||
@@ -48,7 +69,7 @@ class PluginManager:
|
||||
)
|
||||
res["success"] = True
|
||||
else:
|
||||
r = await util_methods[method["method"]](**method["args"])
|
||||
r = await self.utilities.util_methods[method["method"]](**method["args"])
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
@@ -58,7 +79,7 @@ class PluginManager:
|
||||
await self.resolve_method_call(tab, method["id"], res)
|
||||
|
||||
async def method_call_listener(self):
|
||||
tab = next((i for i in await get_tabs() if i.title == "QuickAccess"), None)
|
||||
tab = await get_tab("QuickAccess")
|
||||
await tab.open_websocket()
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"})
|
||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"})
|
||||
@@ -68,11 +89,22 @@ class PluginManager:
|
||||
method = loads(data["params"]["args"][0]["value"])
|
||||
self.loop.create_task(self.handle_method_call(method, tab))
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
await sleep(1)
|
||||
if not await tab_has_element("QuickAccess", "plugin_iframe"):
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
|
||||
async def inject_javascript(self, request=None):
|
||||
await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/plugin_page.js"), "r").read())
|
||||
try:
|
||||
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
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop)
|
||||
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()
|
||||
@@ -2,7 +2,6 @@ class PluginEventTarget extends EventTarget { }
|
||||
method_call_ev_target = new PluginEventTarget();
|
||||
|
||||
window.addEventListener("message", function(evt) {
|
||||
console.log(evt);
|
||||
let ev = new Event(evt.data.call_id);
|
||||
ev.data = evt.data.result;
|
||||
method_call_ev_target.dispatchEvent(ev);
|
||||
@@ -27,6 +26,8 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
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 = `
|
||||
<div id="plugin_install_prompt_${request_id}" style="display: block; background: #304375; border-radius: 5px;">
|
||||
<h3 style="padding-left: 1rem;">Install plugin</h3>
|
||||
<ul style="padding-left: 10px; padding-right: 10px; padding-bottom: 20px; margin: 0;">
|
||||
<li>${artifact}</li>
|
||||
<li>${version}</li>
|
||||
</ul>
|
||||
<div style="text-align: center; padding-bottom: 10px;">
|
||||
<button onclick="installPlugin('${request_id}')" style="display: inline-block; background-color: green;">Install</button>
|
||||
<button onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))"
|
||||
style="display: inline-block; background-color: red;">Ignore</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('plugin_install_list').innerHTML += text;
|
||||
}
|
||||
|
||||
(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">
|
||||
@@ -17,6 +54,8 @@
|
||||
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>`;
|
||||
}
|
||||
|
||||
@@ -31,14 +70,21 @@
|
||||
if (document.hasFocus()) {
|
||||
inject();
|
||||
document.getElementById("plugin_title").onclick = function() {
|
||||
document.getElementById("plugin_iframe").contentWindow.location.href = "http://127.0.0.1:1337/plugins/iframe";
|
||||
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);
|
||||
})();
|
||||
|
||||
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");
|
||||
}
|
||||
})();
|
||||
@@ -1,32 +1,72 @@
|
||||
<link rel="stylesheet" href="/steam_resource/css/2.css">
|
||||
<link rel="stylesheet" href="/steam_resource/css/39.css">
|
||||
<link rel="stylesheet" href="/steam_resource/css/library.css">
|
||||
<script src="/static/library.js"></script>
|
||||
<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(name) {
|
||||
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
|
||||
location.href = `/plugins/load_main/${name}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{% if not plugins|length %}
|
||||
<div 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;">
|
||||
<div 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;">
|
||||
<div class="basicdialog_FieldChildren_279n8" style="color: white; font-size: large; padding-top: 10px;">
|
||||
No plugins installed :(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
{% for plugin in plugins %}
|
||||
{% if plugin.tile_view_html|length %}
|
||||
<div onclick="location.href = '/plugins/load/{{ 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;">
|
||||
{{ plugin.tile_view_html|safe }}
|
||||
{% for plugin in plugins %}
|
||||
{% if plugin.tile_view_html|length %}
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div onclick="loadPlugin('{{ 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;">
|
||||
<iframe id="tile_view_iframe_{{ plugin.name }}" style="display:block; padding: 0; border: none;" scrolling="no"
|
||||
src="/plugins/load_tile/{{ plugin.name }}"></iframe>
|
||||
<script>
|
||||
(function() {
|
||||
let iframe = document.getElementById("tile_view_iframe_{{ plugin.name }}");
|
||||
tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.name }}"));
|
||||
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.name }}');
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div onclick="location.href = '/plugins/load/{{ 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;">
|
||||
<div onclick="loadPlugin('{{ 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;">
|
||||
<div class="basicdialog_FieldChildren_279n8">
|
||||
<button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name }}</button>
|
||||
</div>
|
||||
<button type="button" tabindex="0"
|
||||
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name
|
||||
}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+20
-13
@@ -1,18 +1,25 @@
|
||||
from aiohttp import ClientSession
|
||||
|
||||
async def http_request(method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, **kwargs)
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
self.context = context
|
||||
self.util_methods = {
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request,
|
||||
"confirm_plugin_install": self.confirm_plugin_install
|
||||
}
|
||||
|
||||
async def ping(**kwargs):
|
||||
return "pong"
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||
|
||||
util_methods = {
|
||||
"ping": ping,
|
||||
"http_request": http_request
|
||||
}
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, **kwargs)
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
return "pong"
|
||||
@@ -1,19 +0,0 @@
|
||||
class Plugin:
|
||||
name = "Template Plugin"
|
||||
|
||||
author = "SteamDeckHomebrew"
|
||||
|
||||
main_view_html = "<html><body><h3>Template Plugin</h3></body></html>"
|
||||
|
||||
tile_view_html = ""
|
||||
|
||||
hot_reload = False
|
||||
|
||||
async def __main(self):
|
||||
pass
|
||||
|
||||
async def method_1(self, **kwargs):
|
||||
pass
|
||||
|
||||
async def method_2(self, **kwargs):
|
||||
pass
|
||||
Reference in New Issue
Block a user