From 3494471cad435e4f826ec6b940ba92bcb235e077 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sat, 28 Feb 2026 00:02:47 +0800 Subject: [PATCH] feat: add toast notifications and dialog components for improved user interaction --- public/web/i18n.js | 3 +- public/web/main.js | 247 +++++++++++++++++++++++++++++++++--------- public/web/styles.css | 228 +++++++++++++++++++++++++++++++++++++- 3 files changed, 424 insertions(+), 54 deletions(-) diff --git a/public/web/i18n.js b/public/web/i18n.js index 711e546..61e2e14 100644 --- a/public/web/i18n.js +++ b/public/web/i18n.js @@ -21,7 +21,7 @@ export const I18N = { allItems: 'All Items', noFolder: 'No Folder', searchVault: 'Search vault', - filter: 'Filter', + filter: 'Search', typeAll: 'All items', typeLogin: 'Logins', typeCard: 'Cards', @@ -214,4 +214,3 @@ export const I18N = { langSwitch: 'English', }, }; - diff --git a/public/web/main.js b/public/web/main.js index 47dc6fe..63462d1 100644 --- a/public/web/main.js +++ b/public/web/main.js @@ -15,6 +15,8 @@ export function startNodewardenApp(runtimeConfig) { registerEmail: '', registerPassword: '', registerPassword2: '', + registerShowPassword: false, + registerShowPassword2: false, session: null, profile: null, tab: 'vault', @@ -41,6 +43,7 @@ export function startNodewardenApp(runtimeConfig) { invites: [], loginEmail: '', loginPassword: '', + loginShowPassword: false, loginTotpToken: '', loginTotpError: '', pendingLogin: null, @@ -51,10 +54,14 @@ export function startNodewardenApp(runtimeConfig) { totpDisableError: '', unlockPassword: '', unlockError: '', + unlockShowPassword: false, lockTimeoutMinutes: 15, lockLastActiveTs: Date.now(), lockCheckTimer: 0, - lockChannel: null + lockChannel: null, + toasts: [], + toastSeq: 0, + dialog: null }; var NO_FOLDER_FILTER = '__none__'; var i18n = I18N; @@ -66,9 +73,84 @@ export function startNodewardenApp(runtimeConfig) { } function sessionKey() { return 'nodewarden.web.session.v2'; } function lockSettingsKey() { return 'nodewarden.web.lock.v1'; } - function setMsg(t, ty) { state.msg = t || ''; state.msgType = ty || 'ok'; render(); } - function clearMsg() { state.msg = ''; } - function renderMsg() { return state.msg ? '
' + esc(state.msg) + '
' : ''; } + function dismissToast(id) { + var next = []; + for (var i = 0; i < state.toasts.length; i++) if (state.toasts[i].id !== id) next.push(state.toasts[i]); + if (next.length === state.toasts.length) return; + state.toasts = next; + render(); + } + function setMsg(t, ty) { + var text = String(t || '').trim(); + if (!text) return; + var id = 'toast-' + (++state.toastSeq); + var level = ty === 'err' ? 'error' : (ty === 'warn' ? 'warning' : 'success'); + state.toasts.push({ id: id, text: text, level: level }); + if (state.toasts.length > 4) state.toasts = state.toasts.slice(state.toasts.length - 4); + render(); + setTimeout(function () { dismissToast(id); }, 4500); + } + function clearMsg() {} + function renderMsg() { return ''; } + function renderToasts() { + if (!state.toasts || state.toasts.length === 0) return ''; + var items = ''; + for (var i = 0; i < state.toasts.length; i++) { + var x = state.toasts[i]; + items += '
  • ' + + '
    ' + esc(x.text) + '
    ' + + '' + + '
    ' + + '
  • '; + } + return ''; + } + function askConfirm(opts) { + return new Promise(function (resolve) { + state.dialog = { + type: 'confirm', + title: String(opts && opts.title || 'Confirm'), + message: String(opts && opts.message || ''), + okText: String(opts && opts.okText || 'Yes'), + cancelText: String(opts && opts.cancelText || 'No'), + danger: !!(opts && opts.danger), + resolve: resolve + }; + render(); + }); + } + function askMoveFolder() { + return new Promise(function (resolve) { + state.dialog = { + type: 'move', + title: 'Move selected items', + message: 'Choose destination folder.', + selectedFolderId: '__none__', + resolve: resolve + }; + render(); + }); + } + function closeDialog(result) { + var d = state.dialog; + state.dialog = null; + render(); + if (d && typeof d.resolve === 'function') d.resolve(result); + } + function renderDialog() { + var d = state.dialog; + if (!d) return ''; + if (d.type === 'move') { + var options = ''; + for (var i = 0; i < state.folders.length; i++) { + var f = state.folders[i]; + var id = String(f.id || ''); + options += ''; + } + return '

    ' + esc(d.title) + '

    ' + esc(d.message) + '
    '; + } + return '

    ' + esc(d.title) + '

    ' + esc(d.message) + '
    '; + } function saveSession() { if (!state.session) { localStorage.removeItem(sessionKey()); return; } var persisted = { @@ -197,6 +279,7 @@ export function startNodewardenApp(runtimeConfig) { state.loginTotpError = ''; state.unlockPassword = ''; state.unlockError = ''; + state.unlockShowPassword = false; state.phase = 'locked'; if (broadcast !== false && state.lockChannel) { try { state.lockChannel.postMessage({ type: 'lock', at: Date.now() }); } catch (_) {} @@ -227,6 +310,7 @@ export function startNodewardenApp(runtimeConfig) { state.session.symMacKey = bytesToBase64(symKeyBytes.slice(32, 64)); state.unlockPassword = ''; state.unlockError = ''; + state.unlockShowPassword = false; await loadVault(); await loadAdminData(); state.phase = 'app'; @@ -241,7 +325,7 @@ export function startNodewardenApp(runtimeConfig) { } function logout(){ - state.session=null; state.profile=null; state.ciphers=[]; state.folders=[]; state.users=[]; state.invites=[]; state.folderFilterId=''; state.selectedCipherId=''; state.selectedMap={}; state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; state.unlockPassword=''; state.unlockError=''; state.phase='login'; saveSession(); clearMsg(); render(); + state.session=null; state.profile=null; state.ciphers=[]; state.folders=[]; state.users=[]; state.invites=[]; state.folderFilterId=''; state.selectedCipherId=''; state.selectedMap={}; state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; state.unlockPassword=''; state.unlockError=''; state.unlockShowPassword=false; state.phase='login'; saveSession(); clearMsg(); render(); } async function authFetch(path, options){ @@ -600,7 +684,8 @@ export function startNodewardenApp(runtimeConfig) { } async function deleteSelectedCipher(){ var c=selectedCipher(); if(!c) return; - if(!window.confirm('Delete this item? This operation cannot be undone.')) return; + var ok = await askConfirm({ title: 'Delete item', message: 'Are you sure you want to delete this item?', okText: 'Yes', cancelText: 'No', danger: true }); + if(!ok) return; var r=await authFetch('/api/ciphers/'+encodeURIComponent(c.id),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete failed.', 'err'); closeDetailEdit(); @@ -838,20 +923,20 @@ export function startNodewardenApp(runtimeConfig) { return '' + '
    ' + '
    '+t('langSwitch')+'
    ' - + '
    ' - + '
    ' - + ' ' - + '
    '+t('brand')+'
    ' - + '
    '+t('subtitle')+'
    ' + + '
    ' + + '
    ' + + '
    '+t('login')+'
    ' + + '
    '+t('brand')+'
    ' + '
    ' + renderMsg() + '
    ' + '
    ' - + '
    ' - + ' ' + + '
    ' + + ' ' + '
    ' - + '