diff --git a/static/components/PaneCP.js b/static/components/PaneCP.js index f8f27bb..f273f5c 100644 --- a/static/components/PaneCP.js +++ b/static/components/PaneCP.js @@ -3,22 +3,39 @@ import { fetchScoreText, putScoreText } from '../api.js'; import { patchScore } from '../exporter.js'; import { StatusPoller } from './StatusPoller.js'; +// Short label + identifying meta for each node type. +function shortView(node) { + if (!node) return { typeTag: '?', label: '?', meta: [] }; + switch (node.type) { + case 'score': + return { + typeTag: 'score', + label: node.info?.title ?? '(untitled)', + meta: node.info?.composer ? [{ key: 'composer', value: node.info.composer }] : [], + }; + case 'instrument': + return { typeTag: 'instrument', label: node.name, meta: [] }; + case 'variation': + return { + typeTag: 'variation', + label: node.dependsOn ? `ATTR: ${node.dependsOn}` : '(root variation)', + meta: [], + }; + case 'label_spec': + return { typeTag: 'label', label: node.label ?? '(no label)', meta: [] }; + case 'bar': + return { typeTag: 'bar', label: node.id, meta: [] }; + default: + return { typeTag: node.type, label: node.type, meta: [] }; + } +} + export const PaneCP = { props: ['store', 'onImportClick'], setup(props) { const exporting = ref(false); const exportError = ref(''); - function breadcrumbLabel(node) { - if (!node) return '?'; - if (node.type === 'score') return 'Score'; - if (node.type === 'instrument') return node.name; - if (node.type === 'variation') return node.dependsOn ? `var(${node.dependsOn})` : 'variation'; - if (node.type === 'label_spec') return node.label ?? 'label'; - if (node.type === 'bar') return node.id; - return node.type; - } - async function doExport() { exportError.value = ''; exporting.value = true; @@ -27,7 +44,6 @@ export const PaneCP = { props.store.rawScoreText = raw; const patched = patchScore(raw, props.store.scoreModel.instruments); await putScoreText(patched, props.store.credentials); - // Start polling props.store.synthesisStatus = { frozen: false, progress: 0 }; } catch (e) { exportError.value = e.message; @@ -41,57 +57,65 @@ export const PaneCP = { const model = store.scoreModel; const fp = store.focusPath; + // Build vertical path list: score root + each focus node. + const pathItems = model ? [ + { node: model, idx: -1 }, + ...fp.map((node, idx) => ({ node, idx })), + ] : []; + return h('div', null, [ // Header h('div', { class: 'se-cp-header' }, [ - h('span', { class: 'se-cp-title' }, - model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'), h('button', { class: 'se-btn', disabled: store.isDirty, title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server', onClick: props.onImportClick, - }, 'Import'), + }, '→ Import'), + h('span', { class: 'se-cp-title' }, + model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'), model ? h('button', { class: 'se-btn se-btn-primary', disabled: !store.isDirty || exporting.value, onClick: doExport, - }, exporting.value ? 'Exporting…' : 'Export') : null, + }, exporting.value ? 'Exporting…' : 'Export ↑') : null, ]), - // Breadcrumb - fp.length ? h('div', { class: 'se-breadcrumb' }, [ - h('span', { onClick: () => store.setFocus([]) }, 'Score'), - ...fp.map((node, i) => [ - ' › ', - h('span', { onClick: () => store.setFocus(fp.slice(0, i + 1)) }, - breadcrumbLabel(node)), - ]).flat(), - ]) : null, - - // Score info - model ? h('dl', { style: 'font-size:0.8rem;margin:0.5rem 0' }, [ - h('dt', null, 'Instruments'), - h('dd', null, String(model.instruments.length)), - h('dt', null, 'Bars'), - h('dd', null, String(model.bars.length)), - ]) : null, + // Vertical focus path — short views, clickable to navigate up + pathItems.length ? h('ul', { class: 'se-object-list se-cp-path' }, + pathItems.map(({ node, idx }) => { + const { typeTag, label, meta } = shortView(node); + const isCurrent = idx === fp.length - 1 || (idx === -1 && fp.length === 0); + return h('li', { + class: ['se-object-item', isCurrent ? 'focused' : null], + onClick: () => { + if (idx === -1) store.setFocus([]); + else store.setFocus(fp.slice(0, idx + 1)); + }, + }, [ + h('span', { class: 'se-object-type' }, typeTag), + h('span', { class: 'se-object-label' }, [ + h('strong', null, label), + ...meta.map(({ key, value }) => + h('span', { style: 'color:#888;margin-left:0.5rem;font-size:0.8em' }, + `${key}=${value}`) + ), + ]), + ]); + }) + ) : null, // Export error exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null, - // Status poller (shown after export started) + // Status poller store.synthesisStatus && !store.synthesisStatus.frozen - ? h(StatusPoller, { store }) - : null, + ? h(StatusPoller, { store }) : null, // Result link store.synthesisStatus?.frozen && !store.synthesisStatus?.error - ? h('a', { - href: '/sompyle/result.mp3', - style: 'display:block;margin-top:0.5rem', - }, 'Download result') - : null, + ? h('a', { href: '/sompyle/result.mp3', style: 'display:block;margin-top:0.5rem' }, + 'Download result') : null, ]); }; }, diff --git a/static/components/PaneFO.js b/static/components/PaneFO.js index 69571b0..c0c347e 100644 --- a/static/components/PaneFO.js +++ b/static/components/PaneFO.js @@ -3,6 +3,15 @@ import { ObjectExtended } from './ObjectExtended.js'; import { EnvelopeEditor } from './EnvelopeEditor.js'; import { LinkedInstrumentModal } from './LinkedInstrumentModal.js'; +function scoreInfoFields(info) { + return [ + { key: 'title', value: info?.title ?? '', editable: true }, + { key: 'composer', value: info?.composer ?? '', editable: true }, + { key: 'source', value: info?.source ?? '', editable: true }, + { key: 'encrypter', value: info?.encrypter ?? '', editable: true }, + ]; +} + function instrFields(instr) { return [ { key: 'name', value: instr.name, editable: false }, @@ -61,7 +70,19 @@ export const PaneFO = { const children = []; if (!node || node.type === 'score') { - return h('div', { class: 'se-fo-pane' }, 'Nothing selected'); + const model = props.store.scoreModel; + if (!model) return h('div', { class: 'se-fo-pane' }, 'No score loaded'); + return h('div', { class: 'se-fo-pane' }, [ + h('h4', { style: 'margin:0 0 0.5rem' }, 'Score'), + h(ObjectExtended, { + fields: scoreInfoFields(model.info), + onChange: ({ key, value }) => { + if (!model.info) model.info = {}; + model.info[key] = value; + props.store.markDirty(); + }, + }), + ]); } if (node.type === 'instrument') {