Compare commits

..

20 Commits

Author SHA1 Message Date
tza 2045bedee3 plugin title bug fix 2022-04-08 13:47:06 +03:00
tza e35dd5a028 plugin menu title change bug
Fixed a bug where steam would sometimes fire message events on its own, causing them to be displayed as plugin titles.
2022-04-07 23:22:23 +03:00
tza c65427e693 initial browser/installer commit, injector get_tab and stateful utils
- Integrated plugin downloader/installer. It accepts POST requests at /browser/install_plugin, containing an artifact (basically an author/repo string like you'd find on github), and a release version, then fetches the zip file from the repo releases and unzips it inside the plugin dir, after asking for user confirmation (pop-up message in the plugin menu).
- Injector get_tab method. Basically get_tabs with the usual search for a specific tab. Decided to implement this because it was needed again and again, and we kept pasting the same list search one-liner.
- Utilities now have access to the main PluginManager class
2022-04-07 22:38:26 +03:00
ttay24 0f14f2707b Added support for including styles/scripts in the header of html files (#29)
* Added support for including styles/scripts in the header of html files

* updated route name
2022-04-07 20:25:03 +03:00
WerWolv 2188aa0343 Moved install commands to new line to make them more readable 2022-04-07 10:20:25 +02:00
WerWolv 3dec82672a Reinject loader if steam got restarted 2022-04-07 09:58:26 +02:00
WerWolv a3619d1d3a Fixed first plugin button being smaller than the other ones 2022-04-07 09:08:28 +02:00
WerWolv 3a39c88144 Prevent errors from being printed on first install 2022-04-07 01:17:22 +02:00
TrainDoctor 8c0bb20d05 Update readme to show install instructions (#28)
this is for developers/testers
2022-04-07 00:18:42 +02:00
WerWolv 90bd7df52c Tell curl to follow redirection in install command 2022-04-06 20:10:05 +02:00
WerWolv 6644827094 Directly reference install script in install command
Closes #25
2022-04-06 20:07:58 +02:00
tza fe53bcf127 Change title when inside plugin view 2022-04-06 13:57:21 +03:00
tza a6943dd7a2 enabled logging, fixed loader refresh bug, removed template 2022-04-06 12:53:19 +03:00
ttay24 85e5554c05 Feature/20 plugins as folders (#24)
* updated loader to watch directories correctly and pull in main.py; also made sure we pull in main.py; WIP on pulling in the template correctly

* Making other changes to support pulling in templates from other pages
2022-04-06 12:40:11 +03:00
Spyrex 8e315fd24d Disable scrolling for tile_view_iframe (#19) 2022-04-06 01:42:15 +03:00
Spyrex 3d0c3ef86f Set tile_view_iframe overflow hidden (#18) 2022-04-06 00:29:17 +03:00
tza 30325397d0 added plugin tile view isolation
Tile views will now run each in their own iframe. This makes it more safe as plugins no longer share the same javascript context, and plugin method calls can now be supported from the tile view.
2022-04-05 22:40:24 +03:00
tza 8d0fe5c45a hot reload now refreshes iframe
also fixed fetch_nocors
2022-04-04 20:46:35 +03:00
WerWolv 1d4100fabb Updated installation guide 2022-04-04 19:17:20 +02:00
WerWolv 0c58ca50fa Added release and nightly download scripts 2022-04-04 19:12:57 +02:00
15 changed files with 476 additions and 152 deletions
-1
View File
@@ -10,7 +10,6 @@ __pycache__/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
+15 -8
View File
@@ -3,16 +3,23 @@
![steamuserimages-a akamaihd](https://user-images.githubusercontent.com/10835354/161068262-ca723dc5-6795-417a-80f6-d8c1f9d03e93.jpg)
## 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
+41
View File
@@ -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
+36
View File
@@ -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
View File
@@ -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
-13
View File
@@ -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
+76
View File
@@ -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)
+23 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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);
}
+53 -7
View File
@@ -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");
}
})();
+53 -13
View File
@@ -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
View File
@@ -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"
-19
View File
@@ -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