Feat: Add one-shot pad override controls for export
This commit is contained in:
@@ -5,13 +5,17 @@ import { projectIdAtom } from '~/atoms/project';
|
||||
import CheckboxField from '~/components/form/CheckboxField';
|
||||
import SelectField from '~/components/form/SelectField';
|
||||
import useProject from '~/hooks/useProject';
|
||||
import { hasMultipleNoteVariations } from '~/lib/utils';
|
||||
import { getPadDisplayName, hasMultipleNoteVariations } from '~/lib/utils';
|
||||
import { PadCode } from '~/types/types';
|
||||
import CheckBox from '~/components/ui/CheckBox';
|
||||
import { ExportFormValues } from './exportFormSchema';
|
||||
|
||||
function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
||||
const { control } = useFormContext<ExportFormValues>();
|
||||
const { control, setValue } = useFormContext<ExportFormValues>();
|
||||
const format = useWatch({ control, name: 'format' });
|
||||
const includeArchivedSamples = useWatch({ control, name: 'includeArchivedSamples' });
|
||||
const exportAllPadsWithSamples = useWatch({ control, name: 'exportAllPadsWithSamples' });
|
||||
const abletonOneShotPads = useWatch({ control, name: 'abletonOneShotPads' });
|
||||
const drumRackGroupA = useWatch({ control, name: 'drumRackGroupA' });
|
||||
const drumRackGroupB = useWatch({ control, name: 'drumRackGroupB' });
|
||||
const drumRackGroupC = useWatch({ control, name: 'drumRackGroupC' });
|
||||
@@ -20,6 +24,48 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
||||
const projectId = useAtomValue(projectIdAtom);
|
||||
const { data: projectData } = useProject(projectId);
|
||||
|
||||
const playbackOverrides = useMemo(() => {
|
||||
if (!projectData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const usedPads = new Set(
|
||||
projectData.scenes.flatMap((scene) => scene.patterns.map((pattern) => pattern.pad)),
|
||||
);
|
||||
const result: Array<{ padCode: PadCode; title: string }> = [];
|
||||
|
||||
Object.entries(projectData.pads).forEach(([group, pads]) => {
|
||||
pads.forEach((pad, index) => {
|
||||
const padCode = `${group}${index}` as PadCode;
|
||||
if (
|
||||
pad.soundId <= 0 ||
|
||||
pad.playMode === 'oneshot' ||
|
||||
(!exportAllPadsWithSamples && !usedPads.has(padCode))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const soundName = projectData.sounds.find((sound) => sound.id === pad.soundId)?.meta.name;
|
||||
const mode = pad.playMode === 'key' ? 'KEY' : 'LEGATO';
|
||||
result.push({
|
||||
padCode,
|
||||
title: `${getPadDisplayName(group, index)} · ${soundName || pad.soundId} (EP: ${mode})`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [projectData, exportAllPadsWithSamples]);
|
||||
|
||||
const setOneShotOverride = (padCode: PadCode, checked: boolean) => {
|
||||
const current = abletonOneShotPads || [];
|
||||
setValue(
|
||||
'abletonOneShotPads',
|
||||
checked ? [...new Set([...current, padCode])] : current.filter((code) => code !== padCode),
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
};
|
||||
|
||||
const notesVariationWarnings = useMemo(() => {
|
||||
if (format === 'reaper') {
|
||||
return [];
|
||||
@@ -137,6 +183,25 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
||||
disabled={disabled || !includeArchivedSamples}
|
||||
helperText="Export all samples and create tracks for all pads with assigned samples, even if they are not used in patterns"
|
||||
/>
|
||||
{includeArchivedSamples && playbackOverrides.length > 0 && (
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<span className="text-sm font-semibold">Force One-Shot</span>
|
||||
<span className="text-xs opacity-80">
|
||||
Override selected EP KEY or LEGATO pads when Live Classic cuts them too early.
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 max-h-44 overflow-y-auto border border-black p-2">
|
||||
{playbackOverrides.map((item) => (
|
||||
<CheckBox
|
||||
key={item.padCode}
|
||||
checked={(abletonOneShotPads || []).includes(item.padCode)}
|
||||
onChange={(checked) => setOneShotOverride(item.padCode, checked)}
|
||||
title={item.title}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CheckboxField
|
||||
name="clips"
|
||||
title="Session clips instead of arrangements"
|
||||
|
||||
@@ -43,6 +43,7 @@ function ExportProjectDialog({
|
||||
defaultValues: {
|
||||
format: EXPORT_FORMATS[0].value,
|
||||
abletonVersion: '10',
|
||||
abletonOneShotPads: [],
|
||||
projectName: `Project${projectId}`,
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { ExportFormatId } from '../../../types/types';
|
||||
import { ExportFormatId, PadCode } from '../../../types/types';
|
||||
|
||||
const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
||||
'ableton',
|
||||
@@ -11,6 +11,11 @@ const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
||||
export const exportFormSchema = z.object({
|
||||
format: z.enum(FORMAT_IDS),
|
||||
abletonVersion: z.enum(['10', '11']),
|
||||
abletonOneShotPads: z.array(
|
||||
z.custom<PadCode>(
|
||||
(value) => typeof value === 'string' && /^[abcd]\d+$/.test(value),
|
||||
),
|
||||
),
|
||||
projectName: z.string(),
|
||||
includeArchivedSamples: z.boolean(),
|
||||
exportAllPadsWithSamples: z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user