// Uplink form: choose a binary file (or type hex) and transmit it. import { api } from './api.js'; import { h, clear, toast } from './dom.js'; import { hexView, parseHex } from './hexview.js'; export function uplinkView(ctx) { const ship = ctx.ship; const limit = ctx.state.info?.comm_bytes ?? 1024; const open = ship && (ship.status === 'launching' || ship.status === 'active'); let bytes = null; let label = ''; const preview = h('div', { class: 'uplink-preview' }); const meter = h('div', { class: 'meter', role: 'meter', 'aria-valuemin': '0', 'aria-valuemax': String(limit), 'aria-label': 'Message size' }, h('div', { class: 'fill' })); const sizeText = h('p', { class: 'dim' }, `NO MESSAGE LOADED. LIMIT ${limit} BYTES.`); const err = h('p', { class: 'form-error', role: 'alert' }); const send = h('button', { class: 'btn primary', type: 'button', disabled: true }, ship ? `TRANSMIT TO SHIP #${ship.id}` : 'NO SHIP'); function set(b, from) { bytes = b; label = from; err.textContent = ''; const over = b.length > limit; meter.classList.toggle('over', over); meter.setAttribute('aria-valuenow', String(b.length)); meter.querySelector('.fill').style.setProperty('width', `${Math.min(100, (b.length / limit) * 100)}%`); sizeText.textContent = `${from}: ${b.length} BYTES OF ${limit}${over ? ' -- TOO LARGE' : ''}`; if (over) err.textContent = `MESSAGE EXCEEDS THE ${limit}-BYTE LINK. REMOVE ${b.length - limit} BYTES.`; else if (b.length === 0) err.textContent = 'MESSAGE IS EMPTY.'; send.disabled = !open || over || b.length === 0; clear(preview); if (b.length) { preview.append(hexView(b, { rows: 8 }).el); if (b.length > 128) preview.append(h('p', { class: 'hint' }, `SHOWING THE FIRST 128 OF ${b.length} BYTES.`)); } } // ---- file chooser / drop zone ------------------------------------------ const file = h('input', { type: 'file', id: 'uplink-file', class: 'sr-only' }); const drop = h('label', { class: `drop ${open ? '' : 'disabled'}`, for: 'uplink-file' }, h('span', { class: 'drop-big' }, 'DROP A BINARY FILE HERE'), h('span', { class: 'dim' }, 'or click to choose one'), ); file.disabled = !open; const takeFile = async (f) => { if (!f) return; if (f.size > 1 << 20) { err.textContent = 'THAT FILE IS FAR TOO LARGE FOR THE LINK.'; return; } set(new Uint8Array(await f.arrayBuffer()), f.name.toUpperCase()); }; file.addEventListener('change', () => takeFile(file.files[0])); for (const ev of ['dragenter', 'dragover']) drop.addEventListener(ev, (e) => { e.preventDefault(); if (open) drop.classList.add('over'); }); for (const ev of ['dragleave', 'drop']) drop.addEventListener(ev, () => drop.classList.remove('over')); drop.addEventListener('drop', (e) => { e.preventDefault(); if (open) takeFile(e.dataTransfer.files[0]); }); // ---- hex entry --------------------------------------------------------- const hexIn = h('textarea', { id: 'hex-in', rows: '3', spellcheck: 'false', placeholder: 'DE AD BE EF 00 01 ...', disabled: !open, 'aria-label': 'Message as hexadecimal' }); const useHex = h('button', { class: 'btn small', type: 'button', disabled: !open, onclick: () => { try { set(parseHex(hexIn.value), 'HEX ENTRY'); } catch (e) { err.textContent = `HEX ERROR: ${e.message}`; } } }, 'USE HEX'); send.addEventListener('click', async () => { send.disabled = true; try { await api.setUplink(ship.id, bytes); toast(`UPLINK QUEUED FOR SHIP #${ship.id}: ${bytes.length} BYTES. DELIVERED AT THE START OF THE NEXT RUN.`); } catch (e) { err.textContent = `TRANSMISSION FAILED: ${e.message}`; send.disabled = false; } }); const status = !ship ? 'YOU HAVE NO SHIP.' : open ? `TARGET: SHIP #${ship.id}. A NEW UPLINK REPLACES ONE ALREADY QUEUED.` : `SHIP #${ship.id} IS ${ship.status.toUpperCase()}. UPLINKS CAN ONLY BE SENT TO SHIPS THAT ARE LAUNCHING OR IN THE BELT.`; return h('div', { class: 'grid-2 uplink-grid' }, h('section', { class: 'panel' }, h('div', { class: 'panel-title' }, 'UPLINK TRANSMITTER'), h('div', { class: 'panel-body stack' }, h('p', { class: open ? 'hint' : 'warn' }, status), drop, file, h('label', { for: 'hex-in' }, 'OR ENTER HEX'), hexIn, h('div', null, useHex), meter, sizeText, err, h('div', null, send), ), ), h('section', { class: 'panel' }, h('div', { class: 'panel-title' }, 'OUTGOING MESSAGE'), h('div', { class: 'panel-body' }, preview, h('p', { class: 'hint' }, 'THE MESSAGE IS OPAQUE BYTES. YOUR SHIP\'S PROGRAM DECIDES WHAT IT MEANS. IT ARRIVES IN THE UPLINK BUFFER (RAM 0-1023) BEFORE THE FIRST TICK OF THE NEXT DAILY RUN.'), ), ), ); }