dynamic / index.html
enotkrutoy's picture
Update index.html
13b1c12 verified
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Advanced Leak-based Username Generator — willhaben style</title>
<style>
:root{
--bg:#0b0f14;--panel:#0f1720;--accent:#39a6ff;--muted:#9fb0c8;--text:#e6f1fb;
--ok:#5ee07a;--warn:#ffcc33;--err:#ff6b6b;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter, Roboto, Arial, sans-serif;background:linear-gradient(180deg,#061018,#071820);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
header{padding:20px 22px;border-bottom:1px solid rgba(255,255,255,0.03)}
h1{margin:0;font-size:18px}
main{display:grid;grid-template-columns:380px 1fr;gap:16px;padding:18px;align-items:start}
@media (max-width:980px){main{grid-template-columns:1fr;padding:12px}}
.card{background:linear-gradient(180deg,var(--panel),#0b1118);border-radius:12px;padding:14px;border:1px solid rgba(255,255,255,0.03);box-shadow:0 8px 30px rgba(0,0,0,0.6)}
label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
input[type="file"]{color:var(--text)}
.row{display:flex;gap:8px;align-items:center}
input[type="text"], input[type="number"], select, textarea {
width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:#07121a;color:var(--text);
}
textarea{min-height:120px;resize:vertical;font-family:ui-monospace,Menlo,Consolas,monospace}
button{background:linear-gradient(90deg,var(--accent),#6ad6ff);border:none;color:#04202a;padding:10px 12px;border-radius:8px;font-weight:700;cursor:pointer}
button.ghost{background:transparent;border:1px solid rgba(255,255,255,0.04);color:var(--text)}
.muted{color:var(--muted);font-size:13px}
.small{font-size:12px;color:var(--muted)}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px}
.stat{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px;text-align:center}
.stat b{display:block;font-size:18px;color:var(--text)}
table{width:100%;border-collapse:collapse;font-size:13px;margin-top:8px}
th,td{padding:6px 8px;border-bottom:1px dashed rgba(255,255,255,0.03);text-align:left}
th{color:var(--muted);font-weight:600;font-size:12px}
.controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.progress{height:10px;background:rgba(255,255,255,0.03);border-radius:8px;overflow:hidden}
.progress > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#6ad6ff);width:0%}
.footer{padding:16px;color:var(--muted);font-size:13px;text-align:center}
.tag{display:inline-block;padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:999px;font-size:12px;margin:4px 4px 0 0}
.codebox{background:#041219;border:1px solid rgba(255,255,255,0.02);padding:10px;border-radius:8px;font-family:ui-monospace,Menlo,Consolas,monospace;color:#cfeefb;font-size:13px;max-height:260px;overflow:auto}
.flex{display:flex;gap:8px;align-items:center}
.right-actions{display:flex;gap:8px;align-items:center;justify-content:flex-end}
.switch-container {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #333;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(20px);
}
</style>
</head>
<body>
<header>
<h1>Advanced leak-based username generator — robust & streaming</h1>
<div class="small">Загрузите очищенный список логинов (txt). Анализируем паттерны и генерируем правдоподобные варианты по распределению.</div>
</header>
<main>
<!-- LEFT: Input / Analysis controls -->
<div class="card">
<label>1) Загрузить файл с логинами (txt)</label>
<input id="fileInput" type="file" accept=".txt" />
<div class="small" style="margin-top:8px">Поддерживается чтение больших файлов через поток (если браузер поддерживает), иначе — FileReader. Формат: один email на строку.</div>
<div style="margin-top:12px">
<label>2) Настройки анализа</label>
<div class="small">Автоматическое определение паттернов, доменов, имен, фамилий и суффиксов. Можно задать свои значения или использовать автоматически определенные.</div>
<div class="switch-container">
<label class="small">Автоопределение имен/фамилий</label>
<label class="switch">
<input type="checkbox" id="autoDetectNames" checked>
<span class="slider"></span>
</label>
</div>
<div style="margin-top:8px" class="row">
<input id="sampleNames" type="text" placeholder="Примеры имён через запятую (опц.)" />
<input id="sampleLastnames" type="text" placeholder="Примеры фамилий (опц.)" />
</div>
<div class="switch-container" style="margin-top:8px">
<label class="small">Автоопределение суффиксов</label>
<label class="switch">
<input type="checkbox" id="autoDetectSuffixes" checked>
<span class="slider"></span>
</label>
</div>
<div style="margin-top:8px" class="row">
<label style="margin:0">Доп. суффиксы (через запятую)</label>
</div>
<input id="manualSuffixes" type="text" placeholder="123,007,1984,2005" />
<div class="switch-container" style="margin-top:8px">
<label class="small">Использовать ручной домен</label>
<label class="switch">
<input type="checkbox" id="useManualDomain">
<span class="slider"></span>
</label>
</div>
<input id="manualDomain" type="text" placeholder="Введите домен вручную (например: example.com)" style="margin-top:8px; display: none;" />
</div>
<div style="margin-top:12px">
<label>3) Анализировать / Сброс</label>
<div class="controls">
<button id="analyzeBtn">Проанализировать файл</button>
<button id="resetBtn" class="ghost">Сбросить состояние</button>
</div>
</div>
<div class="stats" style="margin-top:12px">
<div class="stat">
<div class="muted">Строк в файле</div>
<b id="statLines">0</b>
</div>
<div class="stat">
<div class="muted">Уникальных локал-партов</div>
<b id="statUnique">0</b>
</div>
<div class="stat">
<div class="muted">Доменов</div>
<b id="statDomains">0</b>
</div>
</div>
<div style="margin-top:12px">
<label>4) Результаты анализа (топ паттернов & домены)</label>
<div style="max-height:260px;overflow:auto">
<table id="patternsTable">
<thead><tr><th>Паттерн</th><th>Частота</th><th>Примеры</th></tr></thead>
<tbody></tbody>
</table>
<table id="domainsTable" style="margin-top:10px">
<thead><tr><th>Домен</th><th>Частота</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<div style="margin-top:12px">
<label>5) Настройки генерации</label>
<div class="small">Укажите количество генерируемых вариантов и опции экспорта.</div>
<div class="row" style="margin-top:6px">
<input id="genCount" type="number" min="1" value="500" />
<select id="exportFormat" title="Формат экспорта">
<option value="txt">.txt (один в строке)</option>
<option value="csv">.csv (localpart,domain,pattern)</option>
<option value="json">.json (массив объектов)</option>
</select>
</div>
<div style="margin-top:8px" class="row">
<label style="margin:0"><input id="dedupe" type="checkbox" checked /> Удалять дубликаты (может потребовать памяти)</label>
</div>
<div style="margin-top:8px" class="row">
<label style="margin:0"><input id="preferLocalDomains" type="checkbox" checked/> Предпочитать локальные домены (gmx.at, aon.at, liwest.at ...)</label>
</div>
<div class="controls" style="margin-top:10px">
<button id="generateBtn">Сгенерировать</button>
<button id="previewBtn" class="ghost">Показать примеры</button>
</div>
<div style="margin-top:10px">
<div class="small">Прогресс:</div>
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<i id="progressBar"></i>
</div>
</div>
</div>
</div>
<!-- RIGHT: Output / Preview / Export -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<label>Выход / предпросмотр</label>
<div class="small">Вы можете просмотреть выборку, скачать полный результат или скопировать часть.</div>
</div>
<div class="right-actions">
<button id="copyPreview" class="ghost">Копировать видимые</button>
<button id="downloadBtn" class="ghost" disabled>Скачать результат</button>
</div>
</div>
<div style="margin-top:8px" class="codebox" id="previewBox" aria-live="polite"></div>
<div style="margin-top:12px" class="flex">
<div style="flex:1">
<label class="small">Фильтр предпросмотра</label>
<input id="previewFilter" type="text" placeholder="например: .gmx.at или 2005 или alex" />
</div>
<div style="width:160px">
<label class="small">Показать N примеров</label>
<input id="previewLimit" type="number" min="1" value="50" />
</div>
</div>
<div style="margin-top:10px" class="small">Экспорт может быть большим — для больших объёмов генерируем файл как Blob и даём ссылку для скачивания.</div>
<div style="margin-top:12px">
<label class="small">Лог действий</label>
<div id="logBox" class="codebox" style="max-height:160px"></div>
</div>
</div>
</main>
<div class="footer card">
<div class="small">Инструмент анализирует паттерны локальной части email (localpart) — firstname.lastname, initials, nick+digits, pure nicks и т.д. — и строит распределение для генерации. Процесс оптимизирован для больших файлов (поточное чтение, батчи и асинхронная генерация).</div>
</div>
<script>
(() => {
const el = id => document.getElementById(id);
const log = (t) => {
const b = el('logBox');
const time = new Date().toISOString().replace('T',' ').split('.')[0];
b.textContent = `${time}${t}\n` + b.textContent;
};
function safeLower(s){ return (s||'').trim().toLowerCase(); }
function uniqArray(arr){ return [...new Set(arr)]; }
function sum(obj){
return Object.values(obj).reduce((a,b)=>a+(b||0),0);
}
function pickWeighted(map){
const entries = Object.entries(map);
const total = entries.reduce((a,[,w])=>a+w,0);
if (total === 0) return entries.length ? entries[0][0] : null;
let r = Math.random()*total, acc=0;
for(const [k,w] of entries){ acc+=w; if(r < acc) return k; }
return entries[0][0];
}
const PATTERNS = {
'fn.dot.ln': /^[a-zäöüß]+[.][a-zäöüß]+[0-9]*$/i,
'fi.dot.ln': /^[a-zäöüß][.][a-zäöüß]+[0-9]*$/i,
'fnln': /^[a-zäöüß]+[a-zäöüß]+[0-9]*$/i,
'nick_digits': /^[a-z0-9._-]*[0-9]{2,6}$/i,
'nick': /^[a-z][a-z0-9._-]{1,20}$/i,
'initials': /^[a-z]{2,4}$/i,
'contains_special': /[^a-z0-9._-]/i
};
let state = {
totalLines: 0,
uniqueLocalparts: new Set(),
domainCounts: Object.create(null),
patternCounts: Object.create(null),
patternExamples: Object.create(null),
sampleLocalparts: [],
domainsList: [],
manualSuffixes: [],
detectedFirstNames: new Set(),
detectedLastNames: new Set(),
detectedSuffixes: new Set(),
analyzed: false
};
Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); });
el('useManualDomain').addEventListener('change', function() {
el('manualDomain').style.display = this.checked ? 'block' : 'none';
});
async function processFile(file) {
state.totalLines = 0;
state.uniqueLocalparts = new Set();
state.domainCounts = {};
state.detectedFirstNames = new Set();
state.detectedLastNames = new Set();
state.detectedSuffixes = new Set();
Object.keys(state.patternCounts).forEach(k => state.patternCounts[k]=0);
Object.keys(state.patternExamples).forEach(k => state.patternExamples[k].clear());
state.sampleLocalparts = [];
el('statLines').textContent = '0';
el('statUnique').textContent = '0';
el('statDomains').textContent = '0';
el('patternsTable').querySelector('tbody').innerHTML = '';
el('domainsTable').querySelector('tbody').innerHTML = '';
el('previewBox').textContent = '';
el('logBox').textContent = '';
log(`Начало анализа файла: ${file.name}`);
if (file.stream && typeof file.stream === 'function') {
log('Использую File.stream() для по-частного чтения (stream).');
await streamFileProcessing(file);
} else {
log('File.stream() не поддерживается — использую FileReader (fallback).');
await fileReaderProcessing(file);
}
state.domainsList = Object.keys(state.domainCounts).sort((a,b)=>state.domainCounts[b]-state.domainCounts[a]);
el('statLines').textContent = String(state.totalLines);
el('statUnique').textContent = String(state.uniqueLocalparts.size);
el('statDomains').textContent = String(Object.keys(state.domainCounts).length);
if (el('autoDetectNames').checked) {
const firstNames = Array.from(state.detectedFirstNames).slice(0, 20);
const lastNames = Array.from(state.detectedLastNames).slice(0, 20);
if (firstNames.length > 0 && !el('sampleNames').value) {
el('sampleNames').value = firstNames.join(',');
log(`Автоопределено ${firstNames.length} имен`);
}
if (lastNames.length > 0 && !el('sampleLastnames').value) {
el('sampleLastnames').value = lastNames.join(',');
log(`Автоопределено ${lastNames.length} фамилий`);
}
}
if (el('autoDetectSuffixes').checked) {
const suffixes = Array.from(state.detectedSuffixes).slice(0, 15);
if (suffixes.length > 0 && !el('manualSuffixes').value) {
el('manualSuffixes').value = suffixes.join(',');
log(`Автоопределено ${suffixes.length} суффиксов`);
}
}
renderPatternTable();
renderDomainTable();
state.analyzed = true;
log('Анализ завершён.');
}
async function streamFileProcessing(file){
const decoder = new TextDecoder();
const reader = file.stream().getReader();
let { value: chunk, done: readerDone } = await reader.read();
let buffer = '';
while (!readerDone) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split(/\r\n|\n/);
buffer = lines.pop();
await processLineBatch(lines);
const res = await reader.read();
chunk = res.value; readerDone = res.done;
}
if (buffer) await processLineBatch([buffer]);
await reader.releaseLock?.();
}
async function fileReaderProcessing(file){
const CHUNK = 2 * 1024 * 1024;
let offset = 0;
while (offset < file.size) {
const slice = file.slice(offset, offset + CHUNK);
const text = await slice.text();
const lines = text.split(/\r\n|\n/);
if (offset + CHUNK < file.size) {
const last = lines.pop();
await processLineBatch(lines);
offset += CHUNK - (last ? last.length : 0);
} else {
await processLineBatch(lines);
offset += CHUNK;
}
await new Promise(r => setTimeout(r, 0));
}
}
async function processLineBatch(lines) {
for (const rawLine of lines) {
const line = (rawLine||'').trim();
if (!line) continue;
state.totalLines++;
const atIdx = line.indexOf('@');
const local = safeLower(atIdx === -1 ? line : line.slice(0, atIdx));
const domain = atIdx === -1 ? '' : safeLower(line.slice(atIdx+1));
if (!local) continue;
state.uniqueLocalparts.add(local);
if (domain) state.domainCounts[domain] = (state.domainCounts[domain]||0) + 1;
classifyLocal(local);
extractNamesFromLocal(local);
extractSuffixesFromLocal(local);
if (state.sampleLocalparts.length < 200) state.sampleLocalparts.push({local, domain});
}
if (state.totalLines % 500 === 0) {
el('statLines').textContent = String(state.totalLines);
el('statUnique').textContent = String(state.uniqueLocalparts.size);
el('statDomains').textContent = String(Object.keys(state.domainCounts).length);
await new Promise(r=>setTimeout(r,0));
}
}
function classifyLocal(local){
if (PATTERNS['fn.dot.ln'].test(local)) {
state.patternCounts['fn.dot.ln']++;
addExample('fn.dot.ln', local);
return;
}
if (PATTERNS['fi.dot.ln'].test(local)) {
state.patternCounts['fi.dot.ln']++;
addExample('fi.dot.ln', local);
return;
}
if (PATTERNS['nick_digits'].test(local)) {
state.patternCounts['nick_digits']++;
addExample('nick_digits', local);
return;
}
if (PATTERNS['initials'].test(local)) {
state.patternCounts['initials']++;
addExample('initials', local);
return;
}
if (PATTERNS['nick'].test(local)) {
state.patternCounts['nick']++;
addExample('nick', local);
return;
}
if (PATTERNS['contains_special'].test(local)) {
state.patternCounts['contains_special']++;
addExample('contains_special', local);
return;
}
state.patternCounts['fnln']++;
addExample('fnln', local);
}
function addExample(pat, local) {
const s = state.patternExamples[pat];
if (s && s.size < 8) s.add(local);
}
function extractNamesFromLocal(local) {
const fnLnMatch = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i);
if (fnLnMatch) {
state.detectedFirstNames.add(fnLnMatch[1]);
state.detectedLastNames.add(fnLnMatch[2]);
}
const fnDigitsMatch = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i);
if (fnDigitsMatch) {
state.detectedFirstNames.add(fnDigitsMatch[1]);
}
const lnDigitsMatch = local.match(/^[a-zäöüß]{1}[._]([a-zäöüß]{2,20})[0-9]{2,6}$/i);
if (lnDigitsMatch) {
state.detectedLastNames.add(lnDigitsMatch[1]);
}
const initialsLnMatch = local.match(/^[a-z]{1,2}[._]([a-zäöüß]{2,20})/i);
if (initialsLnMatch) {
state.detectedLastNames.add(initialsLnMatch[1]);
}
}
function extractSuffixesFromLocal(local) {
const digitsMatch = local.match(/[a-zäöüß][._-]?([0-9]{2,6})$/i);
if (digitsMatch) {
state.detectedSuffixes.add(digitsMatch[1]);
}
const yearMatch = local.match(/(19[0-9]{2}|20[0-9]{2})$/);
if (yearMatch) {
state.detectedSuffixes.add(yearMatch[1]);
}
const shortNumMatch = local.match(/([0-9]{2,4})$/);
if (shortNumMatch && !yearMatch) {
state.detectedSuffixes.add(shortNumMatch[1]);
}
}
function renderPatternTable(){
const tbody = el('patternsTable').querySelector('tbody');
tbody.innerHTML = '';
const total = sum(state.patternCounts) || 1;
Object.entries(state.patternCounts).sort((a,b)=>b[1]-a[1]).forEach(([p,c])=>{
const tr = document.createElement('tr');
const examples = [...(state.patternExamples[p]||[])].slice(0,5).join(', ');
const pct = ((c/total)*100).toFixed(1) + '%';
tr.innerHTML = `<td>${p}</td><td>${c} (${pct})</td><td class="small">${examples}</td>`;
tbody.appendChild(tr);
});
}
function renderDomainTable(){
const tbody = el('domainsTable').querySelector('tbody');
tbody.innerHTML = '';
Object.entries(state.domainCounts).sort((a,b)=>b[1]-a[1]).slice(0,100).forEach(([d,c])=>{
const tr = document.createElement('tr');
tr.innerHTML = `<td>@${d}</td><td>${c}</td>`;
tbody.appendChild(tr);
});
}
function buildGenerators() {
const patternWeights = {};
Object.keys(state.patternCounts).forEach(k => {
patternWeights[k] = state.patternCounts[k] || 1;
});
let domainWeights = {};
if (el('useManualDomain').checked && el('manualDomain').value.trim()) {
const manualDomain = safeLower(el('manualDomain').value);
domainWeights = {};
domainWeights[manualDomain] = 1000;
log(`Используется ручной домен: ${manualDomain}`);
} else {
Object.entries(state.domainCounts).forEach(([d,w]) => domainWeights[d] = w);
if (Object.keys(domainWeights).length === 0) {
['gmail.com','gmx.at','aon.at','yahoo.com','outlook.com','web.de','chello.at','liwest.at','hotmail.com'].forEach(d=>domainWeights[d] = 10);
}
if (el('preferLocalDomains').checked) {
const localPrefer = ['gmx.at','aon.at','chello.at','liwest.at','inode.at','student.uibk.ac.at','proton.me','protonmail.com'];
localPrefer.forEach(d=>{
if (domainWeights[d]) domainWeights[d] *= 3;
});
}
}
const manual = el('manualSuffixes').value.split(',').map(s=>s.trim()).filter(Boolean);
const manualSuffixes = uniqArray(manual);
const providedFirsts = el('sampleNames').value.split(',').map(x=>safeLower(x)).filter(Boolean);
const providedLasts = el('sampleLastnames').value.split(',').map(x=>safeLower(x)).filter(Boolean);
const guessedFirsts = new Set();
const guessedLasts = new Set();
state.sampleLocalparts.forEach(({local})=>{
const m = local.match(/^([a-zäöüß]{2,20})[._]([a-zäöüß]{2,20})/i);
if (m) { guessedFirsts.add(m[1]); guessedLasts.add(m[2]); }
else {
const m2 = local.match(/^([a-zäöüß]{2,20})[0-9]{2,6}$/i);
if (m2) guessedFirsts.add(m2[1]);
}
});
const firstPool = uniqArray([...providedFirsts, ...Array.from(guessedFirsts)]).filter(Boolean);
const lastPool = uniqArray([...providedLasts, ...Array.from(guessedLasts)]).filter(Boolean);
if (firstPool.length === 0) firstPool.push('alex','maria','stefan','anna','thomas','martin');
if (lastPool.length === 0) lastPool.push('bauer','schmidt','mueller','hofer','gruber','wagner');
const specialLocals = Array.from(state.uniqueLocalparts).filter(x => PATTERNS['contains_special'].test(x));
const gens = {
'fn.dot.ln': () => {
const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
return `${fn}.${ln}`;
},
'fi.dot.ln': () => {
const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
return `${fn[0]}.${ln}`;
},
'fnln': () => {
const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
return `${fn}${ln}`;
},
'nick_digits': () => {
const pick = Math.random()<0.5 ? firstPool[Math.floor(Math.random()*firstPool.length)] : (sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)]);
const digits = getRandomSuffix();
return `${pick}${digits}`;
},
'nick': () => {
return sampleNickFromDataset() || firstPool[Math.floor(Math.random()*firstPool.length)];
},
'initials': () => {
const fn = firstPool[Math.floor(Math.random()*firstPool.length)];
const ln = lastPool[Math.floor(Math.random()*lastPool.length)];
return `${fn[0]}${ln[0]}`;
},
'contains_special': () => {
if (specialLocals.length > 0) {
return specialLocals[Math.floor(Math.random()*specialLocals.length)];
}
return sampleNickFromDataset() || 'user' + Math.floor(100+Math.random()*899);
}
};
function sampleNickFromDataset(){
const arr = Array.from(state.uniqueLocalparts).filter(x => /^[a-z][a-z0-9._-]{1,20}$/i.test(x));
if (arr.length === 0) return null;
return arr[Math.floor(Math.random()*arr.length)].replace(/[._-]+$/,'').replace(/\s+/g,'');
}
function getRandomSuffix(){
const ms = manualSuffixes.filter(Boolean);
const picks = [];
if (ms.length) picks.push(...ms);
const common = ['007','123','69','84','97','2000','2001','2010','2012'];
picks.push(...common);
picks.push(String(100 + Math.floor(Math.random()*899)).slice(1));
return picks[Math.floor(Math.random()*picks.length)];
}
return {patternWeights, domainWeights, gens, firstPool, lastPool};
}
async function generateToBlob(count, opts) {
const {patternWeights, domainWeights, gens} = buildGenerators();
const encoder = new TextEncoder();
const chunks = [];
const dedupeSet = opts.dedupe ? new Set() : null;
let produced = 0;
const totalTarget = count;
const batchSize = 10000;
let buffer = [];
let bufferBytes = 0;
function pickPattern() {
return pickWeighted(patternWeights);
}
function pickDomain(){
return pickWeighted(domainWeights);
}
function genOne(){
const pat = pickPattern();
const local = (gens[pat] ? gens[pat]() : gens['fnln']()).replace(/\s+/g,'').toLowerCase();
const domain = pickDomain();
return {local, domain, pattern: pat};
}
const fmt = opts.exportFormat || 'txt';
if (fmt === 'json') {
chunks.push(encoder.encode('['));
}
while (produced < totalTarget) {
const toMake = Math.min(batchSize, totalTarget - produced);
for (let i=0; i<toMake; i++) {
const item = genOne();
const lineTxt = `${item.local}@${item.domain}`;
if (dedupeSet) {
if (dedupeSet.has(lineTxt)) continue;
dedupeSet.add(lineTxt);
}
let line;
if (fmt === 'txt') {
line = lineTxt + '\n';
} else if (fmt === 'csv') {
line = `"${item.local}","${item.domain}","${item.pattern}"\n`;
} else if (fmt === 'json') {
line = (produced > 0 ? ',' : '') + JSON.stringify({local:item.local,domain:item.domain,pattern:item.pattern}) + '\n';
}
buffer.push(line);
bufferBytes += line.length;
produced++;
if (buffer.length >= 5000 || bufferBytes > 1024 * 1024) {
flushBuffer();
}
}
if (opts.progressCallback && produced % 5000 === 0) {
const currentCount = dedupeSet ? dedupeSet.size : produced;
opts.progressCallback(Math.min(100, Math.round((currentCount / totalTarget) * 100)), currentCount);
await new Promise(r => requestAnimationFrame(r));
}
}
if (buffer.length > 0) {
flushBuffer();
}
if (fmt === 'json') {
chunks.push(encoder.encode(']'));
}
return new Blob(chunks, {type: fmt === 'json' ? 'application/json;charset=utf-8' : 'text/plain;charset=utf-8'});
function flushBuffer() {
if (buffer.length === 0) return;
const text = buffer.join('');
chunks.push(encoder.encode(text));
buffer = [];
bufferBytes = 0;
}
}
let lastGeneratedBlobURL = null;
async function handleGenerate() {
if (!state.analyzed) {
alert('Сначала выполните анализ файла (Analyze file).');
return;
}
const count = Math.max(1, parseInt(el('genCount').value || '100',10));
const format = el('exportFormat').value;
const dedupe = el('dedupe').checked;
el('generateBtn').disabled = true;
el('generateBtn').textContent = 'Генерация...';
log(`Старт генерации: ${count} элементов, формат=${format}, dedupe=${dedupe}`);
try {
const blob = await generateToBlob(count, {
dedupe,
exportFormat: format,
progressCallback: (pct, current) => {
el('progressBar').style.width = pct + '%';
el('progressBar').setAttribute('aria-valuenow', pct);
el('previewBox').textContent = `Генерация: ${current} / ${count} (${pct}%)`;
}
});
if (lastGeneratedBlobURL) URL.revokeObjectURL(lastGeneratedBlobURL);
lastGeneratedBlobURL = URL.createObjectURL(blob);
el('downloadBtn').disabled = false;
el('downloadBtn').textContent = `Скачать (${(blob.size/1024/1024).toFixed(2)} MB)`;
el('downloadBtn').onclick = () => {
const a = document.createElement('a');
let ext = '.txt';
if (format==='csv') ext = '.csv';
if (format==='json') ext = '.json';
a.href = lastGeneratedBlobURL;
a.download = `generated_usernames${ext}`;
document.body.appendChild(a);
a.click();
a.remove();
};
const previewLimit = Math.max(10, Math.min(1000, parseInt(el('previewLimit').value || '50',10)));
const previewTxt = await readBlobHead(blob, previewLimit);
el('previewBox').textContent = previewTxt;
// Apply filter after generation
const filter = (el('previewFilter').value || '').toLowerCase();
if (filter) {
const lines = previewTxt.split(/\r\n|\n/).filter(Boolean);
const shown = lines.filter(l => l.includes(filter)).slice(0, previewLimit);
el('previewBox').textContent = shown.join('\n');
}
log('Генерация завершена успешно.');
} catch (e) {
log('Ошибка при генерации: ' + (e && e.message ? e.message : String(e)));
alert('Ошибка генерации: ' + (e && e.message ? e.message : String(e)));
} finally {
el('generateBtn').disabled = false;
el('generateBtn').textContent = 'Сгенерировать';
el('progressBar').style.width = '0%';
el('progressBar').setAttribute('aria-valuenow', 0);
}
}
async function readBlobHead(blob, limitLines){
const CHUNK = 64 * 1024;
let offset = 0;
let decoder = new TextDecoder();
let accumulated = '';
while (offset < blob.size && accumulated.split(/\r\n|\n/).length - 1 < limitLines) {
const slice = blob.slice(offset, offset + CHUNK);
const text = await slice.text();
accumulated += text;
offset += CHUNK;
await new Promise(r=>setTimeout(r,0));
}
const lines = accumulated.split(/\r\n|\n/).filter(Boolean).slice(0, limitLines);
return lines.join('\n');
}
el('analyzeBtn').addEventListener('click', async () => {
const file = el('fileInput').files && el('fileInput').files[0];
if (!file) { alert('Выберите файл .txt со списком логинов.'); return; }
try {
el('analyzeBtn').disabled = true;
el('analyzeBtn').textContent = 'Анализ...';
await processFile(file);
} catch (e) {
log('Ошибка при анализе: ' + (e && e.message ? e.message : String(e)));
alert('Ошибка при анализе файла: ' + (e && e.message ? e.message : String(e)));
} finally {
el('analyzeBtn').disabled = false;
el('analyzeBtn').textContent = 'Проанализировать файл';
}
});
el('resetBtn').addEventListener('click', () => {
if (!confirm('Сбросить текущее состояние анализа?')) return;
state = {
totalLines: 0,
uniqueLocalparts: new Set(),
domainCounts: Object.create(null),
patternCounts: Object.create(null),
patternExamples: Object.create(null),
sampleLocalparts: [],
domainsList: [],
manualSuffixes: [],
detectedFirstNames: new Set(),
detectedLastNames: new Set(),
detectedSuffixes: new Set(),
analyzed: false
};
Object.keys(PATTERNS).forEach(k => { state.patternCounts[k]=0; state.patternExamples[k]=new Set(); });
el('statLines').textContent = '0';
el('statUnique').textContent = '0';
el('statDomains').textContent = '0';
el('patternsTable').querySelector('tbody').innerHTML = '';
el('domainsTable').querySelector('tbody').innerHTML = '';
el('previewBox').textContent = '';
el('logBox').textContent = '';
el('downloadBtn').disabled = true;
if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; }
log('Состояние сброшено пользователем.');
});
el('generateBtn').addEventListener('click', handleGenerate);
el('previewBtn').addEventListener('click', () => {
if (!state.analyzed) { alert('Сначала выполните анализ файла.'); return; }
const limit = Math.max(10, Math.min(200, parseInt(el('previewLimit').value || '50',10)));
const filter = (el('previewFilter').value || '').toLowerCase();
const samples = state.sampleLocalparts.map(x => `${x.local}@${x.domain||'[no-domain]'}`);
const shown = (filter ? samples.filter(s => s.includes(filter)) : samples).slice(0, limit);
el('previewBox').textContent = shown.join('\n');
});
el('copyPreview').addEventListener('click', async () => {
const txt = el('previewBox').textContent.trim();
if (!txt) return alert('Нет текста для копирования.');
try {
await navigator.clipboard.writeText(txt);
log('Предпросмотр скопирован в буфер обмена.');
} catch (e) {
alert('Не удалось скопировать: ' + (e.message||e));
}
});
el('previewFilter').addEventListener('input', () => {
const filter = (el('previewFilter').value || '').toLowerCase();
if (!el('previewBox').textContent) return;
const lines = el('previewBox').textContent.split(/\r\n|\n/).filter(Boolean);
const shown = filter ? lines.filter(l => l.includes(filter)) : lines;
el('previewBox').textContent = shown.slice(0, Math.max(50, parseInt(el('previewLimit').value||'50'))).join('\n');
});
el('fileInput').addEventListener('change', () => {
el('statLines').textContent = '0';
el('statUnique').textContent = '0';
el('statDomains').textContent = '0';
el('patternsTable').querySelector('tbody').innerHTML = '';
el('domainsTable').querySelector('tbody').innerHTML = '';
el('previewBox').textContent = '';
el('logBox').textContent = '';
state.analyzed = false;
if (lastGeneratedBlobURL) { URL.revokeObjectURL(lastGeneratedBlobURL); lastGeneratedBlobURL = null; el('downloadBtn').disabled = true; }
log('Новый файл выбран, предыдущий анализ отменён.');
});
log('Готов к работе. Загрузите файл и нажмите "Проанализировать файл".');
})();
</script>
</body>
</html>