Add duplicate audio comparison controls

This commit is contained in:
Philip Guzman
2026-07-01 16:12:35 -07:00
parent 139822f8a5
commit 4315d8d4d4
6 changed files with 316 additions and 8 deletions
+34 -4
View File
@@ -18,6 +18,10 @@ const restoreRepairButton = document.querySelector('#restore-repair');
const loadBackupsButton = document.querySelector('#load-backups');
const backupSummary = document.querySelector('#backup-summary');
const backupList = document.querySelector('#backup-list');
const audioPreview = document.querySelector('#audio-preview');
const audioPreviewName = document.querySelector('#audio-preview-name');
const audioPlayer = document.querySelector('#audio-player');
const closeAudioPreview = document.querySelector('#close-audio-preview');
let latestAnalysis = null;
let selectedDuplicateGroup = null;
let previewedRepair = null;
@@ -84,8 +88,8 @@ backupList.addEventListener('click', async (event) => {
});
function detailLines(item) {
if (item.files) {
return item.files.map((file) => `<li>${escapeHtml(file)}</li>`).join('');
if (item.file_previews) {
return item.file_previews.map((file) => `<li class="file-compare-row"><span>${escapeHtml(file.path)}</span><div><button type="button" data-audio-url="${escapeHtml(file.audio_url)}" data-audio-name="${escapeHtml(file.path)}">▶ Play preview</button><button type="button" data-reveal-token="${escapeHtml(file.reveal_token)}">Show in Finder</button></div></li>`).join('');
}
const lines = [];
if (item.artist || item.title) lines.push(`${item.artist || 'Unknown artist'}${item.title || item.filename}`);
@@ -138,7 +142,7 @@ function chooseDuplicate(detailKey, index) {
repairPreview.hidden = true;
repairMessage.textContent = '';
repairChoice.innerHTML = group.files.map((file, fileIndex) => `
<label class="keeper-option"><input type="radio" name="keeper" value="${escapeHtml(file)}" ${fileIndex === 0 ? 'checked' : ''}><span><strong>${fileIndex === 0 ? 'Keep this file' : 'Keep instead'}</strong><small>${escapeHtml(file)}</small></span></label>
<div class="keeper-option"><label><input type="radio" name="keeper" value="${escapeHtml(file)}" ${fileIndex === 0 ? 'checked' : ''}><span><strong>${fileIndex === 0 ? 'Keep this file' : 'Keep instead'}</strong><small>${escapeHtml(file)}</small></span></label><div class="file-actions"><button type="button" data-audio-url="${escapeHtml(group.file_previews[fileIndex].audio_url)}" data-audio-name="${escapeHtml(file)}">▶ Play preview</button><button type="button" data-reveal-token="${escapeHtml(group.file_previews[fileIndex].reveal_token)}">Show in Finder</button></div></div>
`).join('');
repairPanel.hidden = false;
repairPanel.scrollIntoView({behavior: 'smooth', block: 'start'});
@@ -194,7 +198,33 @@ restoreRepairButton.addEventListener('click', async () => {
});
repairChoice.addEventListener('change', () => { previewedRepair = null; applyRepairButton.disabled = true; repairPreview.hidden = true; repairMessage.textContent = 'Keeper changed. Preview the plan again.'; });
drilldownList.addEventListener('click', (event) => { const item = event.target.closest('[data-duplicate-index]'); if (item) chooseDuplicate(document.querySelector('.drill-trigger.selected')?.dataset.detail, Number(item.dataset.duplicateIndex)); });
async function handleFileAction(event) {
const previewButton = event.target.closest('[data-audio-url]');
const revealButton = event.target.closest('[data-reveal-token]');
if (!previewButton && !revealButton) return false;
event.preventDefault(); event.stopPropagation();
if (previewButton) {
audioPlayer.src = previewButton.dataset.audioUrl;
audioPreviewName.textContent = previewButton.dataset.audioName.split('/').pop();
audioPreview.hidden = false;
try { await audioPlayer.play(); } catch (_) { /* Native controls remain available. */ }
} else {
const original = revealButton.textContent;
revealButton.disabled = true; revealButton.textContent = 'Opening…';
try {
const response = await fetch('/api/reveal', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({token: revealButton.dataset.revealToken})});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Could not open Finder');
revealButton.textContent = 'Shown in Finder';
} catch (error) { revealButton.textContent = error.message; }
setTimeout(() => { revealButton.disabled = false; revealButton.textContent = original; }, 1800);
}
return true;
}
closeAudioPreview.addEventListener('click', () => { audioPlayer.pause(); audioPlayer.removeAttribute('src'); audioPlayer.load(); audioPreview.hidden = true; });
repairChoice.addEventListener('click', handleFileAction);
drilldownList.addEventListener('click', async (event) => { if (await handleFileAction(event)) return; const item = event.target.closest('[data-duplicate-index]'); if (item) chooseDuplicate(document.querySelector('.drill-trigger.selected')?.dataset.detail, Number(item.dataset.duplicateIndex)); });
drilldownList.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; const item = event.target.closest('[data-duplicate-index]'); if (item) { event.preventDefault(); chooseDuplicate(document.querySelector('.drill-trigger.selected')?.dataset.detail, Number(item.dataset.duplicateIndex)); } });
function render(data) {
+6 -1
View File
@@ -116,8 +116,13 @@
<div id="backup-summary" class="backup-summary">Analyze your library, then choose a duplicate to preview its automatic backup.</div>
<div id="backup-list" class="backup-list"></div>
</section>
<div id="audio-preview" class="audio-preview" hidden>
<div><span>Now previewing</span><strong id="audio-preview-name"></strong></div>
<audio id="audio-player" controls preload="metadata"></audio>
<button id="close-audio-preview" type="button" aria-label="Close audio preview">×</button>
</div>
</main>
</div>
<script src="/app.js?v=3" defer></script>
<script src="/app.js?v=4" defer></script>
</body>
</html>
+109
View File
@@ -7,3 +7,112 @@
margin-top: 28px;
}
}
.file-compare-row {
display: grid;
gap: 8px;
padding: 9px 0;
border-top: 1px solid rgba(255, 255, 255, .05);
}
.file-compare-row:first-child {
border-top: 0;
}
.file-compare-row div,
.file-actions {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.file-compare-row button,
.file-actions button {
border: 1px solid rgba(155, 135, 245, .25);
border-radius: 8px;
padding: 7px 9px;
background: rgba(155, 135, 245, .08);
color: #d8d2f6;
font: 700 10px/1 inherit;
cursor: pointer;
}
.keeper-option {
justify-content: space-between;
}
.keeper-option > label {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
cursor: pointer;
}
.audio-preview {
position: fixed;
right: 24px;
bottom: 20px;
display: grid;
grid-template-columns: minmax(150px, .7fr) minmax(240px, 1.3fr) auto;
align-items: center;
gap: 16px;
width: min(720px, calc(100vw - 48px));
padding: 13px 14px;
border: 1px solid rgba(102, 217, 232, .3);
border-radius: 14px;
background: rgba(16, 21, 29, .96);
box-shadow: 0 16px 48px rgba(0, 0, 0, .45);
z-index: 5;
}
.audio-preview[hidden] {
display: none;
}
.audio-preview span,
.audio-preview strong {
display: block;
}
.audio-preview span {
color: var(--cyan);
font-size: 9px;
text-transform: uppercase;
letter-spacing: .1em;
}
.audio-preview strong {
max-width: 360px;
margin-top: 4px;
overflow: hidden;
color: var(--text);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.audio-preview audio {
width: 100%;
height: 34px;
}
.audio-preview > button {
border: 0;
background: transparent;
color: var(--muted);
font-size: 22px;
cursor: pointer;
}
@media (max-width: 680px) {
.keeper-option,
.audio-preview {
align-items: stretch;
grid-template-columns: 1fr;
}
.keeper-option {
flex-direction: column;
}
}