Improve track sample generator UI and metadata-based sample filenames
This commit is contained in:
@@ -5,6 +5,8 @@ $release = $release ?? null;
|
||||
$error = $error ?? '';
|
||||
$uploadError = (string)($_GET['upload_error'] ?? '');
|
||||
$storePluginEnabled = (bool)($store_plugin_enabled ?? false);
|
||||
$trackId = (int)($track['id'] ?? 0);
|
||||
$fullSourceUrl = $trackId > 0 ? '/admin/releases/tracks/source?track_id=' . $trackId : '';
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
@@ -53,7 +55,7 @@ ob_start();
|
||||
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
||||
<div id="trackSampleFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
||||
</label>
|
||||
<input class="input" type="file" id="trackSampleFile" name="track_sample" accept="audio/mpeg" style="display:none;">
|
||||
<input class="input" type="file" id="trackSampleFile" name="track_sample" accept=".mp3,audio/mpeg,audio/mp3" style="display:none;">
|
||||
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
||||
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="sample">Upload</button>
|
||||
</div>
|
||||
@@ -70,12 +72,35 @@ ob_start();
|
||||
<div style="font-size:13px; color:var(--text);">or click to upload</div>
|
||||
<div id="trackFullFileName" style="font-size:11px; color:var(--muted);">No file selected</div>
|
||||
</label>
|
||||
<input class="input" type="file" id="trackFullFile" name="track_sample" accept="audio/mpeg" style="display:none;">
|
||||
<input class="input" type="file" id="trackFullFile" name="track_sample" accept=".mp3,audio/mpeg,audio/mp3" style="display:none;">
|
||||
<div style="margin-top:10px; display:flex; justify-content:flex-end;">
|
||||
<button type="submit" class="btn small" formaction="/admin/releases/tracks/upload" formmethod="post" formenctype="multipart/form-data" name="upload_kind" value="full">Upload Full</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($trackId > 0 && (string)($track['full_file_url'] ?? '') !== ''): ?>
|
||||
<div class="admin-card" style="padding:14px; background: rgba(10,10,12,0.6);">
|
||||
<div class="label">Generate sample from full track</div>
|
||||
<div id="trackWaveform" style="margin-top:8px; height:96px; border-radius:12px; border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.25);"></div>
|
||||
<div style="display:grid; gap:10px; margin-top:10px;">
|
||||
<input type="hidden" id="sampleStart" name="sample_start" value="0">
|
||||
<input type="hidden" id="sampleDuration" name="sample_duration" value="90">
|
||||
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:10px;">
|
||||
<button type="button" class="btn outline small" id="previewSampleRegionBtn">Preview</button>
|
||||
<div class="input" style="display:flex; align-items:center; gap:10px; min-height:42px; width:clamp(240px,42vw,440px);">
|
||||
<span style="font-size:11px; letter-spacing:.18em; text-transform:uppercase; color:var(--muted);">Range</span>
|
||||
<span id="sampleRangeText" style="font-family:'IBM Plex Mono',monospace; color:var(--text);">0s -> 90s</span>
|
||||
<span id="sampleDurationText" style="margin-left:auto; font-family:'IBM Plex Mono',monospace; color:var(--muted);">90s</span>
|
||||
</div>
|
||||
<div style="margin-left:auto; display:flex; gap:10px;">
|
||||
<button type="button" class="btn outline small" id="resetSampleRegionBtn">Reset range</button>
|
||||
<button type="submit" class="btn small" formaction="/admin/releases/tracks/sample/generate" formmethod="post">Generate sample</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<label class="label">Full File URL (Store Download)</label>
|
||||
<input class="input" name="full_file_url" value="<?= htmlspecialchars((string)($track['full_file_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="/uploads/media/track-full.mp3">
|
||||
|
||||
@@ -105,6 +130,8 @@ ob_start();
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
|
||||
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const drop = document.getElementById('trackSampleDropzone');
|
||||
@@ -154,6 +181,113 @@ ob_start();
|
||||
fullName.textContent = fullFile.files.length ? fullFile.files[0].name : 'No file selected';
|
||||
});
|
||||
}
|
||||
|
||||
const waveWrap = document.getElementById('trackWaveform');
|
||||
const previewRegionBtn = document.getElementById('previewSampleRegionBtn');
|
||||
const startInput = document.getElementById('sampleStart');
|
||||
const durationInput = document.getElementById('sampleDuration');
|
||||
const rangeText = document.getElementById('sampleRangeText');
|
||||
const durationText = document.getElementById('sampleDurationText');
|
||||
const resetRegionBtn = document.getElementById('resetSampleRegionBtn');
|
||||
if (waveWrap && startInput && durationInput && window.WaveSurfer) {
|
||||
let fixedDuration = 90;
|
||||
let regionRef = null;
|
||||
let isPreviewPlaying = false;
|
||||
const ws = WaveSurfer.create({
|
||||
container: waveWrap,
|
||||
waveColor: 'rgba(175,190,220,0.35)',
|
||||
progressColor: '#22f2a5',
|
||||
cursorColor: '#22f2a5',
|
||||
barWidth: 2,
|
||||
barGap: 2,
|
||||
height: 92,
|
||||
});
|
||||
const regions = ws.registerPlugin(window.WaveSurfer.Regions.create());
|
||||
ws.load('<?= htmlspecialchars($fullSourceUrl, ENT_QUOTES, 'UTF-8') ?>');
|
||||
|
||||
function syncRange(startSec) {
|
||||
const start = Math.max(0, Math.floor(startSec));
|
||||
const end = Math.max(start, Math.floor(start + fixedDuration));
|
||||
startInput.value = String(start);
|
||||
durationInput.value = String(fixedDuration);
|
||||
if (rangeText) rangeText.textContent = start + 's → ' + end + 's';
|
||||
if (durationText) durationText.textContent = fixedDuration + 's';
|
||||
}
|
||||
|
||||
ws.on('ready', () => {
|
||||
const total = Math.floor(ws.getDuration() || 0);
|
||||
fixedDuration = Math.max(10, Math.min(90, total > 0 ? total : 90));
|
||||
syncRange(0);
|
||||
|
||||
regionRef = regions.addRegion({
|
||||
start: 0,
|
||||
end: fixedDuration,
|
||||
drag: true,
|
||||
resize: false,
|
||||
color: 'rgba(34,242,165,0.20)',
|
||||
});
|
||||
|
||||
const updateRegion = () => {
|
||||
if (!regionRef) return;
|
||||
let start = regionRef.start || 0;
|
||||
const maxStart = Math.max(0, (ws.getDuration() || fixedDuration) - fixedDuration);
|
||||
if (start < 0) start = 0;
|
||||
if (start > maxStart) start = maxStart;
|
||||
regionRef.setOptions({ start: start, end: start + fixedDuration });
|
||||
syncRange(start);
|
||||
};
|
||||
|
||||
regionRef.on('update-end', updateRegion);
|
||||
});
|
||||
|
||||
ws.on('interaction', () => {});
|
||||
|
||||
ws.on('audioprocess', () => {
|
||||
if (!isPreviewPlaying || !regionRef) return;
|
||||
const t = ws.getCurrentTime() || 0;
|
||||
if (t >= regionRef.end) {
|
||||
ws.pause();
|
||||
ws.setTime(regionRef.start);
|
||||
isPreviewPlaying = false;
|
||||
if (previewRegionBtn) previewRegionBtn.textContent = 'Preview';
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('pause', () => {
|
||||
if (isPreviewPlaying) {
|
||||
isPreviewPlaying = false;
|
||||
if (previewRegionBtn) previewRegionBtn.textContent = 'Preview';
|
||||
}
|
||||
});
|
||||
|
||||
if (resetRegionBtn) {
|
||||
resetRegionBtn.addEventListener('click', () => {
|
||||
if (!regionRef) return;
|
||||
regionRef.setOptions({ start: 0, end: fixedDuration });
|
||||
syncRange(0);
|
||||
});
|
||||
}
|
||||
|
||||
if (previewRegionBtn) {
|
||||
previewRegionBtn.addEventListener('click', () => {
|
||||
if (!regionRef) return;
|
||||
if (isPreviewPlaying) {
|
||||
ws.pause();
|
||||
isPreviewPlaying = false;
|
||||
previewRegionBtn.textContent = 'Preview';
|
||||
return;
|
||||
}
|
||||
ws.setTime(regionRef.start);
|
||||
ws.play();
|
||||
isPreviewPlaying = true;
|
||||
previewRegionBtn.textContent = 'Stop preview';
|
||||
});
|
||||
}
|
||||
} else if (resetRegionBtn) {
|
||||
resetRegionBtn.addEventListener('click', () => {
|
||||
if (startInput) startInput.value = '0';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user