Add themed confirm dialog modal replacing all native browser confirms

This commit is contained in:
Broque Thomas 2026-03-08 11:16:11 -07:00
parent 266d044797
commit f9e8c8dadd
3 changed files with 174 additions and 29 deletions

View file

@ -4564,6 +4564,25 @@
</div> </div>
</div> </div>
<!-- Confirm Dialog Modal -->
<div class="modal-overlay hidden" id="confirm-modal-overlay">
<div class="confirm-modal">
<div class="confirm-modal-header">
<h2 id="confirm-modal-title">Confirm</h2>
<button class="confirm-modal-close" onclick="resolveConfirmDialog(false)"></button>
</div>
<div class="confirm-modal-content">
<p id="confirm-modal-message"></p>
</div>
<div class="confirm-modal-actions">
<button class="modal-button modal-button--secondary" id="confirm-modal-cancel"
onclick="resolveConfirmDialog(false)">Cancel</button>
<button class="modal-button modal-button--primary" id="confirm-modal-confirm"
onclick="resolveConfirmDialog(true)">Confirm</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='vendor/socket.io.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/socket.io.min.js') }}"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script> <script src="{{ url_for('static', filename='script.js') }}"></script>
</body> </body>

View file

@ -122,6 +122,50 @@ function observeLazyBackgrounds(container) {
elements.forEach(el => lazyBgObserver.observe(el)); elements.forEach(el => lazyBgObserver.observe(el));
} }
// ===============================
// CONFIRM DIALOG (themed replacement for native confirm())
// ===============================
let _confirmResolver = null;
function showConfirmDialog({ title = 'Confirm', message = '', confirmText = 'Confirm', cancelText = 'Cancel', destructive = false } = {}) {
// Resolve any pending dialog as cancelled before opening a new one
if (_confirmResolver) {
_confirmResolver(false);
_confirmResolver = null;
}
const overlay = document.getElementById('confirm-modal-overlay');
const titleEl = document.getElementById('confirm-modal-title');
const messageEl = document.getElementById('confirm-modal-message');
const confirmBtn = document.getElementById('confirm-modal-confirm');
const cancelBtn = document.getElementById('confirm-modal-cancel');
titleEl.textContent = title;
messageEl.textContent = message;
confirmBtn.textContent = confirmText;
cancelBtn.textContent = cancelText;
// Toggle destructive (red) vs primary (accent) confirm button
confirmBtn.className = destructive
? 'modal-button modal-button--cancel'
: 'modal-button modal-button--primary';
overlay.classList.remove('hidden');
return new Promise(resolve => {
_confirmResolver = resolve;
});
}
function resolveConfirmDialog(result) {
const overlay = document.getElementById('confirm-modal-overlay');
overlay.classList.add('hidden');
if (_confirmResolver) {
_confirmResolver(result);
_confirmResolver = null;
}
}
// =============================== // ===============================
// WEBSOCKET CONNECTION MANAGER // WEBSOCKET CONNECTION MANAGER
// =============================== // ===============================
@ -1084,7 +1128,7 @@ async function loadProfileManageList() {
// Bind delete buttons // Bind delete buttons
list.querySelectorAll('.profile-delete-btn').forEach(btn => { list.querySelectorAll('.profile-delete-btn').forEach(btn => {
btn.onclick = async () => { btn.onclick = async () => {
if (!confirm('Delete this profile and all its data?')) return; if (!await showConfirmDialog({ title: 'Delete Profile', message: 'Delete this profile and all its data?', confirmText: 'Delete', destructive: true })) return;
try { try {
const res = await fetch(`/api/profiles/${btn.dataset.id}`, { method: 'DELETE' }); const res = await fetch(`/api/profiles/${btn.dataset.id}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
@ -3691,7 +3735,7 @@ async function testConnection(service) {
} }
async function clearQuarantine() { async function clearQuarantine() {
if (!confirm('Delete all files in the quarantine folder? This cannot be undone.')) return; if (!await showConfirmDialog({ title: 'Clear Quarantine', message: 'Delete all files in the quarantine folder? This cannot be undone.', confirmText: 'Delete', destructive: true })) return;
try { try {
showLoadingOverlay('Clearing quarantine folder...'); showLoadingOverlay('Clearing quarantine folder...');
const response = await fetch('/api/quarantine/clear', { method: 'POST' }); const response = await fetch('/api/quarantine/clear', { method: 'POST' });
@ -3804,7 +3848,7 @@ function copyApiKey() {
} }
async function revokeApiKey(keyId, label) { async function revokeApiKey(keyId, label) {
if (!confirm(`Revoke API key "${label}"? Any apps using this key will stop working.`)) return; if (!await showConfirmDialog({ title: 'Revoke API Key', message: `Revoke API key "${label}"? Any apps using this key will stop working.`, confirmText: 'Revoke', destructive: true })) return;
try { try {
const response = await fetch(`/api/v1/api-keys-internal/revoke/${keyId}`, { method: 'DELETE' }); const response = await fetch(`/api/v1/api-keys-internal/revoke/${keyId}`, { method: 'DELETE' });
@ -3991,7 +4035,7 @@ async function authenticateSpotify() {
} }
async function disconnectSpotify() { async function disconnectSpotify() {
if (!confirm('Disconnect Spotify? The app will switch to Apple Music/iTunes for metadata.')) { if (!await showConfirmDialog({ title: 'Disconnect Spotify', message: 'Disconnect Spotify? The app will switch to Apple Music/iTunes for metadata.' })) {
return; return;
} }
try { try {
@ -8452,7 +8496,7 @@ function closeWishlistOverviewModal() {
async function cleanupWishlistOverview() { async function cleanupWishlistOverview() {
console.log('🧹 cleanupWishlistOverview() called'); console.log('🧹 cleanupWishlistOverview() called');
if (!confirm('This will remove all tracks from the wishlist that already exist in your library. Continue?')) { if (!await showConfirmDialog({ title: 'Cleanup Wishlist', message: 'This will remove all tracks from the wishlist that already exist in your library. Continue?' })) {
return; return;
} }
@ -8503,7 +8547,7 @@ async function cleanupWishlistOverview() {
async function clearEntireWishlist() { async function clearEntireWishlist() {
console.log('🗑️ clearEntireWishlist() called'); console.log('🗑️ clearEntireWishlist() called');
if (!confirm('⚠️ WARNING: This will permanently delete ALL tracks from your wishlist.\n\nThis action cannot be undone.\n\nAre you sure you want to continue?')) { if (!await showConfirmDialog({ title: 'Clear Wishlist', message: 'WARNING: This will permanently delete ALL tracks from your wishlist.\n\nThis action cannot be undone.\n\nAre you sure you want to continue?', confirmText: 'Clear All', destructive: true })) {
console.log('User cancelled confirmation'); console.log('User cancelled confirmation');
return; return;
} }
@ -10015,7 +10059,7 @@ function _renderCandidatesModal(data) {
} }
async function downloadCandidate(taskId, candidate, trackName) { async function downloadCandidate(taskId, candidate, trackName) {
if (!confirm(`Download this file as "${trackName}"?\n\n${candidate.filename?.split(/[/\\]/).pop() || 'Unknown file'}\nfrom ${candidate.username || 'Unknown user'}`)) return; if (!await showConfirmDialog({ title: 'Download File', message: `Download this file as "${trackName}"?\n\n${candidate.filename?.split(/[/\\]/).pop() || 'Unknown file'}\nfrom ${candidate.username || 'Unknown user'}`, confirmText: 'Download' })) return;
try { try {
const resp = await fetch(`/api/downloads/task/${encodeURIComponent(taskId)}/download-candidate`, { const resp = await fetch(`/api/downloads/task/${encodeURIComponent(taskId)}/download-candidate`, {
method: 'POST', method: 'POST',
@ -11526,7 +11570,7 @@ async function clearFinishedDownloads() {
} }
async function cancelAllDownloads() { async function cancelAllDownloads() {
if (!confirm('Cancel ALL active downloads and clear the transfer list? This cannot be undone.')) { if (!await showConfirmDialog({ title: 'Cancel All Downloads', message: 'Cancel ALL active downloads and clear the transfer list? This cannot be undone.', confirmText: 'Cancel All', destructive: true })) {
return; return;
} }
@ -15344,7 +15388,7 @@ function downloadBackup(filename) {
} }
async function restoreBackup(filename) { async function restoreBackup(filename) {
if (!confirm(`Restore database from "${filename}"?\n\nA safety backup of the current database will be created first.`)) return; if (!await showConfirmDialog({ title: 'Restore Backup', message: `Restore database from "${filename}"?\n\nA safety backup of the current database will be created first.`, confirmText: 'Restore' })) return;
try { try {
const res = await fetch(`/api/database/backups/${encodeURIComponent(filename)}/restore`, { method: 'POST' }); const res = await fetch(`/api/database/backups/${encodeURIComponent(filename)}/restore`, { method: 'POST' });
const data = await res.json(); const data = await res.json();
@ -15360,7 +15404,7 @@ async function restoreBackup(filename) {
} }
async function deleteBackup(filename) { async function deleteBackup(filename) {
if (!confirm(`Delete backup "${filename}"? This cannot be undone.`)) return; if (!await showConfirmDialog({ title: 'Delete Backup', message: `Delete backup "${filename}"? This cannot be undone.`, confirmText: 'Delete', destructive: true })) return;
try { try {
const res = await fetch(`/api/database/backups/${encodeURIComponent(filename)}`, { method: 'DELETE' }); const res = await fetch(`/api/database/backups/${encodeURIComponent(filename)}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
@ -19159,7 +19203,7 @@ async function handleDbUpdateButtonClick() {
if (isFullRefresh) { if (isFullRefresh) {
// Replicates the QMessageBox confirmation from the GUI // Replicates the QMessageBox confirmation from the GUI
const confirmed = confirm("⚠️ Full Refresh Warning!\n\nThis will clear and rebuild the database for the active server. It can take a long time. Are you sure you want to proceed?"); const confirmed = await showConfirmDialog({ title: 'Full Refresh', message: 'This will clear and rebuild the database for the active server. It can take a long time.\n\nAre you sure you want to proceed?', confirmText: 'Proceed' });
if (!confirmed) return; if (!confirmed) return;
} }
@ -19275,13 +19319,10 @@ async function handleWishlistButtonClick() {
async function cleanupWishlist(playlistId) { async function cleanupWishlist(playlistId) {
try { try {
// Show information dialog // Show information dialog
const confirmed = confirm( const confirmed = await showConfirmDialog({
"Cleanup Wishlist\n\n" + title: 'Cleanup Wishlist',
"This will check all wishlist tracks against your music library and automatically remove " + message: 'This will check all wishlist tracks against your music library and automatically remove any tracks that already exist in your database.\n\nThis is a safe operation that only removes tracks you already have. Continue with cleanup?'
"any tracks that already exist in your database.\n\n" + });
"This is a safe operation that only removes tracks you already have. " +
"Continue with cleanup?"
);
if (!confirmed) { if (!confirmed) {
return; return;
@ -19340,12 +19381,12 @@ async function cleanupWishlist(playlistId) {
async function clearWishlist(playlistId) { async function clearWishlist(playlistId) {
try { try {
// Show confirmation dialog // Show confirmation dialog
const confirmed = confirm( const confirmed = await showConfirmDialog({
"Clear Wishlist\n\n" + title: 'Clear Wishlist',
"Are you sure you want to clear the entire wishlist?\n\n" + message: 'Are you sure you want to clear the entire wishlist?\n\nThis will permanently remove all failed tracks from the wishlist. This action cannot be undone.',
"This will permanently remove all failed tracks from the wishlist. " + confirmText: 'Clear All',
"This action cannot be undone." destructive: true
); });
if (!confirmed) { if (!confirmed) {
return; return;
@ -29615,7 +29656,7 @@ async function batchRemoveFromWatchlist() {
if (checked.length === 0) return; if (checked.length === 0) return;
const count = checked.length; const count = checked.length;
if (!confirm(`Remove ${count} artist${count !== 1 ? 's' : ''} from your watchlist?`)) return; if (!await showConfirmDialog({ title: 'Remove Artists', message: `Remove ${count} artist${count !== 1 ? 's' : ''} from your watchlist?`, confirmText: 'Remove', destructive: true })) return;
const artistIds = checked.map(cb => cb.getAttribute('data-artist-id')); const artistIds = checked.map(cb => cb.getAttribute('data-artist-id'));
@ -42941,7 +42982,7 @@ function closeMirroredModal() {
* Delete a mirrored playlist after confirmation. * Delete a mirrored playlist after confirmation.
*/ */
async function clearMirroredDiscovery(playlistId, name) { async function clearMirroredDiscovery(playlistId, name) {
if (!confirm(`Clear discovery data for "${name}"? You can re-discover afterwards to get updated cover art.`)) return; if (!await showConfirmDialog({ title: 'Clear Discovery Data', message: `Clear discovery data for "${name}"? You can re-discover afterwards to get updated cover art.` })) return;
try { try {
const res = await fetch(`/api/mirrored-playlists/${playlistId}/clear-discovery`, { method: 'POST' }); const res = await fetch(`/api/mirrored-playlists/${playlistId}/clear-discovery`, { method: 'POST' });
const data = await res.json(); const data = await res.json();
@ -43236,7 +43277,7 @@ function renderPoolList() {
} }
async function removePoolCacheEntry(entryId) { async function removePoolCacheEntry(entryId) {
if (!confirm('Remove this cached match? The track will be re-discovered fresh next time.')) return; if (!await showConfirmDialog({ title: 'Remove Cache Entry', message: 'Remove this cached match? The track will be re-discovered fresh next time.' })) return;
try { try {
const res = await fetch(`/api/discovery-pool/cache/${entryId}`, { method: 'DELETE' }); const res = await fetch(`/api/discovery-pool/cache/${entryId}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
@ -43389,7 +43430,7 @@ async function selectPoolFixTrack(track) {
} }
async function deleteMirroredPlaylist(playlistId, name) { async function deleteMirroredPlaylist(playlistId, name) {
if (!confirm(`Delete mirrored playlist "${name}"?`)) return; if (!await showConfirmDialog({ title: 'Delete Playlist', message: `Delete mirrored playlist "${name}"?`, confirmText: 'Delete', destructive: true })) return;
try { try {
const res = await fetch(`/api/mirrored-playlists/${playlistId}`, { method: 'DELETE' }); const res = await fetch(`/api/mirrored-playlists/${playlistId}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
@ -43792,7 +43833,7 @@ setInterval(() => {
// --- CRUD --- // --- CRUD ---
async function deleteAutomation(id, name) { async function deleteAutomation(id, name) {
if (!confirm('Delete automation "' + name + '"?')) return; if (!await showConfirmDialog({ title: 'Delete Automation', message: `Delete automation "${name}"?`, confirmText: 'Delete', destructive: true })) return;
try { try {
const res = await fetch('/api/automations/' + id, { method: 'DELETE' }); const res = await fetch('/api/automations/' + id, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();

View file

@ -3800,6 +3800,91 @@ body {
animation: matchingModalFadeIn 0.4s ease-out; animation: matchingModalFadeIn 0.4s ease-out;
} }
/* Confirm Dialog Modal */
.confirm-modal {
background: linear-gradient(135deg, #1a1a1a 0%, #121212 100%);
border-radius: 20px;
border: 1px solid rgba(var(--accent-rgb), 0.2);
width: 460px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(var(--accent-rgb), 0.1);
transform: scale(1);
transition: transform 0.4s ease-in-out;
overflow: hidden;
}
.modal-overlay.hidden .confirm-modal {
transform: scale(0.9);
}
.modal-overlay:not(.hidden) .confirm-modal {
animation: matchingModalFadeIn 0.4s ease-out;
}
.confirm-modal-header {
padding: 20px 24px 16px 24px;
border-bottom: 1px solid rgba(var(--accent-rgb), 0.15);
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.03) 0%, transparent 50%);
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
}
#confirm-modal-title {
color: #ffffff;
font-size: 18px;
font-weight: 700;
margin: 0;
}
.confirm-modal-close {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.confirm-modal-close:hover {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
transform: scale(1.1);
}
.confirm-modal-content {
padding: 20px 24px;
}
#confirm-modal-message {
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
line-height: 1.6;
margin: 0;
white-space: pre-line;
}
.confirm-modal-actions {
padding: 16px 24px 20px 24px;
border-top: 1px solid rgba(var(--accent-rgb), 0.1);
display: flex;
justify-content: flex-end;
gap: 10px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
/* GUI-Matching Search Results Styling */ /* GUI-Matching Search Results Styling */
/* Single Track Card (SearchResultItem) */ /* Single Track Card (SearchResultItem) */