diff --git a/renderer/app.js b/renderer/app.js index 7e9a688..f01392f 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2497,7 +2497,7 @@ function hideContextMenu() { async function deleteSelectedRecentFiles() { if (selectedRecentIds.size === 0) return; const count = selectedRecentIds.size; - if (!await showAppConfirm({ title: 'Ausgewählte Einträge entfernen?', message: count === 1 ? 'Ein ausgewählter Eintrag wird aus diesem Panel entfernt.' : `${count} ausgewählte Einträge werden aus diesem Panel entfernt.`, confirmText: 'Entfernen', danger: true })) return; + if (!await showAppConfirm({ title: 'Ausgewählte Einträge entfernen?', message: count === 1 ? 'Ein ausgewählter Eintrag wird aus diesem Panel entfernt.' : `${count} ausgewählte Einträge werden aus diesem Panel entfernt.`, confirmText: 'Entfernen', danger: true, defaultAction: 'confirm' })) return; let removedDone = 0, removedErr = 0; sessionFilesData = sessionFilesData.filter(r => { if (!selectedRecentIds.has(r.order)) return true; @@ -2514,7 +2514,7 @@ async function deleteSelectedRecentFiles() { async function clearAllRecentFiles() { if (sessionFilesData.length === 0) return; - if (!await showAppConfirm({ title: 'Alle Links entfernen?', message: `Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`, confirmText: 'Alle entfernen', danger: true })) return; + if (!await showAppConfirm({ title: 'Alle Links entfernen?', message: `Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`, confirmText: 'Alle entfernen', danger: true, defaultAction: 'confirm' })) return; sessionFilesData = []; _sessionFileKeys.clear(); _sessionDoneCount = 0; @@ -3215,7 +3215,7 @@ async function handleContextAction(action, targetJobId = null) { await copyFailureDetails(targetJobId); } else if (action === 'delete-selected') { const count = selectedJobIds.size; - if (!count || !await showAppConfirm({ title: 'Uploads entfernen?', message: count === 1 ? 'Ein ausgewählter Upload wird aus der Liste entfernt.' : `${count} ausgewählte Uploads werden aus der Liste entfernt.`, confirmText: 'Entfernen', danger: true })) return; + if (!count || !await showAppConfirm({ title: 'Uploads entfernen?', message: count === 1 ? 'Ein ausgewählter Upload wird aus der Liste entfernt.' : `${count} ausgewählte Uploads werden aus der Liste entfernt.`, confirmText: 'Entfernen', danger: true, defaultAction: 'confirm' })) return; const activeIds = [...selectedJobIds].filter(id => { const j = _jobIndexById.get(id); return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server'); @@ -3241,7 +3241,7 @@ async function handleContextAction(action, targetJobId = null) { } else if (action === 'copy-all-links') { copyAllLinks(); } else if (action === 'delete-all') { - if (!queueJobs.length || !await showAppConfirm({ title: 'Alle Uploads entfernen?', message: `${queueJobs.length} ${queueJobs.length === 1 ? 'Upload wird' : 'Uploads werden'} aus der Liste entfernt.`, confirmText: 'Alle entfernen', danger: true })) return; + if (!queueJobs.length || !await showAppConfirm({ title: 'Alle Uploads entfernen?', message: `${queueJobs.length} ${queueJobs.length === 1 ? 'Upload wird' : 'Uploads werden'} aus der Liste entfernt.`, confirmText: 'Alle entfernen', danger: true, defaultAction: 'confirm' })) return; const hasActiveJobs = uploading || queueJobs.some(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server'); if (hasActiveJobs) { _cancelAutoRetry(true); @@ -4719,6 +4719,7 @@ function closeAppAlert(result = false) { if (!modal) return; modal.style.display = 'none'; modal.setAttribute('aria-hidden', 'true'); + delete modal.dataset.defaultAction; _setAppAlertBackgroundInert(false); const resolve = _appAlertResolve; _appAlertResolve = null; @@ -4728,7 +4729,7 @@ function closeAppAlert(result = false) { if (resolve) resolve(result); } -function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelText = 'Abbrechen', alternateText = '', showCancel = false, danger = false } = {}) { +function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelText = 'Abbrechen', alternateText = '', showCancel = false, danger = false, defaultAction = 'cancel' } = {}) { const modal = document.getElementById('appAlertModal'); const titleEl = document.getElementById('appAlertTitle'); const messageEl = document.getElementById('appAlertMessage'); @@ -4748,8 +4749,9 @@ function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelT alternate.hidden = !alternateText; modal.style.display = 'flex'; modal.setAttribute('aria-hidden', 'false'); + modal.dataset.defaultAction = defaultAction === 'confirm' ? 'confirm' : 'cancel'; _setAppAlertBackgroundInert(true); - (showCancel ? cancel : confirm).focus(); + (showCancel && modal.dataset.defaultAction !== 'confirm' ? cancel : confirm).focus(); return new Promise(resolve => { _appAlertResolve = resolve; }); } @@ -4757,8 +4759,8 @@ function showAppAlert(message, title = 'Hinweis') { return showAppDialog({ message, title }); } -function showAppConfirm({ message, title = 'Bestätigen', confirmText = 'Bestätigen', cancelText = 'Abbrechen', danger = false } = {}) { - return showAppDialog({ message, title, confirmText, cancelText, showCancel: true, danger }); +function showAppConfirm({ message, title = 'Bestätigen', confirmText = 'Bestätigen', cancelText = 'Abbrechen', danger = false, defaultAction = 'cancel' } = {}) { + return showAppDialog({ message, title, confirmText, cancelText, showCancel: true, danger, defaultAction }); } function showAppChoice({ message, title, confirmText, alternateText, cancelText = 'Abbrechen' } = {}) { @@ -7686,6 +7688,19 @@ function setupListeners() { } return; } + if (event.key === 'Enter') { + const confirm = document.getElementById('appAlertConfirmBtn'); + const safeActions = new Set([ + document.getElementById('appAlertCancelBtn'), + document.getElementById('appAlertAlternateBtn'), + document.getElementById('appAlertCloseBtn') + ]); + if (modal.dataset.defaultAction !== 'confirm' || safeActions.has(document.activeElement) || confirm?.disabled) return; + event.preventDefault(); + event.stopImmediatePropagation(); + confirm?.click(); + return; + } if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index ce5298c..4cc3ab0 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -142,6 +142,52 @@ contextBridge.exposeInMainWorld('api', { getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; } }); `, 'utf8'); + const appDialogBehaviorScript = `(async () => { + setupListeners(); + const settle = promise => Promise.race([ + promise, + new Promise(resolve => setTimeout(() => resolve('pending'), 30)) + ]); + const safePromise = showAppConfirm({ title: 'Safe', message: 'Safe default', confirmText: 'Continue', danger: true }); + const safeFocus = document.activeElement?.id; + document.getElementById('appAlertCancelBtn').click(); + const safeResult = await safePromise; + const removalPromise = showAppConfirm({ title: 'Remove', message: 'Remove now', confirmText: 'Remove', danger: true, defaultAction: 'confirm' }); + const removalFocus = document.activeElement?.id; + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); + const removalEnterResult = await settle(removalPromise); + if (removalEnterResult === 'pending') document.getElementById('appAlertCancelBtn').click(); + const cancelPromise = showAppConfirm({ title: 'Remove', message: 'Cancel intentionally', confirmText: 'Remove', danger: true, defaultAction: 'confirm' }); + document.getElementById('appAlertCancelBtn').focus(); + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); + const cancelFocusedEnter = await settle(cancelPromise); + document.getElementById('appAlertCancelBtn').click(); + const cancelResult = await cancelPromise; + const realShowAppConfirm = showAppConfirm; + const removalCalls = []; + showAppConfirm = options => { + removalCalls.push({ title: options.title, defaultAction: options.defaultAction }); + return Promise.resolve(false); + }; + selectedRecentIds.clear(); + selectedRecentIds.add(1); + sessionFilesData = [{ order: 1, link: 'https://example.invalid/a', filename: 'a.mp4', host: 'byse.sx' }]; + await deleteSelectedRecentFiles(); + await clearAllRecentFiles(); + queueJobs = [{ id: 'dialog-remove', file: 'C:/dialog-remove.mp4', fileName: 'dialog-remove.mp4', hoster: 'byse.sx', status: 'queued' }]; + selectedJobIds.clear(); + selectedJobIds.add('dialog-remove'); + rebuildJobIndex(); + await handleContextAction('delete-selected'); + await handleContextAction('delete-all'); + showAppConfirm = realShowAppConfirm; + queueJobs = []; + selectedJobIds.clear(); + selectedRecentIds.clear(); + sessionFilesData = []; + rebuildJobIndex(); + return { safeFocus, safeResult, removalFocus, removalEnterResult, cancelFocusedEnter, cancelResult, removalCalls }; + })()`; const onlineBackupBehaviorScript = `(async () => { const ids = { a: 'AAAAAAAAAAAAAAAAAAAAAA', @@ -344,6 +390,7 @@ app.whenReady().then(async () => { const middleY = Math.floor(size.height / 2); await window.loadFile(process.env.MHU_RENDERER_PATH, { query: { language: 'en', version: '2.1.31' } }); const liveSpeedChart = await window.webContents.executeJavaScript('({ baselinePresent: Boolean(document.querySelector(".upload-speed-baseline")), canvasWidth: document.getElementById("uploadSpeedCanvas")?.getBoundingClientRect().width || 0 })'); + const appDialogBehavior = await window.webContents.executeJavaScript(${JSON.stringify(appDialogBehaviorScript)}); const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)}); const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)}); window.setContentSize(760, Math.min(900, display.workAreaSize.height)); @@ -360,6 +407,7 @@ app.whenReady().then(async () => { leftEdge: pixelAt(bitmap, size.width, 0, middleY), rightEdge: pixelAt(bitmap, size.width, size.width - 1, middleY), liveSpeedChart, + appDialogBehavior, onlineBackupBehavior, onlineBackupLayout, onlineBackupNarrowLayout @@ -402,6 +450,20 @@ app.whenReady().then(async () => { assert.ok(result.rightEdge[0] > result.rightEdge[1] && result.rightEdge[2] > result.rightEdge[1]); assert.ok(result.liveSpeedChart.canvasWidth > 0); assert.equal(result.liveSpeedChart.baselinePresent, false); + assert.deepEqual(result.appDialogBehavior, { + safeFocus: 'appAlertCancelBtn', + safeResult: false, + removalFocus: 'appAlertConfirmBtn', + removalEnterResult: true, + cancelFocusedEnter: 'pending', + cancelResult: false, + removalCalls: [ + { title: 'Ausgewählte Einträge entfernen?', defaultAction: 'confirm' }, + { title: 'Alle Links entfernen?', defaultAction: 'confirm' }, + { title: 'Uploads entfernen?', defaultAction: 'confirm' }, + { title: 'Alle Uploads entfernen?', defaultAction: 'confirm' } + ] + }); assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']); assert.deepEqual(result.onlineBackupBehavior.initialWarning, { hidden: false,