Spaces:
Running
Running
Update index.html
Browse files- index.html +813 -14
index.html
CHANGED
|
@@ -296,6 +296,19 @@
|
|
| 296 |
gap: 6px;
|
| 297 |
}
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
@media (max-width: 980px) {
|
| 300 |
.grid {
|
| 301 |
grid-template-columns: 1fr;
|
|
@@ -337,14 +350,17 @@
|
|
| 337 |
<label class="switch"><input id="optExtractNames" type="checkbox" checked /> <span class="small">Экстракт имен/фамилий</span></label>
|
| 338 |
</div>
|
| 339 |
|
| 340 |
-
<
|
| 341 |
-
|
| 342 |
-
<
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
<
|
| 346 |
-
|
| 347 |
-
<
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
<div class="section-title">4) Кастомные шаблоны</div>
|
| 350 |
<div id="customPatternsContainer"></div>
|
|
@@ -356,6 +372,7 @@
|
|
| 356 |
<div class="controls" style="margin-top:10px">
|
| 357 |
<button id="analyzeBtn" class="btn alt">Анализировать файл</button>
|
| 358 |
<button id="resetBtn" class="btn alt">Сбросить</button>
|
|
|
|
| 359 |
</div>
|
| 360 |
|
| 361 |
<div class="stats" style="margin-top:12px">
|
|
@@ -402,14 +419,14 @@
|
|
| 402 |
<option value="auto">Авто (из файла)</option>
|
| 403 |
<option value="local">Локальные (gmx.at, aon.at...)</option>
|
| 404 |
<option value="global">Global (gmail,yahoo,outlook)</option>
|
| 405 |
-
<option value="custom">Жестко заданный домен</option>
|
| 406 |
</select>
|
| 407 |
</div>
|
| 408 |
</div>
|
| 409 |
|
| 410 |
-
<div id="customDomainContainer" style="
|
| 411 |
<label>Жестко заданный домен</label>
|
| 412 |
-
<input id="customDomain" type="text" placeholder="example.com" />
|
| 413 |
<div class="small muted">Все сгенерированные логины будут использовать этот домен</div>
|
| 414 |
</div>
|
| 415 |
|
|
@@ -512,7 +529,8 @@ let analysisState = {
|
|
| 512 |
let customPatterns = [
|
| 513 |
{id: 1, name: 'first.last', template: '{first}.{last}', enabled: true},
|
| 514 |
{id: 2, name: 'firstlast', template: '{first}{last}', enabled: true},
|
| 515 |
-
{id: 3, name: 'first_digit', template: '{first}{digit}', enabled: true}
|
|
|
|
| 516 |
];
|
| 517 |
|
| 518 |
let doneUsernames = new Set();
|
|
@@ -554,9 +572,9 @@ const exportDone = document.getElementById('exportDone');
|
|
| 554 |
const analyzeProgress = fileProgress;
|
| 555 |
const firstSeeds = document.getElementById('firstSeeds');
|
| 556 |
const lastSeeds = document.getElementById('lastSeeds');
|
| 557 |
-
const nickSeeds = document.getElementById('nickSeeds');
|
| 558 |
const addPatternBtn = document.getElementById('addPatternBtn');
|
| 559 |
const customPatternsContainer = document.getElementById('customPatternsContainer');
|
|
|
|
| 560 |
|
| 561 |
/* ---------- Custom Patterns UI ---------- */
|
| 562 |
function renderCustomPatterns() {
|
|
@@ -567,4 +585,785 @@ function renderCustomPatterns() {
|
|
| 567 |
patternEl.className = 'custom-pattern';
|
| 568 |
patternEl.innerHTML = `
|
| 569 |
<div class="pattern-header">
|
| 570 |
-
<div class="pattern-name">${pattern.name}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
gap: 6px;
|
| 297 |
}
|
| 298 |
|
| 299 |
+
.preset-section {
|
| 300 |
+
background: rgba(255, 255, 255, 0.03);
|
| 301 |
+
padding: 12px;
|
| 302 |
+
border-radius: 8px;
|
| 303 |
+
margin: 12px 0;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.preset-title {
|
| 307 |
+
font-weight: 600;
|
| 308 |
+
margin-bottom: 8px;
|
| 309 |
+
color: var(--accent);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
@media (max-width: 980px) {
|
| 313 |
.grid {
|
| 314 |
grid-template-columns: 1fr;
|
|
|
|
| 350 |
<label class="switch"><input id="optExtractNames" type="checkbox" checked /> <span class="small">Экстракт имен/фамилий</span></label>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
+
<div class="section-title">3) Предустановки</div>
|
| 354 |
+
|
| 355 |
+
<div class="preset-section">
|
| 356 |
+
<div class="preset-title">Немецкие фамилии</div>
|
| 357 |
+
<textarea id="lastSeeds" style="height:120px">müller,schmidt,schneider,fischer,weber,meyer,wagner,becker,schulz,hoffmann,schäfer,koch,bauer,richter,klein,wolf,schröder,neumann,schwarz,braun,hofmann,zimmermann,schmitt,hartmann,krüger,schmid,weiss,scholz,maier,köhler,herrmann,lange,schulte,krause,meier,lehmann,schubert,kühn,vogel,peters,fritz</textarea>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
<div class="preset-section">
|
| 361 |
+
<div class="preset-title">Немецкие имена</div>
|
| 362 |
+
<textarea id="firstSeeds" style="height:120px">max,anna,thomas,maria,michael,sabine,andreas,julia,stefan,sandra,peter,christine,klaus,angelika,wolfgang,monika,jürgen,petra,frank,birgit,hans,uta,ralf,susanne,karl,elke,uwe,kirsten,bernd,heike,lukas,sarah,martin,katrin,christoph,nicole,dirk,johanna,rainer,diana,marcus,sylvia,matthias,nina,jan,simone,alexander,claudia,daniel,corinna,stefanie,andrea,patrick,tanja,christian,jessica,oliver,melanie,marcel,anja,tobias,jana,manuel,sabrina,philipp,carina,marco,lena,christina,alexandra,florian,miriam,bastian,nadia,dennis,verena,serdar,jennifer,tim,sonja,rene,antje,mario,silke,dominik,bianca,eric,nadine,fabian,yvonne,kai,ramona,steffen,ines,marius,elena,kristian,patricia,robert,svenja,sebastian,jennifer,nicolas,anika,jens,irene,timo,maren,jörg,eva,volker,anke,heiko,annette,maik,susann,torsten,dagmar,ingo,katja,udo,regina,harald,ilona,lothar,gabriele,gerhard,ute,dieter,brigitte,walter,helga,bernhard,ursula,hermann,elisabeth,kurt,margrit,alfred,gisela,heinz,renate,ernst,hildegard,werner,ingrid,günther,christel,karl-heinz,marianne,franz,barbara,hugo,elke,fritz,anneliese</textarea>
|
| 363 |
+
</div>
|
| 364 |
|
| 365 |
<div class="section-title">4) Кастомные шаблоны</div>
|
| 366 |
<div id="customPatternsContainer"></div>
|
|
|
|
| 372 |
<div class="controls" style="margin-top:10px">
|
| 373 |
<button id="analyzeBtn" class="btn alt">Анализировать файл</button>
|
| 374 |
<button id="resetBtn" class="btn alt">Сбросить</button>
|
| 375 |
+
<button id="applyPresetsBtn" class="btn">Применить предустановки</button>
|
| 376 |
</div>
|
| 377 |
|
| 378 |
<div class="stats" style="margin-top:12px">
|
|
|
|
| 419 |
<option value="auto">Авто (из файла)</option>
|
| 420 |
<option value="local">Локальные (gmx.at, aon.at...)</option>
|
| 421 |
<option value="global">Global (gmail,yahoo,outlook)</option>
|
| 422 |
+
<option value="custom" selected>Жестко заданный домен</option>
|
| 423 |
</select>
|
| 424 |
</div>
|
| 425 |
</div>
|
| 426 |
|
| 427 |
+
<div id="customDomainContainer" style="margin-top:10px">
|
| 428 |
<label>Жестко заданный домен</label>
|
| 429 |
+
<input id="customDomain" type="text" value="gmx.de" placeholder="example.com" />
|
| 430 |
<div class="small muted">Все сгенерированные логины будут использовать этот домен</div>
|
| 431 |
</div>
|
| 432 |
|
|
|
|
| 529 |
let customPatterns = [
|
| 530 |
{id: 1, name: 'first.last', template: '{first}.{last}', enabled: true},
|
| 531 |
{id: 2, name: 'firstlast', template: '{first}{last}', enabled: true},
|
| 532 |
+
{id: 3, name: 'first_digit', template: '{first}{digit}', enabled: true},
|
| 533 |
+
{id: 4, name: 'first.last.year', template: '{first}.{last}{year}', enabled: true}
|
| 534 |
];
|
| 535 |
|
| 536 |
let doneUsernames = new Set();
|
|
|
|
| 572 |
const analyzeProgress = fileProgress;
|
| 573 |
const firstSeeds = document.getElementById('firstSeeds');
|
| 574 |
const lastSeeds = document.getElementById('lastSeeds');
|
|
|
|
| 575 |
const addPatternBtn = document.getElementById('addPatternBtn');
|
| 576 |
const customPatternsContainer = document.getElementById('customPatternsContainer');
|
| 577 |
+
const applyPresetsBtn = document.getElementById('applyPresetsBtn');
|
| 578 |
|
| 579 |
/* ---------- Custom Patterns UI ---------- */
|
| 580 |
function renderCustomPatterns() {
|
|
|
|
| 585 |
patternEl.className = 'custom-pattern';
|
| 586 |
patternEl.innerHTML = `
|
| 587 |
<div class="pattern-header">
|
| 588 |
+
<div class="pattern-name">${pattern.name}</div>
|
| 589 |
+
<div class="pattern-enabled">
|
| 590 |
+
<input type="checkbox" data-id="${pattern.id}" ${pattern.enabled ? 'checked' : ''}>
|
| 591 |
+
<span class="small">Вкл</span>
|
| 592 |
+
</div>
|
| 593 |
+
</div>
|
| 594 |
+
<div class="pattern-template">${pattern.template}</div>
|
| 595 |
+
<div class="pattern-controls">
|
| 596 |
+
<button class="btn alt edit-pattern" data-id="${pattern.id}">Редактировать</button>
|
| 597 |
+
<button class="btn alt danger remove-pattern" data-id="${pattern.id}">Удалить</button>
|
| 598 |
+
</div>
|
| 599 |
+
`;
|
| 600 |
+
customPatternsContainer.appendChild(patternEl);
|
| 601 |
+
});
|
| 602 |
+
|
| 603 |
+
// Add event listeners
|
| 604 |
+
document.querySelectorAll('.edit-pattern').forEach(btn => {
|
| 605 |
+
btn.addEventListener('click', (e) => {
|
| 606 |
+
const id = parseInt(e.target.dataset.id);
|
| 607 |
+
editCustomPattern(id);
|
| 608 |
+
});
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
document.querySelectorAll('.remove-pattern').forEach(btn => {
|
| 612 |
+
btn.addEventListener('click', (e) => {
|
| 613 |
+
const id = parseInt(e.target.dataset.id);
|
| 614 |
+
removeCustomPattern(id);
|
| 615 |
+
});
|
| 616 |
+
});
|
| 617 |
+
|
| 618 |
+
document.querySelectorAll('.pattern-enabled input').forEach(checkbox => {
|
| 619 |
+
checkbox.addEventListener('change', (e) => {
|
| 620 |
+
const id = parseInt(e.target.dataset.id);
|
| 621 |
+
toggleCustomPattern(id, e.target.checked);
|
| 622 |
+
});
|
| 623 |
+
});
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
function addCustomPattern() {
|
| 627 |
+
const newPattern = {
|
| 628 |
+
id: Date.now(),
|
| 629 |
+
name: `pattern_${customPatterns.length + 1}`,
|
| 630 |
+
template: '{first}.{last}',
|
| 631 |
+
enabled: true
|
| 632 |
+
};
|
| 633 |
+
customPatterns.push(newPattern);
|
| 634 |
+
renderCustomPatterns();
|
| 635 |
+
editCustomPattern(newPattern.id);
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
function editCustomPattern(id) {
|
| 639 |
+
const pattern = customPatterns.find(p => p.id === id);
|
| 640 |
+
if (!pattern) return;
|
| 641 |
+
|
| 642 |
+
const newName = prompt('Название шаблона:', pattern.name);
|
| 643 |
+
if (newName === null) return;
|
| 644 |
+
|
| 645 |
+
const newTemplate = prompt('Шаблон (переменные: {first} {last} {nick} {digit} {year}):', pattern.template);
|
| 646 |
+
if (newTemplate === null) return;
|
| 647 |
+
|
| 648 |
+
pattern.name = newName;
|
| 649 |
+
pattern.template = newTemplate;
|
| 650 |
+
renderCustomPatterns();
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
function removeCustomPattern(id) {
|
| 654 |
+
if (customPatterns.length <= 1) {
|
| 655 |
+
alert('Нельзя удалить последний шаблон');
|
| 656 |
+
return;
|
| 657 |
+
}
|
| 658 |
+
customPatterns = customPatterns.filter(p => p.id !== id);
|
| 659 |
+
renderCustomPatterns();
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
function toggleCustomPattern(id, enabled) {
|
| 663 |
+
const pattern = customPatterns.find(p => p.id === id);
|
| 664 |
+
if (pattern) {
|
| 665 |
+
pattern.enabled = enabled;
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
/* ---------- Event Listeners ---------- */
|
| 670 |
+
domainPreset.addEventListener('change', () => {
|
| 671 |
+
customDomainContainer.style.display = domainPreset.value === 'custom' ? 'block' : 'none';
|
| 672 |
+
});
|
| 673 |
+
|
| 674 |
+
addPatternBtn.addEventListener('click', addCustomPattern);
|
| 675 |
+
|
| 676 |
+
applyPresetsBtn.addEventListener('click', () => {
|
| 677 |
+
// Apply presets to pools
|
| 678 |
+
if (firstSeeds.value && !firstPoolTA.value.trim()) {
|
| 679 |
+
firstPoolTA.value = firstSeeds.value;
|
| 680 |
+
}
|
| 681 |
+
if (lastSeeds.value && !lastPoolTA.value.trim()) {
|
| 682 |
+
lastPoolTA.value = lastSeeds.value;
|
| 683 |
+
}
|
| 684 |
+
alert('Предустановки применены к пулам имен и фамилий');
|
| 685 |
+
});
|
| 686 |
+
|
| 687 |
+
/* ---------- File parsing & analysis (chunked with worker) ---------- */
|
| 688 |
+
function resetAnalysis(){
|
| 689 |
+
analysisState = {
|
| 690 |
+
totalLines:0, uniqueLocal:0, domainCounts:{},
|
| 691 |
+
patternCounts:{fn_dot_ln:0,fi_dot_ln:0,fnln:0,fn_digits:0,nick_digits:0,pure_nick:0,other:0},
|
| 692 |
+
suffixCounts:{}, nameCandidates:{first:{},last:{}}, domainMap:{}
|
| 693 |
+
};
|
| 694 |
+
patternList.innerHTML = '';
|
| 695 |
+
statTotal.textContent = '0';
|
| 696 |
+
statUnique.textContent = '0';
|
| 697 |
+
statTopDomain.textContent = '—';
|
| 698 |
+
statPatterns.textContent = '—';
|
| 699 |
+
outList.textContent = '';
|
| 700 |
+
lastGenerated = [];
|
| 701 |
+
fileProgress.style.width = '0%';
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
resetBtn.addEventListener('click', ()=>{
|
| 705 |
+
resetAnalysis();
|
| 706 |
+
fileInput.value='';
|
| 707 |
+
doneFileInput.value='';
|
| 708 |
+
settingsFileInput.value='';
|
| 709 |
+
doneUsernames.clear();
|
| 710 |
+
doneCount.textContent = 'Загружено: 0 логинов';
|
| 711 |
+
});
|
| 712 |
+
|
| 713 |
+
analyzeBtn.addEventListener('click', ()=>{
|
| 714 |
+
const file = fileInput.files && fileInput.files[0];
|
| 715 |
+
if(!file){
|
| 716 |
+
alert('Выберите .txt фай�� с логинами первым.');
|
| 717 |
+
return;
|
| 718 |
+
}
|
| 719 |
+
resetAnalysis();
|
| 720 |
+
analyzeFileChunked(file);
|
| 721 |
+
});
|
| 722 |
+
|
| 723 |
+
function analyzeFileChunked(file){
|
| 724 |
+
const workerCode = `self.onmessage = function(ev){
|
| 725 |
+
const {action, chunk, eof} = ev.data;
|
| 726 |
+
if(action === 'analyzeChunk'){
|
| 727 |
+
// chunk: string (portion of file)
|
| 728 |
+
// We'll split by newlines, extract emails and emit local-parts + domains + pattern counts + suffix detection
|
| 729 |
+
const lines = chunk.split(/\\r?\\n/).map(l=>l.trim()).filter(Boolean);
|
| 730 |
+
const domainCounts = {}; const localSet = new Set();
|
| 731 |
+
const patternCounts = {fn_dot_ln:0, fi_dot_ln:0, fnln:0, fn_digits:0, nick_digits:0, pure_nick:0, other:0};
|
| 732 |
+
const suffixCounts = {};
|
| 733 |
+
const nameCandidates = {first:{},last:{}};
|
| 734 |
+
const domainMap = {};
|
| 735 |
+
for(const raw of lines){
|
| 736 |
+
try{
|
| 737 |
+
const lower = raw.toLowerCase();
|
| 738 |
+
if(!lower.includes('@')) continue;
|
| 739 |
+
const [local, domain] = lower.split('@');
|
| 740 |
+
if(!local) continue;
|
| 741 |
+
localSet.add(local);
|
| 742 |
+
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
|
| 743 |
+
// pattern detection (simple heuristics)
|
| 744 |
+
// fn.ln or fn.lnNN
|
| 745 |
+
if(/^[a-z]+\\.[a-z]+\\d*$/.test(local)){
|
| 746 |
+
patternCounts.fn_dot_ln++;
|
| 747 |
+
const parts = local.split('.');
|
| 748 |
+
if(parts.length>=2){
|
| 749 |
+
const fn = parts[0].replace(/\\d+$/,''); const ln = parts.slice(1).join('.').replace(/\\d+$/,'');
|
| 750 |
+
if(fn) nameCandidates.first[fn] = (nameCandidates.first[fn]||0)+1;
|
| 751 |
+
if(ln) nameCandidates.last[ln] = (nameCandidates.last[ln]||0)+1;
|
| 752 |
+
}
|
| 753 |
+
} else if(/^[a-z]\\.[a-z]+\\d*$/.test(local)){
|
| 754 |
+
patternCounts.fi_dot_ln++;
|
| 755 |
+
} else if(/^[a-z]+[a-z]+\\d*$/.test(local) && /[0-9]/.test(local) && /[a-z]/.test(local)){
|
| 756 |
+
// letters + digits mixed
|
| 757 |
+
// differentiate nick_digits vs fn_digits heuristics by presence of dot or underscore earlier (we checked)
|
| 758 |
+
patternCounts.fn_digits++;
|
| 759 |
+
} else if(/^[a-z]+\\d+$/.test(local)){
|
| 760 |
+
patternCounts.nick_digits++;
|
| 761 |
+
} else if(/^[a-z]+$/.test(local)){
|
| 762 |
+
patternCounts.pure_nick++;
|
| 763 |
+
// candidate could be either first or last; increment in both maps for possible extraction
|
| 764 |
+
nameCandidates.first[local] = (nameCandidates.first[local]||0)+1;
|
| 765 |
+
nameCandidates.last[local] = (nameCandidates.last[local]||0)+1;
|
| 766 |
+
} else {
|
| 767 |
+
patternCounts.other++;
|
| 768 |
+
}
|
| 769 |
+
// suffix extraction (numbers at end)
|
| 770 |
+
const m = local.match(/(\\d{1,8})$/);
|
| 771 |
+
if(m){
|
| 772 |
+
const suf = m[1];
|
| 773 |
+
suffixCounts[suf] = (suffixCounts[suf]||0)+1;
|
| 774 |
+
}
|
| 775 |
+
domainMap[domain] = (domainMap[domain]||0)+1;
|
| 776 |
+
}catch(e){/*ignore per-line errors*/ }
|
| 777 |
+
}
|
| 778 |
+
// respond with partial results
|
| 779 |
+
self.postMessage({action:'chunkResult',domainCounts,patternCounts,localCount: localSet.size,suffixCounts,nameCandidates,domainMap});
|
| 780 |
+
if(eof) self.postMessage({action:'done'});
|
| 781 |
+
}
|
| 782 |
+
};`;
|
| 783 |
+
|
| 784 |
+
const workerBlob = new Blob([workerCode], {type:'application/javascript'});
|
| 785 |
+
const workerUrl = URL.createObjectURL(workerBlob);
|
| 786 |
+
const worker = new Worker(workerUrl);
|
| 787 |
+
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB chunk
|
| 788 |
+
let offset = 0;
|
| 789 |
+
let partial = '';
|
| 790 |
+
const reader = new FileReader();
|
| 791 |
+
|
| 792 |
+
worker.onmessage = function(ev){
|
| 793 |
+
const data = ev.data;
|
| 794 |
+
if(data.action === 'chunkResult'){
|
| 795 |
+
// merge into analysisState
|
| 796 |
+
mergeCounts(analysisState, data);
|
| 797 |
+
updateStatsUI();
|
| 798 |
+
} else if(data.action === 'done'){
|
| 799 |
+
// finalize
|
| 800 |
+
finalizeAnalysis();
|
| 801 |
+
worker.terminate();
|
| 802 |
+
URL.revokeObjectURL(workerUrl);
|
| 803 |
+
}
|
| 804 |
+
};
|
| 805 |
+
|
| 806 |
+
reader.onerror = err => {
|
| 807 |
+
alert('Ошибка чтения файла: '+ err);
|
| 808 |
+
worker.terminate();
|
| 809 |
+
URL.revokeObjectURL(workerUrl);
|
| 810 |
+
};
|
| 811 |
+
|
| 812 |
+
reader.onload = function(e){
|
| 813 |
+
try{
|
| 814 |
+
let text = e.target.result;
|
| 815 |
+
// prepend partial leftover
|
| 816 |
+
text = partial + text;
|
| 817 |
+
// try to keep last line partial if file continues
|
| 818 |
+
const lastNewline = text.lastIndexOf('\n');
|
| 819 |
+
let chunkToSend = text;
|
| 820 |
+
if(lastNewline !== -1 && offset + CHUNK_SIZE < file.size){
|
| 821 |
+
chunkToSend = text.slice(0, lastNewline+1);
|
| 822 |
+
partial = text.slice(lastNewline+1);
|
| 823 |
+
} else { // final chunk or small file
|
| 824 |
+
partial = '';
|
| 825 |
+
}
|
| 826 |
+
const eof = (offset + CHUNK_SIZE) >= file.size;
|
| 827 |
+
worker.postMessage({action:'analyzeChunk', chunk:chunkToSend, eof});
|
| 828 |
+
offset += CHUNK_SIZE;
|
| 829 |
+
// update progress
|
| 830 |
+
const pct = Math.min(100, Math.round((offset / file.size) * 100));
|
| 831 |
+
fileProgress.style.width = pct + '%';
|
| 832 |
+
if(offset < file.size){
|
| 833 |
+
readSlice();
|
| 834 |
+
} else {
|
| 835 |
+
// done reading
|
| 836 |
+
}
|
| 837 |
+
}catch(err){
|
| 838 |
+
console.error(err);
|
| 839 |
+
worker.terminate();
|
| 840 |
+
URL.revokeObjectURL(workerUrl);
|
| 841 |
+
alert('Ошибка обработки чанка: '+err);
|
| 842 |
+
}
|
| 843 |
+
};
|
| 844 |
+
|
| 845 |
+
function readSlice(){
|
| 846 |
+
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
| 847 |
+
reader.readAsText(slice);
|
| 848 |
+
}
|
| 849 |
+
// start
|
| 850 |
+
readSlice();
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
/* merge worker results into analysisState */
|
| 854 |
+
function mergeCounts(state, data){
|
| 855 |
+
// domains
|
| 856 |
+
for(const d in data.domainCounts){
|
| 857 |
+
state.domainCounts[d] = (state.domainCounts[d] || 0) + data.domainCounts[d];
|
| 858 |
+
}
|
| 859 |
+
// patterns
|
| 860 |
+
for(const k in state.patternCounts){
|
| 861 |
+
state.patternCounts[k] = (state.patternCounts[k] || 0) + (data.patternCounts[k] || 0);
|
| 862 |
+
}
|
| 863 |
+
// suffixes
|
| 864 |
+
for(const s in data.suffixCounts){
|
| 865 |
+
state.suffixCounts[s] = (state.suffixCounts[s] || 0) + data.suffixCounts[s];
|
| 866 |
+
}
|
| 867 |
+
// names
|
| 868 |
+
['first','last'].forEach(kind=>{
|
| 869 |
+
const cand = data.nameCandidates?.[kind] || {};
|
| 870 |
+
for(const nm in cand){
|
| 871 |
+
state.nameCandidates[kind][nm] = (state.nameCandidates[kind][nm]||0) + cand[nm];
|
| 872 |
+
}
|
| 873 |
+
});
|
| 874 |
+
// unique local count approximation (we sum partial unique counts, but we will recompute accurately later if needed)
|
| 875 |
+
state.uniqueLocal += data.localCount || 0;
|
| 876 |
+
// domainMap
|
| 877 |
+
for(const d in data.domainMap){
|
| 878 |
+
state.domainMap[d] = (state.domainMap[d] || 0) + data.domainMap[d];
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/* update compact UI */
|
| 883 |
+
function updateStatsUI(){
|
| 884 |
+
const total = Object.values(analysisState.domainMap).reduce((a,b)=>a+b,0);
|
| 885 |
+
statTotal.textContent = total || '0';
|
| 886 |
+
statUnique.textContent = analysisState.uniqueLocal || '0';
|
| 887 |
+
// top domain
|
| 888 |
+
const domainEntries = Object.entries(analysisState.domainCounts||{}).sort((a,b)=>b[1]-a[1]);
|
| 889 |
+
statTopDomain.textContent = domainEntries.length ? `${domainEntries[0][0]} (${domainEntries[0][1]})` : '—';
|
| 890 |
+
// patterns summary
|
| 891 |
+
const pc = analysisState.patternCounts;
|
| 892 |
+
const sumP = Object.values(pc).reduce((a,b)=>a+b,0) || 0;
|
| 893 |
+
statPatterns.textContent = sumP ? Object.entries(pc).map(([k,v])=>`${k}:${v}`).join(' | ') : '—';
|
| 894 |
+
// render pattern list interactive
|
| 895 |
+
renderPatternList(pc);
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
/* render interactive pattern list with checkboxes and weight sliders */
|
| 899 |
+
function renderPatternList(patternCounts){
|
| 900 |
+
patternList.innerHTML = '';
|
| 901 |
+
const total = Object.values(patternCounts).reduce((a,b)=>a+b,0) || 1;
|
| 902 |
+
for(const [k,v] of Object.entries(patternCounts)){
|
| 903 |
+
const pct = Math.round((v/total)*100);
|
| 904 |
+
const item = document.createElement('div');
|
| 905 |
+
item.className = 'pattern-item';
|
| 906 |
+
item.innerHTML = `
|
| 907 |
+
<div style="display:flex;gap:10px;align-items:center">
|
| 908 |
+
<input type="checkbox" data-pattern="${k}" checked />
|
| 909 |
+
<div style="min-width:120px"><strong>${k}</strong></div>
|
| 910 |
+
<div class="small muted">${v} hits</div>
|
| 911 |
+
</div>
|
| 912 |
+
<div style="width:40%">
|
| 913 |
+
<div style="height:8px;background:rgba(255,255,255,0.03);border-radius:6px;overflow:hidden">
|
| 914 |
+
<div class="bar" style="width:${pct}%;"></div>
|
| 915 |
+
</div>
|
| 916 |
+
</div>
|
| 917 |
+
`;
|
| 918 |
+
patternList.appendChild(item);
|
| 919 |
+
}
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
/* finalize analysis: compute derived pools */
|
| 923 |
+
function finalizeAnalysis(){
|
| 924 |
+
// compute normalized name pools from analysisState.nameCandidates (top N)
|
| 925 |
+
const firsts = Object.entries(analysisState.nameCandidates.first || {}).sort((a,b)=>b[1]-a[1]).slice(0,200).map(x=>normalizeStr(x[0]));
|
| 926 |
+
const lasts = Object.entries(analysisState.nameCandidates.last || {}).sort((a,b)=>b[1]-a[1]).slice(0,200).map(x=>normalizeStr(x[0]));
|
| 927 |
+
// put into textareas only if they are empty (user may override)
|
| 928 |
+
if(!firstPoolTA.value.trim()){
|
| 929 |
+
firstPoolTA.value = firsts.join('\n');
|
| 930 |
+
}
|
| 931 |
+
if(!lastPoolTA.value.trim()){
|
| 932 |
+
lastPoolTA.value = lasts.join('\n');
|
| 933 |
+
}
|
| 934 |
+
// preset suffix common list
|
| 935 |
+
const topSuffixes = Object.entries(analysisState.suffixCounts || {}).sort((a,b)=>b[1]-a[1]).slice(0,20).map(x=>x[0]);
|
| 936 |
+
sufCommon.value = topSuffixes.slice(0,12).join(',');
|
| 937 |
+
updateStatsUI();
|
| 938 |
+
renderCustomPatterns();
|
| 939 |
+
alert('Анализ завершён. Проверьте автоматически заполненные пулы имён и фамилий. Отредактируйте при необходимости и нажмите "Генерировать".');
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
/* ---------- Done file handling ---------- */
|
| 943 |
+
doneFileInput.addEventListener('change', (e) => {
|
| 944 |
+
const file = e.target.files[0];
|
| 945 |
+
if (!file || !file.name.endsWith('.txt')) {
|
| 946 |
+
alert('Пожалуйста, выберите файл с расширением .txt');
|
| 947 |
+
return;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
const reader = new FileReader();
|
| 951 |
+
reader.onload = (event) => {
|
| 952 |
+
try {
|
| 953 |
+
const content = event.target.result;
|
| 954 |
+
const lines = content.split(/[\r\n]+/).map(line => line.trim()).filter(Boolean);
|
| 955 |
+
doneUsernames = new Set(lines);
|
| 956 |
+
doneCount.textContent = `Загружено: ${doneUsernames.size} логинов`;
|
| 957 |
+
alert(`Загружено ${doneUsernames.size} уже сгенерированных логинов для пропуска`);
|
| 958 |
+
} catch (err) {
|
| 959 |
+
alert('Ошибка при чтении файла done.txt: ' + err.message);
|
| 960 |
+
}
|
| 961 |
+
};
|
| 962 |
+
reader.readAsText(file);
|
| 963 |
+
});
|
| 964 |
+
|
| 965 |
+
/* ---------- Settings import/export ---------- */
|
| 966 |
+
settingsFileInput.addEventListener('change', (e) => {
|
| 967 |
+
const file = e.target.files[0];
|
| 968 |
+
if (!file || !file.name.endsWith('.json')) {
|
| 969 |
+
alert('Пожалуйста, выберите файл с расширением .json');
|
| 970 |
+
return;
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
const reader = new FileReader();
|
| 974 |
+
reader.onload = (event) => {
|
| 975 |
+
try {
|
| 976 |
+
const content = event.target.result;
|
| 977 |
+
const parsedSettings = JSON.parse(content);
|
| 978 |
+
|
| 979 |
+
if (parsedSettings.settings) {
|
| 980 |
+
// Restore settings
|
| 981 |
+
const settings = parsedSettings.settings;
|
| 982 |
+
optNormalize.checked = settings.normalize;
|
| 983 |
+
optExtractNames.checked = settings.extractNames;
|
| 984 |
+
firstSeeds.value = settings.firstSeeds || '';
|
| 985 |
+
lastSeeds.value = settings.lastSeeds || '';
|
| 986 |
+
genCountInput.value = settings.genCount || 200;
|
| 987 |
+
domainPreset.value = settings.domainPreset || 'auto';
|
| 988 |
+
sufCommon.value = settings.sufCommon || '';
|
| 989 |
+
sufYears.value = settings.sufYears || '';
|
| 990 |
+
firstPoolTA.value = settings.firstPool || '';
|
| 991 |
+
lastPoolTA.value = settings.lastPool || '';
|
| 992 |
+
customDomain.value = settings.customDomain || '';
|
| 993 |
+
|
| 994 |
+
// Restore analysis state if available
|
| 995 |
+
if (parsedSettings.analysisState) {
|
| 996 |
+
analysisState = parsedSettings.analysisState;
|
| 997 |
+
updateStatsUI();
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
// Restore custom patterns
|
| 1001 |
+
if (parsedSettings.customPatterns) {
|
| 1002 |
+
customPatterns = parsedSettings.customPatterns;
|
| 1003 |
+
renderCustomPatterns();
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
// Restore generated usernames
|
| 1007 |
+
if (parsedSettings.generatedUsernames) {
|
| 1008 |
+
lastGenerated = parsedSettings.generatedUsernames;
|
| 1009 |
+
renderOutput(lastGenerated);
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
// Restore done usernames
|
| 1013 |
+
if (parsedSettings.doneUsernames) {
|
| 1014 |
+
doneUsernames = new Set(parsedSettings.doneUsernames);
|
| 1015 |
+
doneCount.textContent = `Загружено: ${doneUsernames.size} логинов`;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
alert('Настройки успешно восстановлены!');
|
| 1019 |
+
} else {
|
| 1020 |
+
alert('Неверный формат файла настроек');
|
| 1021 |
+
}
|
| 1022 |
+
} catch (err) {
|
| 1023 |
+
alert('Ошибка при чтении файла настроек: ' + err.message);
|
| 1024 |
+
}
|
| 1025 |
+
};
|
| 1026 |
+
reader.readAsText(file);
|
| 1027 |
+
});
|
| 1028 |
+
|
| 1029 |
+
/* ---------- Generation logic ---------- */
|
| 1030 |
+
function getSelectedPatterns(){
|
| 1031 |
+
return Array.from(patternList.querySelectorAll('input[type="checkbox"]:checked')).map(cb=>cb.dataset.pattern);
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
function getDomainDistribution(){
|
| 1035 |
+
// If custom domain is set, use it
|
| 1036 |
+
if (domainPreset.value === 'custom' && customDomain.value.trim()) {
|
| 1037 |
+
const domain = customDomain.value.trim().toLowerCase();
|
| 1038 |
+
return { [domain]: 1 };
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
const preset = domainPreset.value;
|
| 1042 |
+
const domainCounts = analysisState.domainCounts || {};
|
| 1043 |
+
|
| 1044 |
+
if(preset === 'auto'){
|
| 1045 |
+
return normalizeDistribution(domainCounts);
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
// define some domain groups
|
| 1049 |
+
const local = ['gmx.at','aon.at','chello.at','liwest.at','inode.at','student.uibk.ac.at','proton.me','protonmail.com','medundmed.at','drei.at','tmo.at'];
|
| 1050 |
+
const global = ['gmail.com','yahoo.com','outlook.com','hotmail.com','live.com','googlemail.com','msn.com','ymail.com'];
|
| 1051 |
+
const dist = {};
|
| 1052 |
+
|
| 1053 |
+
if(preset === 'local'){
|
| 1054 |
+
local.forEach(d=>dist[d]=1);
|
| 1055 |
+
} else if(preset === 'global'){
|
| 1056 |
+
global.forEach(d=>dist[d]=1);
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
return normalizeDistribution(dist);
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
function normalizeDistribution(map){
|
| 1063 |
+
const m = {};
|
| 1064 |
+
const keys = Object.keys(map);
|
| 1065 |
+
if(!keys.length) return {'gmail.com':1};
|
| 1066 |
+
let total = 0;
|
| 1067 |
+
for(const k of keys){ m[k] = Number(map[k]||0); total += m[k]; }
|
| 1068 |
+
if(total === 0){
|
| 1069 |
+
// fallback: equal weights
|
| 1070 |
+
keys.forEach(k=>m[k]=1);
|
| 1071 |
+
total = keys.length;
|
| 1072 |
+
}
|
| 1073 |
+
// return normalized weights (not necessary but keep numbers)
|
| 1074 |
+
return m;
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
/* parse pools */
|
| 1078 |
+
function parsePool(text){
|
| 1079 |
+
if(!text) return [];
|
| 1080 |
+
const arr = text.split(/[\n,;]+/).map(s=>normalizeStr(s)).filter(Boolean);
|
| 1081 |
+
return uniq(arr);
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
/* build pattern application functions */
|
| 1085 |
+
function buildGenerators(selectedPatterns, firstPool, lastPool, nickPool, suffixList, yearRange, domainWeights, customPatterns){
|
| 1086 |
+
const gens = [];
|
| 1087 |
+
// helper small funcs
|
| 1088 |
+
const rnd = arr => arr[Math.floor(Math.random()*arr.length)];
|
| 1089 |
+
const pickDomain = ()=> sampleWeighted(domainWeights) || 'gmail.com';
|
| 1090 |
+
const pickSuffix = ()=> suffixList.length ? suffixList[Math.floor(Math.random()*suffixList.length)] : '';
|
| 1091 |
+
const pickYearSuffix = ()=>{
|
| 1092 |
+
if(!yearRange) return '';
|
| 1093 |
+
const [a,b] = yearRange;
|
| 1094 |
+
const y = a + Math.floor(Math.random()*(b-a+1));
|
| 1095 |
+
return String(y);
|
| 1096 |
+
};
|
| 1097 |
+
const maybeNum = ()=>{
|
| 1098 |
+
if(Math.random()<0.45){
|
| 1099 |
+
if(Math.random()<0.5) return pickSuffix();
|
| 1100 |
+
return pickYearSuffix();
|
| 1101 |
+
}
|
| 1102 |
+
return '';
|
| 1103 |
+
};
|
| 1104 |
+
|
| 1105 |
+
// Add custom pattern generators
|
| 1106 |
+
const enabledCustomPatterns = customPatterns.filter(p => p.enabled);
|
| 1107 |
+
enabledCustomPatterns.forEach(pattern => {
|
| 1108 |
+
gens.push(()=>{
|
| 1109 |
+
const first = rnd(firstPool) || 'john';
|
| 1110 |
+
const last = rnd(lastPool) || 'doe';
|
| 1111 |
+
const nick = rnd(nickPool) || first;
|
| 1112 |
+
const digit = Math.floor(Math.random() * 1000);
|
| 1113 |
+
const year = pickYearSuffix() || '1990';
|
| 1114 |
+
|
| 1115 |
+
let local = pattern.template
|
| 1116 |
+
.replace(/{first}/g, first)
|
| 1117 |
+
.replace(/{last}/g, last)
|
| 1118 |
+
.replace(/{nick}/g, nick)
|
| 1119 |
+
.replace(/{digit}/g, digit.toString())
|
| 1120 |
+
.replace(/{year}/g, year);
|
| 1121 |
+
|
| 1122 |
+
// Add optional suffix
|
| 1123 |
+
if(Math.random()<0.3){
|
| 1124 |
+
local += maybeNum();
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
return `${local}@${pickDomain()}`;
|
| 1128 |
+
});
|
| 1129 |
+
});
|
| 1130 |
+
|
| 1131 |
+
for(const p of selectedPatterns){
|
| 1132 |
+
if(p === 'fn_dot_ln'){
|
| 1133 |
+
gens.push(()=>{
|
| 1134 |
+
const f = rnd(firstPool); const l = rnd(lastPool);
|
| 1135 |
+
let local = `${f}.${l}`;
|
| 1136 |
+
if(Math.random()<0.4){ local += maybeNum(); }
|
| 1137 |
+
return `${local}@${pickDomain()}`;
|
| 1138 |
+
});
|
| 1139 |
+
} else if(p === 'fi_dot_ln'){
|
| 1140 |
+
gens.push(()=>{
|
| 1141 |
+
const f = rnd(firstPool); const l = rnd(lastPool);
|
| 1142 |
+
let local = `${f[0]}.${l}`;
|
| 1143 |
+
if(Math.random()<0.3){ local += maybeNum(); }
|
| 1144 |
+
return `${local}@${pickDomain()}`;
|
| 1145 |
+
});
|
| 1146 |
+
} else if(p === 'fnln'){
|
| 1147 |
+
gens.push(()=>{
|
| 1148 |
+
const f = rnd(firstPool); const l = rnd(lastPool);
|
| 1149 |
+
let local = `${f}${l}`;
|
| 1150 |
+
if(Math.random()<0.35){ local += maybeNum(); }
|
| 1151 |
+
return `${local}@${pickDomain()}`;
|
| 1152 |
+
});
|
| 1153 |
+
} else if(p === 'fn_digits' || p === 'nick_digits'){
|
| 1154 |
+
gens.push(()=>{
|
| 1155 |
+
const chooseNick = Math.random()<0.5;
|
| 1156 |
+
const base = chooseNick ? (rnd(nickPool)||rnd(firstPool)||'user') : (rnd(firstPool)+ (Math.random()<0.3?'.':'' ) + (rnd(lastPool)||''));
|
| 1157 |
+
const su = maybeNum() || pickSuffix();
|
| 1158 |
+
const local = base + su;
|
| 1159 |
+
return `${local}@${pickDomain()}`;
|
| 1160 |
+
});
|
| 1161 |
+
} else if(p === 'pure_nick'){
|
| 1162 |
+
gens.push(()=>{
|
| 1163 |
+
const base = rnd(nickPool) || rnd(firstPool) || 'user';
|
| 1164 |
+
const local = (Math.random()<0.35) ? (base + maybeNum()) : base;
|
| 1165 |
+
return `${local}@${pickDomain()}`;
|
| 1166 |
+
});
|
| 1167 |
+
} else {
|
| 1168 |
+
// fallback generic
|
| 1169 |
+
gens.push(()=>{
|
| 1170 |
+
const f = rnd(firstPool) || 'john'; const l = rnd(lastPool) || 'doe';
|
| 1171 |
+
let local = `${f}.${l}`;
|
| 1172 |
+
if(Math.random()<0.4) local += maybeNum();
|
| 1173 |
+
return `${local}@${pickDomain()}`;
|
| 1174 |
+
});
|
| 1175 |
+
}
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
// ensure at least one generator
|
| 1179 |
+
if(gens.length === 0 && enabledCustomPatterns.length === 0){
|
| 1180 |
+
gens.push(()=>{
|
| 1181 |
+
const f = rnd(firstPool) || 'john'; const l = rnd(lastPool) || 'doe';
|
| 1182 |
+
return `${f}.${l}@gmail.com`;
|
| 1183 |
+
});
|
| 1184 |
+
}
|
| 1185 |
+
return gens;
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
/* parse year range string like "1960-2005" */
|
| 1189 |
+
function parseYearRange(s){
|
| 1190 |
+
if(!s) return null;
|
| 1191 |
+
const m = s.match(/(\d{3,4})\s*-\s*(\d{3,4})/);
|
| 1192 |
+
if(m){
|
| 1193 |
+
const a = Math.max(1900, Number(m[1]));
|
| 1194 |
+
const b = Math.min(2100, Number(m[2]));
|
| 1195 |
+
if(a<=b) return [a,b];
|
| 1196 |
+
}
|
| 1197 |
+
return null;
|
| 1198 |
+
}
|
| 1199 |
+
|
| 1200 |
+
/* generator driver */
|
| 1201 |
+
function generateList(count, options = {}){
|
| 1202 |
+
const selectedPatterns = getSelectedPatterns();
|
| 1203 |
+
const enabledCustomPatterns = customPatterns.filter(p => p.enabled);
|
| 1204 |
+
|
| 1205 |
+
if(selectedPatterns.length === 0 && enabledCustomPatterns.length === 0){
|
| 1206 |
+
alert('Выберите хотя бы один паттерн для генерации.');
|
| 1207 |
+
return [];
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
const firstPool = parsePool(firstPoolTA.value) .length ? parsePool(firstPoolTA.value) : parsePool(firstSeeds.value);
|
| 1211 |
+
const lastPool = parsePool(lastPoolTA.value) .length ? parsePool(lastPoolTA.value) : parsePool(lastSeeds.value);
|
| 1212 |
+
// fallback: if pools empty, derive from analysis top candidates
|
| 1213 |
+
const fPool = firstPool.length ? firstPool : Object.keys(analysisState.nameCandidates.first || {}).slice(0,200).map(k=>normalizeStr(k));
|
| 1214 |
+
const lPool = lastPool.length ? lastPool : Object.keys(analysisState.nameCandidates.last || {}).slice(0,200).map(k=>normalizeStr(k));
|
| 1215 |
+
const domainWeights = getDomainDistribution();
|
| 1216 |
+
const suffixList = sufCommon.value ? uniq(sufCommon.value.split(/[\n,;]+/).map(s=>s.trim()).filter(Boolean)) : Object.keys(analysisState.suffixCounts||{}).slice(0,20);
|
| 1217 |
+
const yearRange = parseYearRange(sufYears.value) || [1970,2005];
|
| 1218 |
+
const gens = buildGenerators(selectedPatterns, fPool, lPool, fPool, suffixList, yearRange, domainWeights, customPatterns);
|
| 1219 |
+
|
| 1220 |
+
const out = new Set();
|
| 1221 |
+
// generation loop with dedup and safety cap
|
| 1222 |
+
const CAP = Math.max(count*10, 2000); // attempts cap to avoid infinite loops
|
| 1223 |
+
let attempts = 0;
|
| 1224 |
+
|
| 1225 |
+
while(out.size < count && attempts < CAP){
|
| 1226 |
+
attempts++;
|
| 1227 |
+
const genFunc = gens[Math.floor(Math.random()*gens.length)];
|
| 1228 |
+
try{
|
| 1229 |
+
const val = genFunc();
|
| 1230 |
+
if(val && typeof val === 'string' && !doneUsernames.has(val)){
|
| 1231 |
+
out.add(val);
|
| 1232 |
+
}
|
| 1233 |
+
}catch(e){ console.error('generator error', e); }
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
lastGenerated = Array.from(out);
|
| 1237 |
+
// show results
|
| 1238 |
+
renderOutput(lastGenerated);
|
| 1239 |
+
return lastGenerated;
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
/* email normalization: keep local part allowed characters and domain as is */
|
| 1243 |
+
function normalizeStrEmail(email){
|
| 1244 |
+
let parts = String(email).trim().toLowerCase().split('@');
|
| 1245 |
+
if(parts.length < 2) return email;
|
| 1246 |
+
let local = parts.slice(0,parts.length-1).join('@'); // in case local had @ (rare)
|
| 1247 |
+
const domain = parts[parts.length-1].trim();
|
| 1248 |
+
// replace diacritics in local
|
| 1249 |
+
local = local.replace(/[^ -~]/g, ch => DIACRIT_MAP[ch] || ch);
|
| 1250 |
+
// allowed chars for local: a-z0-9._-+ (we keep plus signs too as they appear in data)
|
| 1251 |
+
local = local.replace(/[^a-z0-9._\-+]/g,'');
|
| 1252 |
+
// avoid leading/trailing dot
|
| 1253 |
+
local = local.replace(/^\.*|\.*$/g,'');
|
| 1254 |
+
return local + '@' + domain;
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
/* render output */
|
| 1258 |
+
function renderOutput(list){
|
| 1259 |
+
outList.innerText = list.join('\n');
|
| 1260 |
+
}
|
| 1261 |
+
|
| 1262 |
+
/* ---------- UI events ---------- */
|
| 1263 |
+
previewBtn.addEventListener('click', ()=>{
|
| 1264 |
+
const res = generateList(Math.min(25, Number(genCountInput.value) || 25));
|
| 1265 |
+
alert('Предпросмотр: ' + res.length + ' записей сгенерировано (показаны в списке).');
|
| 1266 |
+
});
|
| 1267 |
+
|
| 1268 |
+
genBtn.addEventListener('click', ()=>{
|
| 1269 |
+
const cnt = Math.max(1, Number(genCountInput.value) || 100);
|
| 1270 |
+
generateList(cnt);
|
| 1271 |
+
alert('Генерация завершена: ' + lastGenerated.length + ' уникальных записей.');
|
| 1272 |
+
});
|
| 1273 |
+
|
| 1274 |
+
openSampleBtn.addEventListener('click', ()=>{
|
| 1275 |
+
// show sample from analysis if available
|
| 1276 |
+
const sample = Object.keys(analysisState.domainMap||{}).slice(0,10).map(d=>d+': '+(analysisState.domainMap[d]||0)).join('\n');
|
| 1277 |
+
alert('Top domains samples:\n' + sample);
|
| 1278 |
+
});
|
| 1279 |
+
|
| 1280 |
+
/* exports */
|
| 1281 |
+
function download(filename, text){
|
| 1282 |
+
const blob = new Blob([text], {type:'text/plain;charset=utf-8'});
|
| 1283 |
+
const a = document.createElement('a');
|
| 1284 |
+
a.href = URL.createObjectURL(blob);
|
| 1285 |
+
a.download = filename;
|
| 1286 |
+
document.body.appendChild(a); a.click();
|
| 1287 |
+
setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 100);
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
function getTimestamp() {
|
| 1291 |
+
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
function getFileNameBase() {
|
| 1295 |
+
const file = fileInput.files && fileInput.files[0];
|
| 1296 |
+
const timestamp = getTimestamp();
|
| 1297 |
+
return file ? `${file.name.replace('.txt', '')}_${timestamp}` : `usernames_${timestamp}`;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
exportTxt.addEventListener('click', ()=> {
|
| 1301 |
+
if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; }
|
| 1302 |
+
const fileNameBase = getFileNameBase();
|
| 1303 |
+
download(`${fileNameBase}_${lastGenerated.length}-lines.txt`, lastGenerated.join('\n'));
|
| 1304 |
+
});
|
| 1305 |
+
|
| 1306 |
+
exportCsv.addEventListener('click', ()=> {
|
| 1307 |
+
if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; }
|
| 1308 |
+
const fileNameBase = getFileNameBase();
|
| 1309 |
+
// simple CSV with column email
|
| 1310 |
+
const csv = 'email\n' + lastGenerated.map(e=>`"${e.replace(/"/g,'""')}"`).join('\n');
|
| 1311 |
+
download(`${fileNameBase}_${lastGenerated.length}-lines.csv`, csv);
|
| 1312 |
+
});
|
| 1313 |
+
|
| 1314 |
+
exportJson.addEventListener('click', ()=> {
|
| 1315 |
+
if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; }
|
| 1316 |
+
const fileNameBase = getFileNameBase();
|
| 1317 |
+
download(`${fileNameBase}_${lastGenerated.length}-lines.json`, JSON.stringify(lastGenerated, null, 2));
|
| 1318 |
+
});
|
| 1319 |
+
|
| 1320 |
+
exportSettings.addEventListener('click', ()=> {
|
| 1321 |
+
const fileNameBase = getFileNameBase();
|
| 1322 |
+
const settingsData = {
|
| 1323 |
+
settings: {
|
| 1324 |
+
normalize: optNormalize.checked,
|
| 1325 |
+
extractNames: optExtractNames.checked,
|
| 1326 |
+
firstSeeds: firstSeeds.value,
|
| 1327 |
+
lastSeeds: lastSeeds.value,
|
| 1328 |
+
genCount: genCountInput.value,
|
| 1329 |
+
domainPreset: domainPreset.value,
|
| 1330 |
+
sufCommon: sufCommon.value,
|
| 1331 |
+
sufYears: sufYears.value,
|
| 1332 |
+
firstPool: firstPoolTA.value,
|
| 1333 |
+
lastPool: lastPoolTA.value,
|
| 1334 |
+
customDomain: customDomain.value
|
| 1335 |
+
},
|
| 1336 |
+
analysisState: analysisState,
|
| 1337 |
+
customPatterns: customPatterns,
|
| 1338 |
+
generatedUsernames: lastGenerated,
|
| 1339 |
+
doneUsernames: Array.from(doneUsernames),
|
| 1340 |
+
timestamp: new Date().toISOString()
|
| 1341 |
+
};
|
| 1342 |
+
download(`${fileNameBase}_settings.json`, JSON.stringify(settingsData, null, 2));
|
| 1343 |
+
});
|
| 1344 |
+
|
| 1345 |
+
exportDone.addEventListener('click', ()=> {
|
| 1346 |
+
if(!lastGenerated.length){ alert('Нет сгенерированных данных.'); return; }
|
| 1347 |
+
const fileNameBase = getFileNameBase();
|
| 1348 |
+
download(`${fileNameBase}_done.txt`, lastGenerated.join('\n'));
|
| 1349 |
+
});
|
| 1350 |
+
|
| 1351 |
+
/* ---------- Helpers for UI and sanity ---------- */
|
| 1352 |
+
function safeParseInt(v, d){ const n = parseInt(v,10); return isNaN(n)? d : n; }
|
| 1353 |
+
|
| 1354 |
+
/* ---------- Initialize small defaults ---------- */
|
| 1355 |
+
(function initDefaults(){
|
| 1356 |
+
// prefill sufYears example
|
| 1357 |
+
sufYears.placeholder = 'Пример: 1960-2005';
|
| 1358 |
+
sufCommon.placeholder = 'Например: 007,123,84,2005';
|
| 1359 |
+
renderCustomPatterns();
|
| 1360 |
+
|
| 1361 |
+
// Set custom domain as default
|
| 1362 |
+
domainPreset.value = 'custom';
|
| 1363 |
+
customDomain.value = 'gmx.de';
|
| 1364 |
+
})();
|
| 1365 |
+
|
| 1366 |
+
/* ---------- End of script ---------- */
|
| 1367 |
+
</script>
|
| 1368 |
+
</body>
|
| 1369 |
+
</html>
|