diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3728239 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: + - feat/sample-editor + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one active deployment +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: npm install + + - name: Build + # For a custom domain (e.g., drum.dato.mu), keep BASE_PATH empty. + # For a default github.io subpath (e.g., username.github.io/drum-webapp), + # set BASE_PATH to / (e.g., /drum-webapp). + env: + BASE_PATH: '' + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'build' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 0703f36..6dd5535 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -1,6 +1,6 @@
-

{$_('recorder_title')}

{#if errorMessage}

{errorMessage}

{/if} + {#if downloadState.status === 'error' && downloadState.error} +

{$_('download_failed')}: {downloadState.error}

+ {/if} +
+
+ + +
+
(gainDb = Number(e.currentTarget.value))} aria-label="{$_('recorder_quieter')} / {$_('recorder_louder')}" /> - {gainDb > 0 ? '+' : ''}{gainDb} dB
-
- - {#if capture.status === 'idle'} - - {:else} - {#if audioInputState.devices.length > 1} - - {/if} - - {#if capture.status === 'monitoring' || capture.status === 'recording'} +
+ - {/if} - - {#if capture.status === 'recorded' && capture.recorded} -
- - -
- -
- - - -
- {/if} - {/if} +
+ + +
diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte new file mode 100644 index 0000000..3c7af5b --- /dev/null +++ b/src/lib/components/SettingsModal.svelte @@ -0,0 +1,112 @@ + + + + + + +{#if open} + +{/if} diff --git a/src/lib/components/Track.svelte b/src/lib/components/Track.svelte index 872f82e..5d65f65 100644 --- a/src/lib/components/Track.svelte +++ b/src/lib/components/Track.svelte @@ -20,6 +20,14 @@ samples[selectedSampleIndex]?.midiNoteNumber, ); + // Dim this track when a note on another track is selected + let isDimmed = $derived( + midiNoteState.selectedSample !== null && + !samples.some( + (sample) => sample.midiNoteNumber === midiNoteState.selectedSample, + ), + ); + const voiceIcons = [ "pad_kick.svg", "pad_snare.svg", @@ -60,8 +68,9 @@ }); // Track previous values to distinguish sample-selection changes from resize changes - let prevSelectedSampleIndex = selectedSampleIndex; - let prevContainerWidthPx = 0; + // svelte-ignore state_referenced_locally -- intentional one-time snapshot, updated in the effect below + let prevSelectedSampleIndex = $state(selectedSampleIndex); + let prevContainerWidthPx = $state(0); // Reactive effect to update the translation when selectedSampleIndex or containerWidthPx changes $effect(() => { @@ -85,7 +94,7 @@ }); -
+
= { - 30: "30_kick_disco.wav", - 31: "31_kick_pattern.wav", - 32: "32_kick_big.wav", - 33: "33_kick_cr78.wav", - 34: "34_kick_808_long.wav", - 35: "35_kick_gabber.wav", - 36: "36_kick_808.wav", - 37: "37_kick_deep.wav", - 38: "38_snare_cr78.wav", - 39: "39_snare_808.wav", - 40: "40_snare_disco.wav", - 41: "41_snare_straw.wav", - 42: "42_snare_garage.wav", - 43: "43_snare_heimzap.wav", - 44: "44_snare_machine.wav", - 45: "45_snare_backbone.wav", - 46: "46_var_clap.wav", - 47: "47_var_pop.wav", - 48: "48_var_wood.wav", - 49: "49_var_snap.wav", - 50: "50_var_sepp.wav", - 51: "51_var_pow.wav", - 52: "52_var_ah.wav", - 53: "53_var_rim.wav", - 54: "54_hat_cr78.wav", - 55: "55_hat_808.wav", - 56: "56_hat_maraca.wav", - 57: "57_hat_909_open.wav", - 58: "58_hat_shaker.wav", - 59: "59_hat_electro.wav", - 60: "60_hat_second.wav", - 61: "61_hat_backbone.wav", - }; - // Onset-trigger config disabled - using countdown instead const USE_ONSET_TRIGGER = false; @@ -97,7 +62,7 @@ // Visual feedback for MIDI note trigger let isNoteActive = $state(false); - let flashTimeout: number | undefined; + let flashTimeout: ReturnType | undefined; $effect(() => { // Depend on triggerId to catch every note event, even if repeated @@ -232,87 +197,12 @@ return; } - try { - uploadStatus = "uploading"; - logger.info(`Uploading ${file.name} to slot ${midiNoteNumber}`); - - // Use quick upload (add to queue and start immediately) - await sampleUploadStore.quickUpload(file, midiNoteNumber); - - uploadStatus = "success"; - logger.info( - `Successfully uploaded ${file.name} to slot ${midiNoteNumber}`, - ); - - // Reset success indicator after 2 seconds - setTimeout(() => { - uploadStatus = "idle"; - }, 2000); - } catch (error) { - uploadStatus = "error"; - uploadError = error instanceof Error ? error.message : "Upload failed"; - logger.error(`Upload failed: ${uploadError}`); - - // Reset error after 3 seconds - setTimeout(() => { - uploadStatus = "idle"; - uploadError = null; - }, 3000); - } - } - - // File input handler for browse button - let fileInput: HTMLInputElement; - - function handleBrowseClick() { - fileInput?.click(); - } - - async function handleFileSelect(event: Event) { - const input = event.target as HTMLInputElement; - const files = input.files; - if (!files || files.length === 0) { - return; - } - - // Simulate drop event - const file = files[0]; - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - - await handleDrop({ - preventDefault: () => {}, - dataTransfer, - } as DragEvent); - - // Reset input - input.value = ""; - } - - // Reset to factory sample - async function handleResetClick() { - // Check MIDI connection - if (!midiState.isConnected) { - uploadStatus = "error"; - uploadError = "MIDI not connected"; - logger.error("Cannot reset: MIDI device not connected"); - - // Reset error after 3 seconds - setTimeout(() => { - uploadStatus = "idle"; - uploadError = null; - }, 3000); - return; - } - - // Check if we have a factory sample for this note - const factorySampleFilename = FACTORY_SAMPLES[midiNoteNumber]; - if (!factorySampleFilename) { + // Check for concurrent download + if (isTransferActive()) { uploadStatus = "error"; - uploadError = "No factory sample"; - logger.error(`No factory sample defined for MIDI note ${midiNoteNumber}`); + uploadError = "Transfer in progress"; + logger.error("Cannot upload: another SDS transfer is active"); - // Reset error after 3 seconds setTimeout(() => { uploadStatus = "idle"; uploadError = null; @@ -322,32 +212,14 @@ try { uploadStatus = "uploading"; - logger.info(`Loading factory sample for slot ${midiNoteNumber}`); - - // Fetch factory sample - const response = await fetch(`/factory_kit/${factorySampleFilename}`); - if (!response.ok) { - throw new Error( - `Failed to load factory sample: ${response.statusText}`, - ); - } - - // Create File object from response - const blob = await response.blob(); - const file = new File([blob], factorySampleFilename, { - type: "audio/wav", - }); - - logger.info( - `Uploading factory sample ${factorySampleFilename} to slot ${midiNoteNumber}`, - ); + logger.info(`Uploading ${file.name} to slot ${midiNoteNumber}`); - // Upload via existing upload system + // Use quick upload (add to queue and start immediately) await sampleUploadStore.quickUpload(file, midiNoteNumber); uploadStatus = "success"; logger.info( - `Successfully reset slot ${midiNoteNumber} to factory sample`, + `Successfully uploaded ${file.name} to slot ${midiNoteNumber}`, ); // Reset success indicator after 2 seconds @@ -356,8 +228,8 @@ }, 2000); } catch (error) { uploadStatus = "error"; - uploadError = error instanceof Error ? error.message : "Reset failed"; - logger.error(`Factory reset failed: ${uploadError}`); + uploadError = error instanceof Error ? error.message : "Upload failed"; + logger.error(`Upload failed: ${uploadError}`); // Reset error after 3 seconds setTimeout(() => { @@ -578,46 +450,6 @@ {/if} -
- - - -
- - -