concept-collection / mri-scanner
mri-scanner / src / ui / SequencePanel.tsx
132 lines · 4.1 KBBlameHistoryRaw
1import { useRef, useState } from 'react'
2import type { StoredSequence } from '../storage/seqStore.ts'
4function fmtBytes(n: number): string {
5 if (n < 1024) return `${n} B`
6 if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
7 return `${(n / (1024 * 1024)).toFixed(1)} MB`
8}
10export function SequencePanel({
11 sequences,
12 selectedId,
13 onSelect,
14 onUpload,
15 onRename,
16 onDelete,
17}: {
18 sequences: StoredSequence[]
19 selectedId: string | null
20 onSelect: (id: string) => void
21 onUpload: (files: FileList) => void
22 onRename: (id: string, name: string) => void
23 onDelete: (id: string) => void
24}) {
25 const fileRef = useRef<HTMLInputElement>(null)
26 const [dragging, setDragging] = useState(false)
27 const [editingId, setEditingId] = useState<string | null>(null)
28 const [draft, setDraft] = useState('')
30 const startEdit = (seq: StoredSequence) => {
31 setEditingId(seq.id)
32 setDraft(seq.name)
33 }
34 const commitEdit = () => {
35 if (editingId && draft.trim()) onRename(editingId, draft.trim())
36 setEditingId(null)
37 }
39 return (
40 <section className="panel">
41 <div className="panel-head">
42 <h2>Sequences</h2>
43 <button type="button" className="btn-small" onClick={() => fileRef.current?.click()}>
44 + Upload .seq
45 </button>
46 <input
47 ref={fileRef}
48 type="file"
49 accept=".seq"
50 multiple
51 hidden
52 onChange={(e) => {
53 if (e.target.files?.length) onUpload(e.target.files)
54 e.target.value = ''
55 }}
56 />
57 </div>
59 <div
60 className={`dropzone${dragging ? ' dragging' : ''}`}
61 onDragOver={(e) => {
62 e.preventDefault()
63 setDragging(true)
64 }}
65 onDragLeave={() => setDragging(false)}
66 onDrop={(e) => {
67 e.preventDefault()
68 setDragging(false)
69 if (e.dataTransfer.files?.length) onUpload(e.dataTransfer.files)
70 }}
71 >
72 Drop <code>.seq</code> files here
73 </div>
75 {sequences.length === 0 ? (
76 <p className="muted">
77 No sequences yet. Upload a <code>.seq</code> file — for example one exported from{' '}
78 <a href="https://concept-collection.github.io/seqlab/" target="_blank" rel="noreferrer">
79 seqlab
80 </a>
81 .
82 </p>
83 ) : (
84 <ul className="seq-list">
85 {sequences.map((seq) => (
86 <li
87 key={seq.id}
88 className={seq.id === selectedId ? 'selected' : ''}
89 onClick={() => onSelect(seq.id)}
90 >
91 <div className="seq-row">
92 <input
93 type="radio"
94 checked={seq.id === selectedId}
95 onChange={() => onSelect(seq.id)}
96 onClick={(e) => e.stopPropagation()}
97 />
98 {editingId === seq.id ? (
99 <input
100 className="rename-input"
101 autoFocus
102 value={draft}
103 onChange={(e) => setDraft(e.target.value)}
104 onBlur={commitEdit}
105 onKeyDown={(e) => {
106 if (e.key === 'Enter') commitEdit()
107 if (e.key === 'Escape') setEditingId(null)
108 }}
109 onClick={(e) => e.stopPropagation()}
110 />
111 ) : (
112 <span className="seq-name" title={seq.name}>
113 {seq.name}
114 </span>
115 )}
116 <div className="seq-actions" onClick={(e) => e.stopPropagation()}>
117 <button type="button" className="icon-btn" title="Rename" onClick={() => startEdit(seq)}>
118
119 </button>
120 <button type="button" className="icon-btn danger" title="Delete" onClick={() => onDelete(seq.id)}>
121 🗑
122 </button>
123 </div>
124 </div>
125 <div className="seq-meta">{fmtBytes(seq.size)}</div>
126 </li>
127 ))}
128 </ul>
129 )}
130 </section>
131 )