c0dev0id 2060f109b6 Show numeric sub-variation edge values as bare numbers
Numeric dependsOn values (merge-type variations) are RFC edge values,
not ATTR names. Display them as the number, not as "ATTR: N" or
"subvariation N".
2026-06-23 18:49:17 +02:00

124 lines
5.2 KiB
JavaScript

import { h, ref } from 'vue';
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': {
const dep = node.dependsOn;
const label = dep == null ? '(root variation)'
: isNaN(Number(dep)) ? `ATTR: ${dep}`
: String(dep);
return { typeTag: 'variation', label, 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('');
async function doExport() {
exportError.value = '';
exporting.value = true;
try {
const raw = await fetchScoreText(props.store.credentials);
props.store.rawScoreText = raw;
const patched = patchScore(raw, props.store.scoreModel.instruments);
await putScoreText(patched, props.store.credentials);
props.store.synthesisStatus = { frozen: false, progress: 0 };
} catch (e) {
exportError.value = e.message;
} finally {
exporting.value = false;
}
}
return () => {
const store = props.store;
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('button', {
class: 'se-btn',
disabled: store.isDirty,
title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server',
onClick: props.onImportClick,
}, '→ 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,
]),
// 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
store.synthesisStatus && !store.synthesisStatus.frozen
? 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,
]);
};
},
};