Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Red Team Username/Email Generator</title> | |
| <style> | |
| :root { | |
| --bg: #0f1115; | |
| --panel: #12151d; | |
| --muted: #99a1b3; | |
| --text: #e6eaf2; | |
| --accent: #6ae3ff; | |
| --accent2: #a8ff78; | |
| --bad: #ff6b6b; | |
| --ok: #51cf66; | |
| --chip: #1b2030; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { height: 100%; margin: 0; } | |
| body { | |
| background: linear-gradient(180deg, #0f1115, #0b0d12); | |
| font-family: Inter, system-ui, Segoe UI, Roboto, Arial, sans-serif; | |
| color: var(--text); | |
| -webkit-font-smoothing: antialiased; | |
| line-height: 1.5; | |
| } | |
| header { | |
| padding: 24px 20px 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| flex-wrap: wrap; | |
| } | |
| header h1 { | |
| font-size: 20px; | |
| margin: 0; | |
| font-weight: 700; | |
| letter-spacing: .2px; | |
| } | |
| header .badge { | |
| font-size: 12px; | |
| padding: 6px 10px; | |
| border-radius: 999px; | |
| background: linear-gradient(90deg, var(--accent), var(--accent2)); | |
| color: #071018; | |
| font-weight: 700; | |
| } | |
| main { | |
| display: grid; | |
| grid-template-columns: 360px 1fr; | |
| gap: 18px; | |
| padding: 12px 18px 24px; | |
| } | |
| @media (max-width: 980px) { | |
| main { grid-template-columns: 1fr; } | |
| } | |
| .card { | |
| background: var(--panel); | |
| border: 1px solid #1e2333; | |
| border-radius: 14px; | |
| box-shadow: 0 5px 30px rgba(0,0,0,.35); | |
| } | |
| .card h2 { | |
| margin: 0 0 10px; | |
| font-size: 14px; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| letter-spacing: .12em; | |
| } | |
| .section { | |
| padding: 16px 16px 8px; | |
| border-top: 1px solid #1a1f2d; | |
| } | |
| .section:first-child { border-top: 0; } | |
| label { | |
| display: block; | |
| font-size: 13px; | |
| color: var(--muted); | |
| margin: 10px 0 6px; | |
| } | |
| input[type="text"], input[type="number"], textarea, select { | |
| width: 100%; | |
| padding: 10px 12px; | |
| background: #0c0f17; | |
| border: 1px solid #1f2433; | |
| border-radius: 10px; | |
| color: var(--text); | |
| outline: none; | |
| font-family: inherit; | |
| } | |
| textarea { min-height: 70px; resize: vertical; } | |
| .row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } | |
| .chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .chip { | |
| padding: 6px 10px; | |
| border-radius: 999px; | |
| background: var(--chip); | |
| border: 1px solid #21273a; | |
| color: #cdd6f4; | |
| font-size: 12px; | |
| } | |
| .opts { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| } | |
| .opt { | |
| background: #0c0f17; | |
| border: 1px solid #1f2433; | |
| border-radius: 10px; | |
| padding: 10px; | |
| display: flex; | |
| gap: 8px; | |
| align-items: flex-start; | |
| } | |
| .opt input { margin-top: 2px; } | |
| .btns { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| button { | |
| padding: 10px 14px; | |
| border-radius: 10px; | |
| border: 1px solid #23304b; | |
| background: #131b2b; | |
| color: var(--text); | |
| cursor: pointer; | |
| font-weight: 700; | |
| font-family: inherit; | |
| transition: opacity 0.2s; | |
| } | |
| button.primary { | |
| background: linear-gradient(90deg, #1e88ff, #6ae3ff); | |
| color: #071018; | |
| border: 0; | |
| } | |
| button.good { | |
| background: linear-gradient(90deg, #5ef38c, #a8ff78); | |
| color: #03130a; | |
| border: 0; | |
| } | |
| button.bad { | |
| background: linear-gradient(90deg, #ff5f6d, #ffc371); | |
| color: #2a0808; | |
| border: 0; | |
| } | |
| button:hover { opacity: 0.9; } | |
| button:active { opacity: 0.8; } | |
| button:disabled { opacity: .5; cursor: not-allowed; } | |
| .summary { | |
| display: flex; | |
| gap: 14px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| .stat { | |
| background: #0c0f17; | |
| border: 1px solid #1f2433; | |
| border-radius: 12px; | |
| padding: 10px 12px; | |
| } | |
| .stat b { font-size: 16px; } | |
| .results { | |
| padding: 12px; | |
| max-height: 68vh; | |
| overflow: auto; | |
| } | |
| .res-head { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 8px 4px 8px 8px; | |
| position: sticky; | |
| top: 0; | |
| background: linear-gradient(180deg, #12151d 70%, #12151dd9); | |
| backdrop-filter: blur(6px); | |
| z-index: 1; | |
| border-bottom: 1px solid #1f2433; | |
| } | |
| .search { flex: 1; max-width: 420px; } | |
| .list { margin: 8px 0 0; display: grid; grid-template-columns: 1fr; gap: 6px; } | |
| .line { | |
| display: grid; | |
| grid-template-columns: auto 1fr auto; | |
| gap: 10px; | |
| align-items: center; | |
| padding: 8px 10px; | |
| border: 1px solid #1f2433; | |
| border-radius: 10px; | |
| background: #0c0f17; | |
| transition: background-color 0.2s; | |
| } | |
| .line:hover { background-color: #141a29; } | |
| .line input[type="checkbox"] { transform: translateY(1px); cursor: pointer; } | |
| .email { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } | |
| .muted { color: var(--muted); } | |
| .sep { height: 1px; background: #1a1f2d; margin: 10px 0; } | |
| .small { font-size: 12px; color: var(--muted); } | |
| footer { padding: 10px 18px 24px; color: #8b93a7; } | |
| .linklike { cursor: pointer; text-decoration: underline; } | |
| /* Toast notification */ | |
| .toast { | |
| position: fixed; | |
| right: 18px; | |
| bottom: 18px; | |
| background: linear-gradient(90deg, #1e88ff, #6ae3ff); | |
| color: #071018; | |
| padding: 12px 16px; | |
| border-radius: 10px; | |
| font-weight: 700; | |
| box-shadow: 0 10px 30px rgba(0,0,0,.35); | |
| z-index: 1000; | |
| animation: slideIn 0.3s ease, fadeOut 0.5s ease 1.5s forwards; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100px); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes fadeOut { | |
| from { opacity: 1; } | |
| to { opacity: 0; } | |
| } | |
| /* Loading indicator */ | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255,255,255,.3); | |
| border-radius: 50%; | |
| border-top-color: var(--accent); | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Red Team Username/Email Generator</h1> | |
| <span class="badge">онлайн артефакт</span> | |
| </header> | |
| <main> | |
| <!-- LEFT: Controls --> | |
| <div class="card"> | |
| <div class="section"> | |
| <h2>Входные данные</h2> | |
| <label>Имена (через запятую)</label> | |
| <input id="firstNames" type="text" placeholder="например: claudia, daniel, kevin, anna"> | |
| <label>Фамилии (через запятую)</label> | |
| <input id="lastNames" type="text" placeholder="например: berghoffer, pirker, rohrer, sykora"> | |
| <div class="row"> | |
| <div> | |
| <label>Ники / слова (опц., через запятую)</label> | |
| <input id="nickSeeds" type="text" placeholder="shadow, ranger, chaos, sugarless, underworld, schatz"> | |
| </div> | |
| <div> | |
| <label>Домен(ы)</label> | |
| <input id="domains" type="text" value="gmx.at" placeholder="gmx.at или несколько: gmx.at,example.com"> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div> | |
| <label>Год рождения (опц.)</label> | |
| <input id="birthYear" type="number" min="1900" max="2099" placeholder="1984"> | |
| </div> | |
| <div> | |
| <label>Числовые суффиксы (опц.)</label> | |
| <input id="numSuffixes" type="text" placeholder="1,12,69,70,71,84,85,87,97,123,166,2005"> | |
| </div> | |
| </div> | |
| <div class="small">Совет: можно оставить поля пустыми, генератор подставит реалистичные значения по умолчанию.</div> | |
| </div> | |
| <div class="section"> | |
| <h2>Паттерны</h2> | |
| <div class="opts"> | |
| <label class="opt"><input type="checkbox" id="pName" checked> <span>Имя + фамилия (firstname.lastname / f.lastname / firstname.l / firstnamelastname …)</span></label> | |
| <label class="opt"><input type="checkbox" id="pReverse" checked> <span>Обратные (lastname.firstname / lastnamef …)</span></label> | |
| <label class="opt"><input type="checkbox" id="pSeparators" checked> <span>Разделители . _ - и без разделителя</span></label> | |
| <label class="opt"><input type="checkbox" id="pInitials" checked> <span>Инициалы (f.l / fl / lf / f / l)</span></label> | |
| <label class="opt"><input type="checkbox" id="pNick" checked> <span>Ники / слова (first.nick, nickYY, nickNN …)</span></label> | |
| <label class="opt"><input type="checkbox" id="pNumbers" checked> <span>Числа (YY, YYYY, популярные суффиксы)</span></label> | |
| <label class="opt"><input type="checkbox" id="pNormalize" checked> <span>Нормализация (ä→ae, ö→oe, ü→ue, ß→ss, диакритики)</span></label> | |
| <label class="opt"><input type="checkbox" id="pCompact" checked> <span>Компактные (без точки: flastname, firstnamel)</span></label> | |
| </div> | |
| <div class="btns"> | |
| <button class="primary" id="btnGen">Сгенерировать</button> | |
| <button id="btnClear">Очистить</button> | |
| </div> | |
| <div class="chips"> | |
| <div class="chip">Реалистичные шаблоны</div> | |
| <div class="chip">Дедупликация</div> | |
| <div class="chip">Массовые домены</div> | |
| <div class="chip">Экспорт .txt</div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>Экспорт</h2> | |
| <div class="btns"> | |
| <button class="good" id="btnCopy" disabled>Скопировать выбранные</button> | |
| <button class="good" id="btnCopyAll" disabled>Скопировать всё</button> | |
| <button class="bad" id="btnSave" disabled>Скачать .txt</button> | |
| </div> | |
| <div class="small">Поддерживается фильтр по тексту, выделение чекбоксами, быстрая копипаста и сохранение.</div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Results --> | |
| <div class="card"> | |
| <div class="results"> | |
| <div class="res-head"> | |
| <div class="summary"> | |
| <div class="stat"><span class="muted small">Сгенерировано</span><br><b><span id="count">0</span></b></div> | |
| <div class="stat"><span class="muted small">Выбрано</span><br><b><span id="selCount">0</span></b></div> | |
| <div class="stat"><span class="muted small">Доменов</span><br><b><span id="domCount">1</span></b></div> | |
| </div> | |
| <input id="filter" class="search" type="text" placeholder="Фильтр (например: gmx.at, anna, 2005)…"> | |
| </div> | |
| <div id="list" class="list"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer> | |
| <div class="small">Демонстрационные ники по умолчанию: shadow, ranger, chaos, sugarless, noizegate, facenorth, underworld, tender, big_balls, nike_maus, schatz, ravenation, linchen, dasher.</div> | |
| </footer> | |
| <script> | |
| /* ---------- Helpers ---------- */ | |
| const $ = sel => document.querySelector(sel); | |
| const $$ = sel => [...document.querySelectorAll(sel)]; | |
| // Кэширование элементов DOM | |
| const elements = { | |
| firstNames: $('#firstNames'), | |
| lastNames: $('#lastNames'), | |
| nickSeeds: $('#nickSeeds'), | |
| domains: $('#domains'), | |
| birthYear: $('#birthYear'), | |
| numSuffixes: $('#numSuffixes'), | |
| pName: $('#pName'), | |
| pReverse: $('#pReverse'), | |
| pSeparators: $('#pSeparators'), | |
| pInitials: $('#pInitials'), | |
| pNick: $('#pNick'), | |
| pNumbers: $('#pNumbers'), | |
| pNormalize: $('#pNormalize'), | |
| pCompact: $('#pCompact'), | |
| btnGen: $('#btnGen'), | |
| btnClear: $('#btnClear'), | |
| btnCopy: $('#btnCopy'), | |
| btnCopyAll: $('#btnCopyAll'), | |
| btnSave: $('#btnSave'), | |
| count: $('#count'), | |
| selCount: $('#selCount'), | |
| domCount: $('#domCount'), | |
| filter: $('#filter'), | |
| list: $('#list') | |
| }; | |
| // Состояние приложения | |
| let state = { | |
| generatedItems: [], | |
| currentFilter: '' | |
| }; | |
| function splitCSV(s, def = []) { | |
| if (!s) return def; | |
| return s.split(/[,\n;]/).map(x => x.trim()).filter(Boolean); | |
| } | |
| function normalizeName(s, enable = true) { | |
| if (!enable) return s.toLowerCase(); | |
| const diacriticsMap = { | |
| 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', | |
| 'á': 'a', 'à': 'a', 'â': 'a', 'ã': 'a', 'å': 'a', 'ă': 'a', 'ą': 'a', | |
| 'č': 'c', 'ć': 'c', 'ç': 'c', | |
| 'ď': 'd', 'ḑ': 'd', | |
| 'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e', 'ě': 'e', 'ę': 'e', | |
| 'í': 'i', 'ì': 'i', 'î': 'i', 'ï': 'i', | |
| 'ľ': 'l', 'ł': 'l', | |
| 'ń': 'n', 'ñ': 'n', | |
| 'ó': 'o', 'ò': 'o', 'ô': 'o', 'õ': 'o', 'ő': 'o', 'ø': 'o', | |
| 'ř': 'r', 'ŕ': 'r', | |
| 'š': 's', 'ś': 's', | |
| 'ť': 't', | |
| 'ú': 'u', 'ù': 'u', 'û': 'u', 'ü': 'u', 'ů': 'u', 'ű': 'u', | |
| 'ý': 'y', 'ÿ': 'y', | |
| 'ž': 'z', 'ź': 'z', 'ż': 'z' | |
| }; | |
| return s.toLowerCase() | |
| .replace(/[^\u0000-\u007E]/g, ch => diacriticsMap[ch] || ch) | |
| .replace(/[^\w.-]/g, ''); | |
| } | |
| function uniq(arr) { | |
| return [...new Set(arr)]; | |
| } | |
| function pairs(firsts, lasts) { | |
| const out = []; | |
| for (const f of firsts) { | |
| for (const l of lasts) { | |
| out.push([f, l]); | |
| } | |
| } | |
| return out.length ? out : [['', '']]; | |
| } | |
| function twoDigit(y) { | |
| y = String(y); | |
| return y.length === 4 ? y.slice(2) : y.padStart(2, '0').slice(-2); | |
| } | |
| function commonSuffixesFromYear(y) { | |
| const base = ['1', '12', '69', '70', '71', '84', '85', '87', '97', '123', '166', '2005']; | |
| if (!y) return base; | |
| const yy = twoDigit(y); | |
| const yyyy = String(y); | |
| return uniq(base.concat([yy, yyyy])); | |
| } | |
| function withDomains(localParts, domains) { | |
| if (!domains.length) return localParts; | |
| const res = []; | |
| for (const lp of localParts) { | |
| for (const d of domains) { | |
| res.push(`${lp}@${d}`); | |
| } | |
| } | |
| return res; | |
| } | |
| /* ---------- Generator ---------- */ | |
| function generate() { | |
| // Показать индикатор загрузки | |
| const originalText = elements.btnGen.textContent; | |
| elements.btnGen.innerHTML = '<span class="loading"></span> Генерация...'; | |
| elements.btnGen.disabled = true; | |
| // Используем setTimeout чтобы дать интерфейсу обновиться перед тяжелой операцией | |
| setTimeout(() => { | |
| try { | |
| const opt = { | |
| pName: elements.pName.checked, | |
| pReverse: elements.pReverse.checked, | |
| pSeparators: elements.pSeparators.checked, | |
| pInitials: elements.pInitials.checked, | |
| pNick: elements.pNick.checked, | |
| pNumbers: elements.pNumbers.checked, | |
| pNormalize: elements.pNormalize.checked, | |
| pCompact: elements.pCompact.checked | |
| }; | |
| // Inputs | |
| let firsts = splitCSV(elements.firstNames.value); | |
| let lasts = splitCSV(elements.lastNames.value); | |
| let nicks = splitCSV(elements.nickSeeds.value, []); | |
| let domains = splitCSV(elements.domains.value, ['gmx.at']); | |
| const birthYear = elements.birthYear.value ? parseInt(elements.birthYear.value, 10) : null; | |
| // Defaults if empty (for a good demo OOTB) | |
| if (!firsts.length) firsts = ['claudia', 'daniel', 'anna', 'kevin', 'alexander', 'christiane', 'veronika', 'karina', 'norbert', 'hannes']; | |
| if (!lasts.length) lasts = ['berghoffer', 'pirker', 'rohrer', 'sykora', 'hermann', 'huetter', 'semlitsch', 'steger', 'plettenbacher', 'gfoehler']; | |
| if (!nicks.length) nicks = ['shadow', 'ranger', 'chaos', 'sugarless', 'noizegate', 'facenorth', 'underworld', 'tender', 'big_balls', 'nike_maus', 'schatz', 'ravenation', 'linchen', 'dasher']; | |
| // Normalize | |
| firsts = uniq(firsts.map(s => normalizeName(s, opt.pNormalize))).filter(Boolean); | |
| lasts = uniq(lasts.map(s => normalizeName(s, opt.pNormalize))).filter(Boolean); | |
| nicks = uniq(nicks.map(s => normalizeName(s, opt.pNormalize))).filter(Boolean); | |
| domains = uniq(domains.map(s => s.toLowerCase()).filter(Boolean)); | |
| const sepSet = opt.pSeparators ? ['.', '_', '-', ''] : ['.']; | |
| // Suffixes | |
| let manualSuffixes = splitCSV(elements.numSuffixes.value, []); | |
| manualSuffixes = manualSuffixes.filter(s => /^\d{1,4}$/.test(s)); | |
| const popular = commonSuffixesFromYear(birthYear); | |
| const suffixes = opt.pNumbers ? uniq(popular.concat(manualSuffixes)) : []; | |
| const locals = new Set(); | |
| // Name-based patterns | |
| if (opt.pName || opt.pReverse || opt.pInitials || opt.pCompact) { | |
| for (const [f, l] of pairs(firsts, lasts)) { | |
| const fi = f ? f[0] : ''; | |
| const li = l ? l[0] : ''; | |
| // base combos | |
| const bases = new Set(); | |
| if (opt.pName) { | |
| for (const s of sepSet) { | |
| if (f && l) { bases.add(`${f}${s}${l}`); } | |
| if (opt.pCompact && f && l) bases.add(`${f}${l}`); | |
| if (f && l && s && opt.pInitials) bases.add(`${fi}${s}${l}`); | |
| if (f && l && s && opt.pInitials) bases.add(`${f}${s}${li}`); | |
| } | |
| } | |
| if (opt.pReverse) { | |
| for (const s of sepSet) { | |
| if (f && l) { bases.add(`${l}${s}${f}`); } | |
| if (opt.pCompact && f && l) bases.add(`${l}${f}`); | |
| if (opt.pInitials && f && l && s) bases.add(`${l}${s}${fi}`); | |
| } | |
| } | |
| if (opt.pInitials) { | |
| if (f && l) bases.add(`${fi}${l}`); | |
| if (f && l) bases.add(`${l}${fi}`); | |
| if (f) bases.add(`${fi}`); | |
| if (l) bases.add(`${l}`); | |
| } | |
| // add bases | |
| bases.forEach(b => locals.add(b)); | |
| // numeric suffixing | |
| if (opt.pNumbers && suffixes.length) { | |
| bases.forEach(b => { | |
| for (const suf of suffixes) { | |
| locals.add(`${b}${suf}`); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| // Nick-based patterns | |
| if (opt.pNick && nicks.length) { | |
| const maybeYears = opt.pNumbers ? suffixes : []; | |
| for (const n of nicks) { | |
| locals.add(n); | |
| if (maybeYears.length) { | |
| for (const suf of maybeYears) { locals.add(`${n}${suf}`); } | |
| } | |
| // stitch with firstnames | |
| for (const f of firsts) { | |
| for (const s of sepSet.filter(x => x !== '')) { // nick separators look more human with visible sep | |
| locals.add(`${f}${s}${n}`); | |
| locals.add(`${n}${s}${f}`); | |
| } | |
| } | |
| } | |
| } | |
| // Final list with domains | |
| const localParts = uniq([...locals]).filter(Boolean); | |
| const emails = withDomains(localParts, domains); | |
| // Сохраняем сгенерированные данные в состоянии | |
| state.generatedItems = emails; | |
| // Render | |
| renderList(emails, domains.length); | |
| } catch (error) { | |
| console.error('Generation error:', error); | |
| showToast('Ошибка генерации: ' + error.message, 'error'); | |
| } finally { | |
| // Восстанавливаем кнопку | |
| elements.btnGen.textContent = originalText; | |
| elements.btnGen.disabled = false; | |
| } | |
| }, 10); | |
| } | |
| /* ---------- Render ---------- */ | |
| function renderList(items, domCount) { | |
| elements.list.innerHTML = ''; | |
| const filter = elements.filter.value.trim().toLowerCase(); | |
| let filtered = items; | |
| if (filter) { | |
| filtered = items.filter(x => x.toLowerCase().includes(filter)); | |
| } | |
| elements.count.textContent = String(items.length); | |
| elements.domCount.textContent = String(domCount); | |
| // Используем DocumentFragment для оптимизации рендеринга | |
| const frag = document.createDocumentFragment(); | |
| // Рендерим виртуализированный список (только видимые элементы) | |
| const batchSize = 500; // Размер батча для рендеринга | |
| const renderBatch = (startIdx) => { | |
| const endIdx = Math.min(startIdx + batchSize, filtered.length); | |
| for (let i = startIdx; i < endIdx; i++) { | |
| const email = filtered[i]; | |
| const line = document.createElement('div'); | |
| line.className = 'line'; | |
| line.innerHTML = ` | |
| <input type="checkbox" class="pick"> | |
| <div class="email">${email}</div> | |
| <div class="small muted">${i + 1}</div> | |
| `; | |
| frag.appendChild(line); | |
| } | |
| elements.list.appendChild(frag); | |
| // Если есть еще элементы, рендерим следующий батч | |
| if (endIdx < filtered.length) { | |
| setTimeout(() => renderBatch(endIdx), 0); | |
| } else { | |
| updateSelectionCount(); | |
| const disabled = filtered.length === 0; | |
| elements.btnCopy.disabled = disabled; | |
| elements.btnCopyAll.disabled = disabled; | |
| elements.btnSave.disabled = disabled; | |
| // Attach selection handlers | |
| $$('.pick').forEach(cb => cb.addEventListener('change', updateSelectionCount)); | |
| } | |
| }; | |
| // Начинаем рендеринг с первого батча | |
| renderBatch(0); | |
| } | |
| function updateSelectionCount() { | |
| const picks = $$('.pick:checked'); | |
| elements.selCount.textContent = String(picks.length); | |
| } | |
| /* ---------- Export ---------- */ | |
| async function copyText(text) { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| showToast('Скопировано в буфер обмена'); | |
| } catch (e) { | |
| alert('Не удалось скопировать: ' + e.message); | |
| } | |
| } | |
| function showToast(message, type = 'success') { | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| toast.textContent = message; | |
| if (type === 'error') { | |
| toast.style.background = 'linear-gradient(90deg, #ff5f6d, #ffc371)'; | |
| } else if (type === 'warning') { | |
| toast.style.background = 'linear-gradient(90deg, #ffc371, #ffdd67)'; | |
| toast.style.color = '#2a1c08'; | |
| } | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| if (toast.parentNode) { | |
| toast.parentNode.removeChild(toast); | |
| } | |
| }, 2000); | |
| } | |
| function getVisibleEmails() { | |
| return $$('#list .line .email').map(n => n.textContent.trim()); | |
| } | |
| function getSelectedEmails() { | |
| const rows = $$('#list .line'); | |
| const out = []; | |
| rows.forEach(r => { | |
| const cb = r.querySelector('.pick'); | |
| if (cb && cb.checked) { | |
| out.push(r.querySelector('.email').textContent.trim()); | |
| } | |
| }); | |
| return out; | |
| } | |
| /* ---------- Event Handlers ---------- */ | |
| elements.btnCopyAll.addEventListener('click', () => { | |
| const items = getVisibleEmails(); | |
| if (!items.length) return; | |
| copyText(items.join('\n')); | |
| }); | |
| elements.btnCopy.addEventListener('click', () => { | |
| const items = getSelectedEmails(); | |
| if (!items.length) { | |
| showToast('Ничего не выбрано', 'warning'); | |
| return; | |
| } | |
| copyText(items.join('\n')); | |
| }); | |
| elements.btnSave.addEventListener('click', () => { | |
| const items = getVisibleEmails(); | |
| if (!items.length) return; | |
| const blob = new Blob([items.join('\n')], { type: 'text/plain;charset=utf-8' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'usernames_emails.txt'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| URL.revokeObjectURL(a.href); | |
| a.remove(); | |
| }, 0); | |
| }); | |
| elements.btnGen.addEventListener('click', generate); | |
| elements.btnClear.addEventListener('click', () => { | |
| elements.list.innerHTML = ''; | |
| elements.count.textContent = '0'; | |
| elements.selCount.textContent = '0'; | |
| state.generatedItems = []; | |
| }); | |
| elements.filter.addEventListener('input', () => { | |
| const query = elements.filter.value.trim().toLowerCase(); | |
| state.currentFilter = query; | |
| if (state.generatedItems.length === 0) return; | |
| // Фильтруем результаты | |
| const filtered = query ? | |
| state.generatedItems.filter(x => x.toLowerCase().includes(query)) : | |
| state.generatedItems; | |
| // Обновляем счетчик | |
| elements.count.textContent = String(state.generatedItems.length); | |
| // Перерисовываем список | |
| renderList(state.generatedItems, parseInt(elements.domCount.textContent)); | |
| }); | |
| /* ---------- Auto-generate on first load ---------- */ | |
| window.addEventListener('DOMContentLoaded', () => { | |
| // Восстанавливаем значения из localStorage, если есть | |
| const savedData = localStorage.getItem('usernameGeneratorData'); | |
| if (savedData) { | |
| try { | |
| const data = JSON.parse(savedData); | |
| if (data.firstNames) elements.firstNames.value = data.firstNames; | |
| if (data.lastNames) elements.lastNames.value = data.lastNames; | |
| if (data.nickSeeds) elements.nickSeeds.value = data.nickSeeds; | |
| if (data.domains) elements.domains.value = data.domains; | |
| if (data.birthYear) elements.birthYear.value = data.birthYear; | |
| if (data.numSuffixes) elements.numSuffixes.value = data.numSuffixes; | |
| } catch (e) { | |
| console.error('Error loading saved data:', e); | |
| } | |
| } | |
| generate(); | |
| }); | |
| // Сохраняем данные при изменении | |
| ['firstNames', 'lastNames', 'nickSeeds', 'domains', 'birthYear', 'numSuffixes'].forEach(id => { | |
| $(`#${id}`).addEventListener('change', () => { | |
| const data = { | |
| firstNames: elements.firstNames.value, | |
| lastNames: elements.lastNames.value, | |
| nickSeeds: elements.nickSeeds.value, | |
| domains: elements.domains.value, | |
| birthYear: elements.birthYear.value, | |
| numSuffixes: elements.numSuffixes.value | |
| }; | |
| localStorage.setItem('usernameGeneratorData', JSON.stringify(data)); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |