- New tool: /video with 4 modes (convert formats, compress, extract audio, GIF) - Backend: ffmpeg-based processing with progress tracking - Supports MP4, WebM, MOV, AVI, MKV input, up to 200MB - Frontend: drag-drop upload, mode tabs, progress bar, download - Added to sidebar, dashboard (Images category), WA_CATEGORIES - Total tools: 14
163 lines
12 KiB
JavaScript
163 lines
12 KiB
JavaScript
/* WA Dev Tools — Shared JS (sidebar, theme, tailwind config, categories) */
|
||
|
||
/* Tailwind config */
|
||
if (typeof tailwind !== 'undefined') {
|
||
tailwind.config = {
|
||
darkMode: 'class',
|
||
theme: {
|
||
extend: {
|
||
fontFamily: {
|
||
mono: ['JetBrains Mono', 'monospace'],
|
||
sans: ['Manrope', 'sans-serif'],
|
||
},
|
||
colors: {
|
||
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||
warn: '#ffd600',
|
||
danger: '#ff5252',
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/* Categories */
|
||
const WA_CATEGORIES = [
|
||
{ id: 'images', title: 'Изображения', description: 'Сжатие, редактирование и генерация', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>', tools: ['compress', 'placeholder', 'svgeditor', 'editor', 'video'] },
|
||
{ id: 'code', title: 'Код', description: 'Форматирование, очистка и конвертация', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>', tools: ['formatter', 'sanitizer', 'converter'] },
|
||
{ id: 'web', title: 'Веб', description: 'HTTP-клиент, парсер и анализ', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"/>', tools: ['parser', 'httpclient', 'redirects'] },
|
||
{ id: 'utils', title: 'Утилиты', description: 'Пароли, Markdown и другие', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.645 3.232a.75.75 0 01-1.09-.791l1.078-6.285L1.347 7.06a.75.75 0 01.416-1.28l6.31-.917L10.89.598a.75.75 0 011.341 0l2.816 5.266 6.31.917a.75.75 0 01.416 1.28l-4.416 4.266 1.078 6.285a.75.75 0 01-1.09.791L12 15.17z"/>', tools: ['password', 'md'] },
|
||
];
|
||
|
||
/* Sidebar tools definition */
|
||
const WA_TOOLS = [
|
||
{ id: 'home', path: '/dashboard', title: 'Главная', category: null, icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>' },
|
||
// Изображения
|
||
{ id: 'compress', path: '/compress', title: 'Сжатие', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>' },
|
||
{ id: 'placeholder', path: '/placeholder', title: 'Placeholder', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/>' },
|
||
{ id: 'svgeditor', path: '/svgeditor', title: 'SVG', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/>' },
|
||
{ id: 'editor', path: '/editor', title: 'Фото', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>' },
|
||
{ id: 'video', path: '/video', title: 'Видео', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"/>' },
|
||
// Код
|
||
{ id: 'formatter', path: '/formatter', title: 'Formatter', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>' },
|
||
{ id: 'sanitizer', path: '/sanitizer', title: 'Sanitizer', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/>' },
|
||
{ id: 'converter', path: '/converter', title: 'Converter', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/>' },
|
||
// Веб
|
||
{ id: 'parser', path: '/parser', title: 'Parser', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/>' },
|
||
{ id: 'httpclient', path: '/httpclient', title: 'HTTP', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/>' },
|
||
{ id: 'redirects', path: '/redirects', title: 'Redirects', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>' },
|
||
// Утилиты
|
||
{ id: 'password', path: '/password', title: 'Пароли', category: 'utils', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/>' },
|
||
{ id: 'md', path: '/md', title: 'Markdown', category: 'utils', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/>' },
|
||
// Admin
|
||
{ id: 'logs', path: '/logs', title: 'Логи', category: null, icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z"/>' },
|
||
];
|
||
|
||
/* Build sidebar SVG icon */
|
||
function _svgIcon(innerPath) {
|
||
return `<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">${innerPath}</svg>`;
|
||
}
|
||
|
||
/* Inject sidebar into the page */
|
||
function initSidebar(activeId) {
|
||
if (!activeId) {
|
||
const path = window.location.pathname;
|
||
const match = WA_TOOLS.find(t => t.path !== '/dashboard' && path.startsWith(t.path));
|
||
activeId = match ? match.id : 'home';
|
||
}
|
||
|
||
// Build links grouped by category
|
||
let linksHtml = '';
|
||
let lastCategory = '__none__';
|
||
|
||
for (const t of WA_TOOLS) {
|
||
// Add category divider
|
||
if (t.category !== lastCategory && t.category !== null && lastCategory !== '__none__') {
|
||
linksHtml += '<div class="sidebar-divider"></div>';
|
||
}
|
||
lastCategory = t.category;
|
||
|
||
const isActive = t.id === activeId ? ' active' : '';
|
||
linksHtml += `<a href="${t.path}" class="sidebar-link${isActive}" title="${t.title}">${_svgIcon(t.icon)}</a>\n`;
|
||
}
|
||
|
||
const nav = document.createElement('nav');
|
||
nav.className = 'sidebar';
|
||
nav.innerHTML = `
|
||
<div class="sidebar-logo">WA</div>
|
||
<div class="sidebar-nav">
|
||
${linksHtml}
|
||
</div>
|
||
<div class="sidebar-bottom">
|
||
<div class="sidebar-user" id="sidebarUser" style="display:none;">
|
||
<div class="sidebar-avatar" id="sidebarAvatar" title="Профиль"></div>
|
||
</div>
|
||
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||
<div class="theme-toggle-knob">
|
||
<svg class="wa-sun-icon" width="14" height="14" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||
<svg class="wa-moon-icon" width="14" height="14" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
document.body.prepend(nav);
|
||
_updateThemeIcons();
|
||
_loadUser();
|
||
}
|
||
|
||
/* Load user info for sidebar avatar */
|
||
async function _loadUser() {
|
||
try {
|
||
const res = await fetch('/auth/me');
|
||
if (!res.ok) return;
|
||
const user = await res.json();
|
||
const avatar = document.getElementById('sidebarAvatar');
|
||
const container = document.getElementById('sidebarUser');
|
||
if (!avatar || !container) return;
|
||
|
||
const initials = (user.name || user.email || '?').charAt(0).toUpperCase();
|
||
avatar.textContent = initials;
|
||
avatar.title = user.name || user.email;
|
||
container.style.display = 'block';
|
||
|
||
avatar.addEventListener('click', () => {
|
||
if (confirm('Выйти из аккаунта?')) {
|
||
window.location.href = '/auth/logout';
|
||
}
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
/* Theme management */
|
||
function initTheme() {
|
||
const html = document.documentElement;
|
||
const saved = localStorage.getItem('theme');
|
||
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||
html.classList.add('dark');
|
||
} else if (saved === '') {
|
||
html.classList.remove('dark');
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const toggle = e.target.closest('#themeToggle');
|
||
if (!toggle) return;
|
||
const isDark = html.classList.toggle('dark');
|
||
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||
_updateThemeIcons();
|
||
});
|
||
}
|
||
|
||
function _updateThemeIcons() {
|
||
const isDark = document.documentElement.classList.contains('dark');
|
||
document.querySelectorAll('.wa-sun-icon').forEach(el => el.style.display = isDark ? 'none' : 'block');
|
||
document.querySelectorAll('.wa-moon-icon').forEach(el => el.style.display = isDark ? 'block' : 'none');
|
||
}
|
||
|
||
/* Auto-init on DOM ready */
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initSidebar(window.WA_TOOL_ID);
|
||
});
|
||
|
||
/* Init theme immediately (before DOM ready to prevent flash) */
|
||
initTheme();
|