19 Commits

Author SHA1 Message Date
fb9cb50a90 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 44s
2025-01-28 21:57:26 +11:00
db5e672d7b feat: Update bootstrap and remove sidebar toggle
All checks were successful
Build Docker / Build Image (push) Successful in 49s
2025-01-28 21:56:05 +11:00
8ec23e3a32 feat: Add volume mount info for docker to readme 2025-01-28 17:38:02 +11:00
f84359a74b feat: Move plugin signatures to user data dir
All checks were successful
Build Docker / Build Image (push) Successful in 44s
2025-01-28 17:36:25 +11:00
92f05992ab fix: Add git to docker image
All checks were successful
Build Docker / Build Image (push) Successful in 48s
2025-01-28 17:33:14 +11:00
2ae618c68a feat: Add custom plugins
All checks were successful
Build Docker / Build Image (push) Successful in 46s
2025-01-28 17:26:59 +11:00
cfb814d006 feat: Add public info and troubleshooting modules
All checks were successful
Build Docker / Build Image (push) Successful in 41s
2025-01-28 16:46:06 +11:00
8c61a09e5b fix: Add more logging for batch errors
All checks were successful
Build Docker / Build Image (push) Successful in 44s
2025-01-28 16:30:30 +11:00
5c61bad9a2 fix: Update renewal plugin to get IP from env
All checks were successful
Build Docker / Build Image (push) Successful in 46s
2025-01-28 16:26:24 +11:00
209c3794fc Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 45s
2025-01-14 15:55:55 +11:00
8099320673 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 37s
2024-11-22 09:48:07 +11:00
aa92220756 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 41s
2024-11-21 19:35:56 +11:00
2595503dc0 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 39s
2024-11-21 16:22:07 +11:00
d516e91592 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 33s
2024-11-21 15:21:38 +11:00
b24a3147dd Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 28s
2024-02-17 12:53:32 +11:00
f8e03aca73 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 1m11s
2024-02-17 11:57:24 +11:00
38f08c069c Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 57s
2024-02-13 11:52:04 +11:00
16ac6c7d2b Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 1m1s
2024-02-13 10:01:21 +11:00
b0c7fcf779 Merge branch 'dev'
All checks were successful
Build Docker / Build Image (push) Successful in 20s
2024-02-12 22:08:02 +11:00
28 changed files with 208 additions and 62 deletions

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ plugins/signatures.json
.venv/
user_data/
user_data/
customPlugins/

View File

@@ -10,6 +10,7 @@ COPY . /app
# Add mount point for data volume
# VOLUME /data
RUN apk add git
ENTRYPOINT ["python3"]
CMD ["server.py"]

Binary file not shown.

View File

@@ -34,7 +34,6 @@ Then access the wallet at http://localhost:5000
Also available as a docker image:
To run using a HSD running directly on the host:
```bash
@@ -47,6 +46,8 @@ If you have HSD running on a different IP/container
sudo docker run -p 5000:5000 -e hsd_api=yourapikeyhere -e hsd_ip=hsdcontainer git.woodburn.au/nathanwoodburn/firewallet:latest
```
For Docker you can mount a volume to persist the user data (/app/user_data)
## Features
- Basic wallet functionality
- Create new wallet

22
main.py
View File

