diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2585c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ +__pycache__/ +config.cfg diff --git a/dance.py b/dance.py new file mode 100644 index 0000000..331e1bb --- /dev/null +++ b/dance.py @@ -0,0 +1,306 @@ +import asyncio +import json +import logging +import re +import socket +import time +from configparser import ConfigParser +from enum import Enum + +import hid +from aiohttp import WSMsgType, web +from pyjoycon import ButtonEventJoyCon, JoyCon +from pyjoycon.constants import JOYCON_PRODUCT_IDS, JOYCON_VENDOR_ID + +from joydance import JoyDance, PairingState +from joydance.constants import DEFAULT_CONFIG, JOYDANCE_VERSION + +logging.getLogger('asyncio').setLevel(logging.WARNING) + + +class WsCommand(Enum): + GET_JOYCON_LIST = 'get_joycon_list' + CONNECT_JOYCON = 'connect_joycon' + DISCONNECT_JOYCON = 'disconnect_joycon' + UPDATE_JOYCON_STATE = 'update_joycon_state' + + +class PairingMethod(Enum): + DEFAULT = 'default' + FAST = 'fast' + + +REGEX_PAIRING_CODE = re.compile(r'^\d{6}$') +REGEX_LOCAL_IP_ADDRESS = re.compile(r'^192\.168\.((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.)(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$') + + +async def get_device_ids(): + devices = hid.enumerate(JOYCON_VENDOR_ID, 0) + + out = [] + for device in devices: + vendor_id = device['vendor_id'] + product_id = device['product_id'] + product_string = device['product_string'] + serial = device.get('serial') or device.get('serial_number') + + if product_id not in JOYCON_PRODUCT_IDS: + continue + + if not product_string: + continue + + out.append({ + 'vendor_id': vendor_id, + 'product_id': product_id, + 'serial': serial, + 'product_string': product_string, + }) + + return out + + +async def get_joycon_list(app): + joycons = [] + devices = await get_device_ids() + + for dev in devices: + if dev['serial'] in app['joycons_info']: + info = app['joycons_info'][dev['serial']] + else: + joycon = JoyCon(dev['vendor_id'], dev['product_id'], dev['serial']) + # Wait for initial data + for _ in range(3): + time.sleep(0.05) + battery_level = joycon.get_battery_level() + if battery_level > 0: + break + + color = '#%02x%02x%02x' % joycon.color_body + joycon.__del__() + + info = { + 'vendor_id': dev['vendor_id'], + 'product_id': dev['product_id'], + 'serial': dev['serial'], + 'name': dev['product_string'], + 'color': color, + 'battery_level': battery_level, + 'is_left': joycon.is_left(), + 'state': PairingState.IDLE.value, + 'pairing_code': '', + } + + app['joycons_info'][dev['serial']] = info + + joycons.append(info) + + return sorted(joycons, key=lambda x: (x['name'], x['color'], x['serial'])) + + +async def connect_joycon(app, ws, data): + async def on_joydance_state_changed(serial, state): + print(serial, state) + app['joycons_info'][serial]['state'] = state.value + try: + await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, app['joycons_info'][serial]) + except Exception as e: + print(e) + + print(data) + + serial = data['joycon_serial'] + product_id = app['joycons_info'][serial]['product_id'] + vendor_id = app['joycons_info'][serial]['vendor_id'] + + pairing_method = data['pairing_method'] + host_ip_addr = data['host_ip_addr'] + console_ip_addr = data['console_ip_addr'] + pairing_code = data['pairing_code'] + + if not is_valid_pairing_method(pairing_method): + return + + if pairing_method == PairingMethod.DEFAULT.value: + if not is_valid_ip_address(host_ip_addr) or not is_valid_pairing_code(pairing_code): + return + + if pairing_method == PairingMethod.FAST.value and not is_valid_ip_address(console_ip_addr): + return + + config_parser = parse_config() + config = dict(config_parser.items('joydance')) + config['pairing_code'] = pairing_code + config['pairing_method'] = pairing_method + config['host_ip_addr'] = host_ip_addr + config['console_ip_addr'] = console_ip_addr + config_parser['joydance'] = config + save_config(config_parser) + + app['joycons_info'][serial]['pairing_code'] = pairing_code + joycon = ButtonEventJoyCon(vendor_id, product_id, serial) + + if pairing_method == PairingMethod.DEFAULT.value: + console_ip_addr = None + + joydance = JoyDance( + joycon, + pairing_code=pairing_code, + host_ip_addr=host_ip_addr, + console_ip_addr=console_ip_addr, + on_state_changed=on_joydance_state_changed, + accel_acquisition_freq_hz=config['accel_acquisition_freq_hz'], + accel_acquisition_latency=config['accel_acquisition_latency'], + accel_max_range=config['accel_max_range'], + ) + app['joydance_connections'][serial] = joydance + + asyncio.create_task(joydance.pair()) + + +async def disconnect_joycon(app, ws, data): + print(data) + serial = data['joycon_serial'] + joydance = app['joydance_connections'][serial] + app['joycons_info'][serial]['state'] = PairingState.IDLE + + await joydance.disconnect() + try: + await ws_send_response(ws, WsCommand.UPDATE_JOYCON_STATE, { + 'joycon_serial': serial, + 'state': PairingState.IDLE.value, + }) + except Exception: + pass + + +def parse_config(): + parser = ConfigParser() + parser.read('config.cfg') + + if 'joydance' not in parser: + parser['joydance'] = DEFAULT_CONFIG + else: + tmp_config = DEFAULT_CONFIG.copy() + for key in tmp_config: + if key in parser['joydance']: + val = parser['joydance'][key] + if key == 'pairing_method': + if not is_valid_pairing_method(val): + val = PairingMethod.DEFAULT.value + elif key == 'host_ip_addr' or key == 'console_ip_addr': + if not(is_valid_ip_address(val)): + val = '' + elif key == 'pairing_code': + if not is_valid_pairing_code(val): + val = '' + elif key.startswith('accel_'): + try: + val = int(val) + except Exception: + val = DEFAULT_CONFIG[key] + + tmp_config[key] = val + + parser['joydance'] = tmp_config + + if not parser['joydance']['host_ip_addr']: + host_ip_addr = get_host_ip() + if host_ip_addr: + parser['joydance']['host_ip_addr'] = host_ip_addr + + save_config(parser) + return parser + + +def is_valid_pairing_code(val): + return re.match(REGEX_PAIRING_CODE, val) is not None + + +def is_valid_ip_address(val): + return re.match(REGEX_LOCAL_IP_ADDRESS, val) is not None + + +def is_valid_pairing_method(val): + return val in [PairingMethod.DEFAULT.value, PairingMethod.FAST.value] + + +def get_host_ip(): + try: + for ip in socket.gethostbyname_ex(socket.gethostname())[2]: + if ip.startswith('192.168'): + return ip + except Exception: + pass + + return None + + +def save_config(parser): + with open('config.cfg', 'w') as fp: + parser.write(fp) + + +async def html_handler(request): + config = dict((parse_config()).items('joydance')) + with open('static/index.html', 'r') as f: + html = f.read() + html = html.replace('[[CONFIG]]', json.dumps(config)) + html = html.replace('[[VERSION]]', JOYDANCE_VERSION) + return web.Response(text=html, content_type='text/html') + + +async def ws_send_response(ws, cmd, data): + resp = { + 'cmd': 'resp_' + cmd.value, + 'data': data, + } + await ws.send_json(resp) + + +async def websocket_handler(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + async for msg in ws: + if msg.type == WSMsgType.TEXT: + msg = msg.json() + try: + cmd = WsCommand(msg['cmd']) + except ValueError: + print('Invalid cmd:', msg['cmd']) + continue + + if cmd == WsCommand.GET_JOYCON_LIST: + joycon_list = await get_joycon_list(request.app) + await ws_send_response(ws, cmd, joycon_list) + elif cmd == WsCommand.CONNECT_JOYCON: + await connect_joycon(request.app, ws, msg['data']) + await ws_send_response(ws, cmd, {}) + elif cmd == WsCommand.DISCONNECT_JOYCON: + await disconnect_joycon(request.app, ws, msg['data']) + await ws_send_response(ws, cmd, {}) + elif msg.type == WSMsgType.ERROR: + print('ws connection closed with exception %s' % + ws.exception()) + + return ws + + +def favicon_handler(request): + return web.FileResponse('static/favicon.png') + + +app = web.Application() +app['joydance_connections'] = {} +app['joycons_info'] = {} + +app.add_routes([ + web.get('/', html_handler), + web.get('/favicon.png', favicon_handler), + web.get('/ws', websocket_handler), + web.static('/css', 'static/css'), + web.static('/js', 'static/js'), +]) + +web.run_app(app, port=32623) diff --git a/joydance/__init__.py b/joydance/__init__.py new file mode 100644 index 0000000..62286f5 --- /dev/null +++ b/joydance/__init__.py @@ -0,0 +1,413 @@ +import asyncio +import json +import random +import socket +import ssl +import time +from enum import Enum + +import aiohttp +import websockets + +from .constants import (ACCEL_ACQUISITION_FREQ_HZ, ACCEL_ACQUISITION_LATENCY, + ACCEL_MAX_RANGE, ACCEL_SEND_RATE, JOYCON_UPDATE_RATE, + SHORTCUT_MAPPING, UBI_APP_ID, UBI_SKU_ID, + WS_SUBPROTOCOL, Command, JoyConButton) + + +class PairingState(Enum): + IDLE = 0 + GETTING_TOKEN = 1 + PAIRING = 2 + CONNECTING = 3 + CONNECTED = 4 + DISCONNECTING = 5 + DISCONNECTED = 10 + + ERROR_JOYCON = 101 + ERROR_CONNECTION = 102 + ERROR_INVALID_PAIRING_CODE = 103 + ERROR_PUNCH_PAIRING = 104 + ERROR_HOLE_PUNCHING = 105 + ERROR_CONSOLE_CONNECTION = 106 + + +class JoyDance: + def __init__( + self, + joycon, + pairing_code=None, + host_ip_addr=None, + console_ip_addr=None, + accel_acquisition_freq_hz=ACCEL_ACQUISITION_FREQ_HZ, + accel_acquisition_latency=ACCEL_ACQUISITION_LATENCY, + accel_max_range=ACCEL_MAX_RANGE, + on_state_changed=None): + self.joycon = joycon + self.joycon_is_left = joycon.is_left() + + if on_state_changed: + self.on_state_changed = on_state_changed + + self.pairing_code = pairing_code + self.host_ip_addr = host_ip_addr + self.console_ip_addr = console_ip_addr + self.host_port = self.get_random_port() + + self.accel_acquisition_freq_hz = accel_acquisition_freq_hz + self.accel_acquisition_latency = accel_acquisition_latency + self.accel_max_range = accel_max_range + + self.number_of_accels_sent = 0 + self.should_start_accelerometer = False + self.is_input_allowed = False + self.available_shortcuts = set() + + self.ws = None + self.disconnected = False + + self.headers = { + 'Ubi-AppId': UBI_APP_ID, + 'X-SkuId': UBI_SKU_ID, + } + + self.console_conn = None + + def get_random_port(self): + ''' Randomize a port number, to be used in hole_punching() later ''' + return random.randrange(39000, 39999) + + async def on_state_changed(state): + pass + + async def get_access_token(self): + ''' Log in using a guest account, pre-defined by Ubisoft ''' + headers = { + 'Authorization': 'UbiMobile_v1 t=NTNjNWRjZGMtZjA2Yy00MTdmLWJkMjctOTNhZTcxNzU1OTkyOlcwM0N5eGZldlBTeFByK3hSa2hhQ05SMXZtdz06UjNWbGMzUmZaVzB3TjJOYTpNakF5TVMweE1DMHlOMVF3TVRvME5sbz0=', + 'Ubi-AppId': UBI_APP_ID, + 'User-Agent': 'UbiServices_SDK_Unity_Light_Mobile_2018.Release.16_ANDROID64_dynamic', + 'Ubi-RequestedPlatformType': 'ubimobile', + 'Content-Type': 'application/json', + } + + async with aiohttp.ClientSession(headers=headers) as session: + async with session.post('https://public-ubiservices.ubi.com/v1/profiles/sessions', json={}, ssl=False) as resp: + if resp.status != 200: + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONNECTION) + raise Exception('ERROR: Couldn\'t get access token!') + + # Add ticket to headers + json_body = await resp.json() + self.headers['Authorization'] = 'Ubi_v1 ' + json_body['ticket'] + + async def send_pairing_code(self): + ''' Send pairing code to JD server ''' + url = 'https://prod.just-dance.com/sessions/v1/pairing-info' + + async with aiohttp.ClientSession(headers=self.headers) as session: + async with session.get(url, params={'code': self.pairing_code}, ssl=False) as resp: + if resp.status != 200: + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_INVALID_PAIRING_CODE) + raise Exception('ERROR: Invalid pairing code!') + + json_body = await resp.json() + self.pairing_url = json_body['pairingUrl'].replace('https://', 'wss://') + 'smartphone' + self.requires_punch_pairing = json_body.get('requiresPunchPairing', False) + + async def send_initiate_punch_pairing(self): + ''' Tell console which IP address & port to connect to ''' + url = 'https://prod.just-dance.com/sessions/v1/initiate-punch-pairing' + json_payload = { + 'pairingCode': self.pairing_code, + 'mobileIP': self.host_ip_addr, + 'mobilePort': self.host_port, + } + + async with aiohttp.ClientSession(headers=self.headers) as session: + async with session.post(url, json=json_payload, ssl=False) as resp: + body = await resp.text() + if body != 'OK': + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_PUNCH_PAIRING) + raise Exception('ERROR: Couldn\'t initiate punch pairing!') + + async def hole_punching(self): + ''' Open a port on this machine so the console can connect to it ''' + try: + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.bind(('0.0.0.0', self.host_port)) + conn.listen(5) + + # Accept incoming connection from console + console_conn, addr = conn.accept() + self.console_conn = console_conn + print('Connected with {}:{}'.format(addr[0], addr[1])) + except Exception as e: + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_HOLE_PUNCHING) + raise e + + async def send_message(self, __class, data={}): + ''' Send JSON message to server ''' + if __class != 'JD_PhoneScoringData': + print('>>>', __class, data) + + msg = {'root': {'__class': __class}} + if data: + msg['root'].update(data) + + # Remove extra spaces from JSON to reduce size + + try: + await self.ws.send(json.dumps(msg, separators=(',', ':'))) + except Exception: + await self.disconnect(close_ws=False) + + async def on_message(self, message): + message = json.loads(message) + print('<<<', message) + + __class = message['__class'] + if __class == 'JD_PhoneDataCmdHandshakeContinue': + await self.send_message('JD_PhoneDataCmdSync', {'phoneID': message['phoneID']}) + elif __class == 'JD_PhoneDataCmdSyncEnd': + await self.send_message('JD_PhoneDataCmdSyncEnd', {'phoneID': message['phoneID']}) + await self.on_state_changed(self.joycon.serial, PairingState.CONNECTED) + elif __class == 'JD_EnableAccelValuesSending_ConsoleCommandData': + self.should_start_accelerometer = True + self.number_of_accels_sent = 0 + elif __class == 'JD_DisableAccelValuesSending_ConsoleCommandData': + self.should_start_accelerometer = False + elif __class == 'InputSetup_ConsoleCommandData': + if message.get('isEnabled', 0) == 1: + self.is_input_allowed = True + elif __class == 'EnableCarousel_ConsoleCommandData': + if message.get('isEnabled', 0) == 1: + self.is_input_allowed = True + elif __class == 'JD_EnableLobbyStartbutton_ConsoleCommandData': + if message.get('isEnabled', 0) == 1: + self.is_input_allowed = True + elif __class == 'ShortcutSetup_ConsoleCommandData': + if message.get('isEnabled', 0) == 1: + self.is_input_allowed = True + elif __class == 'JD_PhoneUiShortcutData': + shortcuts = set() + for item in message.get('shortcuts', []): + if item['__class'] == 'JD_PhoneAction_Shortcut': + try: + shortcuts.add(Command(item['shortcutType'])) + except Exception as e: + print('Unknown Command: ', e) + self.available_shortcuts = shortcuts + elif __class == 'JD_OpenPhoneKeyboard_ConsoleCommandData': + await asyncio.sleep(1) + await self.send_message('JD_CancelKeyboard_PhoneCommandData') + elif __class == 'JD_PhoneUiSetupData': + self.is_input_allowed = True + shortcuts = set() + if message.get('setupData', {}).get('gameplaySetup', {}).get('pauseSlider', {}): + self.available_shortcuts.add(Command.PAUSE) + + if message['isPopup'] == 1: + self.is_input_allowed = True + else: + self.is_input_allowed = (message.get('inputSetup', {}).get('isEnabled', 0) == 1) + + async def send_hello(self): + print('Pairing...') + + await self.send_message('JD_PhoneDataCmdHandshakeHello', { + 'accelAcquisitionFreqHz': float(self.accel_acquisition_freq_hz), + 'accelAcquisitionLatency': float(self.accel_acquisition_latency), + 'accelMaxRange': float(self.accel_max_range), + }) + + async for message in self.ws: + await self.on_message(message) + + async def send_accelerometer_data(self): + accel_data = [] + delta_time = 0 + + end = time.time() + + while True: + if self.disconnected: + break + + if not self.should_start_accelerometer: + await asyncio.sleep(0.5) + continue + + start = time.time() + if delta_time > ACCEL_SEND_RATE: + delta_time = 0 + while len(accel_data) > 0: + accels_num = min(len(accel_data), 10) + + await self.send_message('JD_PhoneScoringData', { + 'accelData': accel_data[:accels_num], + 'timeStamp': self.number_of_accels_sent, + }) + + self.number_of_accels_sent += accels_num + accel_data = accel_data[accels_num:] + + try: + await asyncio.sleep(JOYCON_UPDATE_RATE) + joycon_status = self.joycon.get_status() + except OSError: + self.disconnect() + return + + # Accelerator axes on phone & Joy-Con are different so we need to swap some axes + # https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/imu_sensor_notes.md + accel = joycon_status['accel'] + x = accel['y'] * -1 + y = accel['x'] + z = accel['z'] + + accel_data.append([x, y, z]) + + end = time.time() + delta_time += (end - start) * 1000 + + async def send_command(self): + ''' Capture Joycon's input and send to console ''' + while True: + try: + if self.disconnected: + return + + if not self.is_input_allowed and not self.should_start_accelerometer: + await asyncio.sleep(JOYCON_UPDATE_RATE * 5) + continue + + await asyncio.sleep(JOYCON_UPDATE_RATE * 5) + + cmd = None + # Get pressed button + for event_type, status in self.joycon.events(): + if status == 0: # 0 = pressed, 1 = released + continue + + joycon_button = JoyConButton(event_type) + if self.should_start_accelerometer: # Can only send Pause command while playing + if joycon_button == JoyConButton.PLUS or joycon_button == JoyConButton.MINUS: + cmd = Command.PAUSE + else: + if joycon_button == JoyConButton.A or joycon_button == JoyConButton.RIGHT: + cmd = Command.ACCEPT + elif joycon_button == JoyConButton.B or joycon_button == JoyConButton.DOWN: + cmd = Command.BACK + elif joycon_button in SHORTCUT_MAPPING: + # Get command depends on which button was pressed & which shortcuts are available + for shortcut in SHORTCUT_MAPPING[joycon_button]: + if shortcut in self.available_shortcuts: + cmd = shortcut + break + + # Get joystick direction + if not self.should_start_accelerometer and not cmd: + status = self.joycon.get_status() + + # Check which Joycon (L/R) is being used + stick = status['analog-sticks']['left'] if self.joycon_is_left else status['analog-sticks']['right'] + vertical = stick['vertical'] + horizontal = stick['horizontal'] + + if vertical < -0.5: + cmd = Command.DOWN + elif vertical > 0.5: + cmd = Command.UP + elif horizontal < -0.5: + cmd = Command.LEFT + elif horizontal > 0.5: + cmd = Command.RIGHT + + # Send command to server + if cmd: + if cmd == Command.PAUSE: + __class = 'JD_Pause_PhoneCommandData' + data = {} + elif type(cmd.value) == str: + __class = 'JD_Custom_PhoneCommandData' + data = { + 'identifier': cmd.value, + } + else: + __class = 'JD_Input_PhoneCommandData' + data = { + 'input': cmd.value, + } + + # Only send input when it's allowed to, otherwise we might get a disconnection + if self.is_input_allowed: + await self.send_message(__class, data) + await asyncio.sleep(0.01) + except Exception as e: + print(e) + await self.disconnect() + + async def connect_ws(self): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.set_ciphers('ALL') + ssl_context.options &= ~ssl.OP_NO_SSLv3 + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + server_hostname = self.console_conn.getpeername()[0] if self.console_conn else None + try: + async with websockets.connect( + self.pairing_url, + subprotocols=[WS_SUBPROTOCOL], + sock=self.console_conn, + ssl=ssl_context, + ping_timeout=None, + server_hostname=server_hostname + ) as websocket: + try: + self.ws = websocket + await asyncio.gather( + self.send_hello(), + self.send_accelerometer_data(), + self.send_command(), + ) + + except websockets.ConnectionClosed: + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) + await self.disconnect(close_ws=False) + except Exception: + await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) + await self.disconnect(close_ws=False) + + async def disconnect(self, close_ws=True): + print('disconnected') + self.disconnected = True + self.joycon.__del__() + + if close_ws and self.ws: + await self.ws.close() + + async def pair(self): + try: + if self.console_ip_addr: + await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) + self.pairing_url = 'wss://{}:8080/smartphone'.format(self.console_ip_addr) + else: + await self.on_state_changed(self.joycon.serial, PairingState.GETTING_TOKEN) + print('Getting authorication token...') + await self.get_access_token() + + await self.on_state_changed(self.joycon.serial, PairingState.PAIRING) + print('Sending pairing code...') + await self.send_pairing_code() + + await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) + print('Connecting with console...') + if self.requires_punch_pairing: + await self.send_initiate_punch_pairing() + await self.hole_punching() + + await self.connect_ws() + except Exception as e: + await self.disconnect() + print(e) diff --git a/joydance/constants.py b/joydance/constants.py new file mode 100644 index 0000000..a820c0e --- /dev/null +++ b/joydance/constants.py @@ -0,0 +1,128 @@ +from enum import Enum + + +JOYDANCE_VERSION = '0.1' +UBI_APP_ID = '210da0fb-d6a5-4ed1-9808-01e86f0de7fb' +UBI_SKU_ID = 'jdcompanion-android' +WS_SUBPROTOCOL = 'v2.phonescoring.jd.ubisoft.com' + +JOYCON_UPDATE_RATE = 0.02 # 50Hz +ACCEL_SEND_RATE = 40 # ms +ACCEL_ACQUISITION_FREQ_HZ = 50 # Hz +ACCEL_ACQUISITION_LATENCY = 20 # ms +ACCEL_MAX_RANGE = 8 # ±G + +DEFAULT_CONFIG = { + 'pairing_method': 'default', + 'host_ip_addr': '', + 'console_ip_addr': '', + 'pairing_code': '', + 'accel_acquisition_freq_hz': ACCEL_ACQUISITION_FREQ_HZ, + 'accel_acquisition_latency': ACCEL_ACQUISITION_LATENCY, + 'accel_max_range': ACCEL_MAX_RANGE, +} + + +class Command(Enum): + UP = 3690595578 + RIGHT = 1099935642 + DOWN = 2467711647 + LEFT = 3652315484 + ACCEPT = 1084313942 + + PAUSE = 'PAUSE' + + BACK = 'SHORTCUT_BACK' + CHANGE_DANCERCARD = 'SHORTCUT_CHANGE_DANCERCARD' + FAVORITE = 'SHORTCUT_FAVORITE' + GOTO_SONGSTAB = 'SHORTCUT_GOTO_SONGSTAB' + SKIP = 'SHORTCUT_SKIP' + SORTING = 'SHORTCUT_SORTING' + SWAP_GENDER = 'SHORTCUT_SWAP_GENDER' + SWEAT_ACTIVATION = 'SHORTCUT_SWEAT_ACTIVATION' + TOGGLE_COOP = 'SHORTCUT_TOGGLE_COOP' + UPLAY = 'SHORTCUT_UPLAY' + + ACTIVATE_DANCERCARD = 'SHORTCUT_ACTIVATE_DANCERCARD' + DELETE_DANCERCARD = 'SHORTCUT_DELETE_DANCERCARD' + + DELETE_PLAYLIST = 'SHORTCUT_DELETE_PLAYLIST' + SAVE_PLAYLIST = 'SHORTCUT_SAVE_PLAYLIST' + PLAYLIST_RENAME = 'SHORTCUT_PLAYLIST_RENAME' + PLAYLIST_DELETE_SONG = 'SHORTCUT_PLAYLIST_DELETE_SONG' + PLAYLIST_MOVE_SONG_LEFT = 'SHORTCUT_PLAYLIST_MOVE_SONG_LEFT' + PLAYLIST_MOVE_SONG_RIGHT = 'SHORTCUT_PLAYLIST_MOVE_SONG_RIGHT' + + TIPS_NEXT = 'SHORTCUT_TIPS_NEXT' + TIPS_PREVIOUS = 'SHORTCUT_TIPS_PREVIOUS' + + +class JoyConButton(Enum): + # Joy-Con (L) + UP = 'up' + RIGHT = 'right' + DOWN = 'down' + LEFT = 'left' + L = 'l' + ZL = 'zl' + MINUS = 'minus' + CAPTURE = 'capture' + LEFT_STICK = 'stick_l_btn' + LEFT_SR = 'left_sr' + LEFT_SL = 'left_sl' + + # Joy-Con (R) + A = 'a' + B = 'b' + X = 'x' + Y = 'y' + R = 'r' + ZR = 'zr' + PLUS = 'plus' + HOME = 'home' + RIGHT_STICK = 'stick_r_btn' + RIGHT_SL = 'right_sl' + RIGHT_SR = 'right_sr' + CHARGING_GRIP = 'charging-grip' + + +# Assign buttons on Joy-Con (R) with commands +SHORTCUT_MAPPING = { + JoyConButton.X: [ + Command.DELETE_DANCERCARD, + Command.DELETE_PLAYLIST, + Command.GOTO_SONGSTAB, + Command.PLAYLIST_DELETE_SONG, + Command.SKIP, + Command.SORTING, + Command.SWAP_GENDER, + Command.TOGGLE_COOP, + ], + JoyConButton.Y: [ + Command.ACTIVATE_DANCERCARD, + Command.CHANGE_DANCERCARD, + Command.SWEAT_ACTIVATION, + Command.UPLAY, + ], + JoyConButton.PLUS: [ + Command.FAVORITE, + Command.PAUSE, + Command.PLAYLIST_RENAME, + Command.SAVE_PLAYLIST, + ], + JoyConButton.R: [ + Command.PLAYLIST_MOVE_SONG_LEFT, + Command.TIPS_PREVIOUS, + ], + JoyConButton.ZR: [ + Command.PLAYLIST_MOVE_SONG_RIGHT, + Command.TIPS_NEXT, + ], +} + +# Same with Joy-Con (L) +SHORTCUT_MAPPING[JoyConButton.UP] = JoyConButton.X +SHORTCUT_MAPPING[JoyConButton.LEFT] = JoyConButton.Y +SHORTCUT_MAPPING[JoyConButton.MINUS] = JoyConButton.PLUS +SHORTCUT_MAPPING[JoyConButton.L] = JoyConButton.R +SHORTCUT_MAPPING[JoyConButton.ZL] = JoyConButton.ZR diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f20feda --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +https://github.com/redphx/joycon-python/archive/refs/heads/master.zip#egg=joycon-python +websockets==10.2 +aiohttp==3.8.1 +hidapi==0.11.2 +pyglm==2.5.7 diff --git a/static/css/app.css b/static/css/app.css new file mode 100644 index 0000000..8d6198f --- /dev/null +++ b/static/css/app.css @@ -0,0 +1,180 @@ +body { + max-width: 500px; + margin: 10px auto; + background-color: #1c1b1f; + color: #e1e1e1; +} + +.container { + border-radius: 10px; + background-color: #272629; + padding: 20px; + margin: 10px; +} + +.ascii { + text-align: center; + padding: 8px 0; +} + +.ascii pre { + font-family: ui-monospace, 'Segoe UI Mono', 'Roboto Mono', 'Courier New', monospace; + display: inline-block; + text-align: left; + margin: 0; + font-weight: bold; + font-size: 10px; + line-height: 10px; + color: #FFF; +} + +.flex { + display: flex; +} + +.joycons { + margin-top: 10px; +} + +.joycons-wrapper button { + width: 100%; +} + +.joycons .joycons-wrapper { + position: relative; +} + +.joycons .joycons-wrapper .empty { + height: 60px; + line-height: 60px; + margin: auto; + text-align: center; +} + +.joycons .joycons-wrapper .joycons-list { + padding: 0; + margin: 0; + list-style: none; +} + + +.joycons .joycons-wrapper .joycons-list li { + border-bottom: 1px solid #2f2f2f; + padding: 12px 0; +} + +.joycons .joycons-wrapper .joycons-list .joycon-color { + width: 35%; + margin: auto; +} + +.joycons .joycons-wrapper .joycons-list .joycon-name { + align-items: center; + font-size: 16px; + font-weight: bold; +} + +.joycons .joycons-wrapper .joycons-list .joycon-info { + display: flex; + flex-direction: column; +} + +.joycons .joycons-wrapper .joycons-list .joycon-info .joycon-state { + color: #757575; + font-size: 14px; + text-align: left; +} + +.joycons-list .pairing-code { + border-radius: 4px; + font-size: 12px; + background-color: #202020; + display: inline-block; + color: #a6a6a6; + padding: 4px; + margin: auto; +} + +.joycons .btn-refresh { + color: #383000; + background-color: #e2c700; + margin: auto; +} + +input, select { + box-shadow: none !important; + border-width: 2px !important; +} + +input:invalid { + border-color: #e9322d !important; +} + +input[readonly] { + background-color: #ccc !important; + color: #000; +} + +#ipAddr, #pairingCode, .joycons-list .pairing-code { + font-family: ui-monospace, 'Segoe UI Mono', 'Roboto Mono', 'Courier New', monospace; +} + +.footer { + text-align: center; + margin-top: 16px; +} + +.footer a { + text-decoration: none; + color: #424242; + font-size: 14px; +} + +.footer a:hover { + color: #4d4d4d; +} + +.pure-button-primary { + background-color: #1976D2 !important; + color: #E3F2FD !important; +} + +.pure-form fieldset { + padding-bottom: 0; +} + +.battery-level { + display: inline-block; + margin-left: 8px; +} + +.battery-level svg { + width: 16px; + margin: auto; + vertical-align: middle; +} + +.battery-level.full > * { + fill: #8BC34A; +} + +.battery-level.medium > * { + fill: #FFEB3B; +} + +.battery-level.low > * { + fill: #FF9800; +} + +.battery-level.critical > * { + fill: #FF5722; +} + +.battery-level.medium .battery-bar-4, +.battery-level.low .battery-bar-3, +.battery-level.low .battery-bar-4, +.battery-level.critical .battery-bar-2, +.battery-level.critical .battery-bar-3, +.battery-level.critical .battery-bar-4 { + display: none; +} diff --git a/static/css/grids-responsive-min.css b/static/css/grids-responsive-min.css new file mode 100644 index 0000000..fc7f6b5 --- /dev/null +++ b/static/css/grids-responsive-min.css @@ -0,0 +1,7 @@ +/*! +Pure v2.1.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +@media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-12,.pure-u-sm-1-2,.pure-u-sm-1-24,.pure-u-sm-1-3,.pure-u-sm-1-4,.pure-u-sm-1-5,.pure-u-sm-1-6,.pure-u-sm-1-8,.pure-u-sm-10-24,.pure-u-sm-11-12,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-2-24,.pure-u-sm-2-3,.pure-u-sm-2-5,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24,.pure-u-sm-3-24,.pure-u-sm-3-4,.pure-u-sm-3-5,.pure-u-sm-3-8,.pure-u-sm-4-24,.pure-u-sm-4-5,.pure-u-sm-5-12,.pure-u-sm-5-24,.pure-u-sm-5-5,.pure-u-sm-5-6,.pure-u-sm-5-8,.pure-u-sm-6-24,.pure-u-sm-7-12,.pure-u-sm-7-24,.pure-u-sm-7-8,.pure-u-sm-8-24,.pure-u-sm-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-10-24,.pure-u-sm-5-12{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-14-24,.pure-u-sm-7-12{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-15-24,.pure-u-sm-5-8{width:62.5%}.pure-u-sm-16-24,.pure-u-sm-2-3{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-18-24,.pure-u-sm-3-4{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-20-24,.pure-u-sm-5-6{width:83.3333%}.pure-u-sm-21-24,.pure-u-sm-7-8{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-24-24,.pure-u-sm-5-5{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-12,.pure-u-md-1-2,.pure-u-md-1-24,.pure-u-md-1-3,.pure-u-md-1-4,.pure-u-md-1-5,.pure-u-md-1-6,.pure-u-md-1-8,.pure-u-md-10-24,.pure-u-md-11-12,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-2-24,.pure-u-md-2-3,.pure-u-md-2-5,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24,.pure-u-md-3-24,.pure-u-md-3-4,.pure-u-md-3-5,.pure-u-md-3-8,.pure-u-md-4-24,.pure-u-md-4-5,.pure-u-md-5-12,.pure-u-md-5-24,.pure-u-md-5-5,.pure-u-md-5-6,.pure-u-md-5-8,.pure-u-md-6-24,.pure-u-md-7-12,.pure-u-md-7-24,.pure-u-md-7-8,.pure-u-md-8-24,.pure-u-md-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-10-24,.pure-u-md-5-12{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-14-24,.pure-u-md-7-12{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-15-24,.pure-u-md-5-8{width:62.5%}.pure-u-md-16-24,.pure-u-md-2-3{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-18-24,.pure-u-md-3-4{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-20-24,.pure-u-md-5-6{width:83.3333%}.pure-u-md-21-24,.pure-u-md-7-8{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-24-24,.pure-u-md-5-5{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-12,.pure-u-lg-1-2,.pure-u-lg-1-24,.pure-u-lg-1-3,.pure-u-lg-1-4,.pure-u-lg-1-5,.pure-u-lg-1-6,.pure-u-lg-1-8,.pure-u-lg-10-24,.pure-u-lg-11-12,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-2-24,.pure-u-lg-2-3,.pure-u-lg-2-5,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24,.pure-u-lg-3-24,.pure-u-lg-3-4,.pure-u-lg-3-5,.pure-u-lg-3-8,.pure-u-lg-4-24,.pure-u-lg-4-5,.pure-u-lg-5-12,.pure-u-lg-5-24,.pure-u-lg-5-5,.pure-u-lg-5-6,.pure-u-lg-5-8,.pure-u-lg-6-24,.pure-u-lg-7-12,.pure-u-lg-7-24,.pure-u-lg-7-8,.pure-u-lg-8-24,.pure-u-lg-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-10-24,.pure-u-lg-5-12{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-14-24,.pure-u-lg-7-12{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-15-24,.pure-u-lg-5-8{width:62.5%}.pure-u-lg-16-24,.pure-u-lg-2-3{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-18-24,.pure-u-lg-3-4{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-20-24,.pure-u-lg-5-6{width:83.3333%}.pure-u-lg-21-24,.pure-u-lg-7-8{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-24-24,.pure-u-lg-5-5{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-12,.pure-u-xl-1-2,.pure-u-xl-1-24,.pure-u-xl-1-3,.pure-u-xl-1-4,.pure-u-xl-1-5,.pure-u-xl-1-6,.pure-u-xl-1-8,.pure-u-xl-10-24,.pure-u-xl-11-12,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-2-24,.pure-u-xl-2-3,.pure-u-xl-2-5,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24,.pure-u-xl-3-24,.pure-u-xl-3-4,.pure-u-xl-3-5,.pure-u-xl-3-8,.pure-u-xl-4-24,.pure-u-xl-4-5,.pure-u-xl-5-12,.pure-u-xl-5-24,.pure-u-xl-5-5,.pure-u-xl-5-6,.pure-u-xl-5-8,.pure-u-xl-6-24,.pure-u-xl-7-12,.pure-u-xl-7-24,.pure-u-xl-7-8,.pure-u-xl-8-24,.pure-u-xl-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-10-24,.pure-u-xl-5-12{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-14-24,.pure-u-xl-7-12{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-15-24,.pure-u-xl-5-8{width:62.5%}.pure-u-xl-16-24,.pure-u-xl-2-3{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-18-24,.pure-u-xl-3-4{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-20-24,.pure-u-xl-5-6{width:83.3333%}.pure-u-xl-21-24,.pure-u-xl-7-8{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-24-24,.pure-u-xl-5-5{width:100%}}@media screen and (min-width:120em){.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-1-12,.pure-u-xxl-1-2,.pure-u-xxl-1-24,.pure-u-xxl-1-3,.pure-u-xxl-1-4,.pure-u-xxl-1-5,.pure-u-xxl-1-6,.pure-u-xxl-1-8,.pure-u-xxl-10-24,.pure-u-xxl-11-12,.pure-u-xxl-11-24,.pure-u-xxl-12-24,.pure-u-xxl-13-24,.pure-u-xxl-14-24,.pure-u-xxl-15-24,.pure-u-xxl-16-24,.pure-u-xxl-17-24,.pure-u-xxl-18-24,.pure-u-xxl-19-24,.pure-u-xxl-2-24,.pure-u-xxl-2-3,.pure-u-xxl-2-5,.pure-u-xxl-20-24,.pure-u-xxl-21-24,.pure-u-xxl-22-24,.pure-u-xxl-23-24,.pure-u-xxl-24-24,.pure-u-xxl-3-24,.pure-u-xxl-3-4,.pure-u-xxl-3-5,.pure-u-xxl-3-8,.pure-u-xxl-4-24,.pure-u-xxl-4-5,.pure-u-xxl-5-12,.pure-u-xxl-5-24,.pure-u-xxl-5-5,.pure-u-xxl-5-6,.pure-u-xxl-5-8,.pure-u-xxl-6-24,.pure-u-xxl-7-12,.pure-u-xxl-7-24,.pure-u-xxl-7-8,.pure-u-xxl-8-24,.pure-u-xxl-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xxl-1-24{width:4.1667%}.pure-u-xxl-1-12,.pure-u-xxl-2-24{width:8.3333%}.pure-u-xxl-1-8,.pure-u-xxl-3-24{width:12.5%}.pure-u-xxl-1-6,.pure-u-xxl-4-24{width:16.6667%}.pure-u-xxl-1-5{width:20%}.pure-u-xxl-5-24{width:20.8333%}.pure-u-xxl-1-4,.pure-u-xxl-6-24{width:25%}.pure-u-xxl-7-24{width:29.1667%}.pure-u-xxl-1-3,.pure-u-xxl-8-24{width:33.3333%}.pure-u-xxl-3-8,.pure-u-xxl-9-24{width:37.5%}.pure-u-xxl-2-5{width:40%}.pure-u-xxl-10-24,.pure-u-xxl-5-12{width:41.6667%}.pure-u-xxl-11-24{width:45.8333%}.pure-u-xxl-1-2,.pure-u-xxl-12-24{width:50%}.pure-u-xxl-13-24{width:54.1667%}.pure-u-xxl-14-24,.pure-u-xxl-7-12{width:58.3333%}.pure-u-xxl-3-5{width:60%}.pure-u-xxl-15-24,.pure-u-xxl-5-8{width:62.5%}.pure-u-xxl-16-24,.pure-u-xxl-2-3{width:66.6667%}.pure-u-xxl-17-24{width:70.8333%}.pure-u-xxl-18-24,.pure-u-xxl-3-4{width:75%}.pure-u-xxl-19-24{width:79.1667%}.pure-u-xxl-4-5{width:80%}.pure-u-xxl-20-24,.pure-u-xxl-5-6{width:83.3333%}.pure-u-xxl-21-24,.pure-u-xxl-7-8{width:87.5%}.pure-u-xxl-11-12,.pure-u-xxl-22-24{width:91.6667%}.pure-u-xxl-23-24{width:95.8333%}.pure-u-xxl-1,.pure-u-xxl-1-1,.pure-u-xxl-24-24,.pure-u-xxl-5-5{width:100%}} \ No newline at end of file diff --git a/static/css/pure-min.css b/static/css/pure-min.css new file mode 100644 index 0000000..e0cc408 --- /dev/null +++ b/static/css/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v2.1.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/*! +normalize.css v | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-0.43em}.pure-u{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..f27c911 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..99a2172 --- /dev/null +++ b/static/index.html @@ -0,0 +1,17 @@ + + + + JoyDance + + + + + + + + + + + + + diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..b8146d1 --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,507 @@ +import { h, Component, render } from '/js/preact.module.js'; +import htm from '/js/htm.module.js'; + +// Initialize htm with Preact +const html = htm.bind(h); +window.mitty = mitt() + +const SVG_BATTERY_LEVEL = html`` + +const BATTERY_LEVEL = { + 4: 'full', + 3: 'medium', + 2: 'low', + 1: 'critical', + 0: 'critical', +} + +const PairingMethod = { + DEFAULT: 'default', + FAST: 'fast' +} + +const WsCommand = { + GET_JOYCON_LIST: 'get_joycon_list', + CONNECT_JOYCON: 'connect_joycon', + DISCONNECT_JOYCON: 'disconnect_joycon', + UPDATE_JOYCON_STATE: 'update_joycon_state', +} + +const PairingState = { + IDLE: 0, + GETTING_TOKEN: 1, + PAIRING: 2, + CONNECTING: 3, + CONNECTED: 4, + DISCONNECTING: 5, + DISCONNECTED: 10, + + ERROR_JOYCON: 101, + ERROR_CONNECTION: 102, + ERROR_INVALID_PAIRING_CODE: 103, + ERROR_PUNCH_PAIRING: 104, + ERROR_HOLE_PUNCHING: 105, + ERROR_CONSOLE_CONNECTION: 106, +} + +const PairingStateMessage = { + [PairingState.IDLE]: 'Idle', + [PairingState.GETTING_TOKEN]: 'Getting auth token...', + [PairingState.PAIRING]: 'Sending pairing code...', + [PairingState.CONNECTING]: 'Connecting with console...', + [PairingState.CONNECTED]: 'Connected!', + [PairingState.DISCONNECTED]: 'Disconnected', + + [PairingState.ERROR_JOYCON]: 'Joy-Con problem!', + [PairingState.ERROR_CONNECTION]: 'Couldn\'t get auth token!', + [PairingState.ERROR_INVALID_PAIRING_CODE]: 'Invalid pairing code!', + [PairingState.ERROR_PUNCH_PAIRING]: 'Couldn\'t punch pairing!', + [PairingState.ERROR_HOLE_PUNCHING]: 'Couldn\'t connect with console!', + [PairingState.ERROR_CONSOLE_CONNECTION]: 'Couldn\'t connect with console!', +} + +class PairingMethodPicker extends Component { + constructor(props) { + super() + this.state = { + pairing_method: props.pairing_method, + } + + this.onChange = this.onChange.bind(this) + } + + onChange(e) { + const pairing_method = e.target.value + this.setState({ + pairing_method: pairing_method, + }) + + window.mitty.emit('update_method', pairing_method) + } + + render(props) { + return html` + + + ` + } +} + +class PrivateIpAddress extends Component { + constructor(props) { + super(props) + + let lock_host = false + let host_ip_addr = props.host_ip_addr + let console_ip_addr = props.console_ip_addr + + let hostname = window.location.hostname + if (hostname.startsWith('192.168.')) { + host_ip_addr = hostname + lock_host = true + } + + this.state = { + host_ip_addr: host_ip_addr, + console_ip_addr: console_ip_addr, + lock_host: lock_host, + } + + this.onKeyPress = this.onKeyPress.bind(this) + this.onChange = this.onChange.bind(this) + } + + onChange(e) { + const key = this.props.pairing_method == PairingMethod.DEFAULT ? 'host_ip_addr' : 'console_ip_addr' + const value = e.target.value + this.setState({ + [key]: value, + }) + + window.mitty.emit('update_addr', value) + } + + onKeyPress(e) { + if (!/[0-9\.]/.test(e.key)) { + e.preventDefault() + return + } + } + + componentDidMount() { + window.mitty.emit('update_addr', this.state.host_ip_addr) + } + + render(props, state) { + const pairing_method = props.pairing_method + const addr = pairing_method == PairingMethod.DEFAULT ? state.host_ip_addr : state.console_ip_addr + return html` + + + ${pairing_method == PairingMethod.DEFAULT && state.lock_host && html` + + `} + + ${(pairing_method == PairingMethod.FAST || !state.lock_host) && html` + + `} + + ` + } +} + +class PairingCode extends Component { + constructor(props) { + super(props) + this.state = { + pairing_code: props.pairing_code, + } + + this.onChange = this.onChange.bind(this) + } + + onChange(e) { + const value = e.target.value + this.setState({ + pairing_code: value, + }) + + window.mitty.emit('update_code', value) + } + + render(props, state) { + return html` + + ${props.pairing_method == PairingMethod.DEFAULT && html` + !/[0-9]/.test(e.key) && e.preventDefault()} onChange=${this.onChange} /> + `} + ${props.pairing_method == PairingMethod.FAST && html` + + `} + ` + } +} + +class JoyCon extends Component { + constructor(props) { + super(props) + + this.connect = this.connect.bind(this) + this.disconnect = this.disconnect.bind(this) + this.onStateUpdated = this.onStateUpdated.bind(this) + + this.state = { + ...props.joycon, + } + + window.mitty.on('resp_' + WsCommand.DISCONNECT_JOYCON, this.onDisconnected) + window.mitty.on('resp_' + WsCommand.UPDATE_JOYCON_STATE, this.onStateUpdated) + } + + connect() { + window.mitty.emit('req_' + WsCommand.CONNECT_JOYCON, this.props.joycon.serial) + } + + disconnect() { + window.mitty.emit('req_' + WsCommand.DISCONNECT_JOYCON, this.props.joycon.serial) + } + + onStateUpdated(data) { + if (data['serial'] != this.props.joycon.serial) { + return + } + + const state = data['state'] + if (PairingStateMessage.hasOwnProperty(state)) { + this.setState({ + ...data, + }) + } + } + + render(props, { name, state, pairing_code, is_left, color, battery_level }) { + const joyconState = state + const stateMessage = PairingStateMessage[joyconState] + let showButton = true + if ([PairingState.GETTING_TOKEN, PairingState.PAIRING, PairingState.CONNECTING].indexOf(joyconState) > -1) { + showButton = false + } + + let joyconSvg + if (is_left) { + joyconSvg = html`` + } else { + joyconSvg = html`` + } + + const batteryLevel = BATTERY_LEVEL[battery_level] + + return html` +
  • +
    + +
    ${joyconSvg}
    +
    +
    + ${name} + ${SVG_BATTERY_LEVEL} +
    + ${stateMessage} +
    +
    + ${pairing_code && html` + ${pairing_code} + `} +
    +
    + ${showButton && joyconState == PairingState.CONNECTED && html` + + `} + ${showButton && joyconState != PairingState.CONNECTED && html` + + `} +
    +
    +
  • + ` + } +} + +class JoyCons extends Component { + constructor() { + super() + this.state = { + isRefreshing: false, + } + + this.refreshJoyconList = this.refreshJoyconList.bind(this) + } + + refreshJoyconList() { + this.setState({ + isRefreshing: false, + }) + window.mitty.emit('req_' + WsCommand.GET_JOYCON_LIST) + } + + componentDidMount() { + } + + render(props, state) { + return html` +
    +

    Joy-Cons

    + ${state.isRefreshing && html` + + `} +
    +
    + ${props.joycons.length == 0 && html` +

    No Joy-Cons found!

    + `} + + + ${props.joycons.length > 0 && html` + + `} +
    + ` + } +} + +class App extends Component { + constructor(props) { + super() + + this.state = { + pairing_method: window.CONFIG.pairing_method, + host_ip_addr: window.CONFIG.host_ip_addr, + console_ip_addr: window.CONFIG.console_ip_addr, + pairing_code: window.CONFIG.pairing_code, + joycons: [], + } + + this.connectWs = this.connectWs.bind(this) + this.sendRequest = this.sendRequest.bind(this) + this.requestGetJoyconList = this.requestGetJoyconList.bind(this) + this.requestConnectJoycon = this.requestConnectJoycon.bind(this) + this.requestDisconnectJoycon = this.requestDisconnectJoycon.bind(this) + this.handleMethodChange = this.handleMethodChange.bind(this) + this.handleAddrChange = this.handleAddrChange.bind(this) + this.handleCodeChange = this.handleCodeChange.bind(this) + + window.mitty.on('req_' + WsCommand.GET_JOYCON_LIST, this.requestGetJoyconList) + window.mitty.on('req_' + WsCommand.CONNECT_JOYCON, this.requestConnectJoycon) + window.mitty.on('req_' + WsCommand.DISCONNECT_JOYCON, this.requestDisconnectJoycon) + window.mitty.on('update_method', this.handleMethodChange) + window.mitty.on('update_addr', this.handleAddrChange) + window.mitty.on('update_code', this.handleCodeChange) + } + + sendRequest(cmd, data) { + if (!data) { + data = {} + } + const msg = { + cmd: cmd, + data: data, + } + console.log('send', msg) + this.socket.send(JSON.stringify(msg)) + } + + requestGetJoyconList() { + this.sendRequest(WsCommand.GET_JOYCON_LIST); + } + + requestConnectJoycon(serial) { + const state = this.state + const pairing_method = state.pairing_method + let addr = pairing_method == PairingMethod.DEFAULT ? state.host_ip_addr : state.console_ip_addr + if (!addr.match(/^192\.168\.((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.)(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/)) { + alert('ERROR: Invalid IP address!') + document.getElementById('ipAddr').focus() + return + } + + if (pairing_method == PairingMethod.DEFAULT) { + const pairing_code = state.pairing_code + if (!pairing_code.match(/^\d{6}$/)) { + alert('ERROR: Invalid pairing code!') + document.getElementById('pairingCode').focus() + return + } + } + + this.sendRequest(WsCommand.CONNECT_JOYCON, { + pairing_method: state.pairing_method, + host_ip_addr: state.host_ip_addr, + console_ip_addr: state.console_ip_addr, + pairing_code: state.pairing_code, + joycon_serial: serial, + }) + } + + requestDisconnectJoycon(serial) { + this.sendRequest(WsCommand.DISCONNECT_JOYCON, { + joycon_serial: serial, + }) + } + + connectWs() { + const that = this + this.socket = new WebSocket('ws://' + window.location.host + '/ws') + + this.socket.onopen = function(e) { + console.log('[open] Connection established') + that.requestGetJoyconList() + } + + this.socket.onmessage = function(event) { + const msg = JSON.parse(event.data) + console.log(msg) + const cmd = msg['cmd'] + const shortCmd = msg['cmd'].slice(5) // Remove "resp_" prefix + + switch (shortCmd) { + case WsCommand.GET_JOYCON_LIST: + that.setState({ + joycons: msg['data'], + }) + break + + default: + window.mitty.emit(cmd, msg['data']) + } + } + + this.socket.onclose = function(event) { + if (event.wasClean) { + console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + } else { + console.log('[close] Connection died'); + } + } + + this.socket.onerror = function(error) { + console.log(`[error] ${error.message}`); + } + } + + handleMethodChange(pairing_method) { + this.setState({ + pairing_method: pairing_method, + }) + } + + handleAddrChange(addr) { + const key = this.state.pairing_method == PairingMethod.DEFAULT ? 'host_ip_addr' : 'console_ip_addr' + this.setState({ + [key]: addr, + }) + } + + handleCodeChange(pairing_code) { + this.setState({ + pairing_code: pairing_code, + }) + } + + componentDidMount() { + this.connectWs() + } + + render(props, state) { + return html` +
    +
    +
         ░░  ░░░░░░  ░░    ░░ ░░░░░░   ░░░░░  ░░░    ░░  ░░░░░░ ░░░░░░░
    +     ▒▒ ▒▒    ▒▒  ▒▒  ▒▒  ▒▒   ▒▒ ▒▒   ▒▒ ▒▒▒▒   ▒▒ ▒▒      ▒▒
    +     ▒▒ ▒▒    ▒▒   ▒▒▒▒   ▒▒   ▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒  ▒▒ ▒▒      ▒▒▒▒▒
    +▓▓   ▓▓ ▓▓    ▓▓    ▓▓    ▓▓   ▓▓ ▓▓   ▓▓ ▓▓  ▓▓ ▓▓ ▓▓      ▓▓
    + █████   ██████     ██    ██████  ██   ██ ██   ████  ██████ ███████
    +                    
    +
    + +
    +
    +
    +

    Config

    +
    + <${PairingMethodPicker} pairing_method=${state.pairing_method}/> +
    +
    + <${PrivateIpAddress} pairing_method=${state.pairing_method} host_ip_addr=${state.host_ip_addr} console_ip_addr=${state.console_ip_addr} /> +
    +
    + <${PairingCode} pairing_method=${state.pairing_method} pairing_code=${state.pairing_code} /> +
    +
    +
    +
    + +
    + <${JoyCons} pairing_method=${state.pairing_method} joycons=${state.joycons} /> +
    +
    + + ` + } +} + +render(html`<${App} />`, document.body) diff --git a/static/js/htm.module.js b/static/js/htm.module.js new file mode 100644 index 0000000..ca651c6 --- /dev/null +++ b/static/js/htm.module.js @@ -0,0 +1 @@ +var n = function (t, s, r, e) {var u;s[0] = 0;for (var h = 1; h < s.length; h++) {var p = s[h++],a = s[h] ? (s[0] |= p ? 1 : 2, r[s[h++]]) : s[++h];3 === p ? e[0] = a : 4 === p ? e[1] = Object.assign(e[1] || {}, a) : 5 === p ? (e[1] = e[1] || {})[s[++h]] = a : 6 === p ? e[1][s[++h]] += a + "" : p ? (u = t.apply(a, n(t, a, r, ["", null])), e.push(u), a[0] ? s[0] |= 2 : (s[h - 2] = 0, s[h] = u)) : e.push(a);}return e;},t = new Map();export default function (s) {var r = t.get(this);return r || (r = new Map(), t.set(this, r)), (r = n(this, r.get(s) || (r.set(s, r = function (n) {for (var t, s, r = 1, e = "", u = "", h = [0], p = function (n) {1 === r && (n || (e = e.replace(/^\s*\n\s*|\s*\n\s*$/g, ""))) ? h.push(0, n, e) : 3 === r && (n || e) ? (h.push(3, n, e), r = 2) : 2 === r && "..." === e && n ? h.push(4, n, 0) : 2 === r && e && !n ? h.push(5, 0, !0, e) : r >= 5 && ((e || !n && 5 === r) && (h.push(r, 0, e, s), r = 6), n && (h.push(r, n, 0, s), r = 6)), e = "";}, a = 0; a < n.length; a++) {a && (1 === r && p(), p(a));for (var l = 0; l < n[a].length; l++) t = n[a][l], 1 === r ? "<" === t ? (p(), h = [h], r = 3) : e += t : 4 === r ? "--" === e && ">" === t ? (r = 1, e = "") : e = t + e[0] : u ? t === u ? u = "" : e += t : '"' === t || "'" === t ? u = t : ">" === t ? (p(), r = 1) : r && ("=" === t ? (r = 5, s = e, e = "") : "/" === t && (r < 5 || ">" === n[a][l + 1]) ? (p(), 3 === r && (h = h[0]), r = h, (h = h[0]).push(2, 0, r), r = 0) : " " === t || "\t" === t || "\n" === t || "\r" === t ? (p(), r = 2) : e += t), 3 === r && "!--" === e && (r = 4, h = h[0]);}return p(), h;}(s)), r), arguments, [])).length > 1 ? r : r[0];} \ No newline at end of file diff --git a/static/js/mitt.umd.js b/static/js/mitt.umd.js new file mode 100644 index 0000000..3204c15 --- /dev/null +++ b/static/js/mitt.umd.js @@ -0,0 +1 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f?f.push(t):e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&(t?f.splice(f.indexOf(t)>>>0,1):e.set(n,[]))},emit:function(n,t){var f=e.get(n);f&&f.slice().map(function(e){e(t)}),(f=e.get("*"))&&f.slice().map(function(e){e(n,t)})}}}}); diff --git a/static/js/preact.module.js b/static/js/preact.module.js new file mode 100644 index 0000000..c79af61 --- /dev/null +++ b/static/js/preact.module.js @@ -0,0 +1 @@ +var n,l,u,i,t,o,r,f,e = {},c = [],s = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function a(n, l) {for (var u in l) n[u] = l[u];return n;}function h(n) {var l = n.parentNode;l && l.removeChild(n);}function v(l, u, i) {var t,o,r,f = {};for (r in u) "key" == r ? t = u[r] : "ref" == r ? o = u[r] : f[r] = u[r];if (arguments.length > 2 && (f.children = arguments.length > 3 ? n.call(arguments, 2) : i), "function" == typeof l && null != l.defaultProps) for (r in l.defaultProps) void 0 === f[r] && (f[r] = l.defaultProps[r]);return y(l, f, t, o, null);}function y(n, i, t, o, r) {var f = { type: n, props: i, key: t, ref: o, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, __h: null, constructor: void 0, __v: null == r ? ++u : r };return null == r && null != l.vnode && l.vnode(f), f;}function p() {return { current: null };}function d(n) {return n.children;}function _(n, l) {this.props = n, this.context = l;}function k(n, l) {if (null == l) return n.__ ? k(n.__, n.__.__k.indexOf(n) + 1) : null;for (var u; l < n.__k.length; l++) if (null != (u = n.__k[l]) && null != u.__e) return u.__e;return "function" == typeof n.type ? k(n) : null;}function b(n) {var l, u;if (null != (n = n.__) && null != n.__c) {for (n.__e = n.__c.base = null, l = 0; l < n.__k.length; l++) if (null != (u = n.__k[l]) && null != u.__e) {n.__e = n.__c.base = u.__e;break;}return b(n);}}function m(n) {(!n.__d && (n.__d = !0) && t.push(n) && !g.__r++ || r !== l.debounceRendering) && ((r = l.debounceRendering) || o)(g);}function g() {for (var n; g.__r = t.length;) n = t.sort(function (n, l) {return n.__v.__b - l.__v.__b;}), t = [], n.some(function (n) {var l, u, i, t, o, r;n.__d && (o = (t = (l = n).__v).__e, (r = l.__P) && (u = [], (i = a({}, t)).__v = t.__v + 1, j(r, t, i, l.__n, void 0 !== r.ownerSVGElement, null != t.__h ? [o] : null, u, null == o ? k(t) : o, t.__h), z(u, t), t.__e != o && b(t)));});}function w(n, l, u, i, t, o, r, f, s, a) {var h,v,p,_,b,m,g,w = i && i.__k || c,A = w.length;for (u.__k = [], h = 0; h < l.length; h++) if (null != (_ = u.__k[h] = null == (_ = l[h]) || "boolean" == typeof _ ? null : "string" == typeof _ || "number" == typeof _ || "bigint" == typeof _ ? y(null, _, null, null, _) : Array.isArray(_) ? y(d, { children: _ }, null, null, null) : _.__b > 0 ? y(_.type, _.props, _.key, null, _.__v) : _)) {if (_.__ = u, _.__b = u.__b + 1, null === (p = w[h]) || p && _.key == p.key && _.type === p.type) w[h] = void 0;else for (v = 0; v < A; v++) {if ((p = w[v]) && _.key == p.key && _.type === p.type) {w[v] = void 0;break;}p = null;}j(n, _, p = p || e, t, o, r, f, s, a), b = _.__e, (v = _.ref) && p.ref != v && (g || (g = []), p.ref && g.push(p.ref, null, _), g.push(v, _.__c || b, _)), null != b ? (null == m && (m = b), "function" == typeof _.type && _.__k === p.__k ? _.__d = s = x(_, s, n) : s = P(n, _, p, w, b, s), "function" == typeof u.type && (u.__d = s)) : s && p.__e == s && s.parentNode != n && (s = k(p));}for (u.__e = m, h = A; h--;) null != w[h] && ("function" == typeof u.type && null != w[h].__e && w[h].__e == u.__d && (u.__d = k(i, h + 1)), N(w[h], w[h]));if (g) for (h = 0; h < g.length; h++) M(g[h], g[++h], g[++h]);}function x(n, l, u) {for (var i, t = n.__k, o = 0; t && o < t.length; o++) (i = t[o]) && (i.__ = n, l = "function" == typeof i.type ? x(i, l, u) : P(u, i, i, t, i.__e, l));return l;}function A(n, l) {return l = l || [], null == n || "boolean" == typeof n || (Array.isArray(n) ? n.some(function (n) {A(n, l);}) : l.push(n)), l;}function P(n, l, u, i, t, o) {var r, f, e;if (void 0 !== l.__d) r = l.__d, l.__d = void 0;else if (null == u || t != o || null == t.parentNode) n: if (null == o || o.parentNode !== n) n.appendChild(t), r = null;else {for (f = o, e = 0; (f = f.nextSibling) && e < i.length; e += 2) if (f == t) break n;n.insertBefore(t, o), r = o;}return void 0 !== r ? r : t.nextSibling;}function C(n, l, u, i, t) {var o;for (o in u) "children" === o || "key" === o || o in l || H(n, o, null, u[o], i);for (o in l) t && "function" != typeof l[o] || "children" === o || "key" === o || "value" === o || "checked" === o || u[o] === l[o] || H(n, o, l[o], u[o], i);}function $(n, l, u) {"-" === l[0] ? n.setProperty(l, u) : n[l] = null == u ? "" : "number" != typeof u || s.test(l) ? u : u + "px";}function H(n, l, u, i, t) {var o;n: if ("style" === l) {if ("string" == typeof u) n.style.cssText = u;else {if ("string" == typeof i && (n.style.cssText = i = ""), i) for (l in i) u && l in u || $(n.style, l, "");if (u) for (l in u) i && u[l] === i[l] || $(n.style, l, u[l]);}} else if ("o" === l[0] && "n" === l[1]) o = l !== (l = l.replace(/Capture$/, "")), l = l.toLowerCase() in n ? l.toLowerCase().slice(2) : l.slice(2), n.l || (n.l = {}), n.l[l + o] = u, u ? i || n.addEventListener(l, o ? T : I, o) : n.removeEventListener(l, o ? T : I, o);else if ("dangerouslySetInnerHTML" !== l) {if (t) l = l.replace(/xlink[H:h]/, "h").replace(/sName$/, "s");else if ("href" !== l && "list" !== l && "form" !== l && "tabIndex" !== l && "download" !== l && l in n) try {n[l] = null == u ? "" : u;break n;} catch (n) {}"function" == typeof u || (null != u && (!1 !== u || "a" === l[0] && "r" === l[1]) ? n.setAttribute(l, u) : n.removeAttribute(l));}}function I(n) {this.l[n.type + !1](l.event ? l.event(n) : n);}function T(n) {this.l[n.type + !0](l.event ? l.event(n) : n);}function j(n, u, i, t, o, r, f, e, c) {var s,h,v,y,p,k,b,m,g,x,A,P = u.type;if (void 0 !== u.constructor) return null;null != i.__h && (c = i.__h, e = u.__e = i.__e, u.__h = null, r = [e]), (s = l.__b) && s(u);try {n: if ("function" == typeof P) {if (m = u.props, g = (s = P.contextType) && t[s.__c], x = s ? g ? g.props.value : s.__ : t, i.__c ? b = (h = u.__c = i.__c).__ = h.__E : ("prototype" in P && P.prototype.render ? u.__c = h = new P(m, x) : (u.__c = h = new _(m, x), h.constructor = P, h.render = O), g && g.sub(h), h.props = m, h.state || (h.state = {}), h.context = x, h.__n = t, v = h.__d = !0, h.__h = []), null == h.__s && (h.__s = h.state), null != P.getDerivedStateFromProps && (h.__s == h.state && (h.__s = a({}, h.__s)), a(h.__s, P.getDerivedStateFromProps(m, h.__s))), y = h.props, p = h.state, v) null == P.getDerivedStateFromProps && null != h.componentWillMount && h.componentWillMount(), null != h.componentDidMount && h.__h.push(h.componentDidMount);else {if (null == P.getDerivedStateFromProps && m !== y && null != h.componentWillReceiveProps && h.componentWillReceiveProps(m, x), !h.__e && null != h.shouldComponentUpdate && !1 === h.shouldComponentUpdate(m, h.__s, x) || u.__v === i.__v) {h.props = m, h.state = h.__s, u.__v !== i.__v && (h.__d = !1), h.__v = u, u.__e = i.__e, u.__k = i.__k, u.__k.forEach(function (n) {n && (n.__ = u);}), h.__h.length && f.push(h);break n;}null != h.componentWillUpdate && h.componentWillUpdate(m, h.__s, x), null != h.componentDidUpdate && h.__h.push(function () {h.componentDidUpdate(y, p, k);});}h.context = x, h.props = m, h.state = h.__s, (s = l.__r) && s(u), h.__d = !1, h.__v = u, h.__P = n, s = h.render(h.props, h.state, h.context), h.state = h.__s, null != h.getChildContext && (t = a(a({}, t), h.getChildContext())), v || null == h.getSnapshotBeforeUpdate || (k = h.getSnapshotBeforeUpdate(y, p)), A = null != s && s.type === d && null == s.key ? s.props.children : s, w(n, Array.isArray(A) ? A : [A], u, i, t, o, r, f, e, c), h.base = u.__e, u.__h = null, h.__h.length && f.push(h), b && (h.__E = h.__ = null), h.__e = !1;} else null == r && u.__v === i.__v ? (u.__k = i.__k, u.__e = i.__e) : u.__e = L(i.__e, u, i, t, o, r, f, c);(s = l.diffed) && s(u);} catch (n) {u.__v = null, (c || null != r) && (u.__e = e, u.__h = !!c, r[r.indexOf(e)] = null), l.__e(n, u, i);}}function z(n, u) {l.__c && l.__c(u, n), n.some(function (u) {try {n = u.__h, u.__h = [], n.some(function (n) {n.call(u);});} catch (n) {l.__e(n, u.__v);}});}function L(l, u, i, t, o, r, f, c) {var s,a,v,y = i.props,p = u.props,d = u.type,_ = 0;if ("svg" === d && (o = !0), null != r) for (; _ < r.length; _++) if ((s = r[_]) && "setAttribute" in s == !!d && (d ? s.localName === d : 3 === s.nodeType)) {l = s, r[_] = null;break;}if (null == l) {if (null === d) return document.createTextNode(p);l = o ? document.createElementNS("http://www.w3.org/2000/svg", d) : document.createElement(d, p.is && p), r = null, c = !1;}if (null === d) y === p || c && l.data === p || (l.data = p);else {if (r = r && n.call(l.childNodes), a = (y = i.props || e).dangerouslySetInnerHTML, v = p.dangerouslySetInnerHTML, !c) {if (null != r) for (y = {}, _ = 0; _ < l.attributes.length; _++) y[l.attributes[_].name] = l.attributes[_].value;(v || a) && (v && (a && v.__html == a.__html || v.__html === l.innerHTML) || (l.innerHTML = v && v.__html || ""));}if (C(l, p, y, o, c), v) u.__k = [];else if (_ = u.props.children, w(l, Array.isArray(_) ? _ : [_], u, i, t, o && "foreignObject" !== d, r, f, r ? r[0] : i.__k && k(i, 0), c), null != r) for (_ = r.length; _--;) null != r[_] && h(r[_]);c || ("value" in p && void 0 !== (_ = p.value) && (_ !== l.value || "progress" === d && !_ || "option" === d && _ !== y.value) && H(l, "value", _, y.value, !1), "checked" in p && void 0 !== (_ = p.checked) && _ !== l.checked && H(l, "checked", _, y.checked, !1));}return l;}function M(n, u, i) {try {"function" == typeof n ? n(u) : n.current = u;} catch (n) {l.__e(n, i);}}function N(n, u, i) {var t, o;if (l.unmount && l.unmount(n), (t = n.ref) && (t.current && t.current !== n.__e || M(t, null, u)), null != (t = n.__c)) {if (t.componentWillUnmount) try {t.componentWillUnmount();} catch (n) {l.__e(n, u);}t.base = t.__P = null;}if (t = n.__k) for (o = 0; o < t.length; o++) t[o] && N(t[o], u, "function" != typeof n.type);i || null == n.__e || h(n.__e), n.__e = n.__d = void 0;}function O(n, l, u) {return this.constructor(n, u);}function S(u, i, t) {var o, r, f;l.__ && l.__(u, i), r = (o = "function" == typeof t) ? null : t && t.__k || i.__k, f = [], j(i, u = (!o && t || i).__k = v(d, null, [u]), r || e, e, void 0 !== i.ownerSVGElement, !o && t ? [t] : r ? null : i.firstChild ? n.call(i.childNodes) : null, f, !o && t ? t : r ? r.__e : i.firstChild, o), z(f, u);}function q(n, l) {S(n, l, q);}function B(l, u, i) {var t,o,r,f = a({}, l.props);for (r in u) "key" == r ? t = u[r] : "ref" == r ? o = u[r] : f[r] = u[r];return arguments.length > 2 && (f.children = arguments.length > 3 ? n.call(arguments, 2) : i), y(l.type, f, t || l.key, o || l.ref, null);}function D(n, l) {var u = { __c: l = "__cC" + f++, __: n, Consumer: function (n, l) {return n.children(l);}, Provider: function (n) {var u, i;return this.getChildContext || (u = [], (i = {})[l] = this, this.getChildContext = function () {return i;}, this.shouldComponentUpdate = function (n) {this.props.value !== n.value && u.some(m);}, this.sub = function (n) {u.push(n);var l = n.componentWillUnmount;n.componentWillUnmount = function () {u.splice(u.indexOf(n), 1), l && l.call(n);};}), n.children;} };return u.Provider.__ = u.Consumer.contextType = u;}n = c.slice, l = { __e: function (n, l) {for (var u, i, t; l = l.__;) if ((u = l.__c) && !u.__) try {if ((i = u.constructor) && null != i.getDerivedStateFromError && (u.setState(i.getDerivedStateFromError(n)), t = u.__d), null != u.componentDidCatch && (u.componentDidCatch(n), t = u.__d), t) return u.__E = u;} catch (l) {n = l;}throw n;} }, u = 0, i = function (n) {return null != n && void 0 === n.constructor;}, _.prototype.setState = function (n, l) {var u;u = null != this.__s && this.__s !== this.state ? this.__s : this.__s = a({}, this.state), "function" == typeof n && (n = n(a({}, u), this.props)), n && a(u, n), null != n && this.__v && (l && this.__h.push(l), m(this));}, _.prototype.forceUpdate = function (n) {this.__v && (this.__e = !0, n && this.__h.push(n), m(this));}, _.prototype.render = d, t = [], o = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, g.__r = 0, f = 0;export { S as render, q as hydrate, v as createElement, v as h, d as Fragment, p as createRef, i as isValidElement, _ as Component, B as cloneElement, D as createContext, A as toChildArray, l as options }; \ No newline at end of file