Spaces:
Running
Running
File size: 4,887 Bytes
843a4b2 4f5e7fd 843a4b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | /**
* Compose view: type a message, see its Morse, pick who transmits (the robot's
* antennas or this device's speaker), and send it.
*/
import { el, clear, morsePattern } from "./dom.js";
import { textToMorse, morseToTokens } from "../lib/morse.js";
import { tokensToOnsets } from "../lib/wire.js";
export function createCompose(ctx) {
const root = el("section.view#view-compose");
const input = el("input.compose-input", {
type: "text",
placeholder: "Type a message… e.g. SOS",
value: "SOS",
oninput: refresh,
});
const morseOut = el("div.morse-out");
const warn = el("div.warn", { hidden: true });
const duration = el("span.muted");
const emitter = segmented(
"Who sends it?",
[
{ value: "robot", label: "🤖 Robot antennas" },
{ value: "device", label: "📱 This device" },
],
ctx.state.emitter,
(v) => { ctx.state.emitter = v; refresh(); },
);
const sendBtn = el("button.btn.primary.big", { onclick: send }, "Send ▶");
const stopBtn = el("button.btn.ghost", { onclick: stop, hidden: true }, "Stop ◼");
const status = el("div.status muted");
let cancelFn = null;
function currentSchedule() {
const { morse, skipped } = textToMorse(input.value);
const onsets = tokensToOnsets(morseToTokens(morse), ctx.timing());
return { morse, skipped, onsets };
}
function refresh() {
const { morse, skipped, onsets } = currentSchedule();
clear(morseOut);
if (!morse) {
morseOut.append(el("span.muted", {}, "Your Morse will appear here."));
} else {
morse.split(" / ").forEach((word, wi) => {
if (wi > 0) morseOut.append(el("span.wordsep", {}, "/"));
word.split(" ").forEach((p) => morseOut.append(morsePattern(p)));
});
}
if (skipped.length) {
warn.hidden = false;
warn.textContent = `Can't send in Morse: ${[...new Set(skipped)].join(" ")}`;
} else {
warn.hidden = true;
}
const secs = onsets.length ? (onsets[onsets.length - 1] / 1000 + 0.3) : 0;
duration.textContent = onsets.length ? `${onsets.length} taps · ~${secs.toFixed(1)}s` : "";
emitter.setValue(ctx.state.emitter);
}
async function send() {
const { onsets } = currentSchedule();
if (!onsets.length) return;
setSending(true);
const controller = new AbortController();
const highlight = (i) => flashTap(i, onsets.length);
try {
if (ctx.state.emitter === "robot") {
if (!ctx.reachy) { ctx.toast("No robot connected."); return; }
cancelFn = () => controller.abort();
await ctx.tapper().tap(onsets, { onTap: highlight, signal: controller.signal });
} else {
await new Promise((resolve) => {
const cancel = ctx.synth().play(onsets, {
onBlip: highlight,
onDone: resolve,
clickMs: ctx.timing().clickMs,
});
cancelFn = () => { cancel(); resolve(); };
});
}
} catch (e) {
ctx.toast(`Send failed: ${e?.message || e}`);
} finally {
setSending(false);
cancelFn = null;
}
}
function stop() {
cancelFn?.();
ctx.synth().stop();
ctx.tapper()?.stop?.();
setSending(false);
}
function setSending(on) {
sendBtn.hidden = on;
stopBtn.hidden = !on;
input.disabled = on;
}
function flashTap(i, total) {
status.textContent = `Transmitting… ${i + 1}/${total}`;
if (i + 1 >= total) setTimeout(() => { status.textContent = "Sent ✓"; }, 200);
}
root.append(
el("h2", {}, "Compose"),
input,
el("div.row.between", {}, [el("span.muted", {}, "Morse preview"), duration]),
morseOut,
warn,
emitter.node,
el("div.row.send-row", {}, [sendBtn, stopBtn]),
status,
);
refresh();
return { node: root, onShow: refresh };
}
function segmented(label, options, value, onChange) {
const btns = new Map();
const group = el("div.segmented");
options.forEach((o) => {
const b = el("button.seg", {
onclick: () => { setValue(o.value); onChange(o.value); },
}, o.label);
btns.set(o.value, b);
group.append(b);
});
function setValue(v) {
value = v;
btns.forEach((b, k) => b.classList.toggle("active", k === v));
}
setValue(value);
return {
node: el("div.field", {}, [el("label.field-label", {}, label), group]),
setValue,
};
}
|