2026-11-01 19:40:01 +03:00 | | | /** @file manager.js @version 0.3.1 */
|
| | | const managerNoticeTimers = new WeakMap();
|
2026-10-29 14:39:39 +03:00 | | | function managerIcon(name) {
|
| | | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| | | svg.classList.add('icon');
|
| | | svg.setAttribute('aria-hidden', 'true');
|
| | | const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
| | | use.setAttribute('href', '#icon-' + name);
|
| | | svg.append(use);
|
| | | return svg;
|
| | | }
|
| | |
|
| | | async function managerRequest(url, options = {}) {
|
| | | const response = await fetch(url, options);
|
| | | let data;
|
| | | try { data = await response.json(); }
|
| | | catch { throw new Error('Сервер вернул неожиданный ответ. Попробуйте обновить страницу.'); }
|
| | | if (!response.ok) {
|
| | | throw new Error(typeof data.detail === 'string' ? data.detail : 'Проверьте введённые значения.');
|
| | | }
|
| | | return data;
|
| | | }
|
| | |
|
2026-11-01 19:40:01 +03:00 | | | function managerNotice(element, text, type = 'info', timeout = type === 'info' ? 0 : 5000) {
|
| | | clearTimeout(managerNoticeTimers.get(element));
|
| | | const hide = () => {
|
| | | clearTimeout(managerNoticeTimers.get(element));
|
| | | managerNoticeTimers.delete(element);
|
| | | element.classList.add('is-hidden');
|
| | | };
|
| | | if (timeout > 0) managerNoticeTimers.set(element, setTimeout(hide, timeout));
|
| | | if (timeout === 0 && element.dataset.message === text && element.dataset.type === type && !element.classList.contains('is-hidden')) return;
|
2026-10-29 14:39:39 +03:00 | | | element.dataset.message = text;
|
| | | element.dataset.type = type;
|
2026-11-01 19:40:01 +03:00 | | | element.classList.remove('is-hidden', 'notice-info', 'notice-success', 'notice-warning', 'notice-danger');
|
| | | element.classList.add('notice', 'notice-' + type);
|
2026-10-29 14:39:39 +03:00 | | | const indicator = type === 'info' ? document.createElement('span') : managerIcon(type === 'success' ? 'check' : 'terminal');
|
| | | if (type === 'info') { indicator.className = 'spinner'; indicator.setAttribute('aria-hidden', 'true'); }
|
| | | const copy = document.createElement('span');
|
| | | copy.className = 'notice-copy';
|
| | | copy.textContent = text;
|
| | | const close = document.createElement('button');
|
| | | close.type = 'button';
|
| | | close.className = 'icon-button';
|
| | | close.setAttribute('aria-label', 'Скрыть сообщение');
|
| | | close.append(managerIcon('close'));
|
2026-11-01 19:40:01 +03:00 | | | if (timeout > 0) {
|
| | | close.classList.add('notice-timed-close');
|
| | | close.title = 'Скрыть сейчас; автоматически через ' + timeout / 1000 + ' с';
|
| | | const ring = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| | | ring.classList.add('notice-countdown');
|
| | | ring.setAttribute('viewBox', '0 0 32 32');
|
| | | ring.setAttribute('aria-hidden', 'true');
|
| | | ring.style.setProperty('--notice-timeout', timeout + 'ms');
|
| | | for (const name of ['track', 'progress']) {
|
| | | const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
| | | circle.classList.add('notice-countdown-' + name);
|
| | | circle.setAttribute('cx', '16');
|
| | | circle.setAttribute('cy', '16');
|
| | | circle.setAttribute('r', '13');
|
| | | circle.setAttribute('pathLength', '100');
|
| | | ring.append(circle);
|
| | | }
|
| | | close.append(ring);
|
| | | }
|
| | | close.addEventListener('click', hide);
|
2026-10-29 14:39:39 +03:00 | | | element.replaceChildren(indicator, copy, close);
|
| | | }
|
| | |
|
2026-11-01 19:40:01 +03:00 | | | function managerMessage(text, type = 'info', timeout = type === 'info' ? 0 : 5000) {
|
| | | managerNotice(document.getElementById('operation-message'), text, type, timeout);
|
| | | }
|
| | |
|
2026-10-29 14:39:39 +03:00 | | | function managerFormError(form, text = '') {
|
| | | const element = form.querySelector('.form-feedback');
|
| | | if (!element) return;
|
| | | element.textContent = text;
|
| | | element.classList.toggle('is-hidden', !text);
|
| | | }
|
| | |
|
2026-11-01 19:40:01 +03:00 | | | async function managerJob(job, report = null) {
|
2026-10-29 14:39:39 +03:00 | | | while (true) {
|
| | | const warnings = (job.warnings || []).join(' ');
|
2026-11-01 19:40:01 +03:00 | | | if (job.status === 'error') {
|
| | | if (report) report(job);
|
| | | throw new Error(job.error);
|
| | | }
|
2026-10-29 14:39:39 +03:00 | | | if (job.status === 'done') {
|
2026-11-01 19:40:01 +03:00 | | | if (report) report(job);
|
| | | else managerMessage(job.folder + ': готово.' + (warnings ? ' ' + warnings : ''), warnings ? 'warning' : 'success');
|
2026-10-29 14:39:39 +03:00 | | | return job;
|
| | | }
|
2026-11-01 19:40:01 +03:00 | | | if (report) report(job);
|
| | | else managerMessage(job.folder + ': ' + job.stage + (warnings ? '. ' + warnings : ''), warnings ? 'warning' : 'info', 0);
|
2026-10-29 14:39:39 +03:00 | | | await new Promise(resolve => setTimeout(resolve, 1000));
|
| | | job = await managerRequest('/api/jobs/' + encodeURIComponent(job.id));
|
| | | }
|
| | | }
|
| | |
|
| | | function managerJson(method, data) {
|
| | | return {method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)};
|
| | | }
|
| | |
|
| | | document.addEventListener('DOMContentLoaded', () => {
|
| | | document.querySelectorAll('[data-dialog-open]').forEach(button => {
|
| | | button.addEventListener('click', () => {
|
| | | const dialog = document.getElementById(button.dataset.dialogOpen);
|
| | | managerFormError(dialog);
|
| | | dialog.showModal();
|
| | | });
|
| | | });
|
| | | document.querySelectorAll('[data-dialog-close]').forEach(button => {
|
| | | button.addEventListener('click', () => button.closest('dialog').close());
|
| | | });
|
| | | document.querySelectorAll('dialog').forEach(dialog => {
|
| | | dialog.addEventListener('click', event => { if (event.target === dialog) {
|
| | | const rect = dialog.getBoundingClientRect();
|
| | | if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
|
| | | } });
|
| | | dialog.addEventListener('close', () => dialog.querySelectorAll('input[type=password]').forEach(input => { input.value = ''; }));
|
| | | });
|
| | | });
|