enotkrutoy commited on
Commit
5226d85
·
verified ·
1 Parent(s): 07b5f43

Update index.html

Browse files
Files changed (1) hide show
  1. 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
- <label style="margin-top:10px">3) Ручной ввод слов-источников (опционально)</label>
341
- <div class="small">Если оставить пустыми, имена/фамилии будут извлечены из датасета</div>
342
- <label>Имена (через запятую)</label>
343
- <input id="firstSeeds" placeholder="daniel,anna,roman,kevin..." />
344
- <label>Фамилии (через запятую)</label>
345
- <input id="lastSeeds" placeholder="medved,schmidt,pirker..." />
346
- <label>Ники / популярные слова (через запятую)</label>
347
- <input id="nickSeeds" placeholder="shadow,ranger,stazzor,aligator..." />
 
 
 
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="display:none;margin-top:10px">
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>