Overhaul the web UI
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import importlib
|
||||
import json
|
||||
import queue
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
@@ -11,7 +14,7 @@ import logging
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
from flask import Flask
|
||||
from flask import Flask, abort, send_file
|
||||
from flask_cors import CORS
|
||||
build_folder = 'web_interface/build'
|
||||
app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='')
|
||||
@@ -49,6 +52,9 @@ g.health = 20
|
||||
g.food = 20
|
||||
g.sand_origin = None
|
||||
g.reload_lock = threading.RLock()
|
||||
_item_icon_cache = Path(__file__).resolve().parent / 'web_interface' / 'item_cache'
|
||||
_item_icon_locks = {}
|
||||
_item_icon_locks_guard = threading.Lock()
|
||||
|
||||
def _api_json_default(value):
|
||||
"""Serialize live protocol objects without invoking fragile __repr__ code."""
|
||||
@@ -60,6 +66,13 @@ def _api_json_default(value):
|
||||
return {'__type__': type(value).__name__,
|
||||
'packet_name': getattr(value, 'packet_name', None),
|
||||
'fields': fields}
|
||||
if (type(value).__name__ == 'Slot' and
|
||||
all(hasattr(value, key) for key in
|
||||
('present', 'item_id', 'item_count'))):
|
||||
return {'present': value.present, 'item_id': value.item_id,
|
||||
'item_count': value.item_count}
|
||||
if all(hasattr(value, key) for key in ('x', 'y', 'z')):
|
||||
return {'x': value.x, 'y': value.y, 'z': value.z}
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
return value.hex()
|
||||
try:
|
||||
@@ -70,15 +83,84 @@ def _api_json_default(value):
|
||||
|
||||
@app.route('/api/global')
|
||||
def api_global():
|
||||
data = json.dumps(g, default=_api_json_default, indent=4)
|
||||
|
||||
snapshot = dict(g)
|
||||
job = g.get('job')
|
||||
job_state = getattr(job, 'state', None) if job else None
|
||||
snapshot['job_state'] = getattr(job_state, '__name__', 'idle')
|
||||
chunks = g.get('chunks')
|
||||
if chunks is not None and hasattr(chunks, 'chunks'):
|
||||
snapshot['chunk_state'] = {
|
||||
'loaded': len(chunks.chunks),
|
||||
'center': chunks.center,
|
||||
'radius': chunks.radius,
|
||||
'dimension': chunks.dimension,
|
||||
}
|
||||
response = app.response_class(
|
||||
response=data,
|
||||
response=json.dumps(snapshot, default=_api_json_default),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/api/items')
|
||||
def api_items():
|
||||
from mosfet.info.items import ITEM_NAMES
|
||||
data = {str(item_id): name for item_id, name in ITEM_NAMES.items()}
|
||||
return app.response_class(
|
||||
response=json.dumps(data), status=200, mimetype='application/json')
|
||||
|
||||
|
||||
def _item_texture_urls(name):
|
||||
root = 'https://assets.mcasset.cloud/26.3/assets/minecraft/textures'
|
||||
yield root + '/item/{}.png'.format(name)
|
||||
yield root + '/block/{}.png'.format(name)
|
||||
simplified = re.sub(
|
||||
r'_(slab|stairs|wall|fence_gate|fence|door|trapdoor|button|'
|
||||
r'pressure_plate|sign|hanging_sign|carpet|banner|bed|candle)$',
|
||||
'', name)
|
||||
if simplified != name:
|
||||
yield root + '/block/{}.png'.format(simplified)
|
||||
yield root + '/item/{}.png'.format(simplified)
|
||||
|
||||
|
||||
def _fetch_item_texture(name):
|
||||
for url in _item_texture_urls(name):
|
||||
request = urllib.request.Request(
|
||||
url, headers={'User-Agent': 'curl/8.0 Mosfet/26.3'})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=4) as response:
|
||||
image = response.read(2 * 1024 * 1024 + 1)
|
||||
if len(image) <= 2 * 1024 * 1024 and image.startswith(b'\x89PNG\r\n\x1a\n'):
|
||||
return image
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@app.route('/api/item-icon/<name>')
|
||||
def api_item_icon(name):
|
||||
from mosfet.info.items import ITEM_NAMES
|
||||
if not re.fullmatch(r'[a-z0-9_]+', name):
|
||||
abort(404)
|
||||
if name not in set(ITEM_NAMES.values()):
|
||||
abort(404)
|
||||
|
||||
with _item_icon_locks_guard:
|
||||
lock = _item_icon_locks.setdefault(name, threading.Lock())
|
||||
_item_icon_cache.mkdir(parents=True, exist_ok=True)
|
||||
cached = _item_icon_cache / (name + '.png')
|
||||
with lock:
|
||||
if not cached.is_file():
|
||||
image = _fetch_item_texture(name)
|
||||
if image is None:
|
||||
abort(404)
|
||||
temporary = cached.with_suffix('.tmp')
|
||||
temporary.write_bytes(image)
|
||||
temporary.replace(cached)
|
||||
return send_file(cached, mimetype='image/png', max_age=86400)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def root():
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
Reference in New Issue
Block a user