Add duplicate audio comparison controls
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user