@@ -1300,8 +1300,8 @@ def plugins_index():
wallet_status=account_module.getWalletStatus(),
plugins=plugins)
@app.route('/plugin/<plugin>')
def plugin(plugin):
@app.route('/plugin/<ptype>/<path:plugin>')
def plugin(ptype,plugin):
# Check if the user is logged in
if request.cookies.get("account") is None:
return redirect("/login")
@@ -1310,7 +1310,10 @@ def plugin(plugin):
if not account:
return redirect("/logout")
plugin = f"{ptype}/{plugin}"
if not plugins_module.pluginExists(plugin):
print(f"Plugin {plugin} not found")
return redirect("/plugins")
data = plugins_module.getPluginData(plugin)
@@ -1330,10 +1333,10 @@ def plugin(plugin):
wallet_status=account_module.getWalletStatus(),
name=data['name'],description=data['description'],
author=data['author'],version=data['version'],
functions=functions,error=error)
source=data['source'],functions=functions,error=error)
@app.route('/plugin/<plugin>/verify')
def plugin_verify(plugin):
@app.route('/plugin/<ptype>/<path:plugin>/verify')
def plugin_verify(ptype,plugin):
# Check if the user is logged in
if request.cookies.get("account") is None:
return redirect("/login")
@@ -1341,6 +1344,8 @@ def plugin_verify(plugin):
account = account_module.check_account(request.cookies.get("account"))
if not account:
return redirect("/logout")
plugin = f"{ptype}/{plugin}"
if not plugins_module.pluginExists(plugin):
return redirect("/plugins")
@@ -1352,8 +1357,8 @@ def plugin_verify(plugin):
return redirect("/plugin/" + plugin)
@app.route('/plugin/<plugin>/<function>', methods=["POST"])
def plugin_function(plugin,function):
@app.route('/plugin/<ptype>/<path:plugin>/<function>', methods=["POST"])
def plugin_function(ptype,plugin,function):
# Check if the user is logged in
if request.cookies.get("account") is None:
return redirect("/login")
@@ -1362,6 +1367,8 @@ def plugin_function(plugin,function):
if not account:
return redirect("/logout")
plugin = f"{ptype}/{plugin}"
if not plugins_module.pluginExists(plugin):
return redirect("/plugins")
@@ -1394,7 +1401,6 @@ def plugin_function(plugin,function):
return redirect("/plugin/" + plugin + "?error=" + response['error'])
response = render.plugin_output(response,plugins_module.getPluginFunctionReturns(plugin,function))
return render_template("plugin-output.html", account=account, sync=account_module.getNodeSync(),
wallet_status=account_module.getWalletStatus(),
name=data['name'],description=data['description'],output=response)

View File

