Files
2026-09-19 20:21:47 +02:00

102 lines
4.7 KiB
JavaScript

// 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.'),
),
),
);
}