mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 16:02:01 +00:00
Callsigns (#37)
* Plugin callsigns, filechangehandler thread bug fix, plugin file perms - Plugins are now assigned a callsign (a random string), which they use for all internal identification, like resource fetching and method calls. This is to ensure that plugins only access their own resources and methods. - Made FileChangeHandler send off events to a queue, that is then consumed by the Loader, instead of calling import_plugin on its own, since that caused weird issues with the event loop and the thread watchdog is using. - Plugins are now owned by root and have read-only permissions. This is handled automatically. * Improved general look and feel of plugin tab * Make all plugin entries have the same padding between them * Make "No plugins installed" text look the same as "No new notifications" Co-authored-by: WerWolv <werwolv98@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ from concurrent.futures import ProcessPoolExecutor
|
|||||||
from asyncio import get_event_loop
|
from asyncio import get_event_loop
|
||||||
from time import time
|
from time import time
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
class PluginInstallContext:
|
class PluginInstallContext:
|
||||||
def __init__(self, gh_url, version, hash) -> None:
|
def __init__(self, gh_url, version, hash) -> None:
|
||||||
@@ -35,6 +36,8 @@ class PluginBrowser:
|
|||||||
zip_file = ZipFile(zip)
|
zip_file = ZipFile(zip)
|
||||||
zip_file.extractall(self.plugin_path)
|
zip_file.extractall(self.plugin_path)
|
||||||
rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name))
|
rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name))
|
||||||
|
Popen(["chown", "-R", "deck:deck", self.plugin_path])
|
||||||
|
Popen(["chmod", "-R", "555", self.plugin_path])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _install(self, artifact, version, hash):
|
async def _install(self, artifact, version, hash):
|
||||||
|
|||||||
+31
-21
@@ -2,19 +2,21 @@ from aiohttp import web
|
|||||||
from aiohttp_jinja2 import template
|
from aiohttp_jinja2 import template
|
||||||
from watchdog.observers.polling import PollingObserver as Observer
|
from watchdog.observers.polling import PollingObserver as Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
from asyncio import Queue
|
||||||
from os import path, listdir
|
from os import path, listdir
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from time import time
|
||||||
|
|
||||||
from injector import get_tabs, get_tab
|
from injector import get_tabs, get_tab
|
||||||
from plugin import PluginWrapper
|
from plugin import PluginWrapper
|
||||||
|
from traceback import print_exc
|
||||||
|
|
||||||
class FileChangeHandler(FileSystemEventHandler):
|
class FileChangeHandler(FileSystemEventHandler):
|
||||||
def __init__(self, loader, plugin_path) -> None:
|
def __init__(self, queue, plugin_path) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.logger = getLogger("file-watcher")
|
self.logger = getLogger("file-watcher")
|
||||||
self.loader : Loader = loader
|
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
|
self.queue = queue
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
src_path = event.src_path
|
src_path = event.src_path
|
||||||
@@ -31,7 +33,7 @@ class FileChangeHandler(FileSystemEventHandler):
|
|||||||
rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path]))
|
rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path]))
|
||||||
plugin_dir = path.split(rel_path)[0]
|
plugin_dir = path.split(rel_path)[0]
|
||||||
main_file_path = path.join(self.plugin_path, plugin_dir, "main.py")
|
main_file_path = path.join(self.plugin_path, plugin_dir, "main.py")
|
||||||
self.loader.import_plugin(main_file_path, plugin_dir, refresh=True)
|
self.queue.put_nowait((main_file_path, plugin_dir, True))
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
src_path = event.src_path
|
src_path = event.src_path
|
||||||
@@ -46,7 +48,7 @@ class FileChangeHandler(FileSystemEventHandler):
|
|||||||
# file that changed is not necessarily the one that needs to be reloaded
|
# file that changed is not necessarily the one that needs to be reloaded
|
||||||
self.logger.debug(f"file modified: {src_path}")
|
self.logger.debug(f"file modified: {src_path}")
|
||||||
plugin_dir = path.split(path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])))[0]
|
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)
|
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||||
|
|
||||||
class Loader:
|
class Loader:
|
||||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||||
@@ -55,16 +57,18 @@ class Loader:
|
|||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||||
self.plugins = {}
|
self.plugins = {}
|
||||||
|
self.callsigns = {}
|
||||||
self.import_plugins()
|
self.import_plugins()
|
||||||
|
|
||||||
if live_reload:
|
if live_reload:
|
||||||
|
self.reload_queue = Queue()
|
||||||
self.observer = Observer()
|
self.observer = Observer()
|
||||||
self.observer.schedule(FileChangeHandler(self, plugin_path), self.plugin_path, recursive=True)
|
self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
|
||||||
self.observer.start()
|
self.observer.start()
|
||||||
|
self.loop.create_task(self.handle_reloads())
|
||||||
|
|
||||||
server_instance.add_routes([
|
server_instance.add_routes([
|
||||||
web.get("/plugins/iframe", self.plugin_iframe_route),
|
web.get("/plugins/iframe", self.plugin_iframe_route),
|
||||||
web.get("/plugins/reload", self.reload_plugins),
|
|
||||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
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/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
||||||
web.get("/plugins/load_tile/{name}", self.load_plugin_tile_view),
|
web.get("/plugins/load_tile/{name}", self.load_plugin_tile_view),
|
||||||
@@ -75,18 +79,23 @@ class Loader:
|
|||||||
try:
|
try:
|
||||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||||
if plugin.name in self.plugins:
|
if plugin.name in self.plugins:
|
||||||
if not "hot_reload" in plugin.flags and refresh:
|
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")
|
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.plugins[plugin.name].stop(self.loop)
|
self.plugins[plugin.name].stop()
|
||||||
self.plugins.pop(plugin.name, None)
|
self.plugins.pop(plugin.name, None)
|
||||||
|
self.callsigns.pop(plugin.callsign, None)
|
||||||
if plugin.passive:
|
if plugin.passive:
|
||||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||||
self.plugins[plugin.name] = plugin.start(self.loop)
|
callsign = str(time())
|
||||||
|
plugin.callsign = callsign
|
||||||
|
self.plugins[plugin.name] = plugin.start()
|
||||||
|
self.callsigns[callsign] = plugin
|
||||||
self.logger.info(f"Loaded {plugin.name}")
|
self.logger.info(f"Loaded {plugin.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Could not load {file}. {e}")
|
self.logger.error(f"Could not load {file}. {e}")
|
||||||
|
print_exc()
|
||||||
finally:
|
finally:
|
||||||
if refresh:
|
if refresh:
|
||||||
self.loop.create_task(self.refresh_iframe())
|
self.loop.create_task(self.refresh_iframe())
|
||||||
@@ -99,14 +108,15 @@ class Loader:
|
|||||||
self.logger.info(f"found plugin: {directory}")
|
self.logger.info(f"found plugin: {directory}")
|
||||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
|
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
|
||||||
|
|
||||||
async def reload_plugins(self, request=None):
|
async def handle_reloads(self):
|
||||||
self.logger.info("Re-importing plugins.")
|
while True:
|
||||||
self.import_plugins()
|
args = await self.reload_queue.get()
|
||||||
|
self.import_plugin(*args)
|
||||||
|
|
||||||
async def handle_plugin_method_call(self, plugin_name, method_name, **kwargs):
|
async def handle_plugin_method_call(self, callsign, method_name, **kwargs):
|
||||||
if method_name.startswith("_"):
|
if method_name.startswith("_"):
|
||||||
raise RuntimeError("Tried to call private method")
|
raise RuntimeError("Tried to call private method")
|
||||||
return await self.plugins[plugin_name].execute_method(method_name, kwargs)
|
return await self.callsigns[callsign].execute_method(method_name, kwargs)
|
||||||
|
|
||||||
async def get_steam_resource(self, request):
|
async def get_steam_resource(self, request):
|
||||||
tab = (await get_tabs())[0]
|
tab = (await get_tabs())[0]
|
||||||
@@ -116,7 +126,7 @@ class Loader:
|
|||||||
return web.Response(text=str(e), status=400)
|
return web.Response(text=str(e), status=400)
|
||||||
|
|
||||||
async def load_plugin_main_view(self, request):
|
async def load_plugin_main_view(self, request):
|
||||||
plugin = self.plugins[request.match_info["name"]]
|
plugin = self.callsigns[request.match_info["name"]]
|
||||||
|
|
||||||
# open up the main template
|
# open up the main template
|
||||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||||
@@ -124,14 +134,14 @@ class Loader:
|
|||||||
# setup the main script, plugin, and pull in the template
|
# setup the main script, plugin, and pull in the template
|
||||||
ret = f"""
|
ret = f"""
|
||||||
<script src="/static/library.js"></script>
|
<script src="/static/library.js"></script>
|
||||||
<script>const plugin_name = '{plugin.name}' </script>
|
<script>const plugin_name = '{plugin.callsign}' </script>
|
||||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
|
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.callsign}/">
|
||||||
{template_data}
|
{template_data}
|
||||||
"""
|
"""
|
||||||
return web.Response(text=ret, content_type="text/html")
|
return web.Response(text=ret, content_type="text/html")
|
||||||
|
|
||||||
async def handle_sub_route(self, request):
|
async def handle_sub_route(self, request):
|
||||||
plugin = self.plugins[request.match_info["name"]]
|
plugin = self.callsigns[request.match_info["name"]]
|
||||||
route_path = request.match_info["path"]
|
route_path = request.match_info["path"]
|
||||||
self.logger.info(path)
|
self.logger.info(path)
|
||||||
|
|
||||||
@@ -144,7 +154,7 @@ class Loader:
|
|||||||
return web.Response(text=ret)
|
return web.Response(text=ret)
|
||||||
|
|
||||||
async def load_plugin_tile_view(self, request):
|
async def load_plugin_tile_view(self, request):
|
||||||
plugin = self.plugins[request.match_info["name"]]
|
plugin = self.callsigns[request.match_info["name"]]
|
||||||
|
|
||||||
inner_content = ""
|
inner_content = ""
|
||||||
|
|
||||||
@@ -160,7 +170,7 @@ class Loader:
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/static/styles.css">
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
<script src="/static/library.js"></script>
|
<script src="/static/library.js"></script>
|
||||||
<script>const plugin_name = '{plugin.name}';</script>
|
<script>const plugin_name = '{plugin.callsign}';</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="height: fit-content; display: block;">
|
<body style="height: fit-content; display: block;">
|
||||||
{inner_content}
|
{inner_content}
|
||||||
|
|||||||
+21
-15
@@ -18,15 +18,19 @@ from jinja2 import FileSystemLoader
|
|||||||
from os import path
|
from os import path
|
||||||
from asyncio import get_event_loop, sleep
|
from asyncio import get_event_loop, sleep
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
from loader import Loader
|
from loader import Loader
|
||||||
from injector import inject_to_tab, get_tab, tab_has_element
|
from injector import inject_to_tab, get_tab, tab_has_element
|
||||||
from utilities import Utilities
|
from utilities import Utilities
|
||||||
from browser import PluginBrowser
|
from browser import PluginBrowser
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger("Main")
|
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:
|
class PluginManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.loop = get_event_loop()
|
self.loop = get_event_loop()
|
||||||
@@ -37,6 +41,7 @@ class PluginManager:
|
|||||||
|
|
||||||
jinja_setup(self.web_app, loader=FileSystemLoader(path.join(path.dirname(__file__), 'templates')))
|
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(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.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.method_call_listener())
|
||||||
self.loop.create_task(self.loader_reinjector())
|
self.loop.create_task(self.loader_reinjector())
|
||||||
@@ -47,6 +52,21 @@ class PluginManager:
|
|||||||
if context["message"] == "Unclosed connection":
|
if context["message"] == "Unclosed connection":
|
||||||
return
|
return
|
||||||
loop.default_exception_handler(context)
|
loop.default_exception_handler(context)
|
||||||
|
|
||||||
|
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):
|
||||||
|
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):
|
async def resolve_method_call(self, tab, call_id, response):
|
||||||
await tab._send_devtools_cmd({
|
await tab._send_devtools_cmd({
|
||||||
@@ -88,20 +108,6 @@ class PluginManager:
|
|||||||
if not "id" in data and data["method"] == "Runtime.consoleAPICalled" and data["params"]["type"] == "debug":
|
if not "id" in data and data["method"] == "Runtime.consoleAPICalled" and data["params"]["type"] == "debug":
|
||||||
method = loads(data["params"]["args"][0]["value"])
|
method = loads(data["params"]["args"][0]["value"])
|
||||||
self.loop.create_task(self.handle_method_call(method, tab))
|
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):
|
|
||||||
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):
|
def run(self):
|
||||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from importlib.util import spec_from_file_location, module_from_spec
|
from importlib.util import spec_from_file_location, module_from_spec
|
||||||
from asyncio import get_event_loop, start_unix_server, open_unix_connection, sleep, Lock
|
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 os import path, setuid
|
||||||
from json import loads, dumps, load
|
from json import loads, dumps, load
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
@@ -25,6 +25,7 @@ class PluginWrapper:
|
|||||||
self.passive = not path.isfile(self.file)
|
self.passive = not path.isfile(self.file)
|
||||||
|
|
||||||
def _init(self):
|
def _init(self):
|
||||||
|
set_event_loop(new_event_loop())
|
||||||
if self.passive:
|
if self.passive:
|
||||||
return
|
return
|
||||||
setuid(0 if "root" in self.flags else 1000)
|
setuid(0 if "root" in self.flags else 1000)
|
||||||
@@ -46,6 +47,8 @@ class PluginWrapper:
|
|||||||
data = loads((await reader.readline()).decode("utf-8"))
|
data = loads((await reader.readline()).decode("utf-8"))
|
||||||
if "stop" in data:
|
if "stop" in data:
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
while get_event_loop().is_running():
|
||||||
|
await sleep(0)
|
||||||
get_event_loop().close()
|
get_event_loop().close()
|
||||||
return
|
return
|
||||||
d = {"res": None, "success": True}
|
d = {"res": None, "success": True}
|
||||||
@@ -67,17 +70,16 @@ class PluginWrapper:
|
|||||||
except:
|
except:
|
||||||
await sleep(0)
|
await sleep(0)
|
||||||
|
|
||||||
def start(self, loop):
|
def start(self):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
return self
|
return self
|
||||||
executor = ProcessPoolExecutor()
|
get_event_loop().run_in_executor(
|
||||||
loop.run_in_executor(
|
ProcessPoolExecutor(),
|
||||||
executor,
|
|
||||||
self._init
|
self._init
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def stop(self, loop):
|
def stop(self):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
return
|
return
|
||||||
async def _(self):
|
async def _(self):
|
||||||
@@ -85,7 +87,7 @@ class PluginWrapper:
|
|||||||
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
|
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
|
||||||
await self.writer.drain()
|
await self.writer.drain()
|
||||||
self.writer.close()
|
self.writer.close()
|
||||||
loop.create_task(_(self))
|
get_event_loop().create_task(_(self))
|
||||||
|
|
||||||
async def execute_method(self, method_name, kwargs):
|
async def execute_method(self, method_name, kwargs):
|
||||||
if self.passive:
|
if self.passive:
|
||||||
|
|||||||
@@ -19,20 +19,28 @@ function installPlugin(request_id) {
|
|||||||
|
|
||||||
function addPluginInstallPrompt(artifact, version, request_id) {
|
function addPluginInstallPrompt(artifact, version, request_id) {
|
||||||
let text = `
|
let text = `
|
||||||
<div id="plugin_install_prompt_${request_id}" style="display: block; background: #304375; border-radius: 5px;">
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
<h3 style="padding-left: 1rem;">Install plugin</h3>
|
|
||||||
<ul style="padding-left: 10px; padding-right: 10px; padding-bottom: 20px; margin: 0;">
|
<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;">
|
||||||
<li>${artifact}</li>
|
<h3>Install Plugin?</h3>
|
||||||
<li>${version}</li>
|
<p style="font-size: 12px;">
|
||||||
</ul>
|
${artifact}
|
||||||
<div style="text-align: center; padding-bottom: 10px;">
|
Version: ${version}
|
||||||
<button onclick="installPlugin('${request_id}')" style="display: inline-block; background-color: green;">Install</button>
|
</p>
|
||||||
<button onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))"
|
<button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
|
||||||
style="display: inline-block; background-color: red;">Ignore</button>
|
onclick="installPlugin('${request_id}')">
|
||||||
</div>
|
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>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('plugin_install_list').innerHTML += text;
|
document.getElementById('plugin_install_list').innerHTML = text;
|
||||||
|
|
||||||
|
execute_in_tab('SP', false, 'FocusNavController.DispatchVirtualButtonClick(28)')
|
||||||
}
|
}
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -7,64 +7,70 @@
|
|||||||
});
|
});
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
function loadPlugin(name) {
|
function loadPlugin(callsign, name) {
|
||||||
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
|
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
|
||||||
location.href = `/plugins/load_main/${name}`;
|
location.href = `/plugins/load_main/${callsign}`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% if not plugins|length %}
|
{% 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"
|
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||||
style="--indent-level:0;">
|
<div class="quickaccesscontrols_EmptyNotifications_3ZjbM" style="padding-top:7px;">
|
||||||
<div class="basicdialog_FieldChildren_279n8" style="color: white; font-size: large; padding-top: 10px;">
|
No plugins installed
|
||||||
No plugins installed :(
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
{% if plugin.tile_view_html|length %}
|
{% if plugin.tile_view_html|length %}
|
||||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||||
<div onclick="loadPlugin('{{ plugin.name }}')"
|
<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"
|
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;">
|
style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;">
|
||||||
<iframe id="tile_view_iframe_{{ plugin.name }}" style="display:block; padding: 0; border: none;" scrolling="no"
|
<iframe id="tile_view_iframe_{{ plugin.callsign }}"
|
||||||
src="/plugins/load_tile/{{ plugin.name }}"></iframe>
|
scrolling="no" marginwidth="0" marginheight="0"
|
||||||
<script>
|
hspace="0" vspace="0" frameborder="0"
|
||||||
(function() {
|
style="border-radius: 2px;"
|
||||||
let iframe = document.getElementById("tile_view_iframe_{{ plugin.name }}");
|
src="/plugins/load_tile/{{ plugin.callsign }}">
|
||||||
tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.name }}"));
|
</iframe>
|
||||||
iframe.onload = function() {
|
<script>
|
||||||
let html = iframe.contentWindow.document.children[0];
|
(function() {
|
||||||
let last_height = 0;
|
let iframe = document.getElementById("tile_view_iframe_{{ plugin.callsign }}");
|
||||||
setInterval(function() {
|
tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.callsign }}"));
|
||||||
let height = iframe.contentWindow.document.children[0].scrollHeight;
|
|
||||||
if (height != last_height) {
|
iframe.onload = function() {
|
||||||
iframe.height = height + "px";
|
let html = iframe.contentWindow.document.children[0];
|
||||||
last_height = height;
|
let last_height = 0;
|
||||||
}
|
|
||||||
}, 100);
|
setInterval(function() {
|
||||||
iframe.contentWindow.document.body.onclick = function () {
|
let height = iframe.contentWindow.document.children[0].scrollHeight;
|
||||||
loadPlugin('{{ plugin.name }}');
|
if (height != last_height) {
|
||||||
};
|
iframe.height = height + "px";
|
||||||
}
|
last_height = height;
|
||||||
})();
|
}
|
||||||
</script>
|
}, 100);
|
||||||
</div>
|
|
||||||
</div>
|
iframe.contentWindow.document.body.onclick = function () {
|
||||||
{% else %}
|
loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}');
|
||||||
<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;">
|
</script>
|
||||||
<div class="basicdialog_FieldChildren_279n8">
|
</div>
|
||||||
<button type="button" tabindex="0"
|
</div>
|
||||||
class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name
|
{% else %}
|
||||||
}}</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
aiohttp-jinja2==1.5.0
|
aiohttp-jinja2==1.5.0
|
||||||
watchdog==2.1.7
|
watchdog==2.1.7
|
||||||
multiprocess==0.70.12.2
|
|
||||||
Reference in New Issue
Block a user