SequencePanel.svelte 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. <script lang="ts">
  2. import { Service, type Limits, type Sequence } from "./api";
  3. import { app, confirm, run } from "./store.svelte";
  4. import { slotLabel } from "./slots";
  5. // Settings for the selected sequence slot: add, name, BPM, length, remove.
  6. let { slot, seq, limits }: { slot: number; seq: Sequence | undefined; limits: Limits } = $props();
  7. const seqNo = $derived(String(slot + 1).padStart(2, "0"));
  8. // Field values follow the sequence and are sent to Go when a field is
  9. // left or Enter is pressed. A refused value comes back from Go as the old
  10. // one, which resets the field.
  11. let name = $derived(seq?.name ?? "");
  12. let bpm = $derived(seq?.bpm ?? 120);
  13. let bars = $derived(seq?.bars ?? 2);
  14. async function save() {
  15. if (!seq) return;
  16. const s = seq.slot;
  17. const n = name;
  18. const b = Number(bpm);
  19. const l = Number(bars);
  20. if (n === seq.name && b === seq.bpm && l === seq.bars) return;
  21. if (seq.hasNotes && l < seq.bars) {
  22. const ok = await confirm({
  23. title: "Shorten a sequence with notes?",
  24. message: `${seq.name} has recorded notes. Notes after bar ${l} stay in the file; what the device does with them is not yet known.`,
  25. items: [],
  26. action: "Shorten",
  27. });
  28. if (!ok) {
  29. bars = seq.bars;
  30. return;
  31. }
  32. }
  33. await run(() => Service.SetSequence(s, n, b, l));
  34. // run() replaced the state; if Go refused, the fields still hold the
  35. // typed values, so put the stored ones back.
  36. const now = app.state?.sequences?.find((q) => q.slot === s);
  37. if (now) {
  38. name = now.name;
  39. bpm = now.bpm;
  40. bars = now.bars;
  41. }
  42. }
  43. async function remove() {
  44. if (!seq) return;
  45. const s = seq.slot;
  46. const ok = await confirm({
  47. title: `Remove ${seq.name}?`,
  48. message: seq.hasNotes ? "Its recorded notes will be deleted too." : "The sequence has no recorded notes.",
  49. items: [],
  50. action: "Remove",
  51. });
  52. if (ok) await run(() => Service.RemoveSequence(s));
  53. }
  54. function enter(e: KeyboardEvent) {
  55. if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur(); // blur fires change
  56. }
  57. function add() {
  58. const s = slot;
  59. return run(() => Service.AddSequence(s));
  60. }
  61. </script>
  62. <section class="panel settings" aria-label="Sequence settings">
  63. <div class="panel-head">
  64. <div class="slot-id">{slotLabel(slot)}</div>
  65. <div class="title">
  66. <h2 class="eyebrow">Sequence settings</h2>
  67. <div>{seq ? "Sequence " + seqNo : "Empty slot"}</div>
  68. </div>
  69. </div>
  70. {#if !seq}
  71. <div class="empty-box">
  72. <p>No sequence in this slot.</p>
  73. <button type="button" class="btn primary" onclick={add}>Add sequence</button>
  74. </div>
  75. {:else}
  76. <div class="stack">
  77. <div class="control">
  78. <label class="label" for="seq-name">Name</label>
  79. <input id="seq-name" class="field" type="text" spellcheck="false" autocomplete="off" value={name} oninput={(e) => (name = e.currentTarget.value)} onchange={save} onkeydown={enter} />
  80. </div>
  81. <div class="pair">
  82. <div class="control">
  83. <label class="label" for="seq-bpm">BPM ({limits.minBPM}–{limits.maxBPM})</label>
  84. <input id="seq-bpm" class="field mono" type="number" min={limits.minBPM} max={limits.maxBPM} step="any" value={bpm} oninput={(e) => (bpm = e.currentTarget.valueAsNumber)} onchange={save} onkeydown={enter} />
  85. </div>
  86. <div class="control">
  87. <label class="label" for="seq-bars">Length (bars)</label>
  88. <input id="seq-bars" class="field mono" type="number" min="1" max={limits.maxBars} step="1" value={bars} oninput={(e) => (bars = e.currentTarget.valueAsNumber)} onchange={save} onkeydown={enter} />
  89. </div>
  90. </div>
  91. <span class="hint">Notes are recorded on the device. This screen sets name, tempo and length only.</span>
  92. {#if seq.hasNotes}
  93. <span class="hint notes">This sequence has recorded notes.</span>
  94. {/if}
  95. <div class="row divided">
  96. <button type="button" class="btn secondary" onclick={remove}>Remove sequence</button>
  97. </div>
  98. </div>
  99. {/if}
  100. </section>
  101. <style>
  102. .pair {
  103. display: grid;
  104. grid-template-columns: repeat(2, minmax(0, 1fr));
  105. gap: 10px;
  106. }
  107. .notes {
  108. color: var(--accent);
  109. }
  110. </style>