Add boilerplate files and licenses

This commit is contained in:
2026-07-13 04:39:10 +02:00
parent 69cabda489
commit fac7e80bb7
202 changed files with 23125 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useAtom, useAtomValue } from 'jotai';
import { useEffect } from 'preact/hooks';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { feedbackDialogAtom } from '~/atoms/feedbackDialog';
import { projectIdAtom } from '~/atoms/project';
import InputField from '~/components/form/InputField';
import SelectField from '~/components/form/SelectField';
import IconFile from '~/components/icons/file.svg?react';
import Button from '~/components/ui/Button';
import Dialog from '~/components/ui/Dialog';
import useExportProject, { EXPORT_FORMATS } from '~/hooks/useExportProject';
import { usePersistedFormValues } from '~/hooks/usePersistedFormValues';
import { ExportFormatId } from '~/types/types';
import ExportOptions from './ExportOptions';
import ExportScenes from './ExportScenes';
import { ExportFormValues, exportFormSchema, PERSISTED_FIELDS } from './exportFormSchema';
const INVALID_FILENAME_CHARS = /[<>:"/\\|?*]/g;
function sanitizeFileName(value: string): string {
return value.replace(INVALID_FILENAME_CHARS, '');
}
const NOTES: Record<ExportFormatId, string> = {
ableton: `Please note that the exported project won't sound exactly the same as it does on the device.`,
dawproject: `Unfortunately, the DAWproject format does not currently support the "Sampler" instrument, so you will need to manually assign the samples in your DAW.`,
midi: `The simplest format supported by any DAW. But you have to assign the samples manually.`,
reaper: `Only basic sampler features are supported`,
};
function ExportProjectDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const projectId = useAtomValue(projectIdAtom);
const form = useForm<ExportFormValues>({
resolver: zodResolver(exportFormSchema),
defaultValues: {
format: EXPORT_FORMATS[0].value,
projectName: `Project${projectId}`,
includeArchivedSamples: true,
exportAllPadsWithSamples: false,
clips: false,
groupTracks: true,
drumRackGroupA: false,
drumRackGroupB: false,
drumRackGroupC: false,
drumRackGroupD: false,
sendEffects: true,
allScenes: true,
selectedScenes: [],
},
});
usePersistedFormValues(form, PERSISTED_FIELDS, exportFormSchema);
const format = useWatch({ control: form.control, name: 'format' });
const projectName = useWatch({ control: form.control, name: 'projectName' });
const allScenes = useWatch({ control: form.control, name: 'allScenes' });
const selectedScenes = useWatch({ control: form.control, name: 'selectedScenes' });
const trimmedName = (projectName ?? '').trim();
const canExport = (allScenes || (selectedScenes?.length ?? 0) > 0) && trimmedName.length > 0;
const {
startExport,
cancelExport,
isPending,
pendingStatus,
percentage,
reset,
result,
error,
sampleReport,
} = useExportProject();
const [_, openFeedbackDialog] = useAtom(feedbackDialogAtom);
const handleExport = form.handleSubmit(async (values) => {
await startExport({
...values,
projectName: values.projectName.trim(),
});
});
useEffect(() => {
reset();
}, [format]);
return (
<FormProvider {...form}>
<Dialog isOpen={open} onClose={() => onOpenChange(false)} className="min-w-200 max-w-175">
<div className="flex flex-col gap-2 min-w-150">
<h3 className="text-lg font-semibold">Export</h3>
<SelectField name="format" disabled={isPending} className="mr-auto mb-4">
{EXPORT_FORMATS.map((f) => (
<option key={f.value} value={f.value}>
{f.name}
</option>
))}
</SelectField>
<h4 className="font-semibold">Project name</h4>
<InputField
name="projectName"
disabled={isPending}
className="w-100"
placeholder="Enter project name"
transform={sanitizeFileName}
/>
<div className="flex gap-14 mt-2">
<ExportOptions disabled={isPending} />
<ExportScenes disabled={isPending} />
</div>
{NOTES[format] && (
<div className="mt-4">
<h3 className="text-lg font-semibold">Notes</h3>
<div className="text-sm whitespace-pre-line">{NOTES[format]}</div>
</div>
)}
{!result && !percentage && (
<a
href="https://eptoolkit.ep133-to-daw.cc/"
target="_blank"
rel="noopener noreferrer"
className="bg-creme w-full bg-grid border border-black gap-2 p-2 text-black flex mt-4 justify-center items-center"
>
<p>
Want fader automation support? Get <strong>EP Toolkit</strong>: desktop app for
exporting, sample management, backups and more.{' '}
</p>
<img
src="/eptoolkit/eptoolkit-logo.png"
alt="EP Toolkit"
className="size-10 shrink-0 ml-auto"
/>
</a>
)}
</div>
<div className="mt-4 min-h-13.5">
{percentage > 0 && (
<>
<div className="h-7.5 border border-black bg-[#ccc]">
<div className="h-full bg-brand" style={{ width: `${percentage}%` }} />
</div>
<div className="text-sm text-black min-h-5 text-center mt-1">{pendingStatus}</div>
</>
)}
{result && percentage >= 100 && (
<div className="mt-4">
<h3 className="text-lg font-semibold">Files to download</h3>
{result.files.map((f) => (
<div key={f.name} className="flex gap-2 items-center">
<IconFile className="w-6 h-6" />
<a href={f.url} download={f.name} className="text-blue-500">
{f.name}
</a>
<span className="mr-auto text-sm opacity-80 ml-2">
{(f.size / 1024).toFixed(2)}KB
</span>
</div>
))}
{sampleReport && (
<div className="mt-4">
<h3 className="text-lg font-semibold">Sample Report</h3>
<div className="text-sm">
<p className="text-green-600">
Downloaded: {sampleReport.downloaded.length} samples
</p>
{sampleReport.missing.length > 0 && (
<div className="mt-2">
<p className="text-red-600">
Missing: {sampleReport.missing.length} samples
</p>
<ul className="mt-1 ml-4 max-h-25 overflow-y-auto">
{sampleReport.missing.map((missing, index) => (
<li key={index} className="text-red-500 text-xs truncate">
{missing.name}: {missing.error}
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
</div>
)}
{error && (
<div className="my-4 bg-red-100/50 p-4">
<h3 className="text-lg font-semibold text-center">Error</h3>
<p className="text-sm text-red-500 text-center">
Opps, that's not good. Hope the error tracking system helps me to find the problem.
If you can, please submit an error report - it really helps me fix things faster.
Thanks!
</p>
<div className="text-center mt-5"></div>
</div>
)}
</div>
<div className="flex gap-4 mt-5">
<Button variant="secondary" className="mr-auto" onClick={() => openFeedbackDialog(true)}>
Submit error report
</Button>
{isPending ? (
<Button variant="secondary" onClick={cancelExport}>
Cancel
</Button>
) : (
<Button variant="secondary" onClick={() => onOpenChange(false)}>
Close
</Button>
)}
<Button onClick={handleExport} disabled={isPending || !canExport} variant="primary">
Export
</Button>
</div>
</Dialog>
</FormProvider>
);
}
export default ExportProjectDialog;