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 CheckboxField from '~/components/form/CheckboxField';
|
||||||
import SelectField from '~/components/form/SelectField';
|
import SelectField from '~/components/form/SelectField';
|
||||||
import useProject from '~/hooks/useProject';
|
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';
|
import { ExportFormValues } from './exportFormSchema';
|
||||||
|
|
||||||
function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
||||||
const { control } = useFormContext<ExportFormValues>();
|
const { control, setValue } = useFormContext<ExportFormValues>();
|
||||||
const format = useWatch({ control, name: 'format' });
|
const format = useWatch({ control, name: 'format' });
|
||||||
const includeArchivedSamples = useWatch({ control, name: 'includeArchivedSamples' });
|
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 drumRackGroupA = useWatch({ control, name: 'drumRackGroupA' });
|
||||||
const drumRackGroupB = useWatch({ control, name: 'drumRackGroupB' });
|
const drumRackGroupB = useWatch({ control, name: 'drumRackGroupB' });
|
||||||
const drumRackGroupC = useWatch({ control, name: 'drumRackGroupC' });
|
const drumRackGroupC = useWatch({ control, name: 'drumRackGroupC' });
|
||||||
@@ -20,6 +24,48 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
|||||||
const projectId = useAtomValue(projectIdAtom);
|
const projectId = useAtomValue(projectIdAtom);
|
||||||
const { data: projectData } = useProject(projectId);
|
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(() => {
|
const notesVariationWarnings = useMemo(() => {
|
||||||
if (format === 'reaper') {
|
if (format === 'reaper') {
|
||||||
return [];
|
return [];
|
||||||
@@ -137,6 +183,25 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) {
|
|||||||
disabled={disabled || !includeArchivedSamples}
|
disabled={disabled || !includeArchivedSamples}
|
||||||
helperText="Export all samples and create tracks for all pads with assigned samples, even if they are not used in patterns"
|
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
|
<CheckboxField
|
||||||
name="clips"
|
name="clips"
|
||||||
title="Session clips instead of arrangements"
|
title="Session clips instead of arrangements"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function ExportProjectDialog({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
format: EXPORT_FORMATS[0].value,
|
format: EXPORT_FORMATS[0].value,
|
||||||
abletonVersion: '10',
|
abletonVersion: '10',
|
||||||
|
abletonOneShotPads: [],
|
||||||
projectName: `Project${projectId}`,
|
projectName: `Project${projectId}`,
|
||||||
includeArchivedSamples: true,
|
includeArchivedSamples: true,
|
||||||
exportAllPadsWithSamples: false,
|
exportAllPadsWithSamples: false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ExportFormatId } from '../../../types/types';
|
import { ExportFormatId, PadCode } from '../../../types/types';
|
||||||
|
|
||||||
const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
||||||
'ableton',
|
'ableton',
|
||||||
@@ -11,6 +11,11 @@ const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
|||||||
export const exportFormSchema = z.object({
|
export const exportFormSchema = z.object({
|
||||||
format: z.enum(FORMAT_IDS),
|
format: z.enum(FORMAT_IDS),
|
||||||
abletonVersion: z.enum(['10', '11']),
|
abletonVersion: z.enum(['10', '11']),
|
||||||
|
abletonOneShotPads: z.array(
|
||||||
|
z.custom<PadCode>(
|
||||||
|
(value) => typeof value === 'string' && /^[abcd]\d+$/.test(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
projectName: z.string(),
|
projectName: z.string(),
|
||||||
includeArchivedSamples: z.boolean(),
|
includeArchivedSamples: z.boolean(),
|
||||||
exportAllPadsWithSamples: z.boolean(),
|
exportAllPadsWithSamples: z.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user