forked from flow/vue3js-app-proposal-for-sdk-claude
Redesign PaneCP as vertical focus-path menu; add score metadata to FO pane
PaneCP: replace horizontal breadcrumb with vertical list of short views for each node in the focus path (type tag + label/identifier). Clicking any item above current navigates up. PaneFO: show score info fields (title, composer, source, encrypter) as editable DL when at score level instead of 'Nothing selected'.
This commit is contained in:
parent
839ea3f95c
commit
06b2bab5d3
@ -3,22 +3,39 @@ import { fetchScoreText, putScoreText } from '../api.js';
|
|||||||
import { patchScore } from '../exporter.js';
|
import { patchScore } from '../exporter.js';
|
||||||
import { StatusPoller } from './StatusPoller.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 = {
|
export const PaneCP = {
|
||||||
props: ['store', 'onImportClick'],
|
props: ['store', 'onImportClick'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
const exportError = ref('');
|
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() {
|
async function doExport() {
|
||||||
exportError.value = '';
|
exportError.value = '';
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
@ -27,7 +44,6 @@ export const PaneCP = {
|
|||||||
props.store.rawScoreText = raw;
|
props.store.rawScoreText = raw;
|
||||||
const patched = patchScore(raw, props.store.scoreModel.instruments);
|
const patched = patchScore(raw, props.store.scoreModel.instruments);
|
||||||
await putScoreText(patched, props.store.credentials);
|
await putScoreText(patched, props.store.credentials);
|
||||||
// Start polling
|
|
||||||
props.store.synthesisStatus = { frozen: false, progress: 0 };
|
props.store.synthesisStatus = { frozen: false, progress: 0 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
exportError.value = e.message;
|
exportError.value = e.message;
|
||||||
@ -41,57 +57,65 @@ export const PaneCP = {
|
|||||||
const model = store.scoreModel;
|
const model = store.scoreModel;
|
||||||
const fp = store.focusPath;
|
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, [
|
return h('div', null, [
|
||||||
// Header
|
// Header
|
||||||
h('div', { class: 'se-cp-header' }, [
|
h('div', { class: 'se-cp-header' }, [
|
||||||
h('span', { class: 'se-cp-title' },
|
|
||||||
model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'),
|
|
||||||
h('button', {
|
h('button', {
|
||||||
class: 'se-btn',
|
class: 'se-btn',
|
||||||
disabled: store.isDirty,
|
disabled: store.isDirty,
|
||||||
title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server',
|
title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server',
|
||||||
onClick: props.onImportClick,
|
onClick: props.onImportClick,
|
||||||
}, 'Import'),
|
}, '→ Import'),
|
||||||
|
h('span', { class: 'se-cp-title' },
|
||||||
|
model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'),
|
||||||
model ? h('button', {
|
model ? h('button', {
|
||||||
class: 'se-btn se-btn-primary',
|
class: 'se-btn se-btn-primary',
|
||||||
disabled: !store.isDirty || exporting.value,
|
disabled: !store.isDirty || exporting.value,
|
||||||
onClick: doExport,
|
onClick: doExport,
|
||||||
}, exporting.value ? 'Exporting…' : 'Export') : null,
|
}, exporting.value ? 'Exporting…' : 'Export ↑') : null,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Breadcrumb
|
// Vertical focus path — short views, clickable to navigate up
|
||||||
fp.length ? h('div', { class: 'se-breadcrumb' }, [
|
pathItems.length ? h('ul', { class: 'se-object-list se-cp-path' },
|
||||||
h('span', { onClick: () => store.setFocus([]) }, 'Score'),
|
pathItems.map(({ node, idx }) => {
|
||||||
...fp.map((node, i) => [
|
const { typeTag, label, meta } = shortView(node);
|
||||||
' › ',
|
const isCurrent = idx === fp.length - 1 || (idx === -1 && fp.length === 0);
|
||||||
h('span', { onClick: () => store.setFocus(fp.slice(0, i + 1)) },
|
return h('li', {
|
||||||
breadcrumbLabel(node)),
|
class: ['se-object-item', isCurrent ? 'focused' : null],
|
||||||
]).flat(),
|
onClick: () => {
|
||||||
]) : null,
|
if (idx === -1) store.setFocus([]);
|
||||||
|
else store.setFocus(fp.slice(0, idx + 1));
|
||||||
// Score info
|
},
|
||||||
model ? h('dl', { style: 'font-size:0.8rem;margin:0.5rem 0' }, [
|
}, [
|
||||||
h('dt', null, 'Instruments'),
|
h('span', { class: 'se-object-type' }, typeTag),
|
||||||
h('dd', null, String(model.instruments.length)),
|
h('span', { class: 'se-object-label' }, [
|
||||||
h('dt', null, 'Bars'),
|
h('strong', null, label),
|
||||||
h('dd', null, String(model.bars.length)),
|
...meta.map(({ key, value }) =>
|
||||||
]) : null,
|
h('span', { style: 'color:#888;margin-left:0.5rem;font-size:0.8em' },
|
||||||
|
`${key}=${value}`)
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
) : null,
|
||||||
|
|
||||||
// Export error
|
// Export error
|
||||||
exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null,
|
exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null,
|
||||||
|
|
||||||
// Status poller (shown after export started)
|
// Status poller
|
||||||
store.synthesisStatus && !store.synthesisStatus.frozen
|
store.synthesisStatus && !store.synthesisStatus.frozen
|
||||||
? h(StatusPoller, { store })
|
? h(StatusPoller, { store }) : null,
|
||||||
: null,
|
|
||||||
|
|
||||||
// Result link
|
// Result link
|
||||||
store.synthesisStatus?.frozen && !store.synthesisStatus?.error
|
store.synthesisStatus?.frozen && !store.synthesisStatus?.error
|
||||||
? h('a', {
|
? h('a', { href: '/sompyle/result.mp3', style: 'display:block;margin-top:0.5rem' },
|
||||||
href: '/sompyle/result.mp3',
|
'Download result') : null,
|
||||||
style: 'display:block;margin-top:0.5rem',
|
|
||||||
}, 'Download result')
|
|
||||||
: null,
|
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,6 +3,15 @@ import { ObjectExtended } from './ObjectExtended.js';
|
|||||||
import { EnvelopeEditor } from './EnvelopeEditor.js';
|
import { EnvelopeEditor } from './EnvelopeEditor.js';
|
||||||
import { LinkedInstrumentModal } from './LinkedInstrumentModal.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) {
|
function instrFields(instr) {
|
||||||
return [
|
return [
|
||||||
{ key: 'name', value: instr.name, editable: false },
|
{ key: 'name', value: instr.name, editable: false },
|
||||||
@ -61,7 +70,19 @@ export const PaneFO = {
|
|||||||
const children = [];
|
const children = [];
|
||||||
|
|
||||||
if (!node || node.type === 'score') {
|
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') {
|
if (node.type === 'instrument') {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user