diff --git a/internal/server/web/templates/files.html b/internal/server/web/templates/files.html index dc5a8c4..2d6fbb1 100644 --- a/internal/server/web/templates/files.html +++ b/internal/server/web/templates/files.html @@ -63,6 +63,11 @@ dialog::backdrop { background:rgba(0,0,0,.6); } .dlg-h { padding:14px 18px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:10px; } .dlg-b { padding:16px 18px; } + #save { display:inline-flex; align-items:center; gap:8px; } + #save.saving { opacity:.85; cursor:progress; } + #save.saving::before { content:''; width:12px; height:12px; border:2px solid rgba(255,255,255,.35); border-top-color:#fff; + border-radius:50%; animation:spin .7s linear infinite; } + #save[hidden] { display:none; } /* File viewer: the code editor, a picture / video / sound, or the hex editor. */ #viewer { max-width:1100px; } #viewer .dlg-h { flex-wrap:wrap; } @@ -360,12 +365,18 @@ const fsURL = (what, path) => '/api/panel/agents/' + encodeURIComponent(agentID) + '/fs/' + what + '?path=' + encodeURIComponent(path); // Writes a file as it is, with no base64: the request body is the file. async function putFile(path, body) { + // The save button turns into a small spinner meanwhile, and takes no + // second click. + const btn = $('save'); + if (btn.classList.contains('saving')) return {ok: false, error: 'сохранение уже идёт'}; + btn.classList.add('saving'); + btn.disabled = true; setBusy(true); try { const r = await fetch(fsURL('upload', path), {method: 'POST', headers: {'Content-Type': 'application/octet-stream'}, body}); return (r.headers.get('Content-Type') || '').startsWith('application/json') ? await r.json() : {ok: false, error: 'HTTP ' + r.status}; } catch (e) { return {ok: false, error: 'ошибка сети'}; } - finally { setBusy(false); } + finally { setBusy(false); btn.classList.remove('saving'); btn.disabled = false; } } const IMG = /\.(png|jpe?g|gif|webp|bmp|ico|avif|svg)$/i, VID = /\.(mp4|m4v|webm|ogv|mov)$/i, AUD = /\.(mp3|wav|ogg|oga|m4a|aac|flac|opus)$/i;