@@ -3,10 +3,12 @@ import json
import importlib
import sys
import hashlib
import subprocess
def listPlugins():
plugins = []
customPlugins = []
for file in os.listdir("plugins"):
if file.endswith(".py"):
if file != "main.py":
@@ -14,17 +16,50 @@ def listPlugins():
if "info" not in dir(plugin):
continue
details = plugin.info
details["link"] = file[:-3]
details["source"] = "built-in"
details["link"] = f"plugins/{file[:-3]}"
plugins.append(details)
# Check for imported plugins
if not os.path.exists("user_data/plugins.json"):
with open("user_data/plugins.json", "w") as f:
json.dump([], f)
with open("user_data/plugins.json", "r") as f:
importurls = json.load(f)
for importurl in importurls:
# Get only repo name
importPath = importurl.split("/")[-1].removesuffix(".git")
# Git clone into customPlugins/<importPath>
if not os.path.exists(f"customPlugins/{importPath}"):
if os.system(f"git clone {importurl} customPlugins/{importPath}") != 0:
continue
else:
if os.system(f"cd customPlugins/{importPath} && git pull") != 0:
continue
# Import plugins from customPlugins/<importPath>
for file in os.listdir(f"customPlugins/{importPath}"):
if file.endswith(".py"):
if file != "main.py":
plugin = importlib.import_module(f"customPlugins.{importPath}."+file[:-3])
if "info" not in dir(plugin):
continue
details = plugin.info
details["source"] = importPath
details["link"] = f"customPlugins/{importPath}/{file[:-3]}"
plugins.append(details)
# Verify plugin signature
signatures = []
try:
with open("plugins/signatures.json", "r") as f:
with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f)
except:
# Write a new signatures file
with open("plugins/signatures.json", "w") as f:
with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f)
for plugin in plugins:
@@ -39,34 +74,31 @@ def listPlugins():
def pluginExists(plugin: str):
for file in os.listdir("plugins"):
if file == plugin+".py":
return True
return False
return os.path.exists(plugin+".py")
def verifyPlugin(plugin: str):
signatures = []
try:
with open("plugins/signatures.json", "r") as f:
with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f)
except:
# Write a new signatures file
with open("plugins/signatures.json", "w") as f:
with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f)
# Hash the plugin file
pluginHash = hashPlugin(plugin)
if pluginHash not in signatures:
signatures.append(pluginHash)
with open("plugins/signatures.json", "w") as f:
with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f)
def hashPlugin(plugin: str):
BUF_SIZE = 65536
sha256 = hashlib.sha256()
with open("plugins/"+plugin+".py", 'rb') as f:
with open(plugin+".py", 'rb') as f:
while True:
data = f.read(BUF_SIZE)
if not data:
@@ -76,19 +108,31 @@ def hashPlugin(plugin: str):
def getPluginData(pluginStr: str):
plugin = importlib.import_module("plugins."+pluginStr)
plugin = importlib.import_module(pluginStr.replace("/","."))
# Check if the plugin is verified
signatures = []
try:
with open("plugins/signatures.json", "r") as f:
with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f)
except:
# Write a new signatures file
with open("plugins/signatures.json", "w") as f:
with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f)
info = plugin.info
info["source"] = "built-in"
# Check if the plugin is in customPlugins
if pluginStr.startswith("customPlugins"):
# Get git url for dir
print(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin")
url = subprocess.check_output(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin", shell=True).decode("utf-8").strip()
info["source"] = url
# Hash the plugin file
pluginHash = hashPlugin(pluginStr)
if pluginHash not in signatures:
@@ -100,12 +144,12 @@ def getPluginData(pluginStr: str):
def getPluginFunctions(plugin: str):
plugin = importlib.import_module("plugins."+plugin)
plugin = importlib.import_module(plugin.replace("/","."))
return plugin.functions
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str):
plugin_module = importlib.import_module("plugins."+plugin)
plugin_module = importlib.import_module(plugin.replace("/","."))
if function not in plugin_module.functions:
return {"error": "Function not found"}
@@ -118,11 +162,11 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
# Check if the function is in the signature list
signatures = []
try:
with open("plugins/signatures.json", "r") as f:
with open("user_data/plugin_signatures.json", "r") as f:
signatures = json.load(f)
except:
# Write a new signatures file
with open("plugins/signatures.json", "w") as f:
with open("user_data/plugin_signatures.json", "w") as f:
json.dump(signatures, f)
# Hash the plugin file
@@ -141,12 +185,12 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
def getPluginFunctionInputs(plugin: str, function: str):
plugin = importlib.import_module("plugins."+plugin)
plugin = importlib.import_module(plugin.replace("/","."))
return plugin.functions[function]["params"]
def getPluginFunctionReturns(plugin: str, function: str):
plugin = importlib.import_module("plugins."+plugin)
plugin = importlib.import_module(plugin.replace("/","."))
return plugin.functions[function]["returns"]

View File

@@ -500,10 +500,6 @@ def advancedChangeLookahead(params, authentication):
lookahead = int(lookahead)
wallet = authentication.split(":")[0]
password = ":".join(authentication.split(":")[1:])
# curl http://x:api-key@127.0.0.1:14039/wallet/$id/account/$name \
# -X PATCH \
# --data '{"lookahead": $lookahead}'
APIKEY = os.getenv("hsd_api")
ip = os.getenv("hsd_ip")
if ip is None:

114
plugins/customPlugins.py Normal file
View File

@@ -0,0 +1,114 @@
import json
import account
import requests
import os
# Plugin Data
info = {
"name": "Custom Plugin Manager",
"description": "Import custom plugins from git repositories",
"version": "1.0",
"author": "Nathan.Woodburn/"
}
# Functions
functions = {
"add":{
"name": "Add Plugin repo",
"type": "default",
"description": "Add a plugin repo",
"params": {
"url": {
"name":"URL",
"type":"text"
}
},
"returns": {
"status":
{
"name": "Status of the function",
"type": "text"
}
}
},
"remove":{
"name": "Remove Plugins",
"type": "default",
"description": "Remove a plugin repo from the list",
"params": {
"url": {
"name":"URL",
"type":"text"
}
},
"returns": {
"status":
{
"name": "Status of the function",
"type": "text"
}
}
},
"list":{
"name": "List Plugins",
"type": "default",
"description": "List all imported plugins",
"params": {},
"returns": {
"plugins":
{
"name": "List of plugins",
"type": "list"
}
}
}
}
def add(params, authentication):
url = params["url"]
if not os.path.exists("user_data/plugins.json"):
with open("user_data/plugins.json", "w") as f:
json.dump([], f)
with open("user_data/plugins.json", "r") as f:
importurls = json.load(f)
# Check if the plugin is already imported
if url in importurls:
return {"status": "Plugin already imported"}
importurls.append(url)
with open("user_data/plugins.json", "w") as f:
json.dump(importurls, f)
return {"status": "Imported"}
def remove(params, authentication):
url = params["url"]
if not os.path.exists("user_data/plugins.json"):
with open("user_data/plugins.json", "w") as f:
json.dump([], f)
with open("user_data/plugins.json", "r") as f:
importurls = json.load(f)
# Check if the plugin is already imported
if url not in importurls:
return {"status": "Plugin not imported"}
importurls.remove(url)
with open("user_data/plugins.json", "w") as f:
json.dump(importurls, f)
return {"status": "Removed"}
def list(params, authentication):
if not os.path.exists("user_data/plugins.json"):
with open("user_data/plugins.json", "w") as f:
json.dump([], f)
with open("user_data/plugins.json", "r") as f:
importurls = json.load(f)
return {"plugins": importurls}

View File

@@ -51,10 +51,11 @@ def main(params, authentication):
# Unlock wallet
api_key = os.getenv("hsd_api")
ip = os.getenv("hsd_ip")
if api_key is None:
print("API key not set")
return {"status": "API key not set", "transaction": "None"}
response = requests.post(f'http://x:{api_key}@127.0.0.1:12039/wallet/{wallet}/unlock',
response = requests.post(f'http://x:{api_key}@{ip}:12039/wallet/{wallet}/unlock',
json={'passphrase': password, 'timeout': 600})
if response.status_code != 200:
print("Failed to unlock wallet")
@@ -73,11 +74,11 @@ def main(params, authentication):
batchTX = "[" + ", ".join(batch) + "]"
responseContent = f'{{"method": "sendbatch","params":[ {batchTX} ]}}'
response = requests.post(f'http://x:{api_key}@127.0.0.1:12039', data=responseContent)
response = requests.post(f'http://x:{api_key}@{ip}:12039', data=responseContent)
if response.status_code != 200:
print("Failed to create batch")
print(f'Status code: {response.status_code}')
print(f'Response: {response.text}')
print("Failed to create batch",flush=True)
print(f'Status code: {response.status_code}',flush=True)
print(f'Response: {response.text}',flush=True)
return {"status": "Failed", "transaction": "None"}
batch = response.json()
@@ -85,9 +86,9 @@ def main(params, authentication):
print("Verifying tx...")
if batch["error"]:
if batch["error"] != "":
print("Failed to verify batch")
print(batch["error"]["message"])
return {"status": "Failed", "transaction": "None"}
print("Failed to verify batch",flush=True)
print(batch["error"]["message"],flush=True)
return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"}
if 'result' in batch:
if batch['result'] != None:

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -2,7 +2,7 @@
<div class="card shadow border-start-warning py-2">
<div class="card-body">
<div class="text-uppercase fw-bold text-xs mb-1"><span style="color: var(--bs-dark);">{{name}}</span></div>
<div class="text-dark fw-bold h5 mb-0"><span>{{output}}</span></div>
<div class="text-dark fw-bold h5 mb-0"><span>{{output | safe}}</span></div>
</div>
</div>
</div>

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
@@ -67,7 +66,7 @@
<div class="container-fluid" style="margin-bottom: 20px;">
<h3 class="text-dark mb-1">{{name}}</h3>
<h4 class="text-dark mb-1">{{description}}</h4>
<h6 class="text-dark mb-1">Author: {{author}}<br>Version: {{version}}</h6>{{functions|safe}}
<h6 class="text-dark mb-1">Author: {{author}}<br>Version: {{version}}<br>Source: {{source}}</h6>{{functions|safe}}
</div>
</div>
<footer class="sticky-footer" style="background: var(--bs-primary-text-emphasis);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -36,7 +36,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">

View File

@@ -35,7 +35,6 @@
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
</ul>
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
</div>
</nav>
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">