Pomaga w weryfikacji anonsów Escort.club: łączy dane z Escorti.pl i garsoniera.com.pl, analizuje powiązane profile i recenzje, wykrywa rozbieżności oraz dodaje filtry, cache i obserwowanie anonsów.
// ==UserScript== // @name Escort.club Helper // @namespace escort-helper // @version 2.000 // @description Pomaga w weryfikacji anonsów Escort.club: łączy dane z Escorti.pl i garsoniera.com.pl, analizuje powiązane profile i recenzje, wykrywa rozbieżności oraz dodaje filtry, cache i obserwowanie anonsów. // @author Kut1234 // @copyright 2026, Kut1234 // @license GPL-3.0-only // @match https://*.escort.club/* // @match https://escorti.pl/* // @match https://www.escorti.pl/* // @match https://garsoniera.com.pl/forum/* // @match https://www.garsoniera.com.pl/forum/* // @match https://picdetective.com/* // @match https://www.picdetective.com/* // @match https://hivedetect.ai/* // @match https://www.hivedetect.ai/* // @resource escortRadiusLeafletJs https://unpkg.com/[email protected]/dist/leaflet.js // @resource escortRadiusLeafletCss https://unpkg.com/[email protected]/dist/leaflet.css // @grant GM_getResourceText // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant window.close // @grant GM_notification // @grant GM.setValue // @grant GM.getValue // @grant GM.deleteValue // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setClipboard // @grant unsafeWindow // @connect www.garsoniera.com.pl // @connect garsoniera.com.pl // @connect escort.club // @connect escorti.pl // @connect www.escorti.pl // @connect static.escort.club // @connect ktoto.info // @connect www.ktoto.info // @connect numerytelefonu.com // @connect www.numerytelefonu.com // @connect sprawdzenienumeru.pl // @connect www.sprawdzenienumeru.pl // @run-at document-end // ==/UserScript== (async function () { 'use strict'; const helperDialogStack=[]; const GARSO_BASE_URL='https://www.garsoniera.com.pl/forum/'; const SCRIPT_VERSION='2.000'; const GARSO_SEARCH_MIN_GAP_MS=7000; const GARSO_PHONE_SEARCH_CACHE_VERSION=6; const GARSO_EXTENDED_PHONE_SCAN_VERSION=5; const GARSO_SUMMARY_PARSER_VERSION=6; const GARSO_TOPIC_COUNT_CHECK_INTERVAL_MS=60 * 60 * 1000; const GARSO_TOPIC_BRIDGE_PREFIX='vm_garso_topic_bridge_'; const GARSO_TOPIC_REVISION_KEY='vm_garso_topic_analysis_revision'; const GARSO_SEARCH_GLOBAL_SLOT_KEY= 'vm_garso_global_search_slot'; const GARSO_SEARCH_REQUEST_TIMEOUT_MS=20000; const GARSO_SEARCH_GLOBAL_SLOT_LEASE_MS=GARSO_SEARCH_REQUEST_TIMEOUT_MS + 10000; const GARSO_SEARCH_TICKET_PREFIX='vm_garso_search_ticket_'; const GARSO_SEARCH_TICKET_TTL_MS=5000; const GARSO_SEARCH_NATIVE_UNTIL_KEY='vm_garso_native_search_until'; const GARSO_SEARCH_GLOBAL_SLOT_VERIFY_MS=40; const GARSO_SEARCH_GLOBAL_SLOT_SIGNAL_TIMEOUT_MS=1000; let garsoSearchPageClosed=false; const GARSO_SEARCH_INSTANCE_ID= `${Date.now()}-${Math.random().toString(36).slice(2)}`; const GARSO_TOPIC_FETCH_CONCURRENT=6; const GARSO_TOPIC_MIN_GAP_MS=0; const GARSO_TOPIC_MAX_GAP_MS=3000; const GARSO_TOPIC_GAP_STEP_UP_MS=1000; const GARSO_TOPIC_GAP_STEP_DOWN_MS=500; const GARSO_TOPIC_RECOVERY_SUCCESSES=4; const GARSO_TOPIC_LINKS_CACHE_TTL_MS=12 * 60 * 60 * 1000; const GARSO_TOPIC_PAGE_CACHE_PREFIX='vm_garso_topic_page_'; const GARSO_ANTIFLOOD_RETRY_LIMIT=3; const GARSO_REQUEST_MAX_ATTEMPTS=GARSO_ANTIFLOOD_RETRY_LIMIT + 1; const GARSO_OPEN_BRIDGE_PARAM='vm_garso_open'; const GARSO_OPEN_BRIDGE_KEY_PREFIX='vm_garso_open_bridge_'; const ESCORTI_BASE_URL='https://escorti.pl/'; const CURRENT_HOST=location.hostname.toLowerCase(); if (initReverseImageUrlPrefill()) return; function isEscortClubHost(host) { return /^(?:(?:[a-z]{2}|www)\.)?escort\.club$/i.test(String(host || '')); } function getEscortAdIdFromPath(pathname=location.pathname) { return String(pathname || '').match(/^\/(?:anons|personal-ads)\/(\d+)\.html\/?$/i)?.[1] || null; } function getEscortAdPath(adId,host=CURRENT_HOST) { if (!/^\d+$/.test(String(adId || ''))) return ''; const directory=/^(?:(?:www|en)\.)?escort\.club$/i.test(host) ? 'personal-ads':'anons'; return `/${directory}/${adId}.html`; } const IS_ESCORT_CLUB=isEscortClubHost(CURRENT_HOST); const ESCORT_CLUB_ORIGIN=IS_ESCORT_CLUB ? location.origin:'https://pl.escort.club'; if (/(^|\.)escort\.club$/i.test(CURRENT_HOST) && !IS_ESCORT_CLUB) return; const GARSO_MENU_ONLY_MODE=[ 'garsoniera.com.pl', 'www.garsoniera.com.pl' ].includes(CURRENT_HOST); const GARSO_TOPIC_WORKER_ID=GARSO_MENU_ONLY_MODE ? new URLSearchParams(location.hash.slice(1)).get('vm_garso_topic_worker') || '':''; function makeElement(tagName,className='',textContent=null,properties=null) { const element=document.createElement(tagName); if (className) element.className=className; if (textContent !== null) element.textContent=textContent; if (properties) Object.assign(element,properties); return element; } function makeButton(className='',textContent=null,properties=null) { return makeElement('button',className,textContent,{type:'button',...properties}); } function setControlTitle(element,title) { element.title=title; element.setAttribute('aria-label',title); } function stopUiEvent(event) { event.preventDefault(); event.stopPropagation(); } function showStatusText(element,text) { element.style.display='block'; element.textContent=text; } function setTrashIcon(button) { button.textContent=''; button.innerHTML= '<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" ' + 'style="display:block;margin:auto;fill:none;stroke:currentColor;stroke-width:2;' + 'stroke-linecap:round;stroke-linejoin:round">' + '<path d="M4 7h16"/>' + '<path d="M9 7V4h6v3"/>' + '<path d="M6.5 7l1 13h9l1-13"/>' + '<path d="M10 11v5M14 11v5"/>' + '</svg>'; } function makeFavoriteViewButton(view,onClick) { const tiles=view === 'tiles',button=makeButton('vm-ui-button vm-favorites-view-button'); const paths=tiles ? '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>' : '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M3 14h18M9 4v16"/>'; button.innerHTML=`<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">${paths}</svg>`; button.appendChild(makeElement('span','vft-action-label',tiles ? 'Kafelki':'Tabela')); setControlTitle(button,tiles ? 'Otwórz widok kafelkowy Ulubionych':'Otwórz tabelę Ulubionych'); button.addEventListener('click',onClick); return button; } function appendSelectOptions(select,options,selectedValue=null) { const selected=selectedValue == null ? null:String(selectedValue); for (const [value,label] of options) { const option=makeElement('option','',label,{value:String(value)}); if (selected !== null) option.selected=option.value === selected; select.appendChild(option); } return select; } function createAnimationFrameScheduler(callback) { let scheduled=false; return () => { if (scheduled) return; scheduled=true; requestAnimationFrame(() => { scheduled=false; callback(); }); }; } function createConcurrentJobQueue(maxConcurrent,errorLabel) { const queue=[]; const getLimit=typeof maxConcurrent === 'function' ? maxConcurrent:() => maxConcurrent; let activeJobs=0; const pump=() => { while (queue.length && activeJobs < getLimit()) { const job=queue.shift(); activeJobs++; Promise.resolve() .then(job) .catch(error => log(errorLabel,error)) .finally(() => { activeJobs--; pump(); }); } }; return job => { queue.push(job); pump(); }; } function parseGarsoAuth(html) { const hm=html.match(/ipb\.vars\[['"]secure_hash['"]\]\s*=\s*['"]([^'"]+)['"]/) || html.match(/name=["']secure_hash["']\s+value=["']([^"']+)["']/); const sm=html.match(/ipb\.vars\[['"]session_id['"]\]\s*=\s*['"]([^'"]+)['"]/); return hm && sm ? {secureHash:hm[1],sessionId:sm[1]}:null; } async function handleGarsoOpenBridgePage() { if (!GARSO_MENU_ONLY_MODE) return false; const pageUrl=new URL(location.href); const fragment=new URLSearchParams(pageUrl.hash.slice(1)); const token=pageUrl.searchParams.get(GARSO_OPEN_BRIDGE_PARAM) || fragment.get(GARSO_OPEN_BRIDGE_PARAM); const storageKey=GARSO_OPEN_BRIDGE_KEY_PREFIX + token; const pendingKey=GARSO_OPEN_BRIDGE_KEY_PREFIX + 'pending'; let payload=token ? await GM.getValue(storageKey,null):null; if (!token) {try {payload=JSON.parse(sessionStorage.getItem(pendingKey) || 'null');} catch {}} const cleanMarker=() => { if (!token) return; pageUrl.searchParams.delete(GARSO_OPEN_BRIDGE_PARAM); fragment.delete(GARSO_OPEN_BRIDGE_PARAM); pageUrl.hash=fragment.toString(); try {history.replaceState(history.state,'',pageUrl.href);} catch {} }; if (!payload?.term || Date.now() - Number(payload.createdAt || 0) > 5 * 60 * 1000) { if (token) deleteHelperStorageValue(storageKey); try {sessionStorage.removeItem(pendingKey);} catch {} cleanMarker(); return false; } const submit=() => { const inputs=document.querySelectorAll('input[name="search_term"], textarea[name="search_term"], #main_search, #search_term'); for (const input of inputs) { if (!('value' in input)) continue; input.value=payload.term; input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true})); } const andorType=payload.andorType === 'or' ? 'or':'and'; const matchSelect=document.querySelector('select[name="andor_type"]'); if (matchSelect) {matchSelect.value=andorType; matchSelect.dispatchEvent(new Event('change',{bubbles:true}));} if (!token) { try {sessionStorage.removeItem(pendingKey);} catch {} return; } cleanMarker(); const auth=parseGarsoAuth(document.documentElement.outerHTML || '') || payload; if (!auth.secureHash || !auth.sessionId) {deleteHelperStorageValue(storageKey); return;} try {sessionStorage.setItem(pendingKey,JSON.stringify({term:payload.term,andorType,createdAt:Date.now()}));} catch {} const form=makeElement('form','',null,{method:'POST',action:`${location.origin}/forum/index.php?app=core&module=search&do=search&fromMainBar=1&s=${encodeURIComponent(auth.sessionId)}`}); form.style.display='none'; for (const [name,value] of Object.entries({search_term:payload.term,andor_type:andorType,search_app:'forums',secure_hash:auth.secureHash,submit:'Szukaj'})) { const input=makeElement('input','',null,{type:'hidden',name,value}); form.appendChild(input); } document.body.appendChild(form); try {noteGarsoNativeSearch(); HTMLFormElement.prototype.submit.call(form);} catch {detachHelperNode(form); inputs[0]?.focus();} deleteHelperStorageValue(storageKey); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded',submit,{once:true}); else submit(); return !!token; } if (GARSO_MENU_ONLY_MODE) document.addEventListener('submit',event => { if (event.target?.querySelector('[name="search_term"]')) noteGarsoNativeSearch(); },true); const SETTINGS_STORAGE_KEY='vm_garso_settings'; const SETTINGS_ONBOARDING_SEEN_KEY='vm_garso_settings_onboarding_seen'; const WATCH_ITEMS_STORAGE_KEY='vm_garso_watch_items'; const WATCH_AD_DATA_MIRROR_STORAGE_KEY='vm_garso_watch_ad_data'; let hiddenWatchIndex=null; let watchedItemsReadCache=null; let helperSettingsReadCache=null; let watchedAdDataMirrorMemory=null; const helperRawSetValue=GM_setValue,helperRawDeleteValue=GM_deleteValue; function invalidateHelperReadCache(key) { if (key === SETTINGS_STORAGE_KEY) {helperSettingsReadCache=null; watchedItemsReadCache=null; hiddenWatchIndex=null;} if (key === WATCH_ITEMS_STORAGE_KEY) {watchedItemsReadCache=null; hiddenWatchIndex=null;} if (key === WATCH_AD_DATA_MIRROR_STORAGE_KEY) watchedAdDataMirrorMemory=null; } function writeHelperStorageValue(key,value) { invalidateHelperReadCache(key); return helperRawSetValue(key,value); } function deleteHelperStorageValue(key) { invalidateHelperReadCache(key); return helperRawDeleteValue(key); } if (await handleGarsoOpenBridgePage()) return; function readHelperSettingsSnapshot() { return helperSettingsReadCache ||= loadHelperSettingsFromStorage(); } const WATCH_EVENTS_STORAGE_KEY='vm_garso_watch_events'; const FAVORITE_TABLE_STATE_STORAGE_KEY='vm_garso_favorite_table_state'; const FAVORITE_LAST_ALERT_MODES_STORAGE_KEY= 'vm_garso_favorite_last_alert_modes'; const WATCH_CHECK_LOCK_KEY='vm_garso_watch_check_lock'; const SCRIPT_DATA_EXPORT_FORMAT='escort-helper-script-data'; const DIAGNOSTICS_STORAGE_KEY='vm_garso_diagnostics'; const DIAGNOSTICS_TAB_PREFIX=DIAGNOSTICS_STORAGE_KEY + '_tab_'; const DIAGNOSTICS_EPOCH_KEY=DIAGNOSTICS_STORAGE_KEY + '_epoch'; const DIAGNOSTICS_EXPORT_FORMAT='vm-garso-diagnostics-sanitized'; const DIAGNOSTICS_MAX_EVENTS=10; const DIAGNOSTICS_REQUEST_BUCKET_MS=60 * 1000; const DIAGNOSTICS_REQUEST_BUCKET_RETENTION_MS=25 * 60 * 60 * 1000; const DIAGNOSTICS_SAVE_DELAY_MS=5000; const ADAPTIVE_BACKOFF_STORAGE_KEY= 'vm_garso_adaptive_request_backoff'; let diagnosticsState=null; let diagnosticsSaveTimer=null; let diagnosticsSnapshotInProgress=false; const WATCH_BAR_ID='vm-garso-watch-notification-bar'; const WATCH_BAR_SPACER_ID='vm-garso-watch-notification-spacer'; const WATCH_SETTINGS_OVERLAY_ID='vm-garso-watch-settings-overlay'; const HIDDEN_PROFILE_CHECK_INTERVAL_MS=60 * 60 * 1000; const TRACKED_ITEMS_LABEL='Zapisane anonse'; const GLOBAL_QUICK_SEARCH_ID='vm-global-quick-search'; const ESCORT_MULTI_LOCATION_UI_ID='vm-escort-multi-location-ui'; const ESCORT_MULTI_LOCATION_SESSION_KEY='vm_escort_multi_locations_session'; const FAVORITE_SEARCH_EDIT_KEY='vm_favorite_search_edit_session'; const FAVORITE_SEARCH_EDIT_PARAM='vm_edit_search'; const FAVORITE_SEARCH_VIEW_KEY='vm_favorite_search'; let favoriteSearchViewSession=null; let favoriteSearchEditSession=null,favoriteSearchEditReadFilters=null,favoriteSearchEditCancelToken=null; let favoriteSearchEditFrameNotify=null; const ESCORT_MULTI_LOCATION_MAX=30; // Miasta i ID: eksport kolektora z 2026-10-06; współrzędne: GeoNames, CC BY 4.0, zaokrąglone do 4 miejsc. // Źródła: https://pl.escort.club/ - https://download.geonames.org/export/dump/PL.zip const ESCORT_RADIUS_PROVINCES=["dolnośląskie","kujawsko-pomorskie","łódzkie","lubelskie","lubuskie","małopolskie","mazowieckie","opolskie","podkarpackie","podlaskie","pomorskie","śląskie","świętokrzyskie","warmińsko-mazurskie","wielkopolskie","zachodniopomorskie"]; const ESCORT_RADIUS_PROVINCE_IDS=[2,4,10,6,8,12,14,16,18,20,22,24,26,28,30,32]; // Miasto: nazwa, indeks województwa, szerokość, długość, ID Escort.club. const ESCORT_RADIUS_CITIES=[ ["Aleksandrów Kujawski",1,52.8766,18.6934,92],["Andrychów",5,49.855,19.3383,329],["Augustów",9,53.8432,22.9798,508],["Barczewo",13,53.8306,20.6911,723], ["Bardo",0,50.5059,16.7399,76],["Bartoszyce",13,54.2535,20.8082,693],["Będzin",11,50.3261,19.1257,590],["Bełchatów",2,51.3688,19.3567,232], ["Bełżyce",3,51.1741,22.2803,162],["Biała Podlaska",3,52.0324,23.1165,186],["Białobrzegi",6,51.6469,20.9504,337],["Białogard",15,54.007,15.9875,854], ["Białystok",9,53.1333,23.1643,545],["Biecz",5,49.736,21.263,287],["Bielawa",0,50.6908,16.623,3],["Bielsko-Biała",11,49.8225,19.0469,642], ["Bielsk Podlaski",9,52.7651,23.1865,519],["Biłgoraj",3,50.5411,22.722,146],["Błaszki",2,51.6516,18.4347,259],["Bobowa",5,49.7087,20.9477,288], ["Bochnia",5,49.9691,20.4303,276],["Bogatynia",0,50.9075,14.9563,82],["Bolesławiec",0,51.2642,15.5697,1],["Bolków",0,50.922,16.1011,12], ["Braniewo",13,54.3797,19.8196,697],["Brodnica",1,53.2597,19.3965,95],["Brzeg",7,50.8608,17.4674,422],["Brzesko",5,49.9691,20.6061,278], ["Brzeszcze",5,49.982,19.1516,312],["Buk",14,52.3553,16.5196,812],["Busko-Zdrój",12,50.4708,20.7188,661],["Bydgoszcz",1,53.1235,18.0076,140], ["Bytom",11,50.348,18.9328,643],["Bytów",10,54.1706,17.4919,548],["Chełm",3,51.1431,23.4716,187],["Chełmno",1,53.3486,18.4251,100], ["Chmielnik",12,50.6144,20.7521,670],["Chojnice",10,53.6955,17.557,550],["Chorzów",11,50.3058,18.9742,644],["Chrzanów",5,50.1355,19.402,281], ["Ciechanów",6,52.8814,20.62,339],["Ciechocinek",1,52.8791,18.795,93],["Cieszyn",11,49.7513,18.6321,598],["Czarnków",14,52.9021,16.5641,745], ["Czechowice-Dziedzice",11,49.9134,19.0048,596],["Czeladź",11,50.3154,19.0782,591],["Częstochowa",11,50.7965,19.1241,645],["Człuchów",10,53.6672,17.3588,553], ["Czyżew",9,52.7977,22.3124,542],["Dąbrowa Górnicza",11,50.3339,19.2048,646],["Daleszyce",12,50.8023,20.8079,671],["Dębica",8,50.0515,21.4114,459], ["Dobrodzień",7,50.7287,18.445,441],["Drawsko Pomorskie",15,53.5306,15.8097,862],["Dynów",8,49.8151,22.2339,490],["Działdowo",13,53.2396,20.17,700], ["Dzierzgoń",10,53.922,19.347,584],["Dzierżoniów",0,50.7282,16.6514,4],["Elbląg",13,54.1522,19.4088,740],["Ełk",13,53.8282,22.3647,705], ["Garwolin",6,51.8975,21.6147,341],["Gdańsk",10,54.3523,18.6491,586],["Gdynia",10,54.5189,18.5319,587],["Giżycko",13,54.0381,21.7644,706], ["Gliwice",11,50.2976,18.6766,647],["Głogów",0,51.6636,16.0845,8],["Głowno",2,51.9646,19.7157,267],["Głuchołazy",7,50.315,17.3835,436], ["Gniezno",14,52.5348,17.5826,749],["Goleniów",15,53.5639,14.8285,865],["Góra",0,51.6664,16.5349,9],["Góra Kalwaria",6,51.9765,21.2154,375], ["Gorlice",5,49.6556,21.1604,286],["Gorzów Wielkopolski",4,52.7337,15.225,230],["Gostyń",14,51.8825,17.0123,755],["Grodzisk Mazowiecki",6,52.1039,20.6337,348], ["Grodzisk Wielkopolski",14,52.2276,16.3653,759],["Grójec",6,51.8625,20.8676,349],["Grudziądz",1,53.4841,18.7537,141],["Gryfino",15,53.2524,14.4883,874], ["Hajnówka",9,52.7433,23.5812,524],["Hrubieszów",3,50.805,23.8925,152],["Iława",13,53.596,19.5685,708],["Inowrocław",1,52.7989,18.2639,105], ["Jabłonna",6,52.3788,20.9174,3634],["Jarocin",14,51.9727,17.5026,763],["Jarosław",8,50.0162,22.6778,462],["Jasło",8,49.7451,21.4725,465], ["Jastrzębia Góra",10,54.8314,18.313,3646],["Jastrzębie-Zdrój",11,49.9554,18.5748,648],["Jaworzno",11,50.2053,19.275,649],["Jędrzejów",12,50.6394,20.3045,663], ["Jelcz-Laskowice",0,51.0213,17.3165,50],["Jelenia Góra",0,50.8997,15.729,88],["Kalisz",14,51.7611,18.091,850],["Kalwaria Zebrzydowska",5,49.8676,19.6772,330], ["Kartuzy",10,54.3342,18.1974,557],["Katowice",11,50.2597,19.0217,650],["Kędzierzyn-Koźle",7,50.3498,18.2261,428],["Kępno",14,51.2784,17.9891,766], ["Kęty",5,49.8821,19.2233,314],["Kielce",12,50.8703,20.6275,692],["Kłodzko",0,50.4349,16.6614,20],["Kluczbork",7,50.9728,18.2182,430], ["Knurów",11,50.2197,18.6507,605],["Koło",14,52.2002,18.6386,767],["Kołobrzeg",15,54.1756,15.5834,883],["Konin",14,52.2234,18.2512,851], ["Końskie",12,51.1917,20.4061,672],["Konstancin-Jeziorna",6,52.0938,21.1176,376],["Kórnik",14,52.2477,17.0895,814],["Kościan",14,52.0883,16.6487,776], ["Kościerzyna",10,54.1223,17.9812,559],["Kostrzyn",14,52.3985,17.2281,813],["Kostrzyn nad Odrą",4,52.5871,14.6495,190],["Koszalin",15,54.1944,16.1722,916], ["Kozienice",6,51.5829,21.5478,353],["Kraków",5,50.0614,19.9366,334],["Kraśnik",3,50.9236,22.2271,156],["Krobia",14,51.7741,16.9824,756], ["Krośniewice",2,52.2559,19.1704,235],["Krosno",8,49.6887,21.7706,504],["Krotoszyn",14,51.6987,17.4374,783],["Krynica-Zdrój",5,49.4222,20.9594,301], ["Krzeszowice",5,50.1425,19.6322,289],["Kutno",2,52.2306,19.3641,234],["Kwidzyn",10,53.7249,18.9311,560],["Łańcut",8,50.0687,22.2291,478], ["Łask",2,51.5906,19.1328,237],["Lębork",10,54.5392,17.7501,562],["Łęczyca",2,52.0596,19.1997,238],["Legionowo",6,52.4015,20.9266,354], ["Legnica",0,51.2101,16.1619,89],["Leszno",14,51.8403,16.5749,852],["Leżajsk",8,50.2626,22.4193,472],["Limanowa",5,49.7059,20.422,294], ["Łobez",15,53.6392,15.6213,913],["Łódź",2,51.7706,19.4739,273],["Łomianki",6,52.3341,20.886,401],["Łomża",9,53.1781,22.0593,546], ["Łowicz",2,52.1071,19.9453,239],["Lubaczów",8,50.157,23.1234,474],["Lubin",0,51.4009,16.2015,36],["Lublin",3,51.2506,22.5701,188], ["Lubliniec",11,50.669,18.6844,611],["Luboń",14,52.3471,16.8927,810],["Łuków",3,51.929,22.3796,165],["Maków Mazowiecki",6,52.8649,21.1005,358], ["Malbork",10,54.0359,19.0266,564],["Marki",6,52.3207,21.1047,406],["Miasteczko Śląskie",11,50.5026,18.9395,625],["Miechów",5,50.3565,20.0279,296], ["Międzychód",14,52.5988,15.897,787],["Międzyrzecz",4,52.4446,15.578,194],["Międzyzdroje",15,53.9292,14.451,881],["Mielec",8,50.2871,21.4239,479], ["Mielno",15,54.2609,16.0621,921],["Mikołajki",13,53.8029,21.5701,719],["Mikołów",11,50.171,18.9041,614],["Milanówek",6,52.1188,20.6715,346], ["Milicz",0,51.5277,17.2714,43],["Mińsk Mazowiecki",6,52.1793,21.5725,360],["Mława",6,53.1128,20.3841,365],["Mosina",14,52.2454,16.8471,815], ["Mrągowo",13,53.8644,21.3051,718],["Mszana Dolna",5,49.6743,20.0799,295],["Mszczonów",6,51.9742,20.5208,417],["Murowana Goślina",14,52.5746,17.0093,816], ["Myślenice",5,49.8338,19.9383,298],["Mysłowice",11,50.2075,19.1667,651],["Myszków",11,50.5752,19.3246,616],["Nakło nad Notecią",1,53.1421,17.6018,117], ["Namysłów",7,51.0759,17.7228,435],["Niemodlin",7,50.642,17.6193,445],["Nowa Sól",4,51.8033,15.717,197],["Nowe Miasto Lubawskie",13,53.4208,19.5952,721], ["Nowy Dwór Gdański",10,54.2131,19.1177,567],["Nowy Dwór Mazowiecki",6,52.4302,20.7165,366],["Nowy Sącz",5,49.6218,20.6971,335],["Nowy Targ",5,49.4778,20.0323,305], ["Nowy Tomyśl",14,52.3195,16.1284,790],["Nysa",7,50.4738,17.3344,438],["Oborniki",14,52.6474,16.8141,793],["Oława",0,50.9466,17.2926,49], ["Oleśnica",0,51.2134,17.3899,44],["Olesno",7,50.877,18.4209,443],["Olkusz",5,50.2813,19.565,309],["Olsztyn",13,53.7838,20.4927,741], ["Opole",7,50.6712,17.926,456],["Orzesze",11,50.1559,18.7792,615],["Ostróda",13,53.6967,19.9649,728],["Ostrołęka",6,53.0862,21.5757,418], ["Ostrowiec Świętokrzyski",12,50.9294,21.3852,676],["Ostrów Mazowiecka",6,52.8025,21.8951,370],["Ostrów Wielkopolski",14,51.655,17.8069,795],["Ostrzeszów",14,51.4264,17.9336,801], ["Oświęcim",5,50.0344,19.2104,311],["Otmuchów",7,50.4663,17.1735,439],["Otwock",6,52.1058,21.2613,373],["Ozorków",2,51.9634,19.2914,268], ["Pabianice",2,51.6645,19.3547,246],["Piaseczno",6,52.0814,21.024,377],["Piastów",6,52.1844,20.8395,384],["Piła",14,53.1514,16.7378,802], ["Piotrków Trybunalski",2,51.4055,19.7032,274],["Pisz",13,53.6274,21.8125,734],["Pleszew",14,51.8964,17.7855,809],["Płock",6,52.5468,19.7064,419], ["Płońsk",6,52.6235,20.3755,382],["Pniewy",14,52.5094,16.2567,828],["Podkowa Leśna",6,52.1224,20.7266,347],["Polanica-Zdrój",0,50.4037,16.5127,23], ["Polkowice",0,51.5039,16.0726,52],["Poznań",14,52.4069,16.9299,853],["Prudnik",7,50.3212,17.5746,450],["Pruszcz Gdański",10,54.2622,18.6363,556], ["Pruszków",6,52.1707,20.8121,385],["Przedecz",14,52.3344,18.8991,770],["Przemyśl",8,49.785,22.7673,505],["Przeworsk",8,50.0591,22.4941,485], ["Pszczyna",11,49.9804,18.9538,619],["Puławy",3,51.4166,21.9694,170],["Rabka-Zdrój",5,49.6089,19.9665,307],["Racibórz",11,50.0919,18.2193,620], ["Radlin",11,50.0502,18.4763,632],["Radom",6,51.4025,21.1471,420],["Radomsko",2,51.0671,19.4448,253],["Radymno",8,49.9472,22.8238,463], ["Radzymin",6,52.4159,21.1841,409],["Radzyń Podlaski",3,51.7833,22.6167,173],["Rajgród",9,53.731,22.7051,522],["Rawicz",14,51.6095,16.8585,823], ["Reda",10,54.6053,18.3472,581],["Ruda Śląska",11,50.2584,18.8563,653],["Rumia",10,54.5709,18.388,582],["Rybnik",11,50.0971,18.5418,654], ["Rydułtowy",11,50.0586,18.417,633],["Rypin",1,53.066,19.4094,121],["Rzeszów",8,50.0413,21.999,506],["Sandomierz",12,50.6827,21.749,681], ["Sanok",8,49.5557,22.2056,496],["Sędziszów",12,50.5659,20.0556,665],["Sępólno Krajeńskie",1,53.452,17.5317,123],["Serock",6,52.5104,21.0691,355], ["Siedlce",6,52.1677,22.2901,421],["Siemianowice Śląskie",11,50.3274,19.029,655],["Sieradz",2,51.5958,18.7302,258],["Skała",5,50.2305,19.8536,290], ["Skarżysko-Kamienna",12,51.1131,20.8716,684],["Skawina",5,49.9752,19.8287,291],["Skierniewice",2,51.9549,20.1584,275],["Skoczów",11,49.8009,18.7877,601], ["Słubice",4,52.3509,14.5607,204],["Słupca",14,52.2873,17.8719,824],["Słupsk",10,54.4641,17.0287,588],["Sochaczew",6,52.2294,20.2384,396], ["Sokołów Podlaski",6,52.4068,22.2531,397],["Sopot",10,54.4418,18.56,589],["Sosnowiec",11,50.2868,19.1039,656],["Śrem",14,52.0887,17.0151,834], ["Środa Wielkopolska",14,52.2284,17.2762,831],["Stalowa Wola",8,50.5829,22.0533,498],["Starachowice",12,51.0374,21.0713,686],["Stargard",15,53.3367,15.0499,897], ["Starogard Gdański",10,53.964,18.5264,576],["Stęszew",14,52.2837,16.7008,818],["Stryków",2,51.9022,19.6054,271],["Strzegom",0,50.9626,16.3501,60], ["Strzelce Opolskie",7,50.5107,18.3006,453],["Strzelin",0,50.7816,17.0648,54],["Sucha Beskidzka",5,49.7419,19.5943,319],["Sulejówek",6,52.2522,21.269,364], ["Suwałki",9,54.1118,22.9309,547],["Swarzędz",14,52.4129,17.085,819],["Świdnica",0,50.8438,16.4886,57],["Świdnik",3,51.219,22.6962,176], ["Świebodzice",0,50.8597,16.3197,58],["Świebodzin",4,52.2475,15.5335,211],["Świecie",1,53.4095,18.4474,126],["Świętochłowice",11,50.2964,18.9173,657], ["Świnoujście",15,53.9105,14.2471,918],["Szamotuły",14,52.612,16.5779,829],["Szczecin",15,53.4289,14.553,917],["Szczecinek",15,53.7079,16.6994,902], ["Szczyrk",11,49.7172,19.0318,595],["Szczytno",13,53.5626,20.9875,736],["Tarnobrzeg",8,50.573,21.6794,507],["Tarnów",5,50.0138,20.987,336], ["Tarnowo Podgórne",14,52.4664,16.6633,3653],["Tarnowskie Góry",11,50.4455,18.8615,627],["Tczew",10,54.0924,18.7779,578],["Tomaszów Mazowiecki",2,51.5313,20.0085,262], ["Toruń",1,53.0138,18.5981,142],["Trzebnica",0,51.3108,17.0633,64],["Tuchola",1,53.5879,17.859,128],["Turek",14,52.0155,18.5006,835], ["Tychowo",15,53.9277,16.2577,856],["Tychy",11,50.1372,18.9664,658],["Uniejów",2,51.9743,18.7931,252],["Ustka",10,54.5805,16.8619,572], ["Ustroń",11,49.7215,18.802,599],["Wadowice",5,49.8834,19.4929,331],["Wągrowiec",14,52.8084,17.1996,838],["Wałbrzych",0,50.7714,16.2843,91], ["Wałcz",15,53.2779,16.4712,908],["Warka",6,51.7843,21.1909,352],["Warszawa",6,52.2298,21.0118,919],["Wejherowo",10,54.6057,18.2356,583], ["Wieliczka",5,49.9874,20.0647,333],["Wieluń",2,51.221,18.5696,263],["Wisła",11,49.6563,18.8591,600],["Władysławowo",10,54.7909,18.4009,571], ["Włocławek",1,52.6482,19.0678,143],["Wodzisław Śląski",11,50.0038,18.472,634],["Wolbrom",5,50.3796,19.7583,310],["Wołomin",6,52.3401,21.2421,411], ["Wolsztyn",14,52.1155,16.1171,841],["Wrocław",0,51.1029,17.0301,90],["Września",14,52.3251,17.5652,845],["Wyszków",6,52.5928,21.4584,412], ["Ząbki",6,52.2927,21.1054,407],["Ząbkowice Śląskie",0,50.5897,16.8124,77],["Zabrze",11,50.3249,18.7858,659],["Żagań",4,51.6176,15.3149,219], ["Zakopane",5,49.299,19.9489,328],["Zambrów",9,52.9855,22.2432,544],["Zamość",3,50.7231,23.252,189],["Żary",4,51.642,15.1373,224], ["Zator",5,49.996,19.438,315],["Zawiercie",11,50.4877,19.4168,636],["Zduńska Wola",2,51.5992,18.9397,265],["Zgierz",2,51.8556,19.4062,269], ["Zgorzelec",0,51.1494,15.0084,81],["Zielona Góra",4,51.9355,15.5064,231],["Zielonka",6,52.3038,21.1602,408],["Żmigród",0,51.4667,16.9056,65], ["Żnin",1,52.8496,17.7199,139],["Żory",11,50.0452,18.7006,660],["Żyrardów",6,52.0488,20.446,416],["Żywiec",11,49.6853,19.1924,641] ]; const escortMultiLocationResultCounts=new Map(); const escortMultiLocationPageCounts=new Map(); const escortMultiLocationUiRefreshers=new Set(); const WATCH_INSTANCE_ID=`${Date.now()}-${Math.random().toString(36).slice(2)}`; const DEFAULT_WATCH_SETTINGS={ enabled:true, notesEnabled:true, intervalMinutes:30, showOnlineCount:true, showAlertBar:true, nonAlertChangeNewness:'until-open' }; const WATCH_INTERVAL_OPTIONS=[5,15,30,60,180,360]; const WATCH_INTERVAL_SELECT_OPTIONS=WATCH_INTERVAL_OPTIONS.map(minutes => [ minutes, minutes < 60 ? `${minutes} min`:`${minutes / 60} h` ]); const WATCH_NON_ALERT_NEWNESS_OPTIONS=[ ['until-open','Do czasu otwarcia tabeli ulubionych'], ['60','1 h'], ['180','3 h'], ['360','6 h'], ['720','12 h'], ['1440','1 dzień'], ['4320','3 dni'], ['10080','7 dni'], ['20160','14 dni'] ]; const WATCH_NON_ALERT_NEWNESS_VALUES=new Set( WATCH_NON_ALERT_NEWNESS_OPTIONS.map(([value]) => value) ); function normalizeWatchNonAlertChangeNewness( value, fallback=DEFAULT_WATCH_SETTINGS.nonAlertChangeNewness ) { const normalized=String(value ?? '').trim(); return WATCH_NON_ALERT_NEWNESS_VALUES.has(normalized) ? normalized :fallback; } const WATCH_PROFILE_RESOLUTION_OPTIONS=[ ['address-escorti','adres anonsu → Escorti.pl'], ['phone-escorti','nr telefonu → Escorti.pl'], ['phone-escort-search','nr telefonu → wyszukiwarka Escort.club'], ['ask','Zawsze pytaj'] ]; const FAVORITE_SEARCH_MAX_ADS=200; const ESCORT_DATA_FIELD_CATALOG_STORAGE_KEY='vm_escort_data_field_catalog'; const ESCORT_DATA_FIELD_CATALOG_MAX=240; const FAVORITES_MAX_DATA_COLUMNS=6; const SEARCH_RESULT_MAX_EXTRA_FIELDS=6; const DEFAULT_FAVORITE_CATEGORIES=[ 'Do odbycia', 'Odbyte', 'Wtopa', 'Saper', 'Polecane' ]; const DEFAULT_FAVORITE_CATEGORY_COLORS={ 'Do odbycia':'#2e7d32', 'Odbyte':'#a39e9e', 'Wtopa':'#c62424', 'Saper':'#f7c455', 'Polecane':'#287d28' }; const DEFAULT_HISTORY_CLEANUP_KEYWORDS=[ 'escort.club', 'escorti.pl', 'garsoniera.com.pl', 'dolores.sex', 'tineye.com', 'picdetective.com', 'google.com/search?vsrid' ]; const HIVE_IMAGE_DETECTOR={id:'hive-ai',name:'Hive AI',enabled:true,urlPrefix:'https://hivedetect.ai/#vm_image_url='}; const DEFAULT_IMAGE_SEARCH_ENGINES=[ {id:'google-lens',name:'Google',enabled:true,urlPrefix:'https://lens.google.com/uploadbyurl?url='}, {id:'picdetective',name:'PicDet',enabled:true,urlPrefix:'https://picdetective.com/?vm_image_url='}, {id:'yandex',name:'Yandex',enabled:true,urlPrefix:'https://yandex.ru/images/search?rpt=imageview&url='}, HIVE_IMAGE_DETECTOR ]; function normalizeImageSearchUrlPrefix(value) { const prefix=String(value || '').trim(); if (!prefix) return ''; try { const url=new URL(prefix); return /^https?:$/.test(url.protocol) ? prefix:''; } catch {return '';} } function normalizeExternalServices(value,fallback,urlKey,normalizeUrl,namePrefix,idPrefix='custom-') { const source=Array.isArray(value) ? value:fallback; return source.slice(0,20).map((entry,index) => { const item=entry && typeof entry === 'object' ? entry:{}; const url=normalizeUrl(item[urlKey]); return url ? { id:String(item.id || `${idPrefix}${index + 1}`).slice(0,80), name:normalizeEscortAdText(item.name || `${namePrefix} ${index + 1}`).slice(0,40), enabled:item.enabled !== false, [urlKey]:url }:null; }).filter(Boolean); } function normalizeImageSearchEngines(value) { return normalizeExternalServices(value,DEFAULT_IMAGE_SEARCH_ENGINES,'urlPrefix',normalizeImageSearchUrlPrefix,'Wyszukiwarka'); } const DEFAULT_MENU_PAGES=[ { id:'garso-forum', name:'Garso', enabled:true, url:'https://www.garsoniera.com.pl/forum/forum/52-krakow-bohema-i/' }, { id:'escort-search', name:'Escort', enabled:true, url:'https://pl.escort.club/anonse/towarzyskie/warszawa/' } ]; function normalizeMenuPageUrl(value) { const raw=String(value || '').trim(); if (!raw) return ''; try { const url=new URL(raw); return /^https?:$/.test(url.protocol) ? url.href :''; } catch { return ''; } } function normalizeMenuPages(value) { return normalizeExternalServices(value,[],'url',normalizeMenuPageUrl,'Strona','custom-menu-'); } const DEFAULT_PHONE_REPUTATION_SERVICES=[ {id:'ktoto',name:'KtoTo.info',enabled:true,urlTemplate:'https://www.ktoto.info/numer/{phone}/'}, {id:'numerytelefonu',name:'NumeryTelefonu.com',enabled:true,urlTemplate:'https://numerytelefonu.com/nrtel/{phone}'}, {id:'sprawdzenienumeru',name:'SprawdzenieNumeru.pl',enabled:true,urlTemplate:'https://sprawdzenienumeru.pl/numer/{phone48}'}, {id:'google',name:'Google',enabled:true,urlTemplate:'https://www.google.com/search?q={query}'} ]; const PHONE_REPUTATION_AUTO_SOURCE_IDS=new Set([ 'ktoto','numerytelefonu','sprawdzenienumeru' ]); function normalizePhoneReputationUrlTemplate(value) { const template=String(value || '').trim(); if (!template) return ''; try { const probe=template .replaceAll('{phone}','500000000') .replaceAll('{phone48}','48500000000') .replaceAll('{query}','500000000'); const url=new URL(probe); return /^https?:$/.test(url.protocol) ? template:''; } catch {return '';} } function normalizePhoneReputationServices(value) { return normalizeExternalServices(value,DEFAULT_PHONE_REPUTATION_SERVICES,'urlTemplate',normalizePhoneReputationUrlTemplate,'Serwis'); } const DEFAULT_TAG_HIGHLIGHT_GROUPS=[ { id:'group-3', name:'negatywne', color:'#000000', tags:[ 'Jedno zbliżenie w godzinie', 'Nieogolone' ] }, { id:'group-4', name:'wyjątkowe', color:'#28b428', tags:[ 'Nagrywanie video', 'Robienie zdjęć' ] }, { id:'group-1', name:'ważne', color:'#ff2878', tags:[ 'Dwa zbliżenia w godzinie', 'Więcej zbliżeń w godzinie', 'Namiętne Pocałunki', 'Francuz bez zabezpieczenia' ] }, { id:'group-2', name:'mniej ważne', color:'#ffa0c8', tags:[ 'Klimat GFE', 'Finał na twarz', 'Finał w buzi', 'Minetka', 'Pocałunki' ] }, { id:'other', name:'pozostałe', color:'#ffdce6', tags:[], locked:true } ]; function normalizeTagHighlightGroups(value) { const source=Array.isArray(value) ? value:DEFAULT_TAG_HIGHLIGHT_GROUPS; const seenTags=new Set(); const editable=source .filter(item => item && typeof item === 'object' && item.id !== 'other' && item.locked !== true) .slice(0,12) .map((item,index) => { const id=String(item.id || `group-${index + 1}`).slice(0,80); const tags=[]; for (const rawTag of Array.isArray(item.tags) ? item.tags:[]) { const tag=normalizeEscortAdText(rawTag).slice(0,80); const key=normalizeEscortChangeComparisonValue(tag); if (!tag || !key || seenTags.has(key)) continue; seenTags.add(key); tags.push(tag); } const defaultGroup= DEFAULT_TAG_HIGHLIGHT_GROUPS.find( group => group.id === id ) || DEFAULT_TAG_HIGHLIGHT_GROUPS.find( group => group.locked !== true ); return { id, name:(normalizeEscortAdText(item.name) || defaultGroup.name).slice(0,40), color:normalizeFavoriteCategoryColor(item.color) || defaultGroup.color, tags }; }); if (!editable.length) editable.push({...DEFAULT_TAG_HIGHLIGHT_GROUPS[0],tags:[]}); const other=source.find(item => item?.id === 'other' || item?.locked === true) || DEFAULT_TAG_HIGHLIGHT_GROUPS.at(-1); return [...editable,{ id:'other', name:'pozostałe', color:normalizeFavoriteCategoryColor(other?.color) || DEFAULT_TAG_HIGHLIGHT_GROUPS.at(-1).color, tags:[], locked:true }]; } function getKnownEscortTagLabels(extraGroups=[]) { const labels=new Map(); const add=value => { const label=normalizeEscortAdText(value).slice(0,80); const key=normalizeEscortChangeComparisonValue(label); if (label && key && !labels.has(key)) labels.set(key,label); }; for (const group of extraGroups) for (const tag of group?.tags || []) add(tag); try {document.querySelectorAll('.sub-info-box .tags-box .tag').forEach(node => add(node.textContent));} catch {} try { for (const key of getEscortAdDataCacheKeys()) { const data=readPersistentCacheValue(key,null); for (const tag of Array.isArray(data?.tags) ? data.tags:[]) add(tag); } } catch {} return [...labels.values()].sort((a,b) => a.localeCompare(b,'pl-PL')); } const DEFAULT_SETTINGS={ quickAdPreview:true, autoGarsoCheck:true, garsoCheckEscortLinks:true, garsoShowSearchIcons:true, garsoShowTopicSummary:true, garsoShowTopicTools:true, garsoShowTopicHelperData:true, autoGarsoAnalysisMode:'content', mergeEscortAds:true, escortAggregationMode:'address-escorti', compactSummaryComparisonMode:'phone-escorti', showRelatedAdsTileLink:true, relatedAdsTileLinkMode:'phone-escorti', openAllActiveRelatedAdsFromTile:false, maxRelatedAdsTabs:10, showEscortiTileData:true, showPricesInSearchResults:true, searchResultPriceDuration:'60', searchResultExtraFields:['ad:declaredBmi'], hideRecommendedSearchSection:true, hideRecommendedAdSection:true, hideSingleAdContactButtons:true, hideSingleAdActions:true, hideFeaturedSearchSection:true, hidePopularCitiesSection:true, showImageSearchButtons:true, imageSearchEngines:DEFAULT_IMAGE_SEARCH_ENGINES.map(item => ({...item})), imageSearchEnginesRevision:2, showPhoneReputationLinks:true, autoPhoneReputationSearch:true, phoneReputationServices:DEFAULT_PHONE_REPUTATION_SERVICES.map(item => ({...item})), showTopPhoneSearch:true, showBottomLeftMenu:true, showMenuPagesAsFavicons:true, showMenuLinksInline:true, menuPages:DEFAULT_MENU_PAGES.map(item => ({...item})), topPhoneSearchEnterTarget:'escort-club', topPhoneSearchOpenMode:'new-tab', multiLocationEnabled:true, saveSearchLocationsEnabled:true, searchLocations:[], savedLocationGroups:[], showSummarySidePanel:true, showCompactSummaryAboveDescription:true, alarmingTagCountLimit:69, showTagHighlights:true, tagHighlightGroups:DEFAULT_TAG_HIGHLIGHT_GROUPS.map(item => ({...item,tags:[...item.tags]})), showPhoneCopyButton:true, phoneClipboardFormat:'local-hyphens', usePersistentCache:true, showLiveDiagnostics:false, escortLimitPerMinute:120, escortMaxConcurrent:4, escortFastStart:true, watchEnabled:true, notesEnabled:true, watchIntervalMinutes:30, showWatchOnlineCount:true, showWatchAlertBar:true, watchSystemNotifications:true, watchNotificationAnonymous:true, watchNotificationText:'Nowy alert', nonAlertChangeNewness:'until-open', watchProfileResolutionMode:'address-escorti', favoritesDefaultView:'table', favoritesDataColumns:['ad:prices:60'], favoriteCategories:[...DEFAULT_FAVORITE_CATEGORIES], favoriteCategoryColors:{...DEFAULT_FAVORITE_CATEGORY_COLORS}, historyCleanupKeywords:[...DEFAULT_HISTORY_CLEANUP_KEYWORDS] }; const ESCORT_CACHE_REFRESH_HOURS=7 * 24; const GARSO_CACHE_REFRESH_HOURS=14 * 24; const DEFAULT_LIST_PAGES_TO_SHOW=1; const ESCORT_LIST_PAGES_RELOAD_KEY='vm_escort_list_pages_reload_once'; const LIST_PAGE_OPTIONS=[ [1,'1 stronę'], [2,'2 strony'], [3,'3 strony'], [4,'4 strony'], [5,'5 stron'], [10,'10 stron'], ['all','Wszystkie'] ]; const ESCORT_PRICE_DURATION_OPTIONS=[ ['15','15 min'], ['30','0,5 h'], ['60','1 h'], ['120','2 h'], ['night','całą noc'] ]; const ESCORT_PRICE_DISPLAY_OPTIONS=[ ['hide','ukryj'], ...ESCORT_PRICE_DURATION_OPTIONS ]; const PHONE_CLIPBOARD_FORMAT_OPTIONS=[ ['international-spaces','+48 123 456 789'], ['local-hyphens','123-456-789'] ]; const TOP_PHONE_SEARCH_TARGET_OPTIONS=[ ['garso','Garsoniera'], ['escort-club','Escort.club'], ['escorti','Escorti.pl'] ]; const TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS=[ ['new-tab','w nowej karcie'], ['same-tab','na tej samej karcie'] ]; const ESCORT_AGGREGATION_MODE_OPTIONS=[ ['address-escorti','Według adresu anonsu → Escorti.pl'], ['phone-escorti','Według numeru telefonu → Escorti.pl'], ['phone-escort-search','Według numeru telefonu → wyszukiwarka Escort.club'], ['exact-phone','Tylko identyczny numer telefonu → załadowana lista i cache'] ]; const ESCORT_RELATED_ADS_TILE_LINK_MODE_OPTIONS= ESCORT_AGGREGATION_MODE_OPTIONS.slice(0,3); const ESCORT_AGGREGATION_MODE_DESCRIPTIONS={ 'address-escorti': 'Najlepiej znajduje powiązane anonse, ale czasami może błędnie przypisać do jednej grupy niezwiązane anonse.', 'phone-escorti': 'Wyszukuje w Escorti.pl według numeru telefonu, dlatego powoduje mniej błędnych przypisań niż wyszukiwanie według adresu.', 'phone-escort-search': 'Dla numeru telefonu z kafelka przeszukuje Escort.club, a następnie uwzględnia wyniki wyszukiwania, załadowane kafelki oraz anonse zapisane w aktualnym cache.', 'exact-phone': 'Łączy kafelki z aktualnie załadowanej listy, które mają identyczny numer telefonu i uwzględnia również anonse zapisane w aktualnym cache. Zapewnia wysoką pewność powiązania ale nie pokazuje liczby wszystkich ogłoszeń.' }; const ESCORT_COMPACT_SUMMARY_MODE_OPTIONS=[...ESCORT_AGGREGATION_MODE_OPTIONS.slice(0,2), ['exact-phone',ESCORT_AGGREGATION_MODE_OPTIONS[2][1]]]; const ESCORT_COMPACT_SUMMARY_MODE_DESCRIPTIONS={ 'address-escorti':ESCORT_AGGREGATION_MODE_DESCRIPTIONS['address-escorti'], 'phone-escorti':ESCORT_AGGREGATION_MODE_DESCRIPTIONS['phone-escorti'], 'exact-phone': 'Wyszukuje na Escort.club aktualnie dostępne anonse znalezione po numerze telefonu i porównuje ich dane. Cache ogranicza ponowne pobieranie szczegółów, ale nie wyznacza listy porównywanych anonsów.' }; const ESCORT_SAVED_FILTERS_STORAGE_KEY='vm_escort_saved_search_filters'; const ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH=145; const ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH=280; const ESCORT_CUSTOM_FILTERS_STORAGE_KEY='vm_escort_custom_result_filters'; const ESCORT_CUSTOM_FILTER_AGE_UNITS=['days','weeks','months','years']; const ESCORT_CUSTOM_FILTER_AGE_UNIT_OPTIONS=[ ['days','dni'], ['weeks','tygodnie'], ['months','miesiące'], ['years','lata'] ]; const ESCORT_CUSTOM_FILTER_WEEKDAYS=[ ['monday','Poniedziałek'], ['tuesday','Wtorek'], ['wednesday','Środa'], ['thursday','Czwartek'], ['friday','Piątek'], ['saturday','Sobota'], ['sunday','Niedziela'] ]; const DEFAULT_ESCORT_CUSTOM_FILTERS={ expanded:false, hideWithoutOpinions:false, hideProfileAdsOver:false, profileAdsLimit:10, hideLocationChangesOver:false, locationChangesLimit:1, hideActiveAdsOver:false, activeAdsLimit:1, hideAgencyPhone:false, hideCity:false, hideCityQuery:'', hideVisited:false, hideUnvisited:false, hideVisitedOlderThan:false, hideVisitedOlderValue:30, hideVisitedOlderUnit:'days', hideVisitedRecentThan:false, hideVisitedRecentValue:7, hideVisitedRecentUnit:'days', hideHasNote:false, hideHasCategory:false, hideCategory:'', hideTagCountOver:false, tagCountLimit:null, hideYoungerThan:false, youngerValue:2, youngerUnit:'days', hideOlderThan:false, olderValue:2, olderUnit:'days', hideAdYoungerThan:false, adYoungerValue:2, adYoungerUnit:'days', hideAdOlderThan:false, adOlderValue:2, adOlderUnit:'days', hideAlwaysAvailable:false, hideUnavailableAt:false, hideUnavailableDay:'monday', hideUnavailableTime:'12:00', hideAvailableAt:false, hideAvailableDay:'monday', hideAvailableTime:'12:00', hideDescriptionMatch:false, hideDescriptionQuery:'', showOnlyUnavailableAt:false, showOnlyUnavailableDay:'monday', showOnlyUnavailableTime:'12:00', showOnlyAvailableAt:false, showOnlyAvailableDay:'monday', showOnlyAvailableTime:'12:00', showOnlyWithoutOpinions:false, showOnlySingleProfileAd:false, showOnlyAgencyPhone:false, showOnlyBmi:false, showOnlyBmiMin:0, showOnlyBmiMax:100, showOnlyHasNote:false, showOnlyHasCategory:false, showOnlyCategory:'', showOnlyVisitedOlderThan:false, showOnlyVisitedOlderValue:30, showOnlyVisitedOlderUnit:'days', showOnlyVisitedRecentThan:false, showOnlyVisitedRecentValue:7, showOnlyVisitedRecentUnit:'days', showOnlyDescriptionMatch:false, showOnlyDescriptionQuery:'' }; function normalizeCustomFilterInteger(value,fallback,min=0,max=100000) { const number=Math.round(Number(value)); return Number.isFinite(number) ? Math.min(max,Math.max(min,number)) :fallback; } function normalizeCustomFilterNumber( value, fallback, min=0, max=100000 ) { const number=Number( String(value ?? '').replace(',','.') ); return Number.isFinite(number) ? Math.min(max,Math.max(min,number)) :fallback; } function normalizeCustomFilterAgeUnit(value) { return ESCORT_CUSTOM_FILTER_AGE_UNITS.includes(value) ? value:'days'; } function normalizeCustomFilterWeekday(value) { return ESCORT_CUSTOM_FILTER_WEEKDAYS.some(([allowed]) => allowed === value) ? value :'monday'; } function normalizeCustomFilterTime(value,fallback='12:00') { const match=String(value || '').match(/^(\d{1,2}):(\d{2})$/); if (!match) return fallback; const hours=Number(match[1]); const minutes=Number(match[2]); if (hours > 23 || minutes > 59) return fallback; return `${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}`; } function normalizeCustomFilterQuery(value) { return String(value || '').replace(/\s+/g,' ').trim().slice(0,200); } function normalizeCustomFilterCategory(value) { return normalizeEscortAdText(value).slice(0,60); } function appendHelperStyle(id,css) { const style=makeElement('style'); style.id=id; style.textContent=css; document.head.appendChild(style); } const HELPER_STYLE_API_VERSION='2'; const HELPER_STYLE_API_CLASS='vm-helper-ui'; // API motywów pozostaje w wersji 2. Klasy komponentów są dodawane jawnie. function ensureHelperStyleApi() { const root=document.documentElement; if (root) { root.dataset.vmEscortHelperStyleApi=HELPER_STYLE_API_VERSION; } const styleId='vm-escort-helper-style-api'; if (document.getElementById(styleId)) return; const style=makeElement('style'); style.id=styleId; style.textContent=`.vm-helper-ui.vm-ad-summary-box { box-sizing:border-box; padding:8px 9px; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:7px; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 7%,transparent); color:var(--vm-ui-panel-text,var(--vm-ui-text,#222)); font:400 12px/1.3 var(--vm-ui-font-family,Lato,Arial,sans-serif); } .vm-helper-ui[data-vm-style-api="2"] { color-scheme:var(--vm-ui-color-scheme,normal); font-family:var(--vm-ui-font-family,Arial,sans-serif); } .vm-helper-ui input:not([type=checkbox]):not([type=radio]):not([type=color]), .vm-helper-ui textarea, .vm-helper-ui select { background-color:var(--vm-ui-input-bg,#fff); color:var(--vm-ui-input-text,var(--vm-ui-text,#222)); border-color:var(--vm-ui-input-border,#bbb); } .vm-helper-ui input::placeholder, .vm-helper-ui textarea::placeholder { color:var(--vm-ui-text-faint,#999); } .vm-helper-ui input[type=checkbox], .vm-helper-ui input[type=radio] { accent-color:var(--vm-ui-accent,#f54da3); } .vm-helper-ui.vm-ui-overlay { position:fixed; inset:0; z-index:2147483647; display:flex; align-items:center; justify-content:center; box-sizing:border-box; padding:16px; background:var(--vm-ui-overlay,rgba(0,0,0,.55)); } .vm-ui-dialog { width:min(540px,calc(100vw - 32px)); max-height:calc(100vh - 32px); min-width:0; box-sizing:border-box; overflow:auto; padding:var(--vm-ui-dialog-padding,18px); border:0; border-top:4px solid var(--vm-ui-accent,#f54da3); border-radius:var(--vm-ui-dialog-radius,10px); background:var(--vm-ui-bg,#fff); color:var(--vm-ui-text,#222); box-shadow:var(--vm-ui-shadow,0 10px 35px rgba(0,0,0,.35)); font:var(--vm-ui-font-size,13px)/1.4 var(--vm-ui-font-family,Arial,sans-serif); } .vm-ui-title { margin:0 0 12px; color:inherit; font-size:19px; font-weight:700; line-height:1.25; } .vm-ui-header, .vm-ui-row, .vm-ui-actions { display:flex; align-items:center; flex-wrap:wrap; gap:var(--vm-ui-gap,8px); } .vm-ui-header { justify-content:space-between; flex-wrap:nowrap; margin-bottom:12px; } .vm-ui-header .vm-ui-title { margin:0; } .vm-ui-actions { justify-content:flex-end; margin-top:12px; padding-top:12px; border-top:1px solid var(--vm-ui-border,#ddd); } .vm-ui-stack { display:grid; gap:var(--vm-ui-gap,8px); } .vm-ui-panel { min-width:0; box-sizing:border-box; padding:12px; border:1px solid var(--vm-ui-border,#ddd); border-radius:var(--vm-ui-radius,6px); background:var(--vm-ui-bg-alt,#fafafa); color:var(--vm-ui-text,#222); } .vm-ui-button, .vm-ui-field { box-sizing:border-box; min-height:var(--vm-ui-control-height,32px); padding:5px 9px; border:1px solid var(--vm-ui-input-border,#bbb); border-radius:var(--vm-ui-radius,6px); font:var(--vm-ui-font-size,13px)/1.35 var(--vm-ui-font-family,Arial,sans-serif); } .vm-ui-button { display:inline-flex; align-items:center; justify-content:center; gap:4px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-text,#222); cursor:pointer; font-weight:700; text-align:center; } .vm-helper-ui .vm-ui-button.vm-favorites-view-button { flex:none; gap:5px; height:30px; min-height:30px; margin:0; padding:4px 8px; border-radius:6px; font-size:11px; line-height:1.3; white-space:nowrap; border-color:var(--vm-ui-accent,#ce3986); background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 24%,var(--vm-ui-bg,#fff)); color:var(--vm-ui-text,#292c35); } .vm-helper-ui .vm-ui-button.vm-favorites-view-button:is(:hover,:focus-visible) { background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 36%,var(--vm-ui-bg,#fff)); } .vm-helper-ui .vm-favorites-view-button > svg { display:block; flex:none; width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; } .vm-ui-button.-accent { border-color:var(--vm-ui-accent,#f54da3); color:var(--vm-ui-accent,#f54da3); } .vm-ui-button.-primary { border-color:var(--vm-ui-accent,#f54da3); background:var(--vm-ui-accent,#f54da3); color:var(--vm-ui-accent-contrast,#fff); } .vm-ui-button.-danger { border-color:var(--vm-ui-danger,#c62828); color:var(--vm-ui-danger,#c62828); } .vm-ui-button:hover:enabled { box-shadow:inset 0 0 0 1px currentColor; } .vm-ui-button:disabled, .vm-ui-field:disabled { opacity:.5; cursor:not-allowed; } .vm-ui-button:focus-visible, .vm-ui-field:focus-visible { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:2px; box-shadow:var(--vm-ui-focus-ring,none); } .vm-ui-field { min-width:0; background:var(--vm-ui-input-bg,#fff); color:var(--vm-ui-input-text,var(--vm-ui-text,#222)); } input.vm-ui-field:not(.vm-ui-color) { height:24px; min-height:24px; padding:2px 6px; } select.vm-ui-field { height:24px; min-height:24px; padding:2px 24px 2px 6px; } .vm-ui-textarea { display:block; width:100%; resize:vertical; } .vm-ui-compact { min-height:var(--vm-ui-compact-height,24px); padding:2px 6px; font-size:var(--vm-ui-small-font-size,11px); } .vm-ui-icon { flex:0 0 auto; width:var(--vm-ui-control-height,32px); height:var(--vm-ui-control-height,32px); padding:0; font-size:18px; line-height:1; } .vm-ui-icon.vm-ui-compact { width:var(--vm-ui-compact-height,24px); height:var(--vm-ui-compact-height,24px); } .vm-ui-icon.vm-ui-table-icon { width:18px; height:18px; min-height:18px; padding:0; font-size:11px; } .vm-helper-ui .vm-ui-color { appearance:none; -webkit-appearance:none; box-sizing:border-box; width:34px !important; min-width:34px !important; max-width:34px !important; height:24px !important; min-height:24px !important; max-height:24px !important; align-self:center !important; padding:2px !important; border:1px solid var(--vm-ui-input-border,#c9c9c9); border-radius:4px; background:var(--vm-ui-bg,#fff); box-shadow:none; cursor:pointer; } .vm-ui-color::-webkit-color-swatch-wrapper { padding:0; } .vm-ui-color::-webkit-color-swatch { border:0; border-radius:2px; } .vm-ui-category-row { display:grid; grid-template-columns:34px minmax(0,1fr) 28px; gap:var(--vm-ui-gap,8px); align-items:center; } .vm-ui-category-row .vm-ui-field:not(.vm-ui-color) { height:24px; min-height:24px; padding:2px 6px; } .vm-ui-category-row .vm-ui-button.vm-ui-icon { width:28px; height:28px; min-height:28px; } .vm-ui-label { display:block; margin:0 0 6px; font-size:var(--vm-ui-font-size,13px); font-weight:700; } .vm-ui-help { color:var(--vm-ui-text-muted,#666); font-size:var(--vm-ui-small-font-size,11px); line-height:1.4; } .vm-ui-error { color:var(--vm-ui-danger,#c62828); font-weight:700; } .vm-ui-empty { padding:14px; color:var(--vm-ui-text-muted,#666); text-align:center; } .vm-ui-choice { display:flex; align-items:flex-start; gap:var(--vm-ui-gap,8px); margin:8px 0; cursor:pointer; } .vm-ui-choice > input { flex:0 0 auto; width:16px; height:16px; margin:2px 0 0; } .vm-ui-timeline-dialog { height:auto; padding:var(--vm-ui-dialog-padding,18px); overflow:auto; } #vm-escort-list-pages-progress, .vm-custom-filter-progress { display:flex; align-items:center; justify-content:center; gap:10px; box-sizing:border-box; width:100%; margin:2px 0 6px; padding:8px 12px; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:7px; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 12%,var(--vm-ui-bg,#fff)); color:var(--vm-ui-text,#211d22) !important; font:700 12px/16px var(--vm-ui-font-family,Lato,Arial,sans-serif); white-space:nowrap; user-select:none; } #vm-escort-list-pages-progress[hidden], .vm-custom-filter-progress[hidden] { display:none !important; } #vm-escort-list-pages-progress > .vm-list-pages-progress-text, .vm-custom-filter-progress > .vm-custom-filter-progress-text { color:inherit !important; opacity:1; } #vm-escort-list-pages-progress .vm-list-pages-cancel, .vm-custom-filter-progress .vm-custom-filter-cancel { flex:0 0 auto; min-height:24px; padding:3px 10px; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:6px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-text,#211d22) !important; font:800 11px var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor:pointer; } #vm-escort-list-pages-progress .vm-list-pages-cancel:hover, .vm-custom-filter-progress .vm-custom-filter-cancel:hover { background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 8%,var(--vm-ui-bg,#fff)); } `; document.head?.appendChild(style); try { document.dispatchEvent(new CustomEvent( 'vm-escort-helper-style-ready', {detail:{version:HELPER_STYLE_API_VERSION}} )); } catch {} } const HELPER_THEME_CUSTOM_PROPERTIES=[ '--vm-ui-accent', '--vm-ui-accent-contrast', '--vm-ui-bg', '--vm-ui-bg-alt', '--vm-ui-bg-hover', '--vm-ui-text', '--vm-ui-text-muted', '--vm-ui-text-faint', '--vm-ui-border', '--vm-ui-border-strong', '--vm-ui-input-bg', '--vm-ui-input-text', '--vm-ui-input-border', '--vm-ui-overlay', '--vm-ui-shadow', '--vm-ui-panel-bg', '--vm-ui-panel-bg-alt', '--vm-ui-panel-text', '--vm-ui-panel-muted', '--vm-ui-panel-border', '--vm-ui-success', '--vm-ui-warning', '--vm-ui-danger', '--vm-ui-color-scheme', '--vm-ui-font-family', '--vm-ui-control-height','--vm-ui-compact-height','--vm-ui-radius', '--vm-ui-dialog-radius','--vm-ui-dialog-padding','--vm-ui-gap', '--vm-ui-font-size','--vm-ui-small-font-size','--vm-ui-focus-ring' ]; function copyHelperThemeVariablesIntoDocument( targetDocument, sourceDocument=document ) { if ( !targetDocument?.documentElement || !sourceDocument?.documentElement ) { return; } const sourceView= sourceDocument.defaultView || window; let computed=null; try { computed= sourceView.getComputedStyle( sourceDocument.documentElement ); } catch { return; } const targetRoot= targetDocument.documentElement; for (const property of HELPER_THEME_CUSTOM_PROPERTIES) { const value= computed .getPropertyValue(property) .trim(); if (value) { targetRoot.style.setProperty( property, value ); } } targetRoot.dataset.vmEscortHelperStyleApi= HELPER_STYLE_API_VERSION; } function prepareHelperStyleApi(element) { if (!element) return element; ensureHelperStyleApi(); element.classList.add(HELPER_STYLE_API_CLASS); element.dataset.vmStyleApi=HELPER_STYLE_API_VERSION; return element; } function getEscortPagePinkColor() { const fallback='#f54da3'; try { if (typeof getComputedStyle === 'function') { const themed=getComputedStyle( document.documentElement ).getPropertyValue('--vm-ui-accent').trim(); if (themed) return themed; } const sample=[...document.querySelectorAll('.btn.btn-pink')] .find(element => !element.closest('[id^="vm-escort-"]')); if (!sample || typeof getComputedStyle !== 'function') return fallback; const computed=getComputedStyle(sample); const candidates=[computed.backgroundColor,computed.borderTopColor]; return candidates.find(value => value && value !== 'transparent' && value !== 'rgba(0, 0, 0, 0)' ) || fallback; } catch { return fallback; } } ensureHelperStyleApi(); const ESCORT_CUSTOM_FILTER_FIELDS=Object.fromEntries( Object.entries(DEFAULT_ESCORT_CUSTOM_FILTERS).map(([key,fallback]) => { if (typeof fallback === 'boolean') return [key,[Boolean]]; if (key === 'tagCountLimit') return [key,[normalizeCustomFilterInteger,69,0,10000]]; if (typeof fallback === 'number') return [key,key.startsWith('showOnlyBmi') ? [normalizeCustomFilterNumber,fallback,0,100] :[normalizeCustomFilterInteger,fallback,key.endsWith('Value') ? 1:0]]; const normalize=key.endsWith('Unit') ? normalizeCustomFilterAgeUnit :key.endsWith('Day') ? normalizeCustomFilterWeekday :key.endsWith('Time') ? normalizeCustomFilterTime :key.endsWith('Category') ? normalizeCustomFilterCategory :normalizeCustomFilterQuery; return [key,[normalize]]; }) ); function normalizeEscortCustomFilterField(key,value,tagFallback=69) { const [normalize,...args]=ESCORT_CUSTOM_FILTER_FIELDS[key]; if (key === 'tagCountLimit') args[0]=tagFallback; return normalize(value,...args); } function normalizeEscortCustomFilters(value={}) { const filters={...DEFAULT_ESCORT_CUSTOM_FILTERS,...value}; const inheritedTagCountLimit=Object.prototype.hasOwnProperty.call(value || {},'tagCountLimit') && value.tagCountLimit != null ? value.tagCountLimit :getSettings().alarmingTagCountLimit; return Object.fromEntries(Object.keys(ESCORT_CUSTOM_FILTER_FIELDS).map(key => [ key,normalizeEscortCustomFilterField(key,key === 'tagCountLimit' ? inheritedTagCountLimit:filters[key]) ])); } function getEscortCustomFilters() { const session=getFavoriteSearchSession(); if (session) return normalizeEscortCustomFilters(session.search.filters); try { const saved=GM_getValue(ESCORT_CUSTOM_FILTERS_STORAGE_KEY,null); const source=saved && typeof saved === 'object' ? saved:{}; return normalizeEscortCustomFilters(source); } catch { return normalizeEscortCustomFilters(); } } function saveEscortCustomFilters(filters) { const normalized=normalizeEscortCustomFilters(filters); const session=getFavoriteSearchSession(); if (session) { session.search.filters=normalized; persistFavoriteSearchSession(); return normalized; } try { writeHelperStorageValue(ESCORT_CUSTOM_FILTERS_STORAGE_KEY,normalized); } catch (error) { log('Nie udało się zapisać filtrów niestandardowych',error); } return normalized; } function normalizeListPagesToShow(value) { if (value === 'all') return 'all'; const count=Number(value); return LIST_PAGE_OPTIONS.some(([allowed]) => allowed === count) ? count :DEFAULT_LIST_PAGES_TO_SHOW; } function consumeOneTimeListPagesToShow() { // Jednorazowy wybór zużywa tylko główna karta, nigdy ramka. if (window.top !== window.self) return DEFAULT_LIST_PAGES_TO_SHOW; try { const saved=sessionStorage.getItem(ESCORT_LIST_PAGES_RELOAD_KEY); sessionStorage.removeItem(ESCORT_LIST_PAGES_RELOAD_KEY); return saved == null ? DEFAULT_LIST_PAGES_TO_SHOW :normalizeListPagesToShow(saved); } catch { return DEFAULT_LIST_PAGES_TO_SHOW; } } function requestOneTimeListPagesReload(value) { try { sessionStorage.setItem( ESCORT_LIST_PAGES_RELOAD_KEY, String(normalizeListPagesToShow(value)) ); return true; } catch { return false; } } function normalizeOptionValue(value,options,fallback,convert=String) { const normalized=convert(value ?? ''); return options.some(([allowed]) => allowed === normalized) ? normalized :fallback; } function normalizeAutoGarsoAnalysisMode(value) { return value === 'count' || value === 'content' ? value :DEFAULT_SETTINGS.autoGarsoAnalysisMode; } function normalizeEscortPriceDuration(value) { return normalizeOptionValue( value, ESCORT_PRICE_DISPLAY_OPTIONS, DEFAULT_SETTINGS.searchResultPriceDuration ); } function normalizePhoneClipboardFormat(value) { return normalizeOptionValue( value, PHONE_CLIPBOARD_FORMAT_OPTIONS, DEFAULT_SETTINGS.phoneClipboardFormat ); } function normalizeTopPhoneSearchEnterTarget(value) { return normalizeOptionValue( value, TOP_PHONE_SEARCH_TARGET_OPTIONS, DEFAULT_SETTINGS.topPhoneSearchEnterTarget ); } function normalizeTopPhoneSearchOpenMode(value) { return normalizeOptionValue( value, TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS, DEFAULT_SETTINGS.topPhoneSearchOpenMode ); } function normalizeDefaultSearchLocation(value) { const source=value && typeof value === 'object' ? value:{}; const clean=(field,maxLength=100) => String(field ?? '') .replace(/\s+/g,' ') .trim() .slice(0,maxLength); return { countryValue:'33', countryLabel:'Polska', provinceValue:clean(source.provinceValue,20), provinceLabel:clean(source.provinceLabel), cityValue:clean(source.cityValue,20), cityLabel:clean(source.cityLabel), districtValue:clean(source.districtValue,20), districtLabel:clean(source.districtLabel) }; } function normalizeSearchLocations(value) { const source=Array.isArray(value) ? value:[]; const seen=new Set(); const result=[]; for (const candidate of source) { const locationData=normalizeDefaultSearchLocation(candidate); const [province,city,district]=['province','city','district'] .map(key => locationData[key + 'Value'] || locationData[key + 'Label']); if ((!province && !city) || (district && !city)) continue; const key=getEscortLocationKey(locationData); if (seen.has(key)) continue; seen.add(key); result.push(locationData); if (result.length >= ESCORT_MULTI_LOCATION_MAX) break; } return result; } function normalizeSavedLocationGroups(value) { const source=Array.isArray(value) ? value:[]; const groups=[]; const seen=new Set(); for (const candidate of source) { const rawLocations=Array.isArray(candidate) ? candidate :(Array.isArray(candidate?.locations) ? candidate.locations:[candidate]); const locations=normalizeSearchLocations(rawLocations); if (!locations.length) continue; const key=locations .map(getEscortLocationKey) .sort() .join('||'); if (!key || seen.has(key)) continue; seen.add(key); groups.push(locations); if (groups.length >= 30) break; } return groups; } function normalizeEscortAggregationMode(value) { return normalizeOptionValue( value, ESCORT_AGGREGATION_MODE_OPTIONS, DEFAULT_SETTINGS.escortAggregationMode ); } function normalizeEscortRelatedAdsTileLinkMode(value) { return normalizeOptionValue( value, ESCORT_RELATED_ADS_TILE_LINK_MODE_OPTIONS, DEFAULT_SETTINGS.relatedAdsTileLinkMode ); } function normalizeEscortCompactSummaryMode(value) { const normalized=String(value || ''); return normalizeOptionValue( normalized, ESCORT_COMPACT_SUMMARY_MODE_OPTIONS, DEFAULT_SETTINGS.compactSummaryComparisonMode ); } function getEscortListSearchMode(settings=SETTINGS) { return settings?.mergeEscortAds && normalizeEscortAggregationMode(settings.escortAggregationMode) === 'phone-escorti' ? 'phone-escorti' :'address-escorti'; } function getEscortPriceDurationLabel(value) { const normalized=normalizeEscortPriceDuration(value); return ESCORT_PRICE_DISPLAY_OPTIONS.find(([allowed]) => allowed === normalized)?.[1] || '1 h'; } function isEscortSearchPriceDisplayed(settings=SETTINGS) { return !!settings?.showPricesInSearchResults && normalizeEscortPriceDuration(settings.searchResultPriceDuration) !== 'hide'; } function normalizeHistoryCleanupKeywords(value) { const source=value == null ? DEFAULT_HISTORY_CLEANUP_KEYWORDS :(Array.isArray(value) ? value:String(value).split(/[\n,;]+/)); const seen=new Set(); const keywords=[]; for (const candidate of source) { let keyword=String(candidate || '').replace(/\s+/g,' ').trim().slice(0,160); if (keyword.toLocaleLowerCase('pl-PL') === 'lens.google.com') { keyword='google.com/search?vsrid'; } const key=keyword.toLocaleLowerCase('pl-PL'); if (!keyword || seen.has(key)) continue; seen.add(key); keywords.push(keyword); if (keywords.length >= 100) break; } return keywords; } function normalizeWatchProfileResolutionMode(value) { const normalized=String(value || ''); return WATCH_PROFILE_RESOLUTION_OPTIONS.some(([allowed]) => allowed === normalized) ? normalized :DEFAULT_SETTINGS.watchProfileResolutionMode; } function normalizeStoredWatchProfileResolutionMode(value) { const raw=String(value || ''); if (raw === 'search-escort') return raw; if (raw) { const normalized=normalizeWatchProfileResolutionMode(raw); if (normalized !== 'ask') return normalized; } const configured=normalizeWatchProfileResolutionMode( getSettings().watchProfileResolutionMode ); return configured === 'ask' ? 'address-escorti':configured; } function normalizeEscortDataFieldId(value) { const raw=String(value || '').trim(); if (!raw || raw === 'none') return 'none'; if (!/^(?:ad|profile|escorti|garso):/i.test(raw)) return 'none'; const parts=raw.split(':'); if (parts.length < 2 || parts.some(part => !part)) return 'none'; return raw.slice(0,320); } function normalizeEscortDataFieldList(value,maxItems) { const source=Array.isArray(value) ? value:[]; const result=[]; const seen=new Set(); for (const candidate of source) { const field=normalizeEscortDataFieldId(candidate); if (!field || field === 'none' || seen.has(field)) continue; seen.add(field); result.push(field); if (result.length >= maxItems) break; } return result; } function normalizeSearchResultExtraFields(value) { return normalizeEscortDataFieldList( value, SEARCH_RESULT_MAX_EXTRA_FIELDS ); } function getSearchResultExtraFields(settings=SETTINGS) { return normalizeSearchResultExtraFields( settings?.searchResultExtraFields ); } function normalizeFavoritesDefaultView(value) { return value === 'tiles' ? 'tiles':'table'; } function normalizeFavoritesDataColumns(value) { return normalizeEscortDataFieldList( value, FAVORITES_MAX_DATA_COLUMNS ); } const ESCORT_DATA_FIELD_TECHNICAL_ROOT_KEYS=new Set([ 'status','adId','adUrl','phoneId','phoneDigits','checkedAt', 'fetchTransport','phoneFetchStatus','phoneCheckedAt','phoneAttemptedAt','lastVisitedAt', 'pageCheckedAt','visitSnapshot','watchSnapshot', 'watchSnapshotCheckedAt','escortiActivity' ]); const ESCORT_PROFILE_FIELD_TECHNICAL_KEYS=new Set([ 'adDetails','activeUrls','unknownCount','partial','locationPartial','lastKnownCities','escortiData' ]); const ESCORTI_DATA_FIELD_TECHNICAL_KEYS=new Set([ 'status','searchMode','profileIds','profileUrls','profileUrl', 'adIds','adUrls','adLinksVersion','profileSummaries','cityHistory','partial', 'merged','checkedAt' ]); let escortDataFieldCatalogMemory=null; let escortDataFieldCatalogBootstrapped=false; let escortDataFieldCatalogSaveTimer=null; let escortDataFieldCatalogUiTimer=null; function encodeEscortDataFieldSegment(value) { return encodeURIComponent(String(value || '').trim()); } function decodeEscortDataFieldSegment(value) { try {return decodeURIComponent(String(value || ''));} catch {return String(value || '');} } function buildEscortDataFieldId(source,path) { const cleanSource=['profile','escorti','garso'].includes(source) ? source:'ad'; const segments=(path || []).map(encodeEscortDataFieldSegment).filter(Boolean); return segments.length ? `${cleanSource}:${segments.join(':')}`:'none'; } function parseEscortDataFieldId(field) { const normalized=normalizeEscortDataFieldId(field); if (normalized === 'none') return null; const [source,...encodedPath]=normalized.split(':'); return { id:normalized, source, path:encodedPath.map(decodeEscortDataFieldSegment) }; } function prettifyEscortDataFieldKey(value) { const text=String(value || '') .replace(/[_-]+/g,' ') .replace(/([a-ząćęłńóśźż0-9])([A-ZĄĆĘŁŃÓŚŹŻ])/g,'$1 $2') .replace(/\s+/g,' ') .trim(); return text ? text.charAt(0).toLocaleUpperCase('pl-PL') + text.slice(1):'Dane'; } function getEscortDataFieldLabel(source,path) { const [root,second,...rest]=path || []; if (source === 'garso') return `Garso - ${getGarsoDataFields().find(([key]) => key === root)?.[1] || root} (cache)`; if (source === 'profile') { const known={ totalAds:'Liczba anonsów', activeCount:'Znane anonse', cities:'Lokalizacja', profileDates:'Profil - daty założenia powiązanych profili Escorti', online:'Stan online', checkedAt:'Ostatnie sprawdzenie przez skrypt', hidden:'Anons/Profil - czy ukryty' }; if (known[root]) return known[root]; return `Profil - ${[root,second,...rest].filter(Boolean).map(prettifyEscortDataFieldKey).join(' - ')}`; } if (source === 'escorti') { const known={ profiles:'Escorti - liczba profili', adLinks:'Escorti - liczba anonsów', creationDate:'Escorti - data założenia profilu', currentCity:'Escorti - aktualne miasto', historicalCityCount: 'Escorti - liczba miast (historycznie)', garsoTopics:'Escorti - liczba tematów Garso', garsoTopicTitles:'Escorti - tytuły tematów Garso' }; if (known[root]) return known[root]; return `Escorti - ${[root,second,...rest].filter(Boolean).map(prettifyEscortDataFieldKey).join(' - ')}`; } if (root === 'prices' && second) { const knownDuration=ESCORT_PRICE_DURATION_OPTIONS.find(([value]) => String(value) === String(second))?.[1]; const minutes=Number(second); const durationLabel=knownDuration || (Number.isFinite(minutes) && minutes > 0 ? (minutes % 60 === 0 ? `${minutes / 60} h`:`${minutes} min`) :prettifyEscortDataFieldKey(second)); return `Cena ${durationLabel}`; } if (root === 'stats' && second) { return [second,...rest].filter(Boolean).map(prettifyEscortDataFieldKey).join(' - '); } if (root === 'tags' && second) { return `Tagi - ${normalizeEscortAdText(second) || prettifyEscortDataFieldKey(second)}`; } if (root === 'availability' && second) { return `Dostępność - ${[second,...rest].filter(Boolean).map(prettifyEscortDataFieldKey).join(' - ')}`; } if (root === 'location' && second) { const locationKnown={ country:'Kraj', province:'Województwo', city:'Miasto', district:'Dzielnica', area:'Obszar', addressText:'Adres w anonsie', addressParts:'Adres na mapie' }; return `Lokalizacja - ${locationKnown[second] || prettifyEscortDataFieldKey(second)}`; } const known={ title:'Nazwa anonsu', description:'Opis', phone:'Numer telefonu', datePosted:'Data włączenia anonsu', lastVisitedAt:'Ostatnie odwiedziny anonsu', tagCount:'Liczba usług (tagów)', declaredBmi:'Deklarowane BMI', dimensions:'Wymiary', tags:'Tagi', isAgencyOrSalonPhone:'Telefon agencji/salonu' }; if (known[root]) return known[root]; return `Anons - ${[root,second,...rest].filter(Boolean).map(prettifyEscortDataFieldKey).join(' - ')}`; } function getEscortDataFieldKind(source,path,value) { const [root]=path || []; if (source === 'garso') return 'number'; if (source === 'ad' && root === 'prices') return 'price'; if (source === 'ad' && root === 'tags' && path.length > 1) return 'boolean'; if (source === 'ad' && root === 'lastVisitedAt') return 'relative-time'; if ( source === 'ad' && ( root === 'tagCount' || root === 'declaredBmi' ) ) return 'number'; if (source === 'ad' && root === 'dimensions') return 'text'; if ( source === 'escorti' && root === 'historicalCityCount' ) return 'number'; if (root === 'description') return 'long-text'; if (/date/i.test(root || '')) return 'date'; if (/checkedAt$/i.test(root || '')) return 'datetime'; if (typeof value === 'boolean') return 'boolean'; if (Array.isArray(value)) return 'list'; if (typeof value === 'number') return 'number'; return 'text'; } function makeEscortDataFieldDescriptor(source,path,value=null) { const id=buildEscortDataFieldId(source,path); if (id === 'none') return null; return { id, source, path:[...path], label:getEscortDataFieldLabel(source,path), kind:getEscortDataFieldKind(source,path,value) }; } function readEscortDataFieldCatalog() { if (escortDataFieldCatalogMemory) return escortDataFieldCatalogMemory; const map=new Map(); try { const stored=GM_getValue(ESCORT_DATA_FIELD_CATALOG_STORAGE_KEY,[]); for (const entry of Array.isArray(stored) ? stored:[]) { const parsed=parseEscortDataFieldId(entry?.id); if (!parsed) continue; map.set(parsed.id,{ id:parsed.id, source:parsed.source, path:parsed.path, label:getEscortDataFieldLabel(parsed.source,parsed.path), kind:normalizeEscortAdText(entry?.kind) || 'text' }); if (map.size >= ESCORT_DATA_FIELD_CATALOG_MAX) break; } } catch {} escortDataFieldCatalogMemory=map; return map; } function persistEscortDataFieldCatalog() { clearTimeout(escortDataFieldCatalogSaveTimer); escortDataFieldCatalogSaveTimer=null; try { const values=[...readEscortDataFieldCatalog().values()] .sort((a,b) => a.label.localeCompare(b.label,'pl')) .slice(0,ESCORT_DATA_FIELD_CATALOG_MAX) .map(entry => ({id:entry.id,kind:entry.kind})); writeHelperStorageValue(ESCORT_DATA_FIELD_CATALOG_STORAGE_KEY,values); } catch {} } function scheduleEscortDataFieldCatalogChanged() { clearTimeout(escortDataFieldCatalogSaveTimer); escortDataFieldCatalogSaveTimer=setTimeout(persistEscortDataFieldCatalog,450); clearTimeout(escortDataFieldCatalogUiTimer); escortDataFieldCatalogUiTimer=setTimeout(() => { escortDataFieldCatalogUiTimer=null; try {window.dispatchEvent(new CustomEvent('vmEscortDataCatalogChanged'));} catch {} },120); } function isUsefulEscortDataValue(value) { if (value == null || value === '') return false; if (Array.isArray(value)) return value.some(item => item != null && String(item).trim()); if (typeof value === 'object') return Object.keys(value).length > 0; return true; } function registerEscortDataField(source,path,value,schedule=true) { if (!isUsefulEscortDataValue(value)) return false; const descriptor=makeEscortDataFieldDescriptor(source,path,value); if (!descriptor) return false; const catalog=readEscortDataFieldCatalog(); const existing=catalog.get(descriptor.id); if (existing) { if (existing.kind !== 'text' || descriptor.kind === 'text') return false; existing.kind=descriptor.kind; } else { if (catalog.size >= ESCORT_DATA_FIELD_CATALOG_MAX) return false; catalog.set(descriptor.id,descriptor); } if (schedule) scheduleEscortDataFieldCatalogChanged(); return true; } function walkEscortDataFields(source,value,path=[],depth=0,schedule=true) { if (depth > 4 || !isUsefulEscortDataValue(value)) return false; const rootKey=path[0] || ''; if (source === 'ad' && path.length === 1 && ESCORT_DATA_FIELD_TECHNICAL_ROOT_KEYS.has(rootKey)) return false; if (source === 'profile' && path.length === 1 && ESCORT_PROFILE_FIELD_TECHNICAL_KEYS.has(rootKey)) return false; if (source === 'escorti' && path.length === 1 && ESCORTI_DATA_FIELD_TECHNICAL_KEYS.has(rootKey)) return false; if (source === 'ad' && rootKey === 'prices' && path.length === 2) { return registerEscortDataField(source,path,value,schedule); } if (Array.isArray(value) || typeof value !== 'object' || value === null) { return registerEscortDataField(source,path,value,schedule); } let changed=false; for (const [key,child] of Object.entries(value)) { if (!isUsefulEscortDataValue(child)) continue; changed=walkEscortDataFields(source,child,[...path,key],depth + 1,schedule) || changed; } return changed; } function registerEscortDataFieldsFromAdData(adData,schedule=true) { if (!adData || typeof adData !== 'object') return false; let changed=false; for (const [key,value] of Object.entries(adData)) { if (ESCORT_DATA_FIELD_TECHNICAL_ROOT_KEYS.has(key)) continue; if (key === 'tags' && Array.isArray(value)) { for (const rawTag of value) { const tag=normalizeEscortAdText(rawTag); if (!tag) continue; changed=registerEscortDataField('ad',['tags',tag],true,schedule) || changed; } } changed=walkEscortDataFields('ad',value,[key],0,schedule) || changed; } return changed; } function registerEscortDataFieldsFromProfileState(state,schedule=true) { if (!state || typeof state !== 'object') return false; let changed=false; for (const [key,value] of Object.entries(state)) { if (ESCORT_PROFILE_FIELD_TECHNICAL_KEYS.has(key)) continue; changed=walkEscortDataFields('profile',value,[key],0,schedule) || changed; } return changed; } function registerEscortDataFieldsFromEscortiResult(result,schedule=true) { if (!result || typeof result !== 'object') return false; let changed=false; for (const [key,value] of Object.entries(result)) { if (ESCORTI_DATA_FIELD_TECHNICAL_KEYS.has(key)) continue; if (Array.isArray(value) && value.some(item => item && typeof item === 'object')) continue; changed=walkEscortDataFields('escorti',value,[key],0,schedule) || changed; } return changed; } function ensureEscortDataFieldCatalog() { const catalog=readEscortDataFieldCatalog(); if (escortDataFieldCatalogBootstrapped || (deferredNonTopicCache && catalog.size)) return catalog; escortDataFieldCatalogBootstrapped=true; if (catalog.size) return catalog; let changed=false; try { for (const key of getEscortAdDataCacheKeys()) { const value=readPersistentCacheValue(key,null); changed=registerEscortDataFieldsFromAdData(value,false) || changed; } } catch {} try { for (const key of getEscortListCacheKeys()) { const store=readPersistentCacheValue(key,null); for (const [mode,entry] of Object.entries(store?.modes || {})) { const expanded=expandEscortListCacheEntry(entry,mode); changed=registerEscortDataFieldsFromEscortiResult(expanded,false) || changed; } } } catch {} try { for (const item of getWatchedItems()) { changed=registerEscortDataFieldsFromProfileState(item?.state,false) || changed; } } catch {} if (changed) scheduleEscortDataFieldCatalogChanged(); return catalog; } function getEscortDataFieldDescriptor(field) { const parsed=parseEscortDataFieldId(field); if (!parsed) return null; const stored=ensureEscortDataFieldCatalog().get(parsed.id); return stored || makeEscortDataFieldDescriptor(parsed.source,parsed.path,null); } function getEscortDataFieldOptions(selectedField='none',forFavorites=false) { const selected=normalizeEscortDataFieldId(selectedField); const descriptors=[...ensureEscortDataFieldCatalog().values()]; if (!forFavorites) { for (const [alias,canonical] of [['profile:totalAds','escorti:adLinks'],['profile:profileDates','escorti:creationDate']]) { if (descriptors.some(entry => entry.id === alias) && !descriptors.some(entry => entry.id === canonical)) { descriptors.push(getEscortDataFieldDescriptor(canonical)); } } } const virtualDescriptors=[ ...getGarsoDataFields().map(([key]) => makeEscortDataFieldDescriptor('garso',[key])), ...(forFavorites ? [makeEscortDataFieldDescriptor('profile',['hidden'],false)]:[]), makeEscortDataFieldDescriptor( 'ad', ['lastVisitedAt'], null ), makeEscortDataFieldDescriptor( 'ad', ['tagCount'], null ), makeEscortDataFieldDescriptor( 'ad', ['declaredBmi'], null ), makeEscortDataFieldDescriptor( 'ad', ['dimensions'], null ), makeEscortDataFieldDescriptor( 'escorti', ['historicalCityCount'], null ) ].filter(Boolean); for (const descriptor of virtualDescriptors) { if (!descriptors.some(entry => entry.id === descriptor.id)) descriptors.push(descriptor); } if (selected !== 'none' && !descriptors.some(entry => entry.id === selected)) { const descriptor=getEscortDataFieldDescriptor(selected); if (descriptor) descriptors.push(descriptor); } descriptors.sort((a,b) => { const sourceOrder={ad:0,escorti:1,profile:2}; if (a.source !== b.source) return (sourceOrder[a.source] ?? 9) - (sourceOrder[b.source] ?? 9); return a.label.localeCompare(b.label,'pl'); }); const hasTopicTitles=descriptors.some(entry => entry.id === 'escorti:garsoTopicTitles') && getEscortListCacheKeys().some(key => Object.values(readPersistentCacheValue(key,null)?.modes || {}) .some(entry => entry?.garsoTopicTitles?.some(title => String(title || '').trim()))); return [['none','- brak -'],...descriptors.filter(entry => (entry.id !== 'escorti:garsoTopicTitles' || hasTopicTitles) && (forFavorites || entry.id === selected || !['profile:totalAds','profile:profileDates'].includes(entry.id)) ).map(entry => [entry.id,entry.label])]; } function getEscortDataValueByPath(object,path) { let value=object; for (const key of path || []) { if (value == null || typeof value !== 'object') return null; if (Object.prototype.hasOwnProperty.call(value,key)) { value=value[key]; continue; } const wanted=normalizeEscortChangeComparisonValue(key).replace(/:\s*$/,''); const matchedKey=Object.keys(value).find(candidate => normalizeEscortChangeComparisonValue(candidate).replace(/:\s*$/,'') === wanted ); if (!matchedKey) return null; value=value[matchedKey]; } return value; } function getDeclaredEscortBmi(adData) { if ( !adData || !adData.stats || typeof adData.stats !== 'object' ) { return null; } let heightCm=null; let weightKg=null; for (const [rawLabel,rawValue] of Object.entries( adData.stats )) { const label= normalizeEscortChangeComparisonValue( rawLabel ).replace(/:\s*$/,''); const match=String(rawValue ?? '') .replace(',','.') .match(/\d+(?:\.\d+)?/); const number=match ? Number(match[0]) :NaN; if (!Number.isFinite(number)) continue; if (label === 'wzrost') { heightCm=number; } else if (label === 'waga') { weightKg=number; } } if ( !Number.isFinite(heightCm) || !Number.isFinite(weightKg) || heightCm <= 0 || weightKg <= 0 ) { return null; } const heightM=heightCm / 100; const bmi= weightKg / (heightM * heightM); return Number.isFinite(bmi) ? bmi :null; } function getEscortiHistoricalCityStats( results ) { const counts=new Map(); const displayNames=new Map(); const seen=new Set(); const add=( cityValue, profileIdentity, dateIdentity='current' ) => { const city= normalizeEscortCity(cityValue); if (!city) return; const cityKey= normalizeEscortCityKey(city); if (!cityKey) return; const profileKey= String( profileIdentity || 'profil' ); const occurrenceKey= `${profileKey}|${cityKey}|${ String(dateIdentity || '') }`; if (seen.has(occurrenceKey)) return; seen.add(occurrenceKey); if (!displayNames.has(cityKey)) { displayNames.set( cityKey, city ); } counts.set( cityKey, (counts.get(cityKey) || 0) + 1 ); }; for (const result of Array.isArray(results) ? results :[]) { const profiles= getEscortiProfileSummariesForResearch( result ); for (const profile of profiles) { const profileIdentity= profile?.profileUrl || profile?.profileName || result?.profileUrl || 'profil'; add( profile?.currentCity, profileIdentity, 'current' ); for (const entry of Array.isArray( profile?.cityHistory ) ? profile.cityHistory :[]) { add( entry?.city, profileIdentity, entry?.date || entry?.city ); } } } return [...counts.entries()] .map(([cityKey,count]) => ({ city: displayNames.get(cityKey) || cityKey, count })) .sort((a,b) => b.count - a.count || a.city.localeCompare( b.city, 'pl-PL' ) ); } function getEscortiCityHistoryOverlaps(results) { const days=new Map(); for (const result of results) for (const profile of getEscortiProfileSummariesForResearch(result)) { const identity=profile.profileUrl || profile.profileName; if (!identity) continue; for (const entry of profile.cityHistory || []) { const date=formatEscortiDate(entry.date),city=normalizeEscortCity(entry.city); if (!date || !city) continue; const key=`${identity}|${date}`; if (!days.has(key)) days.set(key,{date,url:profile.profileUrl,cities:new Map()}); days.get(key).cities.set(normalizeEscortCityKey(city),city); } } return [...days.values()].filter(day => day.cities.size > 1); } function getResearchConsistencySections(facts) { const sameEscorti=facts.sameEscortiResult === true || areSameEscortiResults(facts.escortiAddressResult,facts.escortiPhoneResult); const sources=[ ['Escorti.pl',sameEscorti ? 'anonse znalezione po adresie ogłoszenia i numerze telefonu na Escorti.pl' :'anonse znalezione po adresie ogłoszenia na Escorti.pl',facts.escortiAddressSummary], ...(!sameEscorti ? [['Escorti.pl - nr tel.','anonse znalezione po numerze telefonu na Escorti.pl',facts.escortiPhoneSummary]]:[]), ['Escort.club - nr tel.','anonse znalezione po numerze telefonu na Escort.club',facts.escortPhoneSummary] ]; const sections=new Map(); for (const [title,subtitle,summary] of sources) { const statFields=[]; const warnings=getEscortActiveAdsConsistencyWarnings(summary,statFields); if (!warnings.length) continue; const ads=(summary.results || []).filter(item => item.active && item.adData); const ids=ads.map(item => parseAdIdFromUrl(item.url)).filter(Boolean); const key=ids.length === ads.length ? [...new Set(ids)].sort().join('|'):title; const existing=sections.get(key); if (existing) { existing.title += ` / ${title}`; existing.subtitle += `; ${subtitle}`; } else sections.set(key,{title,subtitle,warnings,statFields}); } return [...sections.values()]; } function getEscortiHistoricalCitiesFromResults( results ) { return getEscortiHistoricalCityStats( results ).map(item => item.city); } function getEscortiHistoricalCityCount(result) { const count= getEscortiHistoricalCityStats( [result] ).length; return count || null; } function getEscortDataFieldRawValue(object,descriptor) { if (!descriptor) return null; if (descriptor.source === 'ad' && descriptor.path?.[0] === 'tags' && descriptor.path.length > 1) { const tags=object?.tags; if (!Array.isArray(tags)) return null; const wanted=normalizeEscortChangeComparisonValue(descriptor.path[1]); return tags.some(tag => normalizeEscortChangeComparisonValue(tag) === wanted); } if ( descriptor.source === 'ad' && descriptor.path?.[0] === 'tagCount' ) { return Array.isArray(object?.tags) ? object.tags.length :null; } if ( descriptor.source === 'ad' && descriptor.path?.[0] === 'declaredBmi' ) { return getDeclaredEscortBmi(object); } if ( descriptor.source === 'ad' && descriptor.path?.[0] === 'dimensions' ) { return getFavoriteTileDimensionsText(object) || null; } return getEscortDataValueByPath( object, descriptor.path ); } function formatEscortDataDateValue(value) { if (value == null || value === '') return ''; if (Number.isFinite(Number(value)) && Number(value) > 100000000000) { try { return new Intl.DateTimeFormat('pl-PL',{dateStyle:'short',timeStyle:'short'}) .format(new Date(Number(value))); } catch {} } return formatEscortiDate(value) || normalizeEscortAdText(value); } function formatEscortUniversalDataValue(descriptor,value,compact=false) { if (!descriptor || value == null || value === '') return ''; if (descriptor.source === 'garso') return descriptor.path[0] === 'reviewCount' ? String(value):`${value}/10`; if ( descriptor.source === 'ad' && descriptor.path?.[0] === 'declaredBmi' ) { const number=Number(value); return Number.isFinite(number) ? number .toFixed(1) .replace('.',',') :''; } if (descriptor.kind === 'price') { return formatEscortTilePrice(value); } if (descriptor.kind === 'relative-time') { return formatEscortLastVisit(value); } if (Array.isArray(value)) { const parts=value.map(item => descriptor.kind === 'date' || descriptor.kind === 'datetime' ? formatEscortDataDateValue(item) :normalizeEscortAdText(item) ).filter(Boolean); const joined=[...new Set(parts)].join(' • '); return compact && joined.length > 70 ? `${joined.slice(0,67)}…`:joined; } if (descriptor.kind === 'date' || descriptor.kind === 'datetime') return formatEscortDataDateValue(value); if (descriptor.kind === 'boolean') { if (descriptor.source === 'profile' && descriptor.path?.[0] === 'online') return value ? 'online':'offline'; return value ? 'tak':'nie'; } if (typeof value === 'object') { try { const text=JSON.stringify(value); return compact && text.length > 70 ? `${text.slice(0,67)}…`:text; } catch {return '';} } const text=normalizeEscortAdText(String(value)); if (!compact) return text; const limit=descriptor.kind === 'long-text' ? 70:90; return text.length > limit ? `${text.slice(0,limit - 1)}…`:text; } function normalizeFavoriteCategoryList(value) { const source=Array.isArray(value) ? value:[value]; const seen=new Set(); const result=[]; for (const candidate of source) { const label=normalizeEscortAdText(candidate).slice(0,60); const key=label.toLocaleLowerCase('pl-PL'); if (!label || seen.has(key)) continue; seen.add(key); result.push(label); if (result.length >= 40) break; } return result; } function normalizeFavoriteCategories(value) { return normalizeFavoriteCategoryList( Array.isArray(value) ? value :DEFAULT_FAVORITE_CATEGORIES ); } function normalizeFavoriteAssignedCategories(value) { return normalizeFavoriteCategoryList(value); } function normalizeFavoriteCategoryColor(value) { const color=String(value || '').trim(); return /^#[0-9a-f]{6}$/i.test(color) ? color.toLowerCase():''; } function normalizeFavoriteCategoryColors(value,categories=[]) { const source=value && typeof value === 'object' ? value:{}; const result={}; for (const category of normalizeFavoriteCategories(categories)) { const color=normalizeFavoriteCategoryColor(source[category]); if (color) result[category]=color; } return result; } function favoriteCategoryTint(color,alpha=0.11) { const normalized=normalizeFavoriteCategoryColor(color); if (!normalized) return ''; const r=parseInt(normalized.slice(1,3),16); const g=parseInt(normalized.slice(3,5),16); const b=parseInt(normalized.slice(5,7),16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function ensureFavoriteCategoryHoverStyle() { if (document.getElementById('vm-favorites-category-hover-style')) return; appendHelperStyle('vm-favorites-category-hover-style',` .vm-favorites-category-chip { display:inline-flex;align-items:center;gap:3px;max-width:100%;min-width:0;height:18px; padding:0 3px 0 6px;border:1px solid var(--vm-favorite-category-color);border-radius:999px; background:var(--vm-favorite-category-tint);color:var(--vm-fav-text,var(--vm-ui-text,#222)); font-size:8.2px;font-weight:700;line-height:16px;box-sizing:border-box; } .vm-favorites-category-chip-label { overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0; } .vm-favorites-category-remove { flex:0 0 13px;width:13px;height:13px;padding:0;margin:0;border:0;border-radius:50%; background:transparent;color:var(--vm-ui-text-muted,#777);font-size:11px;font-weight:700; line-height:12px;text-align:center;cursor:pointer;opacity:.30;transition:opacity .10s ease; } .vm-favorites-category-add-button { display:inline-flex;flex:0 0 18px;align-items:center;justify-content:center;width:18px;min-width:18px; height:18px;padding:0;margin:0;border:1px solid var(--vm-ui-border,#d4d4d8);border-radius:999px; background:var(--vm-ui-bg-alt,rgba(250,250,250,.82));color:var(--vm-ui-text-muted,#777); font-size:13px;font-weight:700;line-height:16px;text-align:center;box-sizing:border-box;cursor:pointer; opacity:.28;visibility:visible;pointer-events:auto;touch-action:manipulation; transition:opacity .10s ease,border-color .10s ease,background .10s ease,color .10s ease,box-shadow .10s ease; } .vm-favorites-category-cell .vm-favorites-category-remove, .vm-favorites-category-cell .vm-favorites-category-add-button { opacity:.28 !important; } .vm-favorites-category-cell:hover .vm-favorites-category-remove, .vm-favorites-category-cell:hover .vm-favorites-category-add-button { opacity:1 !important; } .vm-favorites-category-cell:hover .vm-favorites-category-add-button { border-color:var(--vm-ui-border-strong,#a9a9af) !important;background:var(--vm-ui-bg,#fff) !important; color:var(--vm-ui-text,#444) !important;box-shadow:var(--vm-ui-shadow,0 1px 4px rgba(0,0,0,.12)) !important; } `); } function createFavoriteCategoryChip(value,color,onRemove) { ensureFavoriteCategoryHoverStyle(); const chip=makeElement('span','vm-favorites-category-chip'); chip.title=`${value} - kliknij ×, aby usunąć`; chip.style.setProperty('--vm-favorite-category-color',color); chip.style.setProperty('--vm-favorite-category-tint',favoriteCategoryTint(color,.10)); const label=makeElement('span','vm-favorites-category-chip-label',value); const remove=makeElement('button','vm-favorites-category-remove','×',{type:'button',title:`Kategoria „${value}”. ${trackedRemovalHint('category')}`}); remove.addEventListener('click',event => { stopUiEvent(event); onRemove(value); }); chip.append(label,remove); return chip; } function createFavoriteCategoryAddButton(onActivate,pointerDown=false) { ensureFavoriteCategoryHoverStyle(); const button=makeElement('button','vm-favorites-category-add-button','+'); button.type='button'; button.title='Dodaj kategorię'; const activate=event => { if (pointerDown && event.button != null && event.button !== 0) return; stopUiEvent(event); onActivate(button); }; if (pointerDown) { button.addEventListener('pointerdown',activate,true); button.addEventListener('click',stopUiEvent); } else { button.addEventListener('click',activate); } return button; } function normalizeEscortRequestLimitPerMinute(value,fallback=DEFAULT_SETTINGS.escortLimitPerMinute,max=840) { const number=typeof value === 'number' || (typeof value === 'string' && value.trim()) ? Number(value):NaN; return Number.isFinite(number) ? Math.min(max,Math.max(1,Math.floor(number))):fallback; } function normalizeEscortRequestMaxConcurrent(value,fallback=DEFAULT_SETTINGS.escortMaxConcurrent,max=24) { return normalizeEscortRequestLimitPerMinute(value,fallback,max); } function normalizeEscortQueueSettings(settings={}) { // Zachowaj sumę dawnych limitów, także zaokrąglenia ustawień sprzed 1.976. const legacyRate=settings.escortRequestLimitPerMinute == null ? null: Math.ceil(Math.max(5,normalizeEscortRequestLimitPerMinute(settings.escortRequestLimitPerMinute,240,420)) / 2); const hasLegacyRate=legacyRate !== null || settings.escortGetLimitPerMinute != null || settings.escortHeadLimitPerMinute != null; const combinedRate=hasLegacyRate ? normalizeEscortRequestLimitPerMinute(settings.escortGetLimitPerMinute ?? legacyRate,100,420) + normalizeEscortRequestLimitPerMinute(settings.escortHeadLimitPerMinute ?? legacyRate,120,420) :DEFAULT_SETTINGS.escortLimitPerMinute; const legacyConcurrent=settings.escortRequestMaxConcurrent == null ? null: normalizeEscortRequestMaxConcurrent(settings.escortRequestMaxConcurrent,4,12); const combinedConcurrent=settings.escortGetMaxConcurrent != null || settings.escortHeadMaxConcurrent != null || legacyConcurrent !== null ? normalizeEscortRequestMaxConcurrent(settings.escortGetMaxConcurrent ?? (legacyConcurrent === null ? null:Math.ceil(legacyConcurrent / 3)),4,12) + normalizeEscortRequestMaxConcurrent(settings.escortHeadMaxConcurrent ?? (legacyConcurrent === null ? null:Math.ceil(legacyConcurrent * 2 / 3)),4,12) :DEFAULT_SETTINGS.escortMaxConcurrent; return { escortLimitPerMinute:normalizeEscortRequestLimitPerMinute(settings.escortLimitPerMinute,combinedRate), escortMaxConcurrent:normalizeEscortRequestMaxConcurrent(settings.escortMaxConcurrent,combinedConcurrent) }; } function normalizeWatchNotificationText(value) { return String(value ?? '').trim().slice(0,240) || DEFAULT_SETTINGS.watchNotificationText; } function loadHelperSettingsFromStorage() { try { const saved=GM_getValue(SETTINGS_STORAGE_KEY,null); if (saved && typeof saved === 'object') { const normalized=normalizeHelperSettings({...DEFAULT_SETTINGS,...saved,...normalizeEscortQueueSettings(saved)},false); for (const key of ['escortGetLimitPerMinute','escortHeadLimitPerMinute','escortRequestLimitPerMinute','escortRequestMaxConcurrent']) delete normalized[key]; normalized.garsoShowTopicHelperData=Object.prototype.hasOwnProperty.call(saved,'garsoShowTopicHelperData') ? saved.garsoShowTopicHelperData !== false :(saved.garsoShowTrackedTopics !== false || saved.garsoShowVisitedAds !== false); delete normalized.garsoShowTrackedTopics; delete normalized.garsoShowVisitedAds; delete normalized.showMenuPageNamesOnTiles; if (!(Number(saved.imageSearchEnginesRevision) >= 2)) normalized.imageSearchEngines=normalized.imageSearchEngines .filter(item => item.id !== 'tineye' && !/(^|\.)tineye\.com$/i.test(new URL(item.urlPrefix).hostname)); if (!(Number(saved.imageSearchEnginesRevision) >= 1) && normalized.imageSearchEngines.length < 20 && !normalized.imageSearchEngines.some(item => item.id === HIVE_IMAGE_DETECTOR.id || /^https:\/\/(?:www\.)?hivedetect\.ai(?:[/?#]|$)/i.test(item.urlPrefix))) { normalized.imageSearchEngines.push({...HIVE_IMAGE_DETECTOR}); } return normalized; } return {...DEFAULT_SETTINGS}; } catch { return {...DEFAULT_SETTINGS}; } } function getSettings() { return copyWatchedValue(readHelperSettingsSnapshot()); } function normalizeHelperSettings(settings,forStorage=true) { const {escortRequestLimitPerMinute,escortRequestMaxConcurrent,escortGetMaxConcurrent,escortHeadMaxConcurrent,...current}=settings; const normalized=forStorage ? {}:current; for (const [keys,read] of [ [['garsoCheckEscortLinks','garsoShowSearchIcons','garsoShowTopicSummary','garsoShowTopicTools','garsoShowTopicHelperData', 'showRelatedAdsTileLink','showEscortiTileData','showPhoneReputationLinks','autoPhoneReputationSearch', 'showTopPhoneSearch','showBottomLeftMenu','multiLocationEnabled','saveSearchLocationsEnabled', 'showSummarySidePanel','showCompactSummaryAboveDescription','showTagHighlights','showPhoneCopyButton', 'escortFastStart','showWatchAlertBar','watchNotificationAnonymous'],value => value !== false], [['showMenuPagesAsFavicons','showLiveDiagnostics','watchEnabled','watchSystemNotifications'],value => value === true], [['openAllActiveRelatedAdsFromTile','showPricesInSearchResults','hideRecommendedAdSection', 'hideSingleAdContactButtons','hideSingleAdActions','showWatchOnlineCount'],Boolean], [['autoGarsoCheck','mergeEscortAds','hideRecommendedSearchSection','hideFeaturedSearchSection', 'hidePopularCitiesSection','showImageSearchButtons'],value => forStorage ? !!value:value] ]) { for (const key of keys) normalized[key]=read(settings[key]); } const normalizers={ autoGarsoAnalysisMode:normalizeAutoGarsoAnalysisMode, escortAggregationMode:normalizeEscortAggregationMode, compactSummaryComparisonMode:normalizeEscortCompactSummaryMode, relatedAdsTileLinkMode:normalizeEscortRelatedAdsTileLinkMode, searchResultPriceDuration:normalizeEscortPriceDuration, searchResultExtraFields:normalizeSearchResultExtraFields, imageSearchEngines:normalizeImageSearchEngines, phoneReputationServices:normalizePhoneReputationServices, menuPages:normalizeMenuPages, topPhoneSearchEnterTarget:normalizeTopPhoneSearchEnterTarget, topPhoneSearchOpenMode:normalizeTopPhoneSearchOpenMode, searchLocations:normalizeSearchLocations, savedLocationGroups:normalizeSavedLocationGroups, tagHighlightGroups:normalizeTagHighlightGroups, phoneClipboardFormat:normalizePhoneClipboardFormat, watchNotificationText:normalizeWatchNotificationText, nonAlertChangeNewness:normalizeWatchNonAlertChangeNewness, watchProfileResolutionMode:normalizeWatchProfileResolutionMode, favoritesDefaultView:normalizeFavoritesDefaultView, favoritesDataColumns:normalizeFavoritesDataColumns, favoriteCategories:normalizeFavoriteCategories, historyCleanupKeywords:normalizeHistoryCleanupKeywords }; for (const [key,read] of Object.entries(normalizers)) normalized[key]=read(settings[key]); return Object.assign(normalized,normalizeEscortQueueSettings(settings),{ quickAdPreview:forStorage ? settings.quickAdPreview !== false:settings.quickAdPreview, maxRelatedAdsTabs:Math.max(1,Math.floor(Number(settings.maxRelatedAdsTabs) || DEFAULT_SETTINGS.maxRelatedAdsTabs)), imageSearchEnginesRevision:forStorage ? 2:settings.imageSearchEnginesRevision, showMenuLinksInline:true, usePersistentCache:true, notesEnabled:settings.watchEnabled === true, alarmingTagCountLimit:normalizeCustomFilterInteger(settings.alarmingTagCountLimit,DEFAULT_SETTINGS.alarmingTagCountLimit,0,10000), watchIntervalMinutes:WATCH_INTERVAL_OPTIONS.includes(Number(settings.watchIntervalMinutes)) ? Number(settings.watchIntervalMinutes):DEFAULT_SETTINGS.watchIntervalMinutes, favoriteCategoryColors:normalizeFavoriteCategoryColors(settings.favoriteCategoryColors,settings.favoriteCategories) }); } function saveSettings(settings,waitForStorage=false) { const normalized=normalizeHelperSettings(settings); // Odświeżenie strony musi poczekać na potwierdzenie zapisu przez rozszerzenie. if (waitForStorage) return GM.setValue(SETTINGS_STORAGE_KEY,normalized).then(() => { invalidateHelperReadCache(SETTINGS_STORAGE_KEY); return normalized; }); writeHelperStorageValue(SETTINGS_STORAGE_KEY,normalized); return normalized; } function getShareableHelperSettings(settings) { const normalized=normalizeHelperSettings({...DEFAULT_SETTINGS,...settings,...normalizeEscortQueueSettings(settings)}); const keys=Object.keys(DEFAULT_SETTINGS).filter(key => ['boolean','number'].includes(typeof DEFAULT_SETTINGS[key])); keys.push('autoGarsoAnalysisMode','escortAggregationMode','compactSummaryComparisonMode','relatedAdsTileLinkMode', 'searchResultPriceDuration','topPhoneSearchEnterTarget','topPhoneSearchOpenMode','phoneClipboardFormat', 'nonAlertChangeNewness','watchProfileResolutionMode','favoritesDefaultView'); const migrated=new Set(); if (['escortGetLimitPerMinute','escortHeadLimitPerMinute','escortRequestLimitPerMinute'].some(key => Object.hasOwn(settings,key))) { migrated.add('escortLimitPerMinute'); } if (['escortRequestMaxConcurrent','escortGetMaxConcurrent','escortHeadMaxConcurrent'].some(key => Object.hasOwn(settings,key))) { migrated.add('escortMaxConcurrent'); } return Object.fromEntries(keys.filter(key => Object.hasOwn(settings,key) || migrated.has(key)).map(key => [key,normalized[key]])); } function buildScriptDataExportPayload(settingsOnly=false) { if (settingsOnly) return { format:'escort-helper-settings-only', exportedAt:new Date().toISOString(), settings:getShareableHelperSettings(getSettings()) }; return { format:SCRIPT_DATA_EXPORT_FORMAT, exportedAt:new Date().toISOString(), data:getScriptDataStorageKeys().map(key => [key,GM_getValue(key,null)]) }; } function downloadJsonFile(payload,fileName) { const blob=new Blob( [JSON.stringify(payload,null,2)], {type:'application/json;charset=utf-8'} ); const url=URL.createObjectURL(blob); const link=makeElement('a','',null,{href:url,download:fileName}); link.style.display='none'; document.body.appendChild(link); link.click(); detachHelperNode(link); setTimeout(() => URL.revokeObjectURL(url),1000); } function downloadScriptDataExport(settingsOnly=false) { const payload=buildScriptDataExportPayload(settingsOnly); const date=new Date().toISOString().slice(0,10); downloadJsonFile( payload, `escort-helper-ustawienia-${settingsOnly ? 'bez-danych-prywatnych-':''}${date}.json` ); } function validateScriptDataImportPayload(payload) { if (payload?.format === 'escort-helper-settings-only') { if (!payload.settings || typeof payload.settings !== 'object' || Array.isArray(payload.settings)) { throw new Error('Plik nie zawiera prawidłowych ustawień.'); } const settings=getShareableHelperSettings(payload.settings); if (!Object.keys(settings).length) throw new Error('Plik nie zawiera obsługiwanych ustawień.'); return {settings}; } if (!payload || typeof payload !== 'object' || payload.format !== SCRIPT_DATA_EXPORT_FORMAT) { throw new Error('To nie jest plik kopii danych tej wersji Escort Helper.'); } if (!Array.isArray(payload.data)) { throw new Error('Plik nie zawiera prawidłowych danych skryptu.'); } const data=payload.data.map(entry => { if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string') { throw new Error('Plik zawiera nieprawidłowy wpis danych.'); } if (entry[0].startsWith('vm_garso_watch_notification_claim:') || isRemovedDiagnosticStorageKey(entry[0])) return null; if (isScriptCacheStorageKey(entry[0]) || isTemporaryScriptStorageKey(entry[0])) { throw new Error('Plik zawiera dane cache lub tymczasowe i nie może zostać zaimportowany.'); } return entry; }).filter(Boolean); return {data}; } async function restoreScriptDataExport(payload) { const checked=validateScriptDataImportPayload(payload); if (checked.settings) { await GM.setValue(SETTINGS_STORAGE_KEY,normalizeHelperSettings({...getSettings(),...checked.settings},false)); invalidateHelperReadCache(SETTINGS_STORAGE_KEY); return Object.keys(checked.settings).length; } const {data}=checked; await clearScriptDataStorage(); for (const [key,value] of data) { await GM.setValue(key,value); invalidateHelperReadCache(key); } return data.length; } if (typeof GM_addValueChangeListener === 'function') { for (const key of [SETTINGS_STORAGE_KEY,WATCH_ITEMS_STORAGE_KEY,WATCH_AD_DATA_MIRROR_STORAGE_KEY]) GM_addValueChangeListener(key,() => invalidateHelperReadCache(key)); } let SETTINGS=getSettings(); function updateSettings(patchOrUpdater) { const current=getSettings(); const patch= typeof patchOrUpdater === 'function' ? patchOrUpdater(current) :patchOrUpdater; const normalized=saveSettings({ ...current, ...(patch && typeof patch === 'object' ? patch:{}) }); SETTINGS={ ...DEFAULT_SETTINGS, ...normalized }; applyFavoriteSearchPageSettings(); return SETTINGS; } let LIST_PAGES_TO_SHOW=consumeOneTimeListPagesToShow(); const ESCORT_CACHED_AD_HASH_KEY='vm_cached_ad'; const ESCORT_AD_TEMPLATE_CACHE_KEY='vm_escort_ad_template_v1'; const ESCORT_CACHED_AD_ARCHIVE_VIEW_HASH_KEY='vm_cached_archive_view'; const ESCORT_CACHED_AD_SESSION_KEY='vm_cached_ad_navigation'; let escortCachedArchiveRuntimeOverride=false; const PERSISTENT_CACHE_DB_NAME='vm-garso-escorti-cache'; const PERSISTENT_CACHE_DB_VERSION=1; const PERSISTENT_CACHE_STORE_NAME='entries'; const ESCORT_CACHE_HOST_SUFFIX=IS_ESCORT_CLUB && CURRENT_HOST !== 'pl.escort.club' ? '_' + CURRENT_HOST:''; const PERSISTENT_CACHE_STATS_STORAGE_KEY='vm_garso_cache_stats_snapshot' + ESCORT_CACHE_HOST_SUFFIX; const ESCORT_AD_CACHE_INDEX_STORAGE_KEY='vm_escort_ad_cache_index' + ESCORT_CACHE_HOST_SUFFIX; const PERSISTENT_CACHE_BRIDGE_PARAM='vm_cache_bridge'; const PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX='vm_garso_cache_bridge_request_'; const PERSISTENT_CACHE_BRIDGE_RESPONSE_PREFIX='vm_garso_cache_bridge_response_'; const ESCORT_VISIT_SUMMARY_ID='vm-escort-visit-summary'; const ESCORT_LAST_VISIT_ID='vm-escort-last-visited'; const ESCORT_CHANGE_STYLE_ID='vm-escort-change-style'; const ESCORT_PHONE_ENDPOINT=`${ESCORT_CLUB_ORIGIN}/includes/ajax.show-phone.php`; const LIST_MAX_CONCURRENT=12; const ESCORT_AD_DATA_MAX_CONCURRENT=6; const ESCORT_AD_DATA_FETCH_TIMEOUT_MS=12000; const ESCORT_ACTIVE_CHECK_CONCURRENT=6; const ESCORT_ACTIVE_PROBE_CONCURRENT=12; const escortActiveAdsSummaryCache=new Map(); const escortActiveAdCheckCache=new Map(); const escortActiveAdProbeCache=new Map(); function getListCacheTtlMs() { return ESCORT_CACHE_REFRESH_HOURS * 60 * 60 * 1000; } function getGarsoCacheTtlMs() { return GARSO_CACHE_REFRESH_HOURS * 60 * 60 * 1000; } const persistentCacheMemory=new Map(); const persistentCacheEntrySizeBytes=new Map(); let persistentCacheDb=null; let persistentCacheUsesIndexedDb=false; let persistentCacheDbOperationChain=Promise.resolve(); let fallbackPersistentCacheStatsMemo=null; let persistentCacheStatsSnapshotTimer=null; let deferredTopicCacheRange=null; let deferredTopicCacheCount=0; // Odbudowywalny indeks; źródłem prawdy są rekordy DB. const PERSISTENT_STARTUP_INDEX_KEY='vm_helper_startup_index_v1'; const PERSISTENT_CACHE_CHANGE_KEY='vm_helper_cache_changed' + ESCORT_CACHE_HOST_SUFFIX; let persistentCacheCatalog=null, persistentCacheCatalogStartedAt=0; let deferredNonTopicCache=false, persistentFullCachePromise=null; const persistentCacheLoadedKeys=new Set(),persistentCacheKeyJobs=new Map(); const persistentCacheMutations=new Map(),persistentCacheChangedKeys=new Set(); let persistentCacheChangeTimer=null,persistentCacheClearEpoch=0; let persistentRelatedListIndex=null,persistentPhoneAdIndex=null; function isDeferredGarsoPageKey(key) {return String(key).startsWith(GARSO_TOPIC_PAGE_CACHE_PREFIX);} function makePersistentCacheMetadata(record) { const key=String(record.key),value=record.value; const metadata={key,sizeBytes:Number(record.sizeBytes) || getPersistentCacheEntrySizeBytes(key,value)}; if (/^vm_escort_ad_data_\d+$/.test(key)) { Object.assign(metadata,{status:value?.status,checkedAt:Number(value?.checkedAt) || 0, phone:getEscortAdPhoneDigits(value),lastVisitedAt:Number(value?.lastVisitedAt) || 0}); } else if (/^vm_escorti_list_\d+$/.test(key)) { metadata.modes=Object.fromEntries(Object.entries(value?.modes || {}).map(([mode,entry]) => [mode,{ adIds:Array.isArray(entry?.adIds) ? entry.adIds.map(String).filter(id => /^\d+$/.test(id)):[], profileIds:Array.isArray(entry?.profileIds) ? entry.profileIds.map(String).filter(id => /^\d+$/.test(id)):[], adLinksVersion:entry?.adLinksVersion,checkedAt:Number(entry?.checkedAt) || 0 }])); } return metadata; } function notePersistentCacheMetadata(record) { if (!persistentCacheCatalog || !isIndexedDbCacheKey(record?.key) || isDeferredGarsoPageKey(record.key)) return; persistentCacheCatalog.set(record.key,makePersistentCacheMetadata(record)); persistentRelatedListIndex=null; persistentPhoneAdIndex=null; } function getPersistentCacheRelatedListIndex() { if (persistentRelatedListIndex) return persistentRelatedListIndex; const result=new Map(),profiles=new Map(); for (const [key,metadata] of persistentCacheCatalog || []) { const id=key.match(/^vm_escorti_list_(\d+)$/)?.[1]; if (!id) continue; for (const relatedId of new Set([id,...Object.values(metadata.modes || {}).flatMap(mode => mode.adIds || [])])) { if (!result.has(relatedId)) result.set(relatedId,new Set()); result.get(relatedId).add(key); } } for (const [key,metadata] of persistentCacheCatalog || []) { if (!/^vm_escorti_list_\d+$/.test(key)) continue; for (const mode of Object.values(metadata.modes || {})) { if (mode.profileIds?.length !== 1 || (mode.adIds?.length && mode.adLinksVersion !== 1)) continue; const profile=mode.profileIds[0]; if (!profiles.has(profile)) profiles.set(profile,{keys:new Set(),ids:new Set()}); profiles.get(profile).keys.add(key); for (const id of [key.slice('vm_escorti_list_'.length),...(mode.adIds || [])]) profiles.get(profile).ids.add(id); } } for (const profile of profiles.values()) for (const id of profile.ids) { if (!result.has(id)) result.set(id,new Set()); for (const key of profile.keys) result.get(id).add(key); } return persistentRelatedListIndex=result; } function getPersistentCachePhoneAdIndex() { if (persistentPhoneAdIndex) return persistentPhoneAdIndex; const result=new Map(); for (const [key,metadata] of persistentCacheCatalog || []) { const id=key.match(/^vm_escort_ad_data_(\d+)$/)?.[1]; if (!id || metadata.status !== 'ok' || !metadata.phone) continue; if (!result.has(metadata.phone)) result.set(metadata.phone,new Set()); result.get(metadata.phone).add(id); } return persistentPhoneAdIndex=result; } function readPersistentDbKeys(db,range=null,indexName=null) { return new Promise((resolve,reject) => { const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readonly'); const store=transaction.objectStore(PERSISTENT_CACHE_STORE_NAME); const request=(indexName ? store.index(indexName):store).getAllKeys(range); request.onsuccess=() => resolve(request.result || []); request.onerror=() => reject(request.error); transaction.onabort=() => reject(transaction.error); }); } function readPersistentDbRecords(db,keys) { if (!keys.length) return Promise.resolve([]); return new Promise((resolve,reject) => { const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readonly'); const store=transaction.objectStore(PERSISTENT_CACHE_STORE_NAME),results=new Array(keys.length); keys.forEach((key,index) => { const request=store.get(key); request.onsuccess=() => {results[index]=request.result || null;}; }); transaction.oncomplete=() => resolve(results); transaction.onerror=() => reject(transaction.error); transaction.onabort=() => reject(transaction.error); }); } async function refreshPersistentCacheCatalog(db,stored=null) { const startedAt=Date.now(); const keys=await readPersistentDbKeys(db); const wanted=new Set(keys.filter(key => isIndexedDbCacheKey(key) && !isDeferredGarsoPageKey(key))); const entries=stored?.schema === 1 && Array.isArray(stored.entries) && stored.startedAt > 0 && stored.startedAt <= startedAt ? new Map(stored.entries.filter(item => item?.key && wanted.has(item.key)).map(item => [item.key,item])):null; if (!entries) return null; const changed=await readPersistentDbKeys(db,IDBKeyRange.lowerBound(stored.startedAt),'updatedAt'); const reload=[...new Set([...changed.filter(key => wanted.has(key)),...[...wanted].filter(key => !entries.has(key))])]; for (let offset=0; offset < reload.length; offset += 128) { const records=await readPersistentDbRecords(db,reload.slice(offset,offset + 128)); for (let index=0; index < records.length; index++) { const record=records[index]; if (record) entries.set(record.key,makePersistentCacheMetadata(record)); else entries.delete(reload[offset + index]); } if (offset + 128 < reload.length) await yieldHelperWork(); } persistentCacheCatalog=entries; persistentCacheCatalogStartedAt=startedAt; persistentRelatedListIndex=null; persistentPhoneAdIndex=null; return {keys,entries}; } function savePersistentCacheCatalog() { if (!persistentCacheDb || !persistentCacheCatalog || !persistentCacheCatalogStartedAt) return Promise.resolve(); // Zachowaj początek skanu: późniejsze zapisy innych kart wymagają ponownego odczytu. const value={schema:1,startedAt:persistentCacheCatalogStartedAt,entries:[...persistentCacheCatalog.values()]}; return enqueuePersistentCacheDbOperation(() => putPersistentCacheRecords(persistentCacheDb,[{ key:PERSISTENT_STARTUP_INDEX_KEY,value,updatedAt:0,sizeBytes:0 }])); } function persistentCacheKeyIsLoaded(key) { return !persistentCacheUsesIndexedDb || persistentCacheLoadedKeys.has(key) || persistentCacheMemory.has(key) || (!deferredNonTopicCache && !isDeferredGarsoPageKey(key)); } async function ensurePersistentCacheKeys(keys,{refresh=false}={}) { if (!persistentCacheUsesIndexedDb || !persistentCacheDb) return; const wanted=[...new Set(keys.filter(key => key && isIndexedDbCacheKey(key)))]; const pending=[],fresh=[]; for (const key of wanted) { if (persistentCacheKeyJobs.has(key)) pending.push(persistentCacheKeyJobs.get(key)); else if (refresh || !persistentCacheKeyIsLoaded(key)) fresh.push(key); } if (fresh.length) { const epoch=persistentCacheClearEpoch; const generations=new Map(fresh.map(key => [key,persistentCacheMutations.get(key) || 0])); const job=(async () => { for (let offset=0; offset < fresh.length; offset += 96) { const batch=fresh.slice(offset,offset + 96); const records=await readPersistentDbRecords(persistentCacheDb,batch); for (let i=0; i < batch.length; i++) { const key=batch[i]; if (epoch !== persistentCacheClearEpoch || (persistentCacheMutations.get(key) || 0) !== generations.get(key)) continue; if (records[i]) rememberPersistentCacheRecord(records[i]); else {persistentCacheMemory.delete(key); persistentCacheEntrySizeBytes.delete(key); persistentCacheCatalog?.delete(key);} persistentCacheLoadedKeys.add(key); } persistentRelatedListIndex=null; persistentPhoneAdIndex=null; if (offset + 96 < fresh.length) await yieldHelperWork(); } })(); for (const key of fresh) persistentCacheKeyJobs.set(key,job); pending.push(job); job.finally(() => {for (const key of fresh) if (persistentCacheKeyJobs.get(key) === job) persistentCacheKeyJobs.delete(key);}).catch(() => {}); } await Promise.all(pending); } function getPersistentGarsoKeys(adId,phone='') { const address=buildGarsoAdLinkSearchTerm(adId),term=getGarsoSearchTerms(phone,'phone')[0] || ''; return [garsoExtendedCacheKey(adId),garsoSearchCountCacheKey('address',address),garsoSummaryCacheKey('address',address), garsoSummaryCacheKey('combined',buildCombinedGarsoSummaryTerm(term,address)), ...(term ? [garsoSearchCountCacheKey('phone',term),garsoSummaryCacheKey('phone',term), phoneReputationCacheKey(normalizePhoneReputationPhone(phone))]:[])].filter(Boolean); } async function ensureEscortPageCache(adIds,phone='') { if (!persistentCacheUsesIndexedDb) return; const ids=new Set(adIds.map(String).filter(id => /^\d+$/.test(id))); const listKeys=new Set(),related=getPersistentCacheRelatedListIndex(); for (const id of ids) { listKeys.add(listCacheKey(id)); for (const key of related.get(id) || []) listKeys.add(key); } await ensurePersistentCacheKeys([...listKeys,...[...ids].map(escortAdDataCacheKey)]); for (const key of listKeys) { for (const entry of Object.values(persistentCacheMemory.get(key)?.modes || {})) { for (const id of entry.adIds || []) if (/^\d+$/.test(String(id))) ids.add(String(id)); } } const phoneIndex=getPersistentCachePhoneAdIndex(); for (const id of [...ids]) { const digits=getEscortAdPhoneDigits(persistentCacheMemory.get(escortAdDataCacheKey(id))) || persistentCacheCatalog?.get(escortAdDataCacheKey(id))?.phone; if (digits && ['exact-phone','phone-escort-search'].includes(SETTINGS.escortAggregationMode)) { for (const other of phoneIndex.get(digits) || []) ids.add(other); } } await ensurePersistentCacheKeys([...ids].flatMap(id => [escortAdDataCacheKey(id),listCacheKey(id)])); await ensurePersistentCacheKeys([...ids].flatMap(id => getPersistentGarsoKeys(id, (adIds.map(String).includes(id) ? phone:'') || getEscortAdPhoneDigits(persistentCacheMemory.get(escortAdDataCacheKey(id))) || ''))); } function getInitialEscortCacheIds() { const ids=[getEscortAdIdFromPath(),getEscortCachedArchiveMarkerFromHash()]; if (location.pathname === '/') ids.push(readRecentEscortCachedAdNavigation()); const context=getEscortClubListContext(document,location.href); for (const anchor of context?.anchors || []) ids.push(parseAdIdFromUrl(anchor.href)); return [...new Set(ids.filter(Boolean))]; } function notePersistentCacheMutation(key) { persistentCacheMutations.set(key,(persistentCacheMutations.get(key) || 0) + 1); persistentCacheLoadedKeys.add(key); persistentCacheChangedKeys.add(key); persistentRelatedListIndex=null; persistentPhoneAdIndex=null; if (!persistentCacheChangeTimer) persistentCacheChangeTimer=setTimeout(() => { persistentCacheChangeTimer=null; const keys=[...persistentCacheChangedKeys]; persistentCacheChangedKeys.clear(); persistentCacheDbOperationChain.then(() => { writeHelperStorageValue(PERSISTENT_CACHE_CHANGE_KEY,{session:GARSO_SEARCH_INSTANCE_ID,keys,at:Date.now()}); }).catch(() => {}); },150); } function initPersistentCacheChanges() { if (!IS_ESCORT_CLUB || typeof GM_addValueChangeListener !== 'function') return; GM_addValueChangeListener(PERSISTENT_CACHE_CHANGE_KEY,(_key,_old,value) => { if (!value || value.session === GARSO_SEARCH_INSTANCE_ID || !Array.isArray(value.keys)) return; const keys=value.keys.filter(isIndexedDbCacheKey); ensurePersistentCacheKeys(keys,{refresh:true}).then(() => { invalidateEscortiProfileAdIndexMemory(); document.dispatchEvent(new CustomEvent('vm-helper-cache-change',{detail:{keys}})); }).catch(error => log('Nie udało się odświeżyć cache z innej karty',error)); }); } function deferUntilFullPersistentCacheReady(action) { if (deferUntilPersistentCacheReady(action)) return true; if (!deferredNonTopicCache) return false; showWatchToast('Wczytywanie zapisanych danych…'); ensureFullPersistentCache().then(() => {detachHelperNode(document.getElementById('vm-garso-watch-toast')); action();}) .catch(error => {log('Nie udało się odczytać zapisanych danych',error); showWatchToast('Nie udało się odczytać danych. Spróbuj ponownie.',true);}); return true; } function getPersistentCacheEntrySizeBytes(key,value) { try { const encoder=new TextEncoder(); return encoder.encode(String(key || '')).length + encoder.encode(JSON.stringify(value) ?? '').length; } catch { return 0; } } let escortiProfileAdIndexMemory=null; function isIndexedDbCacheKey(key) { const value=String(key || ''); return /^vm_garso_search_count_/i.test(value) || /^vm_garso_summary_/i.test(value) || /^vm_garso_extended_\d+$/i.test(value) || /^vm_garso_topic_page_/i.test(value) || /^vm_escorti_list_\d+$/i.test(value) || /^vm_escort_ad_data_\d+$/i.test(value) || /^vm_phone_reputation_/i.test(value); } function openPersistentCacheDb() { return new Promise((resolve,reject) => { let indexedDbApi=null; try { indexedDbApi=globalThis.indexedDB || unsafeWindow?.indexedDB || null; } catch { indexedDbApi=globalThis.indexedDB || null; } if (!indexedDbApi) { reject(new Error('IndexedDB jest niedostępne')); return; } const request=indexedDbApi.open( PERSISTENT_CACHE_DB_NAME, PERSISTENT_CACHE_DB_VERSION ); request.onupgradeneeded=() => { const db=request.result; const transaction=request.transaction; let store; if (!db.objectStoreNames.contains(PERSISTENT_CACHE_STORE_NAME)) { store=db.createObjectStore( PERSISTENT_CACHE_STORE_NAME, {keyPath:'key'} ); } else { store=transaction.objectStore( PERSISTENT_CACHE_STORE_NAME ); } if (!store.indexNames.contains('updatedAt')) { store.createIndex( 'updatedAt', 'updatedAt', {unique:false} ); } }; request.onsuccess=() => resolve(request.result); request.onerror=() => reject(request.error || new Error('Nie udało się otworzyć IndexedDB')); request.onblocked=() => reject(new Error('Otwarcie IndexedDB zostało zablokowane')); }); } function waitForIndexedDbTransaction(transaction) { return new Promise((resolve,reject) => { transaction.oncomplete=() => resolve(); transaction.onerror=() => reject( transaction.error || new Error('Błąd transakcji IndexedDB') ); transaction.onabort=() => reject( transaction.error || new Error('Transakcja IndexedDB została przerwana') ); }); } async function loadPersistentCacheRecords(db,ranges,onBatch) { const batchSize=256; for (const initialRange of ranges) { let range=initialRange; while (true) { const records=await new Promise((resolve,reject) => { const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readonly'); const request=transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).getAll(range,batchSize); request.onsuccess=() => resolve(request.result || []); request.onerror=() => reject(request.error || new Error('Błąd odczytu IndexedDB')); transaction.onabort=() => reject(transaction.error || new Error('Przerwano odczyt IndexedDB')); }); onBatch(records); if (records.length < batchSize) break; const lastKey=records[records.length - 1].key; if (initialRange?.upper !== undefined && indexedDB.cmp(lastKey,initialRange.upper) >= 0) break; range=initialRange?.upper !== undefined ? IDBKeyRange.bound(lastKey,initialRange.upper,true,initialRange.upperOpen) :IDBKeyRange.lowerBound(lastKey,true); } } } function rememberPersistentCacheRecord(record) { if (!record?.key || !isIndexedDbCacheKey(record.key)) return; persistentCacheMemory.set(record.key,record.value); persistentCacheLoadedKeys.add(record.key); notePersistentCacheMetadata(record); persistentCacheEntrySizeBytes.set(record.key, Number.isFinite(Number(record.sizeBytes)) ? Math.max(0,Number(record.sizeBytes)) :getPersistentCacheEntrySizeBytes(record.key,record.value)); } async function ensureFullPersistentCache() { if (!persistentCacheUsesIndexedDb || (!deferredNonTopicCache && !deferredTopicCacheRange)) return; if (persistentFullCachePromise) return persistentFullCachePromise; persistentFullCachePromise=(async () => { const loadRanges=ranges => { const epoch=persistentCacheClearEpoch,generations=new Map(persistentCacheMutations); return loadPersistentCacheRecords(persistentCacheDb,ranges,records => { for (const record of records) if (epoch === persistentCacheClearEpoch && (persistentCacheMutations.get(record.key) || 0) === (generations.get(record.key) || 0)) rememberPersistentCacheRecord(record); }); }; if (deferredNonTopicCache) { await loadRanges([IDBKeyRange.upperBound(GARSO_TOPIC_PAGE_CACHE_PREFIX,true), IDBKeyRange.lowerBound(GARSO_TOPIC_PAGE_CACHE_PREFIX + '\uffff',true)]); deferredNonTopicCache=false; escortDataFieldCatalogBootstrapped=false; } if (deferredTopicCacheRange) { await loadRanges([deferredTopicCacheRange]); deferredTopicCacheRange=null; deferredTopicCacheCount=0; } await rebuildWatchedAdDataMirrorFromCentralCache(); savePersistentCacheStatsSnapshot(calculatePersistentCacheStats()); })().finally(() => {persistentFullCachePromise=null;}); return persistentFullCachePromise; } async function putPersistentCacheRecords(db,records) { if (!records.length) return; const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readwrite'); const store=transaction.objectStore(PERSISTENT_CACHE_STORE_NAME); for (const record of records) store.put(record); await waitForIndexedDbTransaction(transaction); } function enqueuePersistentCacheDbOperation(operation) { const result=persistentCacheDbOperationChain .catch(() => {}) .then(operation); persistentCacheDbOperationChain=result.catch(error => { log('Błąd zapisu trwałego cache w IndexedDB',error); }); return result; } function readPersistentCacheValue(key,fallback=null) { const diagnosticCacheKey=isIndexedDbCacheKey(key); if (persistentCacheUsesIndexedDb && diagnosticCacheKey) { if (!persistentCacheKeyIsLoaded(key)) { return fallback; } const hit=persistentCacheMemory.has(key); recordDiagnosticCacheLookup(hit); return hit ? persistentCacheMemory.get(key):fallback; } try { const value=GM_getValue(key,fallback); if (diagnosticCacheKey) { recordDiagnosticCacheLookup( value !== null && value !== undefined && value !== fallback ); } return value; } catch { if (diagnosticCacheKey) recordDiagnosticCacheLookup(false); return fallback; } } let escortAdCacheAvailabilityIndexMemory=null; let escortAdCacheAvailabilityIndexSaveTimer=null; if (!IS_ESCORT_CLUB && typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(ESCORT_AD_CACHE_INDEX_STORAGE_KEY,() => {escortAdCacheAvailabilityIndexMemory=null;}); } function readEscortAdCacheAvailabilityIndex() { if (escortAdCacheAvailabilityIndexMemory) { return escortAdCacheAvailabilityIndexMemory; } let ids=[]; try { const stored=GM_getValue(ESCORT_AD_CACHE_INDEX_STORAGE_KEY,[]); ids=Array.isArray(stored) ? stored:[]; } catch {} escortAdCacheAvailabilityIndexMemory=new Set( ids.map(String).filter(id => /^\d+$/.test(id)) ); return escortAdCacheAvailabilityIndexMemory; } function persistEscortAdCacheAvailabilityIndex() { clearTimeout(escortAdCacheAvailabilityIndexSaveTimer); escortAdCacheAvailabilityIndexSaveTimer=null; try { writeHelperStorageValue( ESCORT_AD_CACHE_INDEX_STORAGE_KEY, [...readEscortAdCacheAvailabilityIndex()] ); } catch {} } function scheduleEscortAdCacheAvailabilityIndexSave() { clearTimeout(escortAdCacheAvailabilityIndexSaveTimer); escortAdCacheAvailabilityIndexSaveTimer=setTimeout( persistEscortAdCacheAvailabilityIndex, 250 ); } function updateEscortAdCacheAvailabilityIndexFromKey(key,available) { if (!IS_ESCORT_CLUB) return; const match=String(key || '').match(/^vm_escort_ad_data_(\d+)$/i); if (!match) return; const index=readEscortAdCacheAvailabilityIndex(); if (available) index.add(match[1]); else index.delete(match[1]); scheduleEscortAdCacheAvailabilityIndexSave(); } const escortForeignCacheIndices=new Map(); function getEscortCachedAdHost(adId) { const id=String(adId || ''); if (!/^\d+$/.test(id)) return ''; if (readEscortAdCacheAvailabilityIndex().has(id)) return IS_ESCORT_CLUB ? CURRENT_HOST:'pl.escort.club'; try { for (const key of GM_listValues()) { if (key === ESCORT_AD_CACHE_INDEX_STORAGE_KEY || !isStorageKeyInNamespace(key,'vm_escort_ad_cache_index')) continue; const host=key === 'vm_escort_ad_cache_index' ? 'pl.escort.club':key.slice('vm_escort_ad_cache_index_'.length); if (!isEscortClubHost(host)) continue; if (!escortForeignCacheIndices.has(key)) { const stored=GM_getValue(key,[]); escortForeignCacheIndices.set(key,new Set(Array.isArray(stored) ? stored.map(String):[])); if (typeof GM_addValueChangeListener === 'function') GM_addValueChangeListener(key,(_key,_old,value) => { escortForeignCacheIndices.set(key,new Set(Array.isArray(value) ? value.map(String):[])); }); } if (escortForeignCacheIndices.get(key).has(id)) return host; } } catch {} return ''; } function writePersistentCacheValue(key,value) { fallbackPersistentCacheStatsMemo=null; if (/^vm_escorti_list_\d+$/i.test(String(key || ''))) { invalidateEscortiProfileAdIndexMemory(); } if ( /^vm_escort_ad_data_\d+$/i.test(String(key || '')) && value?.status === 'ok' && Number(value.checkedAt) > 0 ) { updateEscortAdCacheAvailabilityIndexFromKey(key,true); } if (!persistentCacheUsesIndexedDb || !isIndexedDbCacheKey(key)) { writeHelperStorageValue(key,value); schedulePersistentCacheStatsSnapshot(); return Promise.resolve(); } notePersistentCacheMutation(key); persistentCacheMemory.set(key,value); notePersistentCacheMetadata({key,value}); const sizeBytes=getPersistentCacheEntrySizeBytes(key,value); persistentCacheEntrySizeBytes.set(key,sizeBytes); schedulePersistentCacheStatsSnapshot(); return enqueuePersistentCacheDbOperation(async () => { await putPersistentCacheRecords(persistentCacheDb,[{ key, value, updatedAt:Date.now(), sizeBytes }]); }).catch(() => undefined); } function deletePersistentCacheValue(key) { fallbackPersistentCacheStatsMemo=null; if (/^vm_escorti_list_\d+$/i.test(String(key || ''))) { invalidateEscortiProfileAdIndexMemory(); } updateEscortAdCacheAvailabilityIndexFromKey(key,false); if (!persistentCacheUsesIndexedDb || !isIndexedDbCacheKey(key)) { try {deleteHelperStorageValue(key);} catch {} schedulePersistentCacheStatsSnapshot(); return Promise.resolve(); } notePersistentCacheMutation(key); persistentCacheCatalog?.delete(key); persistentCacheMemory.delete(key); persistentCacheEntrySizeBytes.delete(key); schedulePersistentCacheStatsSnapshot(); return enqueuePersistentCacheDbOperation(async () => { const transaction=persistentCacheDb.transaction( PERSISTENT_CACHE_STORE_NAME, 'readwrite' ); transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).delete(key); await waitForIndexedDbTransaction(transaction); }); } function listPersistentCacheKeys(pattern) { if (persistentCacheUsesIndexedDb) { return [...persistentCacheMemory.keys()].filter(key => pattern.test(key)); } try { return GM_listValues().filter(key => pattern.test(key)); } catch { return []; } } function invalidateEscortiProfileAdIndexMemory() { escortiProfileAdIndexMemory=null; } function getEscortiProfileAdIndexMemory() { if (escortiProfileAdIndexMemory) return escortiProfileAdIndexMemory; const profiles=new Map(); for (const key of (persistentCacheCatalog ? [...persistentCacheCatalog.keys()].filter(key => /^vm_escorti_list_\d+$/.test(key)):getEscortListCacheKeys())) { const match=String(key).match(/^vm_escorti_list_(\d+)$/i); if (!match) continue; const store=persistentCacheCatalog?.get(key) || readPersistentCacheValue(key,null); if (!store?.modes || typeof store.modes !== 'object') continue; const sourceAdId=match[1]; for (const entry of Object.values(store.modes)) { if (entry?.adIds?.length && entry.adLinksVersion !== 1) continue; const profileIds=[...new Set( (Array.isArray(entry?.profileIds) ? entry.profileIds:[]) .map(value => String(value || '').trim()) .filter(value => /^\d+$/.test(value)) )]; if (profileIds.length !== 1) continue; const profileId=profileIds[0]; if (!profiles.has(profileId)) profiles.set(profileId,new Set()); const adIds=profiles.get(profileId); adIds.add(sourceAdId); for (const adId of Array.isArray(entry?.adIds) ? entry.adIds:[]) { const normalizedId=String(adId || '').trim(); if (/^\d+$/.test(normalizedId)) adIds.add(normalizedId); } } } escortiProfileAdIndexMemory=profiles; return profiles; } async function initializePersistentCacheStorage() { if (!IS_ESCORT_CLUB) return; const startedAt=Date.now(); try { const db=await openPersistentCacheDb(); persistentCacheDb=db; db.onversionchange=() => {db.close(); log('Cache zmienił wersję; odśwież kartę.');}; const token=new URL(location.href).searchParams.get(PERSISTENT_CACHE_BRIDGE_PARAM); const request=token ? GM_getValue(persistentCacheBridgeRequestKey(token),null):null; if (request?.action === 'read-ad-data') { persistentCacheUsesIndexedDb=true; deferredNonTopicCache=true; await ensurePersistentCacheKeys((request.adIds || []).flatMap(id => [escortAdDataCacheKey(id),listCacheKey(id)])); return; } const topicEnd=GARSO_TOPIC_PAGE_CACHE_PREFIX + '\uffff'; const topicRange=IDBKeyRange.bound(GARSO_TOPIC_PAGE_CACHE_PREFIX,topicEnd); const stored=(await readPersistentDbRecords(db,[PERSISTENT_STARTUP_INDEX_KEY]))[0]?.value; let fast=null; try {if (stored?.schema === 1) fast=await refreshPersistentCacheCatalog(db,stored);} catch (error) {log('Odbudowuję indeks uruchamiania cache',error);} if (fast) { try { deferredTopicCacheCount=fast.keys.filter(isDeferredGarsoPageKey).length; deferredTopicCacheRange=deferredTopicCacheCount ? topicRange:null; persistentCacheUsesIndexedDb=true; deferredNonTopicCache=true; await ensureEscortPageCache(getInitialEscortCacheIds(),getFormattedPhoneNumber() || ''); } catch (error) { log('Odczyt selektywny nie powiódł się; wczytuję pełny cache',error); fast=null; deferredNonTopicCache=false; persistentCacheMemory.clear(); persistentCacheLoadedKeys.clear(); persistentCacheEntrySizeBytes.clear(); } } if (!fast) { persistentCacheCatalog=new Map(); persistentCacheCatalogStartedAt=startedAt; await loadPersistentCacheRecords(db,[IDBKeyRange.upperBound(GARSO_TOPIC_PAGE_CACHE_PREFIX,true), IDBKeyRange.lowerBound(topicEnd,true)],records => {for (const record of records) rememberPersistentCacheRecord(record);}); deferredTopicCacheCount=(await readPersistentDbKeys(db,topicRange)).length; deferredTopicCacheRange=deferredTopicCacheCount ? topicRange:null; persistentCacheUsesIndexedDb=true; } const adIds=new Set([...persistentCacheCatalog].filter(([key,value]) => /^vm_escort_ad_data_\d+$/.test(key) && value.status === 'ok' && value.checkedAt > 0).map(([key]) => key.slice('vm_escort_ad_data_'.length))); const storedIds=GM_getValue(ESCORT_AD_CACHE_INDEX_STORAGE_KEY,[]); escortAdCacheAvailabilityIndexMemory=adIds; if (!Array.isArray(storedIds) || storedIds.length !== adIds.size || storedIds.some(id => !adIds.has(String(id)))) persistEscortAdCacheAvailabilityIndex(); // Zapis indeksu nie blokuje gotowości UI. savePersistentCacheCatalog().catch(error => log('Nie udało się zapisać indeksu uruchamiania cache',error)); if (!deferredTopicCacheRange && !deferredNonTopicCache) savePersistentCacheStatsSnapshot(calculatePersistentCacheStats()); } catch (error) { persistentCacheDb?.close?.(); persistentCacheDb=null; deferredTopicCacheRange=null; deferredTopicCacheCount=0; deferredNonTopicCache=false; persistentCacheCatalog=null; persistentCacheUsesIndexedDb=false; persistentCacheMemory.clear(); persistentCacheEntrySizeBytes.clear(); persistentCacheLoadedKeys.clear(); log('IndexedDB niedostępne - pozostawiam cache w Violentmonkey',error); } } function getGarsoSearchCountCacheKeys() { return listPersistentCacheKeys(/^vm_garso_search_count_/i); } function getGarsoExtendedCacheKeys() { return listPersistentCacheKeys(/^vm_garso_extended_\d+$/i); } function getGarsoSummaryCacheKeys() { return listPersistentCacheKeys(/^vm_garso_summary_/i); } function getGarsoTopicPageCacheKeys() { return listPersistentCacheKeys(/^vm_garso_topic_page_/i); } function getEscortListCacheKeys() { return listPersistentCacheKeys(/^vm_escorti_list_\d+$/i); } function getEscortAdDataCacheKeys() { return listPersistentCacheKeys(/^vm_escort_ad_data_\d+$/i); } function getPhoneReputationCacheKeys() { return listPersistentCacheKeys(/^vm_phone_reputation_/i); } function calculatePersistentCacheStats() { if (deferredTopicCacheRange || deferredNonTopicCache) { const snapshot=readPersistentCacheStatsSnapshot(); if (snapshot) return snapshot; } if (!persistentCacheUsesIndexedDb && fallbackPersistentCacheStatsMemo) { return {...fallbackPersistentCacheStatsMemo}; } const keys=persistentCacheUsesIndexedDb ? (deferredNonTopicCache && persistentCacheCatalog ? [...persistentCacheCatalog.keys()]:[...persistentCacheMemory.keys()]) :(() => { try {return GM_listValues().filter(isIndexedDbCacheKey);} catch {return [];} })(); let entries=0; let garsoEntries=0; let listEntries=0; let escortAdDataEntries=0; let phoneReputationEntries=0; let bytes=0; for (const key of keys) { if (!isIndexedDbCacheKey(key)) continue; entries++; if ( /^vm_garso_search_count_/i.test(key) || /^vm_garso_summary_/i.test(key) || /^vm_garso_extended_\d+$/i.test(key) || /^vm_garso_topic_page_/i.test(key) ) garsoEntries++; else if (/^vm_escorti_list_\d+$/i.test(key)) listEntries++; else if (/^vm_escort_ad_data_\d+$/i.test(key)) escortAdDataEntries++; else if (/^vm_phone_reputation_/i.test(key)) phoneReputationEntries++; if (persistentCacheUsesIndexedDb) { bytes += persistentCacheEntrySizeBytes.get(key) || persistentCacheCatalog?.get(key)?.sizeBytes || 0; } else { bytes += getPersistentCacheEntrySizeBytes( key, readPersistentCacheValue(key,null) ); } } const stats={ entries:entries + deferredTopicCacheCount, garsoEntries:garsoEntries + deferredTopicCacheCount, listEntries, escortAdDataEntries, phoneReputationEntries, bytes, megabytes:bytes / (1024 * 1024), backend:persistentCacheUsesIndexedDb ? 'IndexedDB':'Violentmonkey' }; if (!persistentCacheUsesIndexedDb) fallbackPersistentCacheStatsMemo={...stats}; return stats; } function savePersistentCacheStatsSnapshot(stats) { if (!IS_ESCORT_CLUB || deferredTopicCacheRange || deferredNonTopicCache) return; try { writeHelperStorageValue(PERSISTENT_CACHE_STATS_STORAGE_KEY,{ ...stats, updatedAt:Date.now() }); } catch {} } function schedulePersistentCacheStatsSnapshot() { if (!IS_ESCORT_CLUB) return; clearTimeout(persistentCacheStatsSnapshotTimer); persistentCacheStatsSnapshotTimer=setTimeout(() => { persistentCacheStatsSnapshotTimer=null; savePersistentCacheStatsSnapshot(calculatePersistentCacheStats()); },150); } function readPersistentCacheStatsSnapshot() { try { const stats=GM_getValue(PERSISTENT_CACHE_STATS_STORAGE_KEY,null); if (!stats || typeof stats !== 'object') return null; const bytes=Number(stats.bytes); const entries=Number(stats.entries); if (!Number.isFinite(bytes) || bytes < 0 || !Number.isFinite(entries) || entries < 0) { return null; } return { entries, garsoEntries:Math.max(0,Number(stats.garsoEntries) || 0), listEntries:Math.max(0,Number(stats.listEntries) || 0), escortAdDataEntries:Math.max(0,Number(stats.escortAdDataEntries) || 0), bytes, megabytes:bytes / (1024 * 1024), backend:stats.backend || 'IndexedDB', updatedAt:Number(stats.updatedAt) || 0 }; } catch { return null; } } function getSharedEscortActivityCacheKeys() { try {return GM_listValues().filter(key => key.startsWith(ESCORT_SHARED_ACTIVITY_CACHE_PREFIX));} catch {return [];} } function getPersistentCacheStats() { const base=(!IS_ESCORT_CLUB && readPersistentCacheStatsSnapshot()) || calculatePersistentCacheStats(); const activity=getGmStorageStatsForKeys(getSharedEscortActivityCacheKeys()); let templateKeys=[]; try {templateKeys=GM_listValues().filter(key => key === 'vm_escort_ad_template_v1');} catch {} const template=getGmStorageStatsForKeys(templateKeys),bytes=base.bytes + activity.bytes + template.bytes; return {...base,entries:base.entries + activity.entries + template.entries, activityEntries:activity.entries,activityBytes:activity.bytes,templateEntries:template.entries,templateBytes:template.bytes, bytes,megabytes:bytes / (1024 * 1024)}; } function formatCacheSize(megabytes) { const mb=Number(megabytes) || 0; const decimals=mb < 0.1 ? 3:2; return `${mb.toFixed(decimals).replace('.',',')} MB`; } function isStorageKeyInNamespace(key,canonicalKey) { const value=String(key || ''); const canonical=String(canonicalKey || ''); return !!canonical && (value === canonical || value.startsWith(`${canonical}_`)); } function isAuxiliaryCacheStorageKey(key) { const value=String(key || ''); return value === 'vm_escort_ad_template_v1' || value.startsWith(ESCORT_SHARED_ACTIVITY_CACHE_PREFIX) || isStorageKeyInNamespace(value,'vm_garso_cache_stats_snapshot') || isStorageKeyInNamespace(value,WATCH_AD_DATA_MIRROR_STORAGE_KEY) || isStorageKeyInNamespace(value,ESCORT_DATA_FIELD_CATALOG_STORAGE_KEY) || isStorageKeyInNamespace(value,'vm_escort_ad_cache_index') || value.startsWith(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX) || value.startsWith(PERSISTENT_CACHE_BRIDGE_RESPONSE_PREFIX); } function isScriptCacheStorageKey(key) { return isIndexedDbCacheKey(String(key || '')) || isAuxiliaryCacheStorageKey(key); } function isRemovedDiagnosticStorageKey(key) { return key === 'vm_escort_ad_data_last_error' || key === 'vm_temp_garso_parser_samples_v1' || key.startsWith('vm_garso_request_trace_') || key === 'vm_garso_search_queue_test_visible' || key.startsWith('vm_garso_search_queue_test:') || key === 'vm_helper_request_audit_epoch' || key.startsWith('vm_helper_request_audit_v1:') || ['traffic','activity','live'].some(name => key.startsWith(`vm_helper_temporary_${name}_v1:`)); } function clearRemovedDiagnosticStorage() { try {for (const key of GM_listValues()) if (isRemovedDiagnosticStorageKey(key)) deleteHelperStorageValue(key);} catch (error) {log('Nie udało się usunąć starej diagnostyki',error);} } function isTemporaryScriptStorageKey(key) { const value=String(key || ''); if (['vm_escort_ad_request_v1_','vm_escort_ad_result_v1_','vm_escort_ad_consumer_v1_'].some(prefix => value.startsWith(prefix))) return true; return value === WATCH_CHECK_LOCK_KEY || value === DIAGNOSTICS_STORAGE_KEY + '_salt' || isRemovedDiagnosticStorageKey(value) || value.startsWith('vm_helper_request_rates_v1:') || value.startsWith('vm_escort_activity_pending_v1_') || value.startsWith('vm_escort_request_queue_') || value.startsWith('vm_garso_watch_notification_claim:') || value.startsWith('vm_helper_cache_read_tab_') || value.startsWith(GARSO_SEARCH_TICKET_PREFIX) || value.startsWith(GARSO_OPEN_BRIDGE_KEY_PREFIX) || value.startsWith('vm_escort_location_tab_') || /^vm_escorti_(?:result|boot)_(?:list|watch)_/i.test(value); } function isTechnicalScriptStorageKey(key) { const value=String(key || ''); return isAuxiliaryCacheStorageKey(value) || isStorageKeyInNamespace(value,DIAGNOSTICS_STORAGE_KEY) || value === GARSO_SEARCH_GLOBAL_SLOT_KEY || value === GARSO_SEARCH_NATIVE_UNTIL_KEY || value === ADAPTIVE_BACKOFF_STORAGE_KEY || value === 'vm_escorti_profile_ad_index' || isTemporaryScriptStorageKey(value); } function getTechnicalScriptStorageKeys() { try { return GM_listValues().filter(isTechnicalScriptStorageKey); } catch { return []; } } function getScriptDataStorageKeys() { try { return GM_listValues().filter(key => !isScriptCacheStorageKey(key) && !isTemporaryScriptStorageKey(key) ); } catch { return []; } } function getGmStorageStatsForKeys(keys) { const uniqueKeys=[...new Set(Array.isArray(keys) ? keys:[])]; let bytes=0; const encoder=new TextEncoder(); for (const key of uniqueKeys) { try { const value=GM_getValue(key,null); const serialized=JSON.stringify(value) ?? String(value ?? ''); bytes += encoder.encode(String(key)).length + encoder.encode(serialized).length; } catch {} } return {entries:uniqueKeys.length,bytes}; } function getScriptDataStats() { return getGmStorageStatsForKeys(getScriptDataStorageKeys()); } function getUserDataStorageKeys() { return getScriptDataStorageKeys().filter( key => !isTechnicalScriptStorageKey(key) ); } function getUserDataStats() { return getGmStorageStatsForKeys(getUserDataStorageKeys()); } function getTechnicalScriptDataStats() { return getGmStorageStatsForKeys(getTechnicalScriptStorageKeys()); } function getViolentmonkeyStorageStats() { try { return getGmStorageStatsForKeys(GM_listValues()); } catch { return {entries:0,bytes:0}; } } async function clearScriptDataStorage() { const keys=getScriptDataStorageKeys(); await Promise.all(keys.map(async key => { await GM.deleteValue(key); invalidateHelperReadCache(key); })); try {sessionStorage.removeItem(ESCORT_LIST_PAGES_RELOAD_KEY);} catch {} try {sessionStorage.removeItem(ESCORT_MULTI_LOCATION_SESSION_KEY);} catch {} return keys.length; } function clearTechnicalScriptDataStorage() { const keys=getTechnicalScriptStorageKeys().filter(key => key !== GARSO_SEARCH_GLOBAL_SLOT_KEY && key !== GARSO_SEARCH_NATIVE_UNTIL_KEY && !key.startsWith(GARSO_SEARCH_TICKET_PREFIX)); clearTimeout(escortDataFieldCatalogSaveTimer); escortDataFieldCatalogSaveTimer=null; clearTimeout(escortAdCacheAvailabilityIndexSaveTimer); escortAdCacheAvailabilityIndexSaveTimer=null; clearTimeout(persistentCacheStatsSnapshotTimer); persistentCacheStatsSnapshotTimer=null; if (diagnosticsSaveTimer) { clearTimeout(diagnosticsSaveTimer); diagnosticsSaveTimer=null; } for (const key of keys) { try {deleteHelperStorageValue(key);} catch {} } try {watchedAdDataMirrorMemory=null;} catch {} escortDataFieldCatalogMemory=null; escortDataFieldCatalogBootstrapped=false; escortAdCacheAvailabilityIndexMemory=null; invalidateEscortiProfileAdIndexMemory(); fallbackPersistentCacheStatsMemo=null; clearDiagnostics(); clearSharedEscortActivityCache(); return keys.length; } function clearCacheEntries(getKeys,errorMessage,afterClear=null) { try { const keys=getKeys(); for (const key of keys) deletePersistentCacheValue(key); afterClear?.(); return keys.length; } catch (error) { log(errorMessage,error); return 0; } } function clearGarsoCache() { return clearCacheEntries( () => [ ...getGarsoSearchCountCacheKeys(), ...getGarsoSummaryCacheKeys(), ...getGarsoExtendedCacheKeys(), ...getGarsoTopicPageCacheKeys() ], 'Błąd czyszczenia cache Garsoniery' ); } function clearEscortListCache() { clearEscortiRecentProfileData(); const removed=clearCacheEntries( getEscortListCacheKeys, 'Błąd czyszczenia cache Escorti' ) + clearSharedEscortActivityCache(); invalidateEscortiProfileAdIndexMemory(); return removed; } function clearEscortAdDataCache() { return clearCacheEntries( () => [...getEscortAdDataCacheKeys(), ...GM_listValues().filter(key => key === 'vm_escort_ad_template_v1')], 'Błąd czyszczenia cache danych Escort.club' ); } function clearPhoneReputationCache() { return clearCacheEntries( getPhoneReputationCacheKeys, 'Błąd czyszczenia cache sprawdzania numerów telefonu' ); } function clearPersistentCacheAuxiliaryData() { clearEscortiRecentProfileData(); clearSharedEscortActivityCache(); clearTimeout(escortDataFieldCatalogSaveTimer); escortDataFieldCatalogSaveTimer=null; clearTimeout(escortAdCacheAvailabilityIndexSaveTimer); escortAdCacheAvailabilityIndexSaveTimer=null; try { for (const key of GM_listValues()) { if ( key === 'vm_escort_ad_template_v1' || isStorageKeyInNamespace(key,WATCH_AD_DATA_MIRROR_STORAGE_KEY) || isStorageKeyInNamespace(key,ESCORT_DATA_FIELD_CATALOG_STORAGE_KEY) || key === ESCORT_AD_CACHE_INDEX_STORAGE_KEY ) deleteHelperStorageValue(key); } } catch {} try {watchedAdDataMirrorMemory=null;} catch {} escortDataFieldCatalogMemory=null; escortDataFieldCatalogBootstrapped=false; escortAdCacheAvailabilityIndexMemory=null; invalidateEscortiProfileAdIndexMemory(); } function clearPersistentCache() { if (persistentCacheUsesIndexedDb) { const entries=persistentCacheMemory.size; persistentCacheMemory.clear(); persistentCacheEntrySizeBytes.clear(); persistentCacheClearEpoch++; for (const key of persistentCacheCatalog?.keys() || []) notePersistentCacheMutation(key); persistentCacheCatalog?.clear(); persistentCacheLoadedKeys.clear(); deferredNonTopicCache=false; deferredTopicCacheRange=null; deferredTopicCacheCount=0; persistentRelatedListIndex=null; persistentPhoneAdIndex=null; try {escortAdDataMemoryCache.clear();} catch {} fallbackPersistentCacheStatsMemo=null; clearPersistentCacheAuxiliaryData(); savePersistentCacheStatsSnapshot(calculatePersistentCacheStats()); enqueuePersistentCacheDbOperation(async () => { const transaction=persistentCacheDb.transaction( PERSISTENT_CACHE_STORE_NAME, 'readwrite' ); transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).clear(); await waitForIndexedDbTransaction(transaction); }); return entries; } const removed=clearGarsoCache() + clearEscortListCache() + clearEscortAdDataCache() + clearPhoneReputationCache(); clearPersistentCacheAuxiliaryData(); try {escortAdDataMemoryCache.clear();} catch {} return removed; } function persistentCacheBridgeRequestKey(token) { return `${PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX}${token}`; } function persistentCacheBridgeResponseKey(token) { return `${PERSISTENT_CACHE_BRIDGE_RESPONSE_PREFIX}${token}`; } async function handlePersistentCacheBridgePage(sharedStateReady=false) { if (!IS_ESCORT_CLUB) return false; const pageUrl=new URL(location.href); const token=pageUrl.searchParams.get(PERSISTENT_CACHE_BRIDGE_PARAM); if (!token) return false; const requestKey=persistentCacheBridgeRequestKey(token); const responseKey=persistentCacheBridgeResponseKey(token); const request=GM_getValue(requestKey,null); if (request?.action === 'read-ad-data' && !sharedStateReady) return false; if (request?.action === 'read-ad-data' || GM_getValue(persistentCacheReadTabKey(location.hostname),null)?.token === token) { initEscortAdCacheReader(token); await servePersistentCacheAdRequest(token,true); return true; } let response; try { if ( !request || typeof request !== 'object' || Date.now() - Number(request.createdAt || 0) > 2 * 60 * 1000 ) { throw new Error('Żądanie dostępu do cache wygasło lub jest nieprawidłowe.'); } await ensureFullPersistentCache(); let removed=0; if (request.action === 'clear-all') { removed=clearPersistentCache(); } else if (request.action === 'clear-expired') { removed=clearExpiredPersistentCache({ garsoTtlMs:Math.max(0,Number(request.garsoTtlMs) || getGarsoCacheTtlMs()), escortTtlMs:Math.max(0,Number(request.escortTtlMs) || getListCacheTtlMs()) }); } else { throw new Error('Nieznana operacja cache.'); } await persistentCacheDbOperationChain.catch(() => {}); const stats=calculatePersistentCacheStats(); savePersistentCacheStatsSnapshot(stats); response={ok:true,removed,stats,completedAt:Date.now()}; } catch (error) { response={ ok:false, error:formatHelperDiagnosticError(error), completedAt:Date.now() }; } try {deleteHelperStorageValue(requestKey);} catch {} try {writeHelperStorageValue(responseKey,response);} catch {} setTimeout(() => { try {window.close();} catch {} },80); return true; } function initEscortAdCacheReader(auxiliaryToken='') { if (!IS_ESCORT_CLUB || window.top !== window.self || persistentCacheReaderInstalled || typeof GM_addValueChangeListener !== 'function') return; persistentCacheReaderInstalled=true; const ownerKey=persistentCacheReaderKey(location.hostname); let heartbeat=null,listenerId=null,idleSince=0,stopped=false; const publish=() => { if (stopped) return; if (auxiliaryToken) { const key=persistentCacheReadTabKey(location.hostname),lease=GM_getValue(key,null); const now=Date.now(),pending=findPendingPersistentCacheReadToken(location.hostname); if (pending) idleSince=0; else if (!idleSince) idleSince=now; // Brak żywych żądań kończy również odczyt zawieszony w IndexedDB. if ((idleSince && now - idleSince >= 2000) || (lease?.token !== auxiliaryToken && !persistentCacheReaderBusy)) { stopped=true; clearInterval(heartbeat); if (listenerId != null && typeof GM_removeValueChangeListener === 'function') GM_removeValueChangeListener(listenerId); if (lease?.token === auxiliaryToken) deleteHelperStorageValue(key); if (GM_getValue(ownerKey,null)?.session === GARSO_SEARCH_INSTANCE_ID) deleteHelperStorageValue(ownerKey); try {window.close();} catch {} return; } } const owner=getReadyPersistentCacheReader(location.hostname); if (auxiliaryToken || !owner || owner.session === GARSO_SEARCH_INSTANCE_ID) writeHelperStorageValue(ownerKey, {protocol:1,host:location.hostname,session:GARSO_SEARCH_INSTANCE_ID,ready:true,auxiliary:!!auxiliaryToken,token:auxiliaryToken,at:Date.now()}); }; publish(); if (stopped) return; heartbeat=setInterval(publish,5000); window.addEventListener('pageshow',event => { if (event.persisted) {publish(); clearInterval(heartbeat); if (!stopped) heartbeat=setInterval(publish,5000);} }); listenerId=GM_addValueChangeListener(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX + 'signal',(_key,_old,token) => { servePersistentCacheAdRequest(token).catch(error => log('Błąd odbiorcy cache',error)); }); window.addEventListener('pagehide',() => { clearInterval(heartbeat); if (getReadyPersistentCacheReader(location.hostname)?.session === GARSO_SEARCH_INSTANCE_ID) deleteHelperStorageValue(ownerKey); }); for (const key of GM_listValues()) if (key.startsWith(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX) && !key.includes('reader_') && !key.endsWith('signal')) servePersistentCacheAdRequest(key.slice(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX.length)).catch(() => {}); } function requestPersistentCacheBridgeAction(action,options={}) { if (action !== 'read-ad-data' || IS_ESCORT_CLUB) return requestPersistentCacheBridgeDirect(action,options); if (options.cancelToken?.cancelled) return Promise.reject(createOperationCancelledError(options.cancelToken.reason)); const host=String(options.host || 'pl.escort.club').toLowerCase(); let batch=persistentReadBridgeJobs.get(host); if (!batch) { batch={consumers:new Set(),token:createOperationCancelToken('Odczyt cache Escort.club')}; persistentReadBridgeJobs.set(host,batch); setTimeout(async () => { if (persistentReadBridgeJobs.get(host) === batch) persistentReadBridgeJobs.delete(host); if (!batch.consumers.size) return; try { const adIds=[...new Set([...batch.consumers].flatMap(item => item.ids))]; const result=await requestPersistentCacheBridgeDirect(action,{host,adIds,cancelToken:batch.token}); for (const item of batch.consumers) {item.unsubscribe?.(); item.resolve(copyWatchedValue({...result,ads:(result.ads || []).filter(ad => item.ids.includes(String(ad.adData?.adId)))}));} } catch (error) {for (const item of batch.consumers) {item.unsubscribe?.(); item.reject(error);}} finally {batch.consumers.clear();} },20); } return new Promise((resolve,reject) => { const item={resolve,reject,ids:(options.adIds || []).map(String).filter(id => /^\d+$/.test(id))}; batch.consumers.add(item); item.unsubscribe=options.cancelToken?.onCancel(reason => { batch.consumers.delete(item); reject(createOperationCancelledError(reason)); if (!batch.consumers.size) {if (persistentReadBridgeJobs.get(host) === batch) persistentReadBridgeJobs.delete(host); batch.token.cancel(reason);} }); }); } async function runPersistentCacheClearAcrossDomains( action, options={} ) { if (!IS_ESCORT_CLUB) { return requestPersistentCacheBridgeAction( action, options ); } await ensureFullPersistentCache(); const removed= action === 'clear-expired' ? clearExpiredPersistentCache(options) :clearPersistentCache(); await persistentCacheDbOperationChain.catch(() => {}); const stats=calculatePersistentCacheStats(); savePersistentCacheStatsSnapshot(stats); return {ok:true,removed,stats}; } async function clearPersistentCacheAcrossDomains() { return runPersistentCacheClearAcrossDomains( 'clear-all' ); } async function clearExpiredPersistentCacheAcrossDomains(options={}) { return runPersistentCacheClearAcrossDomains( 'clear-expired', options ); } function isPersistentCacheValueExpired(value,ttlMs,now=Date.now()) { const checkedAt=Number(value?.checkedAt); return !Number.isFinite(checkedAt) || checkedAt <= 0 || now - checkedAt >= ttlMs; } function scanExpiredPersistentCache({ garsoTtlMs=getGarsoCacheTtlMs(), escortTtlMs=getListCacheTtlMs() }={}) { if ( !IS_ESCORT_CLUB && readPersistentCacheStatsSnapshot()?.backend === 'IndexedDB' ) { return { available:false, entries:0, bytes:0, megabytes:0, actions:[], listCacheChanged:false }; } const now=Date.now(); const actions=[]; let entries=0; let bytes=0; let listCacheChanged=false; const readValue=key => { if ( persistentCacheUsesIndexedDb && isIndexedDbCacheKey(key) ) { return persistentCacheMemory.has(key) ? persistentCacheMemory.get(key) :null; } try { return GM_getValue(key,null); } catch { return null; } }; const entrySize=(key,value) => { if (persistentCacheUsesIndexedDb) { return ( persistentCacheEntrySizeBytes.get(key) || getPersistentCacheEntrySizeBytes(key,value) ); } return getPersistentCacheEntrySizeBytes(key,value); }; const addDelete=(key,value,count=1) => { const normalizedCount=Math.max( 1, Number(count) || 1 ); const actionBytes=entrySize(key,value); actions.push({ type:'delete', key, count:normalizedCount, bytes:actionBytes }); entries += normalizedCount; bytes += actionBytes; }; const addWrite=( key, currentValue, nextValue, count ) => { const normalizedCount=Math.max( 1, Number(count) || 1 ); const currentBytes= entrySize(key,currentValue); const nextBytes= getPersistentCacheEntrySizeBytes( key, nextValue ); const actionBytes=Math.max( 0, currentBytes - nextBytes ); actions.push({ type:'write', key, value:nextValue, count:normalizedCount, bytes:actionBytes }); entries += normalizedCount; bytes += actionBytes; }; for (const key of getSharedEscortActivityCacheKeys()) { const value=readValue(key); if (!isSharedEscortActivityEntryValid(value,now)) addDelete(key,value); } const garsoKeys=[ ...getGarsoSearchCountCacheKeys(), ...getGarsoSummaryCacheKeys(), ...getGarsoExtendedCacheKeys(), ...getGarsoTopicPageCacheKeys() ]; for (const key of garsoKeys) { const value=readValue(key); if ( isPersistentCacheValueExpired( value, garsoTtlMs, now ) ) { addDelete(key,value); } } for (const key of getEscortListCacheKeys()) { const value=readValue(key); const sourceModes= value?.modes && typeof value.modes === 'object' ? value.modes :{}; const freshModes=Object.fromEntries( Object.entries(sourceModes).filter( ([,entry]) => !isPersistentCacheValueExpired( entry, escortTtlMs, now ) ) ); const expiredModes= Object.keys(sourceModes).length - Object.keys(freshModes).length; if (!Object.keys(freshModes).length) { addDelete( key, value, Math.max(1,expiredModes) ); listCacheChanged=true; } else if (expiredModes > 0) { addWrite( key, value, { ...value, modes:freshModes }, expiredModes ); listCacheChanged=true; } } const watchedAdIds=new Set( getWatchedItems().flatMap(item => getWatchedItemAdUrls(item,false) .map(parseAdIdFromUrl) .filter(Boolean) .map(String) ) ); for (const key of getEscortAdDataCacheKeys()) { const value=readValue(key); if ( !isPersistentCacheValueExpired( value, escortTtlMs, now ) ) continue; const adId= String(key).match( /(\d+)(?!.*\d)/ )?.[1] || ''; if ( adId && watchedAdIds.has(adId) ) continue; addDelete(key,value); } for (const key of getPhoneReputationCacheKeys()) { const value=readValue(key); if ( isPersistentCacheValueExpired( value, escortTtlMs, now ) ) { addDelete(key,value); } } return { available:true, entries, bytes, megabytes:bytes / (1024 * 1024), actions, listCacheChanged }; } function getExpiredPersistentCacheStats( options={} ) { const scan= scanExpiredPersistentCache(options); return { available:scan.available, entries:scan.entries, bytes:scan.bytes, megabytes:scan.megabytes }; } function clearExpiredPersistentCache( options={} ) { const scan= scanExpiredPersistentCache(options); if (!scan.available) return 0; for (const action of scan.actions) { if (action.type === 'write') { writePersistentCacheValue( action.key, action.value ); } else { deletePersistentCacheValue( action.key ); } } if (scan.listCacheChanged) { invalidateEscortiProfileAdIndexMemory(); } return scan.entries; } let persistentCacheReady=!IS_ESCORT_CLUB; const persistentCacheInitialization=initializePersistentCacheStorage().finally(() => { persistentCacheReady=true; }); let pendingCacheUiAction=null; function deferUntilPersistentCacheReady(action) { if (persistentCacheReady) return false; if (!pendingCacheUiAction) { showWatchToast('Wczytywanie zapisanych danych…'); persistentCacheInitialization.then(() => { const resume=pendingCacheUiAction; pendingCacheUiAction=null; detachHelperNode(document.getElementById('vm-garso-watch-toast')); resume?.(); }); } pendingCacheUiAction=action; return true; } const BUTTON_COLORS={ checking:'#777777', found:'#28a745', empty:'#2878c8', error:'#dc3545' }; const CATEGORY_DEFS=[ { key:'appointment', label:'Umówienie', patterns:[ 'kontakt / umówienie','kontakt/umówienie','umówienie się','umowienie sie', 'the date','setup','umówienie','umowienie','umawianie','ustawka','kontakt','formalizmy' ] }, { key:'location', label:'Lokalizacja', patterns:[ 'miejsce spotkania','lokalizacja','lokalizacjia','mieszkanie', 'the place','apartment','miejscówka','miejscowka','lokum','lokal','miejsce','łazienka','lazienka' ] }, { key:'girl', label:'Wygląd', patterns:[ 'ocena dziewczyny','ocena wyglądu','ocena wygladu','wygląd dziewczyny','wyglad dziewczyny','wygląd panny','wyglad panny', 'wygląd pani','wyglad pani','zgodność ze zdjęciami','zgodnosc ze zdjeciami', 'wygląd laski','wyglad laski','wygląd','wyglad','appearance','masażystka','masazystka', 'dziewoja','dziewczyna','bohaterka','figura','uroda','ciało','cialo','twarz', 'buzia','panna','pani','diva','laska','osoba' ] }, { key:'oral', label:'Francuz', patterns:[ 'seks oralny','sex oralny','francuz bez gumki','francuz w gumce','francuski','francuz', 'fbg / fwg','fwg / fbg','fbg/fwg','fwg/fbg','fbg','fwg', 'fgb','bj','blowjob','oral','lodowanie','lodzik','francja','lód','lod' ] }, { key:'action', label:'Akcja', patterns:[ 'zgodność usługi z ofertą','zgodnosc uslugi z oferta','przebieg spotkania', 'akcja / serwis','akcja + fbg','akcja / seks','akcja / sex','akcja/fbg', 'akcja/seks','akcja/sex','seks/akcja','sex/akcja','sex/anal', 'całość akcji','calosc akcji','akcja właściwa','akcja wlasciwa', 'anal fingering','anal sex','feet licking','zakończenie','zakonczenie', 'usługi','uslugi','przebieg','klasyczny','seks analny','sex analny','klasyka','klasyk', 'rimming','fisting','uległość','uleglosc','serwis', 'action','zabawy','analny','akcja','seks','sex','anal','piss' ] }, {key:'massage',label:'Masaż',patterns:['masaż','masaz','massage']}, { key:'atmosphere', label:'Atmosfera', shortPatterns:['charakter'], patterns:[ 'podejście do klienta','podejscie do klienta','klimat spotkania', 'erotyka','the atmosphere','atmosphere','atmosfera','aranżacja','aranzacja','klimat','atmo' ] }, { key:'summary', label:'Podsumowanie', overall:true, patterns:[ 'ogólna ocena','ogolna ocena','ocena ogólna','ocena ogolna', 'wrażenie ogólne','wrazenie ogolne', 'overall','całościowo','calosciowo','podsumowując','podsumowujac','podsumowanie','całokształt','caloksztalt', 'ogółem','ogolem','ogólnie','ogolnie','całość','calosc', 'spotkanie','ogół','ogol','ocena' ] } ]; const summaryCache=new Map(); let garsoAuthPromise=null; let garsoRequestQueue=Promise.resolve(); const garsoAutoSearchJobs=new Map(); let garsoTopicStartQueue=Promise.resolve(); let garsoTopicLastStartedAt=0; let garsoTopicGapMs=GARSO_TOPIC_MIN_GAP_MS; let garsoTopicSuccessStreak=0; let garsoTopicActiveRequests=0; const garsoTopicRequestWaiters=[]; const enqueueListJob=createConcurrentJobQueue( LIST_MAX_CONCURRENT, 'Błąd kolejki listy' ); const enqueueEscortAdDataJob=createConcurrentJobQueue(() => Math.max(ESCORT_AD_DATA_MAX_CONCURRENT, normalizeEscortQueueSettings(readHelperSettingsSnapshot()).escortMaxConcurrent), 'Błąd kolejki danych Escort.club'); const escortAdDataMemoryCache=new Map(); const escortAdDataInflight=new Map(); const pendingEscortiActivityCacheWrites=new Map(); function yieldHelperWork() { if (typeof globalThis.scheduler?.yield === 'function') return globalThis.scheduler.yield(); return new Promise(resolve => setTimeout(resolve,0)); } window.addEventListener('pagehide',() => { garsoSearchPageClosed=true; deleteHelperStorageValue(GARSO_SEARCH_TICKET_PREFIX + GARSO_SEARCH_INSTANCE_ID); flushDiagnostics(); }); window.addEventListener('pageshow',() => {garsoSearchPageClosed=false;}); document.addEventListener('visibilitychange',() => { if (document.visibilityState === 'hidden') {flushDiagnostics();} }); function createEmptyDiagnostics() { return {updatedAt:Date.now(),counters:{requests:0,garsoRequests:0,escortiRequests:0,escortClubRequests:0, garsoRetries:0,http429:0,antiflood:0,errors:0,parserIssues:0,parserEscortClub:0,parserEscorti:0,parserGarso:0, parserOther:0,cacheLookups:0,cacheHits:0,cacheMisses:0},requestBuckets:{},network:{escortClub:{ lastSuccessAt:0,lastErrorAt:0,lastErrorStatus:0,last429At:0,last503At:0,last403At:0}}, watch:{lastScheduledRunAt:0,lastScheduledFullRunAt:0},errors:[],parserIssues:[]}; } function normalizeDiagnostics(value) { const result=createEmptyDiagnostics(),source=value && typeof value === 'object' ? value:{}; const count=value => Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value):0; for (const [target,input] of [[result.counters,source.counters],[result.network.escortClub,source.network?.escortClub],[result.watch,source.watch]]) for (const key of Object.keys(target)) target[key]=count(input?.[key]); const network=result.network.escortClub; if ([403,503].includes(network.lastErrorStatus)) network[`last${network.lastErrorStatus}At`]=Math.max(network[`last${network.lastErrorStatus}At`],network.lastErrorAt); result.updatedAt=count(source.updatedAt); const now=Date.now(),cutoff=now - DIAGNOSTICS_REQUEST_BUCKET_RETENTION_MS; for (const [at,input] of Object.entries(source.requestBuckets || {})) { if (!Number.isSafeInteger(Number(at)) || Number(at) < cutoff || Number(at) > now) continue; result.requestBuckets[at]=Object.fromEntries(['requests','garso','escorti','escortClub','other','http429'].map(key => [key,count(input?.[key])])); } for (const key of ['errors','parserIssues']) result[key]=mergeDiagnosticEvents((Array.isArray(source[key]) ? source[key]:[]) .slice(-100).filter(event => getDiagnosticEventTimestamp(event)).map(event => ({ at:getDiagnosticEventTimestamp(event),scriptVersion:/^[\d.]{1,24}$/.test(event.scriptVersion || '') ? event.scriptVersion:'', area:sanitizeDiagnosticText(event.area),message:sanitizeDiagnosticText(event.message),origin:normalizeDiagnosticErrorOrigin(event.origin), count:Math.min(1000000000,Math.max(1,Math.floor(count(event.count)))), firstAt:Math.min(getDiagnosticEventTimestamp(event),getDiagnosticEventTimestamp({at:event.firstAt}) || getDiagnosticEventTimestamp(event))}))); return result; } function getDiagnosticsState() { const epoch=String(GM_getValue(DIAGNOSTICS_EPOCH_KEY,'0')); if (!diagnosticsState || diagnosticsState.epoch !== epoch) diagnosticsState={...createEmptyDiagnostics(),epoch}; return diagnosticsState; } function flushDiagnostics() { clearTimeout(diagnosticsSaveTimer); diagnosticsSaveTimer=null; if (!diagnosticsState) return; try { if (diagnosticsState.epoch !== String(GM_getValue(DIAGNOSTICS_EPOCH_KEY,'0'))) {diagnosticsState=null; return;} writeHelperStorageValue(DIAGNOSTICS_TAB_PREFIX + diagnosticsState.epoch + ':' + GARSO_SEARCH_INSTANCE_ID,diagnosticsState); } catch {} } function getMergedDiagnosticsState() { const local=getDiagnosticsState(),epoch=local.epoch,prefix=DIAGNOSTICS_TAB_PREFIX + epoch + ':'; const merged=normalizeDiagnostics(epoch === '0' ? GM_getValue(DIAGNOSTICS_STORAGE_KEY,null):null); const states=[local,...GM_listValues().filter(key => key.startsWith(prefix) && key !== prefix + GARSO_SEARCH_INSTANCE_ID) .map(key => GM_getValue(key,null)).filter(state => state?.epoch === epoch)]; for (const value of states) { const state=normalizeDiagnostics(value); for (const key of Object.keys(merged.counters)) merged.counters[key]+=state.counters[key]; for (const [minute,bucket] of Object.entries(state.requestBuckets)) { const target=merged.requestBuckets[minute] ||= {}; for (const [key,count] of Object.entries(bucket)) target[key]=(target[key] || 0) + count; } for (const key of ['errors','parserIssues']) merged[key].push(...state[key]); const network=merged.network.escortClub,next=state.network.escortClub; if (next.lastErrorAt > network.lastErrorAt) network.lastErrorStatus=next.lastErrorStatus; for (const key of ['lastSuccessAt','lastErrorAt','last429At','last503At','last403At']) network[key]=Math.max(network[key],next[key]); for (const key of Object.keys(merged.watch)) merged.watch[key]=Math.max(merged.watch[key],state.watch[key]); merged.updatedAt=Math.max(merged.updatedAt,state.updatedAt); } for (const key of ['errors','parserIssues']) merged[key]=mergeDiagnosticEvents(merged[key]); return merged; } function scheduleDiagnosticsSave() { if (!diagnosticsSaveTimer) diagnosticsSaveTimer=setTimeout(flushDiagnostics,DIAGNOSTICS_SAVE_DELAY_MS); } function incrementDiagnosticCounter(key,amount=1) { const state=getDiagnosticsState(); if (!Object.prototype.hasOwnProperty.call(state.counters,key)) return; state.counters[key]=Math.max(0,state.counters[key] + (Number(amount) || 0)); state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } function mergeDiagnosticEvents(events) { const groups=new Map(); for (const event of events) { const frame=event.origin?.[0],key=JSON.stringify([event.scriptVersion,event.area,event.message,frame?.fn,frame?.line,frame?.column]); const old=groups.get(key); groups.set(key,{...(old && old.at > event.at ? old:event), count:Math.min(1000000000,(old?.count || 0) + (event.count || 1)), firstAt:Math.min(old?.firstAt || event.firstAt || event.at,event.firstAt || event.at)}); } return [...groups.values()].sort((a,b) => a.at - b.at).slice(-DIAGNOSTICS_MAX_EVENTS); } function addDiagnosticEvent(bucket,area,message='',origin=[]) { const state=getDiagnosticsState(); if (!['errors','parserIssues'].includes(bucket)) return; const now=Date.now(); state[bucket]=mergeDiagnosticEvents([...state[bucket],{at:now,firstAt:now,count:1,scriptVersion:SCRIPT_VERSION, area:sanitizeDiagnosticText(area || 'skrypt'),message:sanitizeDiagnosticText(message),origin}]); state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } function recordDiagnosticError(area,error='') { if (isOperationCancelledError(error)) return; incrementDiagnosticCounter('errors'); addDiagnosticEvent('errors',area,formatHelperDiagnosticError(error),getDiagnosticErrorOrigin(error)); } function getDiagnosticEventTimestamp(event) { const numeric=Number(event?.at); const at=Number.isFinite(numeric) ? numeric:Date.parse(event?.at); return at > 0 && Number.isFinite(new Date(at).getTime()) ? at:0; } function getParserIssueServiceKey(area) { const value=String(area || '').toLocaleLowerCase('pl-PL'); if (value.includes('escort.club')) return 'escortClub'; if (value.includes('escorti.pl')) return 'escorti'; if (value.includes('garso')) return 'garso'; return 'other'; } function recordParserIssue(area,message='') { incrementDiagnosticCounter('parserIssues'); const service=getParserIssueServiceKey(area); const counterKey={ escortClub:'parserEscortClub', escorti:'parserEscorti', garso:'parserGarso', other:'parserOther' }[service]; if (counterKey) { incrementDiagnosticCounter(counterKey); } addDiagnosticEvent('parserIssues',area,message); } function getDiagnosticRequestHostKey(url) { try { const host=new URL(url || '',location.href) .hostname.toLowerCase(); if (/garsoniera\.com\.pl$/.test(host)) return 'garso'; if (/(?:^|\.)escorti\.pl$/.test(host)) return 'escorti'; if (isEscortClubHost(host)) return 'escortClub'; } catch {} return 'other'; } function getDiagnosticRequestBucket(state,at=Date.now()) { const bucketAt= Math.floor(at / DIAGNOSTICS_REQUEST_BUCKET_MS) * DIAGNOSTICS_REQUEST_BUCKET_MS; const key=String(bucketAt); if (!state.requestBuckets[key]) { state.requestBuckets[key]={ requests:0, garso:0, escorti:0, escortClub:0, other:0, http429:0 }; } const cutoff=at - DIAGNOSTICS_REQUEST_BUCKET_RETENTION_MS; for (const oldKey of Object.keys(state.requestBuckets)) { if ((Number(oldKey) || 0) < cutoff) { delete state.requestBuckets[oldKey]; } } return state.requestBuckets[key]; } function addDiagnosticRollingRequest(hostKey) { const state=getDiagnosticsState(); const bucket=getDiagnosticRequestBucket(state); bucket.requests++; bucket[hostKey]=Number(bucket[hostKey] || 0) + 1; state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } function addDiagnosticRolling429() { const state=getDiagnosticsState(); const bucket=getDiagnosticRequestBucket(state); bucket.http429=Number(bucket.http429 || 0) + 1; state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } function recordDiagnosticResponse(url,status=0) { const numericStatus=Number(status) || 0; const hostKey= getDiagnosticRequestHostKey(url); if (hostKey !== 'escortClub') return; const state=getDiagnosticsState(); const network=state.network.escortClub; const now=Date.now(); if ([403,429,503].includes(numericStatus)) network[`last${numericStatus}At`]=now; if (numericStatus === 429) addDiagnosticRolling429(); if (numericStatus === 403) captureEscortBanEvidence(); if (numericStatus >= 200 && numericStatus < 400) { network.lastSuccessAt=now; } else if (numericStatus >= 400) { network.lastErrorAt=now; network.lastErrorStatus=numericStatus; } state.updatedAt=now; scheduleDiagnosticsSave(); } function recordDiagnosticRequestFailure(url,error='') { if (Number(error?.status) >= 400) {recordDiagnosticResponse(url,error.status); return;} if (getDiagnosticRequestHostKey(url) !== 'escortClub') return; const state=getDiagnosticsState(); state.network.escortClub.lastErrorAt=Date.now(); state.network.escortClub.lastErrorStatus= Number(error?.status) || 0; state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } function recordDiagnosticCacheLookup(hit) { try { if (diagnosticsSnapshotInProgress) return; const state=getDiagnosticsState(); state.counters.cacheLookups= Number(state.counters.cacheLookups || 0) + 1; if (hit) { state.counters.cacheHits= Number(state.counters.cacheHits || 0) + 1; } else { state.counters.cacheMisses= Number(state.counters.cacheMisses || 0) + 1; } state.updatedAt=Date.now(); scheduleDiagnosticsSave(); } catch {} } function recordDiagnosticWatchScheduleRun(fullCheckCompleted=false) { const state=getDiagnosticsState(); const now=Date.now(); state.watch.lastScheduledRunAt=now; if (fullCheckCompleted) { state.watch.lastScheduledFullRunAt=now; } state.updatedAt=now; scheduleDiagnosticsSave(); } function getDiagnosticRequestWindow(state,minutes) { const cutoff=Date.now() - minutes * 60 * 1000; const result={ requests:0, garso:0, escorti:0, escortClub:0, other:0, http429:0 }; for (const [key,bucket] of Object.entries( state.requestBuckets || {} )) { if ((Number(key) || 0) < cutoff) continue; for (const field of Object.keys(result)) { result[field] += Number(bucket?.[field]) || 0; } } return result; } function recordDiagnosticRequest(url,method='GET',requestContext=null,transport='gm') { incrementDiagnosticCounter('requests'); const hostKey=getDiagnosticRequestHostKey(url),counter={garso:'garsoRequests',escorti:'escortiRequests',escortClub:'escortClubRequests'}[hostKey]; if (counter) incrementDiagnosticCounter(counter); addDiagnosticRollingRequest(hostKey); recordRequestRate(hostKey,method); return beginDiagnosticRequest(hostKey,method,requestContext,transport,url); } function clearDiagnostics() { writeHelperStorageValue(DIAGNOSTICS_EPOCH_KEY,`${Date.now()}-${Math.random()}`); deleteHelperStorageValue(DIAGNOSTICS_STORAGE_KEY); deleteHelperStorageValue(DIAGNOSTICS_STORAGE_KEY + '_ban'); deleteHelperStorageValue(DIAGNOSTICS_STORAGE_KEY + '_salt'); for (const key of GM_listValues()) if (key.startsWith(DIAGNOSTICS_TAB_PREFIX)) deleteHelperStorageValue(key); diagnosticsState=null; clearTimeout(diagnosticsSaveTimer); diagnosticsSaveTimer=null; clearLiveDiagnosticsReports(); } function sanitizeDiagnosticText(value) { return String(value || '') .replace(/https?:\/\/[^\s"'<>]+/gi,'[URL]') .replace(/[\w.+-]+@[\w.-]+\.[a-z]{2,}/gi,'[E-MAIL]') .replace(/(?:\+?48[\s.-]*)?(?:\d[\s.-]*){9,}/g,'[NUMER]') .replace(/\b\d{5,}\b/g,'[ID]') .replace(/[„“”"]([^„“”"]{2,80})[„“”"]/g,'[NAZWA]') .slice(0,500); } function getWatchDiagnosticsSnapshot(state=getMergedDiagnosticsState()) { const settings=getWatchSettings(); const items=getWatchedItems(); const now=Date.now(); let nextDueAt=Infinity; const result={ enabled:settings.enabled, tracked:items.length, favorites:items.filter(item => item.favorite).length, profiles:items.filter( item => item.sourceType !== 'escort-ad' ).length, ads:items.filter( item => item.sourceType === 'escort-ad' ).length, onlineAlerts:0, locationAlerts:0, changesAlerts:0, newAdsAlerts:0, withErrors:0, overdue:0, intervalMinutes:settings.intervalMinutes, lastScheduledRunAt: Number(state.watch?.lastScheduledRunAt) || 0, lastScheduledFullRunAt: Number(state.watch?.lastScheduledFullRunAt) || 0, nextScheduledRunAt:0 }; for (const item of items) { if (item.modes?.online) result.onlineAlerts++; if (item.modes?.location) result.locationAlerts++; if (item.modes?.changes) result.changesAlerts++; if (item.modes?.newAds) result.newAdsAlerts++; if (item.lastError) result.withErrors++; if (settings.enabled && item.hidden && item.sourceType !== 'escort-ad') { nextDueAt=Math.min(nextDueAt,(Number(item.hiddenLastCheckedAt) || 0) + HIDDEN_PROFILE_CHECK_INTERVAL_MS); } const {eligible,expectedMs,referenceAt,dueAt}=getWatchedItemCheckTiming(item,settings); if (!eligible) continue; nextDueAt=Math.min(nextDueAt,dueAt); const graceMs=Math.max( 2 * 60 * 1000, expectedMs * 0.5 ); if ( !referenceAt || now - referenceAt > expectedMs + graceMs ) { result.overdue++; } } if (Number.isFinite(nextDueAt)) result.nextScheduledRunAt=Math.max(now,nextDueAt); return result; } function formatDiagnosticBackoffStatus(label,state) { if (state?.reloadRequired) return `${label}: wstrzymano po HTTP 403 do odświeżenia strony`; const seconds=Math.max(0,Math.ceil(Number(state?.waitMs || 0) / 1000)); const reason=state?.reason ? ` (${state.reason})`:''; if (state?.limitPerMinute > 0) { const rate=`${state.limitPerMinute}/min łącznie (HEAD + GET/POST)`; if (seconds) return `${label}: pauza ok. ${seconds} s${reason}; po wznowieniu ${rate}.`; if (state.recovery503) return `${label}: ${state.probePending ? 'sprawdzanie':'oczekiwanie na próbę'} po HTTP 503; limit ${rate}.`; if (state.rateFactor < 1) return `${label}: tempo ${state.rateFactor * 100}% po HTTP 503; limit ${rate}.`; return `${label}: wspólny limit ${rate}`; } if (state?.adaptiveEnabled === false) return `${label}: automatyczne spowalnianie wyłączone`; const gaps=state?.adaptiveEnabled ? `; odstęp wyszukiwania ${Number(state.searchGapMs || 0) / 1000} s, tematów ${Number(state.topicGapMs || 0) / 1000} s`:''; if (seconds) return `${label}: pauza ok. ${seconds} s${reason}${gaps}`; return `${label}: ${state?.active ? 'zwiększony odstęp między zapytaniami':'normalne tempo'}${gaps}`; } function getDiagnosticsSnapshot() { flushDiagnostics(); const state=getMergedDiagnosticsState(),rate=getLiveEscortDiagnostics(Date.now(),true),requestWindows={day:getDiagnosticRequestWindow(state,1440)}; for (const [key,seconds] of [['minute',60],['fiveMinutes',300],['halfHour',1800]]) { const counts=rate.requestCounts[seconds]; requestWindows[key]={escortClub:counts[0]+counts[1],escorti:counts[2],garso:counts[3]}; } diagnosticsSnapshotInProgress=true; let cache={},storage={}; try { const stats=getPersistentCacheStats(); cache={enabled:true,backend:stats.backend || 'pamięć',entries:stats.entries || 0,bytes:stats.bytes || 0, garsoEntries:stats.garsoEntries || 0,escortiEntries:stats.listEntries || 0,escortAdEntries:stats.escortAdDataEntries || 0, phoneReputationEntries:stats.phoneReputationEntries || 0,activityEntries:stats.activityEntries || 0,templateEntries:stats.templateEntries || 0}; for (const [key,read] of [['violentmonkey',getViolentmonkeyStorageStats],['userData',getUserDataStats]]) { const value=read(); storage[key]={bytes:Number(value.bytes) || 0,entries:Number(value.entries) || 0}; } } catch (error) {cache.error=sanitizeDiagnosticText(formatHelperDiagnosticError(error));} finally {diagnosticsSnapshotInProgress=false;} const adaptiveBackoff=getAdaptiveBackoffDiagnostics(),watch=getWatchDiagnosticsSnapshot(state),problems=[]; for (const value of Object.values(adaptiveBackoff)) if (value.reason) value.reason=sanitizeDiagnosticText(value.reason); if (adaptiveBackoff.escortClub.reloadRequired) problems.push('Escort.club: BAN na IP — pobieranie wstrzymane do odświeżenia strony'); else if (adaptiveBackoff.escortClub.waitMs > 0) problems.push('Escort.club: pauza po odpowiedzi serwera'); else if (adaptiveBackoff.escortClub.recovery503) problems.push('Escort.club: oczekiwanie na wznowienie po HTTP 503'); else if (adaptiveBackoff.escortClub.rateFactor < 1) problems.push(`Escort.club: tempo ${adaptiveBackoff.escortClub.rateFactor * 100}% po HTTP 503`); if (adaptiveBackoff.garso.active) problems.push(adaptiveBackoff.garso.waitMs > 0 ? 'Garso: pauza po komunikacie serwera':'Garso: zwiększony odstęp między zapytaniami'); if (watch.overdue) problems.push(`${watch.overdue} obserwowanych pozycji opóźnionych w odświeżaniu`); for (const [events,label] of [[state.errors,'błędów'],[state.parserIssues,'problemów parsera']]) { const recent=events.filter(event => event.at >= Date.now() - 86400000).length; if (recent) problems.push(`Grupy ${label} z ostatnich 24 h: ${recent}`); } if (cache.error) problems.push('Nie udało się odczytać stanu cache'); return {format:DIAGNOSTICS_EXPORT_FORMAT,scriptVersion:SCRIPT_VERSION,generatedAt:new Date().toISOString(),sanitized:true, requestEvidence:getRequestEvidenceExport(),counters:state.counters,requestWindows,network:state.network,adaptiveBackoff,watch,cache,cacheDetails:cache,storage, errors:state.errors,parserIssues:state.parserIssues,health:{ok:!problems.length,problems,text:problems.join(' • ')}}; } function downloadDiagnosticsExport() { const payload=getDiagnosticsSnapshot(); const stamp=new Date().toISOString().replace(/[:.]/g,'-'); downloadJsonFile( payload, `escort-helper-diagnostyka-${stamp}.json` ); } function getResponseHeader(headers,name) { try { if (typeof headers?.get === 'function') return String(headers.get(name) || ''); if (headers && typeof headers === 'object') return String(Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1] || ''); return String(headers || '').match(new RegExp('^' + name + ':\\s*([^\\r\\n]*)','im'))?.[1]?.trim() || ''; } catch {return '';} } function classifyResponseProtection(status,body='',headers=null) { const text=typeof body === 'string' ? body.slice(0,120000):''; const signals=[]; if (status === 403) signals.push('http-403'); if (status === 429) signals.push('http-429'); if (status === 503) signals.push('http-503'); const markers=[['rate-limit',/too many requests|rate.limit.exceeded|zbyt wiele zapyta[nń]|zbyt du[zż]o zapyta[nń]/i], ['access-denied',/<title[^>]*>[^<]*(?:access denied|forbidden|blocked)|your (?:ip|access).{0,40}(?:block|ban)|(?:dost[eę]p|adres\s+ip|twoje\s+ip).{0,60}zablokowan|(?:ip address|adres\s+ip).{0,60}(?:banned|blocked)|<title[^>]*>[^<]*site unavailable/i], ['challenge',/cf-chl-|challenge-platform|<title[^>]*>\s*just a moment|verify (?:that )?you are human|checking your browser/i], ['antiflood',/kontrola antyfloodowa|flood control|anty.?flood/i], ['captcha',/<(?:iframe|div)[^>]*(?:g-recaptcha|h-captcha)|<title[^>]*>[^<]*captcha/i]]; for (const [name,pattern] of markers) if (pattern.test(text)) signals.push(name); return {blockSignals:signals}; } var requestRateState; function getRequestRateState() { const epoch=String(GM_getValue(DIAGNOSTICS_EPOCH_KEY,'0')); if (!requestRateState || requestRateState.epoch !== epoch) requestRateState={epoch,startedAt:Date.now(),buckets:new Map()}; return requestRateState; } function getRequestRateRows(now=Date.now()) { const state=getRequestRateState(),cutoff=Math.floor(now / 1000) * 1000 - 1800000; for (const at of state.buckets.keys()) if (at < cutoff || at > now) state.buckets.delete(at); return [...state.buckets].map(([at,counts]) => [at,...counts]); } function recordRequestRate(service,method) { const index=service === 'escortClub' ? String(method).toUpperCase() === 'HEAD' ? 1:0: service === 'escorti' ? 2:service === 'garso' ? 3:-1; if (index < 0) return; const state=getRequestRateState(),at=Math.floor(Date.now() / 1000) * 1000; const counts=state.buckets.get(at) || [0,0,0,0]; counts[index]++; state.buckets.set(at,counts); publishLiveDiagnosticsReport(); } function getLiveEscortQueueState(queue,status,now) { if (status.reloadRequired) return {text:'BAN na IP — do odświeżenia',tone:'blocked'}; if (status.blockedUntil > now) return {text:`${status.recovery503 ? 'HTTP 503 · pauza':'Pauza serwera'} · ${Math.ceil((status.blockedUntil - now) / 1000)} s`,tone:'blocked'}; if (status.recovery503) return {text:'Ograniczenie po HTTP 503',tone:'waiting'}; if (status.rateFactor < 1) return {text:`HTTP 503 · tempo ${status.rateFactor * 100}% · ${status.limitPerMinute}/min`,tone:'waiting'}; if (Array.isArray(queue.states)) { const waits=queue.states.filter(state => state.tone === 'waiting' || state.tone === 'blocked'); if (waits.length) return {text:waits.map(state => state.text).join(' · '),tone:waits.some(state => state.tone === 'blocked') ? 'blocked':'waiting'}; return {text:'',tone:queue.active ? 'running':'idle'}; } if (!queue.queued) return {text:'',tone:queue.active ? 'running':'idle'}; if ((queue.maxConcurrent > 0 && (queue.sharedActive ?? queue.globalActive ?? queue.active) >= queue.maxConcurrent) || ['global-concurrency-limit','concurrency-limit'].includes(queue.waitReason)) return {text:'Limit równoczesnych zapytań',tone:'waiting'}; if (queue.wakeAt && now - queue.wakeAt > 2000) return {text:'Opóźnione wybudzenie kolejki',tone:'waiting'}; if (queue.waitReason === 'rate-limit' && queue.nextEligibleAt > now) return {text:'Wspólny limit zapytań/min',tone:'waiting'}; return {text:'',tone:'running'}; } function ensureLiveDiagnosticsStyles() { if (document.getElementById('vm-live-diagnostics-style')) return; appendHelperStyle('vm-live-diagnostics-style',` .vm-helper-ui.vm-live-floating { position:fixed; left:12px; bottom:12px; z-index:2147482999; width:80px; box-sizing:border-box; padding:9px 17px 9px 9px; border:1px solid #fff2; border-radius:12px 12px 0 12px; --vm-live-bg:rgba(0,0,0,.6); background:var(--vm-live-bg) !important; color:#fff !important; box-shadow:0 3px 14px #0002; text-shadow:0 1px 2px #0006; backdrop-filter:blur(9px); -webkit-backdrop-filter:blur(9px); font:11px/1.3 var(--vm-ui-font-family,Arial,sans-serif); } .vm-live-floating .vm-live-name { display:flex; align-items:center; gap:4px; margin:0 -8px 4px 0; color:#f0f2f5 !important; font-size:9px; white-space:nowrap; } .vm-live-floating .vm-live-name::before { content:''; width:4px; height:4px; flex:none; border-radius:50%; background:currentColor; } .vm-live-floating .vm-live-metrics { display:grid; gap:6px; } .vm-live-floating .vm-live-value { display:block; height:23px; color:#fff !important; font-size:19px; font-weight:600; line-height:23px; letter-spacing:-.5px; font-variant-numeric:tabular-nums; white-space:nowrap; } .vm-live-floating .vm-live-value[data-high],.vm-live-floating[data-tone="blocked"] .vm-live-name { color:#ffdddd !important; } .vm-live-floating[data-tone="waiting"] .vm-live-name { color:#fff0c2 !important; } .vm-live-floating .vm-live-unit { display:block; height:13px; color:#f0f2f5 !important; font-size:9px; white-space:nowrap; } .vm-live-floating :is(.vm-live-trail,.vm-live-status) { position:absolute; left:calc(100% + 1px); width:min(240px,calc(100vw - 104px)); } .vm-live-floating .vm-live-trail { bottom:15px; height:62px; pointer-events:none; color:color-mix(in srgb,var(--vm-ui-accent,#64748b) 25%,#fff); } .vm-live-floating .vm-live-status { bottom:-1px; height:16px; box-sizing:border-box; padding:1px 4px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; background:linear-gradient(to right,var(--vm-live-bg),transparent); color:#f0f2f5 !important; font-size:10px; line-height:14px; text-shadow:0 1px 2px #000,0 0 3px #000; } .vm-live-floating[data-tone="blocked"] .vm-live-status { color:#ffdddd !important; } .vm-live-floating[data-tone="waiting"] .vm-live-status { color:#fff0c2 !important; } .vm-live-floating :is(.vm-live-trail,.vm-live-trail-bars) { background:transparent; border:0; box-shadow:none; } .vm-live-floating .vm-live-trail-bars { position:absolute; inset:0; display:grid; grid-template-columns:repeat(60,minmax(0,1fr)); align-items:end; gap:1px; mask-image:linear-gradient(to right,#000,transparent); -webkit-mask-image:linear-gradient(to right,#000,transparent); } .vm-live-floating .vm-live-trail-bars::before { content:''; position:absolute; inset:50% 0 0; background:linear-gradient(to top,var(--vm-live-bg) 4px,transparent); } .vm-live-floating .vm-live-trail-bar { min-width:0; position:relative; border-radius:2px 2px 0 0; background:currentColor; box-shadow:inset 0 0 0 1px #0009; } .vm-live-floating .vm-live-scale { position:absolute; left:-15px; width:12px; text-align:right; color:#f0f2f5 !important; font:9px/1 var(--vm-ui-font-family,Arial,sans-serif); font-variant-numeric:tabular-nums; } .vm-live-floating .vm-live-scale-top { top:0; } .vm-live-floating .vm-live-scale-zero { bottom:0; } `); } function createLiveDiagnosticsBadge() { ensureLiveDiagnosticsStyles(); const element=prepareHelperStyleApi(makeElement('section','vm-live-floating')); element.setAttribute('aria-label','Tempo zapytań Escort.club ze wszystkich kart'); const name=makeElement('div','vm-live-name','Escort.club'),status=makeElement('div','vm-live-status'); name.title='Zapytania ze wszystkich raportujących kart. Kropka: żółta — oczekiwanie, czerwona — blokada serwera.'; const metrics=makeElement('div','vm-live-metrics'); const fields=['zap./sek','zap./min','zap./5 min'].map(label => { const cell=makeElement('div','vm-live-metric'),value=makeElement('span','vm-live-value','—'); cell.append(value,makeElement('span','vm-live-unit',label)); metrics.append(cell); return {cell,value}; }); const chart=makeElement('div','vm-live-trail'),columns=makeElement('div','vm-live-trail-bars'); const scale=makeElement('span','vm-live-scale vm-live-scale-top'); chart.setAttribute('role','img'); const bars=Array(60).fill().map(() => {const bar=makeElement('span','vm-live-trail-bar'); columns.append(bar); return bar;}); chart.append(columns,scale,makeElement('span','vm-live-scale vm-live-scale-zero','0')); element.append(name,metrics,chart,status); const format=new Intl.NumberFormat('pl-PL',{maximumFractionDigits:0}); return {element,render(snapshot=getLiveEscortDiagnostics()) { const {get,head}=snapshot.lanes,values=[(get.last5 + head.last5) / 5,get.minute + head.minute,get.fiveMinutes + head.fiveMinutes]; const windows=[5,60,300]; fields.forEach(({cell,value},index) => { value.textContent=snapshot.available ? format.format(values[index]):'—'; value.toggleAttribute('data-high',snapshot.available && values[index] > [Infinity,60,220][index]); cell.title=(index === 0 ? `Zaokrąglona średnia zapytań na sekundę (${get.last5 + head.last5} zap. / 5 s)`:'Liczba wysłanych zapytań') + ` z ostatnich ${windows[index]} pełnych sekund. HEAD + GET/POST ze wszystkich raportujących kart.` + (index === 2 && snapshot.historyLimited ? ' Starsze wersje w innych kartach raportują tylko minutę historii.':'') + (snapshot.unavailable || snapshot.truncated ? ' Niepełny odczyt innych kart.':''); }); const states=['get','head'].map(lane => getLiveEscortQueueState(snapshot.lanes[lane],snapshot.status,snapshot.now)); element.dataset.tone=states.some(state => state.tone === 'blocked') ? 'blocked':states.some(state => state.tone === 'waiting') ? 'waiting':'running'; status.textContent=[...new Set(states.filter(state => state.tone === 'blocked' || state.tone === 'waiting').map(state => state.text))].join(' · '); status.title=status.textContent ? status.textContent + '. Stan wszystkich raportujących kart.':''; const peak=Math.max(1,...snapshot.points.map(point => point.get + point.head)); scale.textContent=String(peak); snapshot.points.forEach((point,index) => {bars[bars.length - 1 - index].style.height=`${(point.get + point.head) / peak * 100}%`;}); chart.setAttribute('aria-label',`Zapytania na sekundę, ostatnie 60 sekund. Skala 0–${peak}. Najnowsze po lewej, przy panelu.`); }}; } let liveDiagnosticsView=null,liveDiagnosticsTimer=null,liveDiagnosticsInstalled=false; function refreshLiveDiagnosticsPanel() { if (!liveDiagnosticsView || document.hidden) return; liveDiagnosticsView.render(); } function syncLiveDiagnosticsPanel() { clearInterval(liveDiagnosticsTimer); liveDiagnosticsTimer=null; if (readHelperSettingsSnapshot().showLiveDiagnostics !== true) { if (liveDiagnosticsView) detachHelperNode(liveDiagnosticsView.element); liveDiagnosticsView=null; return; } if (document.hidden) return; if (!liveDiagnosticsView) {liveDiagnosticsView=createLiveDiagnosticsBadge(); document.body.append(liveDiagnosticsView.element);} refreshLiveDiagnosticsPanel(); liveDiagnosticsTimer=setInterval(refreshLiveDiagnosticsPanel,1000); } function initLiveDiagnosticsPanel() { if (liveDiagnosticsInstalled || window.top !== window.self) return; liveDiagnosticsInstalled=true; initLiveDiagnosticsReports(); if (typeof GM_addValueChangeListener === 'function') GM_addValueChangeListener(SETTINGS_STORAGE_KEY,syncLiveDiagnosticsPanel); document.addEventListener('visibilitychange',syncLiveDiagnosticsPanel); window.addEventListener('pageshow',syncLiveDiagnosticsPanel); window.addEventListener('pagehide',() => {clearInterval(liveDiagnosticsTimer); liveDiagnosticsTimer=null;}); syncLiveDiagnosticsPanel(); } const LIVE_DIAGNOSTICS_PREFIX='vm_helper_request_rates_v1:'; let liveDiagnosticsPublisher=null,liveDiagnosticsLastPublish=0,liveDiagnosticsSignature='',liveDiagnosticsClosed=false; let liveDiagnosticsRemoteCache={at:0,keysAt:0,keys:[],pages:[],truncated:false,unavailable:false}; function getLiveDiagnosticsPage() { const state=getRequestRateState(); return {pageId:ESCORT_REQUEST_INSTANCE_ID,epoch:state.epoch,startedAt:state.startedAt}; } function makeLiveDiagnosticsReport(now=Date.now()) { const status=getEscortRequestQueueStatus(); const queues=Object.fromEntries(['get','head'].map(lane => { const value=status.queues[lane],wait=escortRequestQueueDiagnostics[lane]; return [lane,{queued:value.queued,active:value.active,maxConcurrent:value.maxConcurrent, sharedActive:value.sharedActive,waitReason:wait.waitReason,nextEligibleAt:wait.nextEligibleAt,wakeAt:wait.wakeAt, waitSince:wait.waitSince,lastWakeAt:wait.lastWakeAt,wakeLatenessMs:wait.wakeLatenessMs, oldestQueuedAt:escortRequestQueues[lane].jobs.reduce((at,job) => Math.min(at || job.queuedAt,job.queuedAt),0)}]; })); return {schemaVersion:1,scriptVersion:SCRIPT_VERSION,queueHistory:getEscortQueueEvidence(now),cacheDecisions:getRequestRateState().cacheDecisions || [],...getLiveDiagnosticsPage(),updatedAt:now,closed:liveDiagnosticsClosed, service:getDiagnosticRequestHostKey(location.href),limits:[status.configuredLimitPerMinute,status.configuredMaxConcurrent, readHelperSettingsSnapshot().escortFastStart === false ? 0:1], queues,rows:getRequestRateRows(now),requests:getDiagnosticRequestRows(now),discarded:getRequestRateState().discarded || 0}; } function publishLiveDiagnosticsReport(force=false) { try { if (!force && Date.now() - liveDiagnosticsLastPublish < 2000) return; const report=makeLiveDiagnosticsReport(),{updatedAt,...payload}=report,signature=JSON.stringify(payload); if (!force && signature === liveDiagnosticsSignature && updatedAt - liveDiagnosticsLastPublish < 15000) return; writeHelperStorageValue(LIVE_DIAGNOSTICS_PREFIX + report.pageId,report); liveDiagnosticsSignature=signature; liveDiagnosticsLastPublish=updatedAt; } catch {} } function clearLiveDiagnosticsReports() { for (const key of GM_listValues()) if (key.startsWith(LIVE_DIAGNOSTICS_PREFIX)) deleteHelperStorageValue(key); requestRateState=undefined; liveDiagnosticsSignature=''; liveDiagnosticsLastPublish=0; liveDiagnosticsRemoteCache={at:0,keysAt:0,keys:[],pages:[],truncated:false,unavailable:false}; } function initLiveDiagnosticsReports() { if (liveDiagnosticsPublisher !== null) return; try { const now=Date.now(),epoch=getLiveDiagnosticsPage().epoch; for (const key of GM_listValues()) if (key.startsWith(LIVE_DIAGNOSTICS_PREFIX)) { const page=GM_getValue(key,null); if (!page || page.epoch !== epoch || !Number.isFinite(page.updatedAt) || page.updatedAt < now - 1800000) deleteHelperStorageValue(key); } } catch {} const resume=() => { liveDiagnosticsClosed=false; publishLiveDiagnosticsReport(true); clearInterval(liveDiagnosticsPublisher); liveDiagnosticsPublisher=setInterval(publishLiveDiagnosticsReport,2000); }; window.addEventListener('pagehide',() => { liveDiagnosticsClosed=true; publishLiveDiagnosticsReport(true); clearInterval(liveDiagnosticsPublisher); liveDiagnosticsPublisher=null; }); window.addEventListener('pageshow',resume); document.addEventListener('visibilitychange',() => publishLiveDiagnosticsReport(true)); resume(); } function readLiveDiagnosticsReports(now,force=false) { const cache=liveDiagnosticsRemoteCache; if (!force && cache.at && now >= cache.at && now - cache.at < 2000) return cache; try { if (force || !cache.keysAt || now < cache.keysAt || now - cache.keysAt >= 5000) { cache.keys=GM_listValues().filter(key => key.startsWith(LIVE_DIAGNOSTICS_PREFIX)); cache.keysAt=now; } cache.truncated=cache.keys.length > 128; cache.pages=cache.keys.slice(-128).map(key => GM_getValue(key,null)).filter(page => page?.schemaVersion === 1 && typeof page.pageId === 'string' && /^[a-z0-9-]{1,80}$/.test(page.pageId) && Number.isFinite(page.updatedAt) && page.updatedAt <= now && page.updatedAt >= now - 1800000); cache.unavailable=false; } catch {cache.pages=[]; cache.unavailable=true;} cache.at=now; return cache; } function getLiveEscortDiagnostics(now=Date.now(),force=false) { const own=makeLiveDiagnosticsReport(now),remote=readLiveDiagnosticsReports(now,force),end=Math.floor(now / 1000) * 1000; const status=getEscortRequestQueueStatus(),points=Array(60).fill().map((_,index) => ({at:end - (60 - index) * 1000,get:0,head:0})); const requestCounts={60:[0,0,0,0],300:[0,0,0,0],1800:[0,0,0,0]},lanes={}; for (const lane of ['get','head']) lanes[lane]={...status.queues[lane],queued:0,active:0,states:[]}; const count=value => Number.isSafeInteger(value) && value >= 0 ? value:0,seen=new Set(); for (const report of [own,...remote.pages]) { if (report.epoch !== own.epoch || seen.has(report.pageId)) continue; seen.add(report.pageId); if (!report.closed && now - report.updatedAt <= 90000) for (const lane of ['get','head']) { const value=report.queues?.[lane] || {},queue=lanes[lane]; queue.queued+=count(value.queued); queue.active+=count(value.active); if (now - report.updatedAt > 5000 || !count(value.queued)) continue; const state=getLiveEscortQueueState(value,status,report.updatedAt); if (state.text && !queue.states.some(entry => entry.text === state.text)) queue.states.push(state); } const seconds=new Set(); for (const row of (Array.isArray(report.rows) ? report.rows:[]).slice(-1801)) { const at=row?.[0]; if (!Number.isSafeInteger(at) || at % 1000 || at < end - 1800000 || at >= end || seconds.has(at)) continue; seconds.add(at); for (const seconds of [60,300,1800]) if (at >= end - seconds * 1000) for (let index=0;index<4;index++) requestCounts[seconds][index]+=count(row[index + 1]); const index=(at - points[0].at) / 1000; if (index >= 0) {points[index].get+=count(row[1]); points[index].head+=count(row[2]);} } } for (const [index,lane] of [[0,'get'],[1,'head']]) { const queue=lanes[lane]; queue.active=status.queues[lane].globalActive ?? queue.active; queue.minute=requestCounts[60][index]; queue.fiveMinutes=requestCounts[300][index]; queue.last5=points.slice(-5).reduce((sum,point) => sum + point[lane],0); } return {now,points,status,available:true,requestCounts,lanes,truncated:remote.truncated,unavailable:remote.unavailable}; } function normalizeDiagnosticErrorOrigin(value) { if (!Array.isArray(value)) return []; return value.slice(0,5).filter(frame => Number.isSafeInteger(frame?.line) && frame.line > 0 && frame.line <= 1000000 && Number.isSafeInteger(frame.column) && frame.column >= 0 && frame.column <= 1000000).map(frame => ({ fn:/^(?:[A-Za-z_$][\w.$<>]{0,79}|<anonymous>)$/.test(frame.fn || '') && !/\d{5}/.test(frame.fn) ? frame.fn:'<anonymous>', line:frame.line,column:frame.column})); } function getDiagnosticErrorOrigin(error) { try { if (!error || typeof error !== 'object' || typeof error.stack !== 'string') return []; const frames=[]; for (const line of error.stack.slice(0,8000).split('\n')) { const match=line.match(/^(?:\s*at (?:(.*?) \()?|\s*([^@\s]*)@).+?:(\d+):(\d+)\)?$/); if (!match) continue; frames.push({fn:(match[1] || match[2] || '').replace(/^(?:async|new) /,''),line:Number(match[3]),column:Number(match[4])}); if (frames.length === 5) break; } return normalizeDiagnosticErrorOrigin(frames); } catch {return [];} } function getDiagnosticRequestRows(now=Date.now()) { const state=getRequestRateState(),rows=state.requests || []; state.requests=rows.filter(row => row[0] >= now - 600000).slice(-1000); state.discarded=(state.discarded || 0) + rows.length - state.requests.length; return state.requests; } function beginDiagnosticRequest(service,method,context,transport,url) { try { if (!['escortClub','escorti','garso'].includes(service)) return; if (service === 'escortClub') recordDiagnosticCacheDecision(context); const state=getRequestRateState(),queue=service === 'escortClub' ? getEscortRequestQueueStatus({ foregroundWork:context?.source !== 'access.probe' && (context?.manual || !/^(?:watch[.-]|cache-bridge$)/.test(context?.source || ''))}):{}; const label=value => /^[a-z][a-z.-]{0,59}$/i.test(value || '') ? value:'unknown'; const row=[Date.now(),0,0,service,['GET','HEAD','POST','IFRAME'].includes(String(method).toUpperCase()) ? String(method).toUpperCase():'OTHER',label(context?.source),label(transport), document.hidden ? 0:1,queue.limitPerMinute || 0,queue.maxConcurrent || 0,'pending','',0,{},state.requestSequence=(state.requestSequence || 0) + 1]; const rows=getDiagnosticRequestRows(); rows.push(row); if (rows.length > 1000) {rows.shift(); state.discarded++;} if (service === 'escortClub' && ['GET','HEAD'].includes(row[4]) && url) hashDiagnosticRequestTarget(url).then(hash => { if (state === getRequestRateState()) {row[11]=hash; publishLiveDiagnosticsReport();} }).catch(() => {}); return (status,outcome='response',response=null) => { try { if (row[1] || state !== getRequestRateState()) return; row[10]=outcome; if (!row[12] && ((Number(status) >= 100 && Number(status) <= 599) || (response && ['headers','response'].includes(outcome)))) { row[2]=Number(status) || 0; row[12]=Date.now(); if (response) row[13]=getDiagnosticResponseMeta(response,url); } if (outcome !== 'headers') row[1]=Date.now(); publishLiveDiagnosticsReport(); } catch {} }; } catch {} } function normalizeRequestEvidence(value) { const number=value => Number.isSafeInteger(value) && value >= 0 ? value:0; const label=value => /^[a-z][a-z.-]{0,59}$/i.test(value || '') ? value:'unknown'; const now=number(value?.at),pages=[],seen=new Set(); for (const page of (Array.isArray(value?.pages) ? value.pages:[]).slice(0,128)) { if (!/^[a-z0-9-]{1,80}$/.test(page?.pageId || '') || seen.has(page.pageId)) continue; seen.add(page.pageId); const queues={}; for (const lane of ['get','head']) { const queue=page.queues?.[lane] || {}; queues[lane]=Object.fromEntries(['queued','active','sharedActive','maxConcurrent','nextEligibleAt','wakeAt', 'waitSince','lastWakeAt','wakeLatenessMs','oldestQueuedAt'].map(key => [key,number(queue[key])])); queues[lane].waitReason=label(queue.waitReason); } const rows=(Array.isArray(page.rows) ? page.rows:[]).slice(-1801) .filter(row => Array.isArray(row) && Number.isSafeInteger(row[0]) && row[0] > 0 && row[0] >= now - 1800000 && row[0] <= now) .map(row => Array(5).fill().map((_,index) => number(row[index]))); const requests=(Array.isArray(page.requests) ? page.requests:[]).slice(-1000) .filter(row => Array.isArray(row) && Number.isSafeInteger(row[0]) && row[0] > 0 && row[0] >= now - 600000 && row[0] <= now) .map(row => { const end=number(row[1]) <= now ? number(row[1]):0,headers=number(row[12]) <= now ? number(row[12]):0; const known=end || headers || (!row[1] && !row[12]); return [number(row[0]),end,known ? number(row[2]):0,label(row[3]), ['GET','HEAD','POST','IFRAME'].includes(row[4]) ? row[4]:'OTHER',label(row[5]),label(row[6]), row[7] === 1 ? 1:0,number(row[8]),number(row[9]),end ? label(row[10]):headers ? 'headers':'pending', /^[a-f0-9]{32}$/.test(row[11] || '') ? row[11]:'',headers,normalizeDiagnosticResponseMeta(known ? row[13]:null),number(row[14]),Math.min(now,number(row[15]) || number(page.updatedAt))]; }); pages.push({pageId:page.pageId,service:label(page.service),updatedAt:number(page.updatedAt), startedAt:number(page.startedAt),closed:page.closed === true,discarded:number(page.discarded), scriptVersion:/^[\d.]{1,24}$/.test(page.scriptVersion || '') ? page.scriptVersion:'',enrichedAt:number(page.enrichedAt), queueSnapshotUnavailable:page.queueSnapshotUnavailable === true,queueHistory:normalizeDiagnosticQueueHistory(page.queueHistory,now), cacheDecisions:getDiagnosticCacheDecisions(page.cacheDecisions), limits:(Array.isArray(page.limits) ? page.limits:[]).slice(0,3).map(number),queues,rows,requests}); } return {at:now,truncated:value?.truncated === true || value?.pages?.length > 128,unavailable:value?.unavailable === true,pages}; } function getRequestEvidence(now=Date.now()) { const own=makeLiveDiagnosticsReport(now),remote=readLiveDiagnosticsReports(now,true); return normalizeRequestEvidence({at:now,truncated:remote.truncated,unavailable:remote.unavailable, pages:[own,...remote.pages.filter(page => page.epoch === own.epoch && page.pageId !== own.pageId)]}); } function captureEscortBanEvidence() { try { const key=DIAGNOSTICS_STORAGE_KEY + '_ban',previous=GM_getValue(key,null),now=Date.now(); if (previous?.at && getEscortRequestQueueStatus().reloadRequired) return; writeHelperStorageValue(key,{at:now,scriptVersion:SCRIPT_VERSION,hour:getDiagnosticRequestWindow(getMergedDiagnosticsState(),60).escortClub,evidence:getRequestEvidence(now)}); flushDiagnostics(); const epoch=String(GM_getValue(DIAGNOSTICS_EPOCH_KEY,'0')); for (const delay of [3000,10000]) setTimeout(() => enrichEscortBanEvidence(now,epoch),delay); } catch {} } function getRequestEvidenceExport() { const previous=GM_getValue(DIAGNOSTICS_STORAGE_KEY + '_ban',null),current=getRequestEvidence(); const evidence=previous?.at ? normalizeRequestEvidence(previous.evidence):null; return {schemaVersion:2,scope:'script requests; remote reports may lag; iframe HTTP status unknown', cacheDecisionColumns:['source','kind','missing','expired','incomplete','forced','joined'], admissionColumns:['queuedAt','admittedAt','startedAt','lane','seenActive','lastStartedAt','rateLimit','concurrentLimit','fastStart'], requestColumns:['startAt','endAt','httpStatus','service','method','source','transport','visible','rateLimit','concurrentLimit','outcome','targetFingerprint','firstResponseAt','responseMeta','sequence','observedUntilAt'], rowColumns:['at','escortGetPost','escortHead','escorti','garso'],limitColumns:['perMinute','concurrent','fastStartEnabled'], current,analysis:analyzeRequestEvidence(current),lastBan:Number.isSafeInteger(previous?.at) && previous.at > 0 ? {at:previous.at,enrichedAt:Number.isSafeInteger(previous.enrichedAt) ? previous.enrichedAt:0,hour:Number.isSafeInteger(previous.hour) ? previous.hour:0, scriptVersion:/^[\d.]{1,24}$/.test(previous.scriptVersion || '') ? previous.scriptVersion:'', evidence,analysis:analyzeRequestEvidence(evidence)}:null}; } function analyzeRequestEvidence(evidence) { const at=Number(evidence?.at) || 0,end=Math.floor(at / 1000) * 1000,seconds=new Map(),trace=[],seen=new Set(); const coverage={windowEndAt:end,counterWindowSeconds:1800,traceWindowSeconds:600,pages:0,stalePages:0,closedPages:0, discardedRequests:0,oldestTraceAt:0,remoteUnavailable:evidence?.unavailable === true,truncated:evidence?.truncated === true}; const count=value => Number.isSafeInteger(value) && value >= 0 ? value:0; for (const page of evidence?.pages || []) { if (seen.has(page.pageId)) continue; seen.add(page.pageId); coverage.pages++; if (page.closed) coverage.closedPages++; else if (at - page.updatedAt > 5000) coverage.stalePages++; coverage.discardedRequests+=count(page.discarded); const bucketTimes=new Set(); for (const row of page.rows || []) { if (row[0] < end - 1800000 || row[0] >= end || row[0] % 1000 || bucketTimes.has(row[0])) continue; bucketTimes.add(row[0]); seconds.set(row[0],(seconds.get(row[0]) || 0) + count(row[1]) + count(row[2])); } for (const row of page.requests || []) if (row[3] === 'escortClub' && row[0] >= at - 600000 && row[0] <= at) { const observed=Number.isSafeInteger(row[15]) && row[15] >= 0 ? row[15]:page.updatedAt; const complete=row[1] >= row[0] && row[1] <= at,until=Math.min(at,complete ? row[1]:observed); trace.push({row,complete,until:Math.max(row[0],until)}); } } const buckets=[...seconds].sort((a,b) => a[0] - b[0]),windows={},peaks={}; for (const size of [5,60,300,1800]) { windows[size]=buckets.reduce((sum,[time,value]) => sum + (time >= end - size * 1000 ? value:0),0); if (size === 1800) continue; let sum=0,left=0,peak=0,peakEndAt=0; for (let right=0;right<buckets.length;right++) { sum+=buckets[right][1]; while (buckets[left][0] <= buckets[right][0] - size * 1000) sum-=buckets[left++][1]; if (sum > peak) {peak=sum; peakEndAt=buckets[right][0] + 1000;} } peaks[size]={count:peak,endAt:peakEndAt}; } trace.sort((a,b) => a.row[0] - b.row[0]); coverage.oldestTraceAt=trace[0]?.row[0] || 0; const gaps=[],durations=[],events=[],sources=new Map(),targets=new Map(); const statuses=Object.create(null),outcomes=Object.create(null),methods=Object.create(null); const violations={spacing:0,concurrency:0,spacingToleranceMs:5,spacingExamples:[],concurrencyExamples:[]}; let pending=0,repeatedTargets=0,overlappingTargets=0,fingerprinted=0; for (let index=0;index<trace.length;index++) { const {row,complete,until}=trace[index],start=row[0],method=row[4]; const knownStatus=complete || (row[12] > 0 && row[12] <= at) || (!row[1] && !row[12]); const status=knownStatus && row[2] >= 100 && row[2] <= 599 ? row[2]:0; const outcome=complete ? row[10]:'pending',source=row[5]; statuses[status]=(statuses[status] || 0) + 1; outcomes[outcome]=(outcomes[outcome] || 0) + 1; methods[method]=(methods[method] || 0) + 1; sources.set(source,(sources.get(source) || 0) + 1); if (complete) durations.push(row[1] - start); else pending++; if (index) { const gap=start - trace[index - 1].row[0],expected=row[8] > 0 ? Math.ceil(60000 / row[8]):0; gaps.push(gap); if (gap + violations.spacingToleranceMs < expected) { violations.spacing++; if (violations.spacingExamples.length < 5) violations.spacingExamples.push({at:start,gapMs:gap,expectedMs:expected}); } } if (until > start) {events.push([start,1,row[9]]); events.push([until,-1,0]);} if (/^[a-z0-9-]{8,80}$/i.test(row[11] || '')) { fingerprinted++; const key=method + ':' + row[11],previous=targets.get(key); if (previous !== undefined) {repeatedTargets++; if (previous > start) overlappingTargets++;} targets.set(key,Math.max(previous || 0,until)); } } events.sort((a,b) => a[0] - b[0] || a[1] - b[1] || (a[2] || 1000000) - (b[2] || 1000000)); let active=0,peakConcurrent=trace.length ? 1:0; for (const [time,delta,limit] of events) { active+=delta; peakConcurrent=Math.max(peakConcurrent,active); if (delta > 0 && limit > 0 && active > limit) { violations.concurrency++; if (violations.concurrencyExamples.length < 5) violations.concurrencyExamples.push({at:time,active,limit}); } } const distribution=values => { values.sort((a,b) => a - b); return {samples:values.length,min:values[0] ?? null,median:values.length ? (values[Math.floor((values.length - 1) / 2)] + values[Math.floor(values.length / 2)]) / 2:null, p95:values.length ? values[Math.ceil(values.length * 0.95) - 1]:null}; }; return {coverage,countedRequests:windows,peakWindows:peaks,traceRequests:trace.length,pendingAtCapture:pending, observedPeakConcurrent:peakConcurrent,startGapMs:distribution(gaps),durationMs:distribution(durations), statuses,outcomes,methods,sources:[...sources].sort((a,b) => b[1] - a[1]).slice(0,10),violations, repeatedTargets:{fingerprinted,requests:repeatedTargets,overlapping:overlappingTargets}, notes:'Counters use complete seconds. Trace and overlap are observed lower bounds; repeated targets may be legitimate retries. Spacing flags allow 5 ms jitter and require review. Reports do not identify IP changes.'}; } function getDiagnosticCacheDecisions(value) { return (Array.isArray(value) ? value:[]).slice(0,32).filter(row => Array.isArray(row) && /^[a-z][a-z.-]{0,59}$/i.test(row[0]) && ['details','phone','activity'].includes(row[1])) .map(row => [row[0],row[1],...Array(5).fill().map((_,i) => Number.isSafeInteger(row[i + 2]) && row[i + 2] > 0 ? row[i + 2]:0)]); } function recordDiagnosticCacheDecision(context,reason=context?.cacheReason) { try { const index=['missing','expired','incomplete','forced','joined'].indexOf(reason),kind=context?.cacheKind; if (index < 0 || !['details','phone','activity'].includes(kind)) return; const state=getRequestRateState(),rows=state.cacheDecisions ||= []; const source=/^[a-z][a-z.-]{0,59}$/i.test(context.source || '') ? context.source:'unknown'; let row=rows.find(row => row[0] === source && row[1] === kind); if (!row) {if (rows.length >= 32) return; rows.push(row=[source,kind,0,0,0,0,0]);} row[index + 2]++; } catch {} } async function hashDiagnosticRequestTarget(value) { try { if (typeof crypto === 'undefined' || !crypto.subtle) return ''; const key=DIAGNOSTICS_STORAGE_KEY + '_salt'; let salt=GM_getValue(key,''); if (!/^[a-f0-9]{64}$/.test(salt)) { salt=[...crypto.getRandomValues(new Uint8Array(32))].map(n => n.toString(16).padStart(2,'0')).join(''); writeHelperStorageValue(key,salt); } const url=new URL(value,location.href),ad=url.pathname.match(/^\/(?:anons|personal-ads)\/(\d+)\.html\/?$/); url.hash=''; for (const name of [...url.searchParams.keys()]) if (name.startsWith('vm_')) url.searchParams.delete(name); url.searchParams.sort(); const input=ad ? 'escort-ad:' + ad[1]:url.href; const hash=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(salt + ':' + input)); return [...new Uint8Array(hash).slice(0,16)].map(n => n.toString(16).padStart(2,'0')).join(''); } catch {return '';} } function normalizeDiagnosticResponseMeta(value) { const number=value => Number.isSafeInteger(value) && value >= 0 ? value:0; return {type:['html','json','text','image','other'].includes(value?.type) ? value.type:'unknown', redirect:['same','same-ad','home','login','external','other'].includes(value?.redirect) ? value.redirect:'unknown', retryAfterMs:Math.min(86400000,number(value?.retryAfterMs)),bytes:number(value?.bytes),challenge:value?.challenge === true}; } function getDiagnosticResponseMeta(response,url) { const headers=response?.headers || response?.responseHeaders; const type=getResponseHeader(headers,'content-type').split(';')[0].toLowerCase(),retry=getResponseHeader(headers,'retry-after'); let redirect='unknown'; try { if (response?.finalUrl || response?.url) { const from=new URL(url,location.href),to=new URL(response.finalUrl || response.url,from); redirect=from.href === to.href ? 'same':from.origin !== to.origin ? 'external': /\/(?:login|logowanie|sign-in|auth)(?:\/|$)/i.test(to.pathname) ? 'login': to.pathname === '/' && from.pathname !== '/' ? 'home': /\/(?:anons|personal-ads)\/\d+\.html/.test(from.pathname) && from.pathname === to.pathname ? 'same-ad':'other'; } } catch {} return normalizeDiagnosticResponseMeta({type:/html/.test(type) ? 'html':/json/.test(type) ? 'json': /^text\//.test(type) ? 'text':/^image\//.test(type) ? 'image':type ? 'other':'unknown',redirect, retryAfterMs:retry ? Math.max(0,/^\d+(?:\.\d+)?$/.test(retry) ? Math.round(Number(retry) * 1000):Date.parse(retry) - Date.now()):0, bytes:Number(getResponseHeader(headers,'content-length')),challenge:getResponseHeader(headers,'cf-mitigated') === 'challenge'}); } function normalizeDiagnosticQueueHistory(value,now) { const number=value => Number.isSafeInteger(value) && value >= 0 ? value:0; const reasons=['none','reload-required','global-concurrency-limit','recovery-probe','pending-dispatch','other-tab-ticket', 'cooldown','start-lease','legacy-lease','rate-limit','verifying-start','concurrency-limit']; const lanes={}; for (const lane of ['get','head']) { const source=value?.lanes?.[lane] || {}; lanes[lane]=Object.fromEntries(['leaseConflicts','expiredReservations','expiredStartLeases','permitReschedules', 'admissionRetries','lateWakes','maxWakeLatenessMs'].map(key => [key,number(source[key])])); lanes[lane].waitMs=Object.fromEntries(reasons.filter(key => source.waitMs?.[key] > 0).map(key => [key,number(source.waitMs[key])])); } return {since:number(value?.since),lanes,admissions:(Array.isArray(value?.admissions) ? value.admissions:[]).slice(-40) .filter(row => Array.isArray(row) && row.length >= 9 && number(row[1]) > 0 && row[1] >= now - 1800000 && row[1] <= now && ['get','head'].includes(row[3])) .map(row => row.slice(0,9).map((v,i) => i === 3 ? v:i === 8 ? Number(v === 1):i === 2 && v > now ? 0:number(v))), events:(Array.isArray(value?.events) ? value.events:[]).slice(-40) .filter(row => Array.isArray(row) && Number.isSafeInteger(row[0]) && row[0] >= now - 1800000 && row[0] <= now && ['get','head'].includes(row[1]) && ['wait','block','timer'].includes(row[2]) && reasons.includes(row[3])) .map(row => [row[0],row[1],row[2],row[3],number(row[4]),number(row[5]),number(row[6])])}; } function enrichEscortBanEvidence(at,epoch) { try { const key=DIAGNOSTICS_STORAGE_KEY + '_ban',previous=GM_getValue(key,null),now=Date.now(); if (previous?.at !== at || String(GM_getValue(DIAGNOSTICS_EPOCH_KEY,'0')) !== epoch || now - at > 30000) return; const recent=getRequestEvidence(now),frozen=normalizeRequestEvidence(previous.evidence),byId=new Map(frozen.pages.map(page => [page.pageId,page])); for (const page of recent.pages) { if (page.startedAt > at) continue; const known=byId.get(page.pageId),cut=normalizeRequestEvidence({at,pages:[page]}).pages[0]; if (!cut) continue; if (known) { const rows=new Map(known.requests.map((row,index) => [row[14] || 'old-' + index,row])); for (const row of cut.requests) if (row[14]) { const old=rows.get(row[14]); if (!old || (row[15] >= old[15] && (row[1] || !old[1]) && row[12] >= old[12])) { if (old && !row[11]) row[11]=old[11]; rows.set(row[14],row); } else if (!old[11] && row[11]) old[11]=row[11]; } known.requests=[...rows.values()].sort((a,b) => a[0] - b[0]).slice(-1000); const seconds=new Map(known.rows.map(row => [row[0],row])); for (const row of cut.rows) if (row[0] + 1000 <= at) { const old=seconds.get(row[0]); seconds.set(row[0],row.map((count,index) => index ? Math.max(count,old?.[index] || 0):count)); } known.rows=[...seconds.values()].sort((a,b) => a[0] - b[0]); known.enrichedAt=now; } else { cut.queues={}; cut.queueHistory=null; cut.cacheDecisions=[]; cut.limits=[]; cut.queueSnapshotUnavailable=true; cut.rows=cut.rows.filter(row => row[0] + 1000 <= at); cut.enrichedAt=now; frozen.pages.push(cut); byId.set(page.pageId,cut); } } frozen.truncated ||= recent.truncated; frozen.unavailable ||= recent.unavailable; writeHelperStorageValue(key,{...previous,enrichedAt:now,evidence:normalizeRequestEvidence(frozen)}); } catch {} } function createRequestContext(source='',parent=null) { return {source,manual:parent?.manual === true || /manual|^tile\.related$/.test(source)}; } function createOperationCancelledError(reason='Operacja przerwana') { const error=new Error(reason); error.name='OperationCancelledError'; error.code='VM_OPERATION_CANCELLED'; return error; } function isOperationCancelledError(error) { return error?.code === 'VM_OPERATION_CANCELLED' || error?.name === 'OperationCancelledError'; } function createOperationCancelToken(label) { const listeners=new Set(); return { label:String(label || 'Długa operacja'), cancelled:false, reason:'', cancel(reason='Przerwano na żądanie użytkownika') { if (this.cancelled) return; this.cancelled=true; this.reason=String(reason || 'Operacja przerwana'); for (const listener of [...listeners]) { try {listener(this.reason);} catch {} } listeners.clear(); }, throwIfCancelled() { if (this.cancelled) { throw createOperationCancelledError(this.reason); } }, onCancel(listener) { if (typeof listener !== 'function') return () => {}; if (this.cancelled) { listener(this.reason); return () => {}; } listeners.add(listener); return () => listeners.delete(listener); } }; } function waitWithCancellation(delayMs,cancelToken=null) { if (!cancelToken) { return new Promise(resolve => setTimeout(resolve,delayMs)); } cancelToken.throwIfCancelled(); return new Promise((resolve,reject) => { const timer=setTimeout(() => { unsubscribe(); resolve(); },Math.max(0,Number(delayMs) || 0)); const unsubscribe=cancelToken.onCancel(reason => { clearTimeout(timer); reject(createOperationCancelledError(reason)); }); }); } function log(msg,data='') { console.log(`[GARSO-ESCORTI] ${msg}`,data); if ( data instanceof Error || /(?:\bbłąd\b|nie udało|error|wyjątek)/i.test(String(msg || '')) ) { recordDiagnosticError(msg,data); } } function normalizeText(text) {return (text || '').replace(/\s+/g,' ').trim().toLowerCase();} function escapeHtml(text) {return String(text ?? '').replace(/[&<>"']/g,c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));} function setButtonColor(btn,status) { if (btn?.classList?.contains('vm-research-panel-row')) { btn.dataset.vmResultStatus=status; btn.style.backgroundColor='transparent'; const resultLine=btn.querySelector('.vm-research-panel-value'); if (resultLine) { resultLine.style.color={ stale:'#9ca3af', checking:'var(--vm-ui-panel-muted, #d8c8d9)', found:'var(--vm-ui-success, #62cf7b)', empty:'var(--vm-ui-accent, #80b9ee)', error:'var(--vm-ui-danger, #ff6262)' }[status] || 'var(--vm-ui-panel-muted, #d8c8d9)'; } return; } btn.style.backgroundColor=BUTTON_COLORS[status] || BUTTON_COLORS.checking; } function avg(arr) {return arr.length ? arr.reduce((a,b) => a+b,0) / arr.length:null;} function round1(n) {return n == null ? null:Math.round(n * 10) / 10;} function median(arr) { if (!arr.length) return null; const x=[...arr].sort((a,b) => a-b); const m=Math.floor(x.length / 2); return x.length % 2 ? x[m]:(x[m-1] + x[m]) / 2; } function digitsOnly(phone) { let digits=String(phone || '').replace(/\D/g,''); if (digits.length === 11 && digits.startsWith('48')) digits=digits.substring(2); return digits; } function formatEscortiDate(value) { if (!value) return null; let m=value.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (m) return `${m[3]}.${m[2]}.${m[1]}`; m=value.match(/^(\d{2})[.\-/](\d{2})[.\-/](\d{4})$/); if (m) return `${m[1]}.${m[2]}.${m[3]}`; return value; } function formatShortDate(value) { const full=formatEscortiDate(value); if (!full) return '?'; const m=full.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); return m ? `${m[1]}.${m[2]}.${m[3].slice(-2)}`:full; } function profileDateToTime(value) { const full=formatEscortiDate(value); const m=full && full.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); return m ? Date.UTC(Number(m[3]),Number(m[2]) - 1,Number(m[1])):null; } function normalizeTopicTitleKey(value) { return normalizeText(String(value || '').normalize('NFKC')); } const SETTINGS_UI_STYLE_ID='vm-garso-settings-style'; function ensureSettingsUiStyles() { if (document.getElementById(SETTINGS_UI_STYLE_ID)) return; appendHelperStyle(SETTINGS_UI_STYLE_ID,` .vm-helper-ui .vm-settings-box { max-height: calc(100vh - 32px); width: min(1060px, calc(100vw - 32px)); height: min(96vh, 980px); display: flex; flex-direction: column; overflow: hidden; } .vm-helper-ui .vm-settings-title { margin-bottom: 4px; } .vm-helper-ui .vm-settings-subtitle { margin-bottom: 10px; } .vm-helper-ui .vm-settings-tabs { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 0; margin: 0 0 10px; border-bottom: 1px solid var(--vm-ui-border, #d8d8d8); } .vm-helper-ui .vm-settings-tab-button { margin: 0 1px -1px 0; padding: 8px 11px 7px; border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; color: var(--vm-ui-text-muted, #666); cursor: pointer; font-size: 11px; font-weight: 600; line-height: 1.2; } .vm-helper-ui .vm-settings-tab-button:hover { background: var(--vm-ui-bg-hover, #f7f7f7); color: var(--vm-ui-text, #333); } .vm-helper-ui .vm-settings-tab-button.-active { border-bottom-color: var(--vm-settings-pink); background: var(--vm-ui-bg, #fff); color: var(--vm-settings-pink); font-weight: 700; } .vm-helper-ui .vm-settings-grid { min-height: 0; flex: 1 1 auto; display: block; overflow-y: auto; padding: 1px 5px 3px 1px; } .vm-helper-ui .vm-settings-tab-panel { display: none; } .vm-helper-ui .vm-settings-tab-panel.-active { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 12px; } .vm-helper-ui .vm-settings-section { min-width: 0; } .vm-helper-ui .vm-settings-section.-full-width { grid-column: 1 / -1; } .vm-helper-ui .vm-settings-section-title { margin-bottom: 7px; color: var(--vm-ui-text, #222); font-size: 14px; font-weight: 700; } .vm-helper-ui .vm-settings-section-title.-with-description { margin-bottom: 3px; } .vm-helper-ui .vm-settings-section-description { margin-bottom: 7px; } .vm-helper-ui .vm-settings-request-limits > .vm-settings-inline-row { display:grid; grid-template-columns:minmax(0,1fr) 64px; gap:7px; max-width:390px; margin:6px 0; white-space:normal; } .vm-helper-ui .vm-settings-request-limits .vm-settings-field-label { white-space:normal; } .vm-helper-ui .vm-settings-actions { flex: 0 0 auto; } .vm-helper-ui .vm-settings-note { margin: 0 auto 0 0; color: var(--vm-ui-text-muted, #666); font-size: 12px; } .vm-helper-ui .vm-settings-footer-button { min-width: 92px; } .vm-helper-ui .vm-settings-inline-row { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; } .vm-helper-ui .vm-settings-inline-row.-indented { margin-left: 25px; } .vm-helper-ui .vm-settings-inline-row.-nowrap { flex-wrap: nowrap; white-space: nowrap; } .vm-helper-ui .vm-settings-info.-indented { margin-left: 25px; } .vm-helper-ui .vm-settings-options-box { margin: -1px 0 10px 25px; } .vm-helper-ui .vm-settings-field-label { flex: 0 0 auto; margin-bottom: 0; color: var(--vm-ui-text, #444); font-size: 12px; font-weight: 600; white-space: nowrap; } .vm-helper-ui .vm-settings-field-description { margin-top: 5px; } .vm-helper-ui .vm-settings-cache-size, .vm-helper-ui .vm-settings-transfer-status { min-width: 0; } .vm-helper-ui .vm-settings-cache-size { white-space: nowrap; } .vm-helper-ui .vm-settings-watch-interval { margin: 7px 0 5px 25px; color: var(--vm-ui-text-muted, #555); white-space: nowrap; } .vm-helper-ui .vm-settings-runtime-info { margin: 2px 0 5px 25px; } .vm-helper-ui .vm-settings-auto-warning { margin: 3px 0 2px; } .vm-helper-ui .vm-settings-auto-box { margin: -2px 0 8px 25px; } .vm-helper-ui .vm-settings-top-phone-option { margin: 0; color: var(--vm-ui-text-muted, #555); font-size: 12px; } .vm-helper-ui .vm-settings-top-phone-options { column-gap: 20px; row-gap: 7px; margin: 3px 0 10px 25px; } .vm-helper-ui .vm-settings-phone-copy-row { gap: 8px; margin: 10px 0 4px; } .vm-helper-ui .vm-settings-cache-controls-stack { display: grid; gap: 5px; margin: 2px 0 4px 25px; } .vm-helper-ui .vm-settings-cache-control-row { display: flex; align-items: center; gap: 8px; min-width: 0; } .vm-helper-ui .vm-settings-transfer-row { gap: 8px; } .vm-helper-ui .vm-diagnostics-health { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px; margin:9px 0; } .vm-helper-ui .vm-diagnostics-health-text { min-width:0; flex:1 1 240px; overflow-wrap:anywhere; } .vm-helper-ui .vm-diagnostics-health-version { flex:none; text-decoration:none; white-space:nowrap; } .vm-helper-ui .vm-diagnostics-health.-ok { border-color:var(--vm-ui-success,#2e9d55); } .vm-helper-ui .vm-diagnostics-health.-warning { border-color:var(--vm-ui-warning,#c78b00); } .vm-helper-ui .vm-diagnostics-detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin:12px 0; } .vm-helper-ui :is(.vm-diagnostics-detail-title,.vm-diagnostics-list-title) { margin-bottom:6px; color:var(--vm-ui-text,#222); font-size:12px; font-weight:700; } .vm-helper-ui .vm-diagnostics-detail-line { color:var(--vm-ui-text-muted,#666); margin:3px 0; overflow-wrap:anywhere; } .vm-helper-ui .vm-diagnostics-overview { display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:8px; margin:8px 0; } .vm-helper-ui .vm-diagnostics-card { grid-column:span 3; text-align:center; background:color-mix(in srgb,var(--vm-ui-accent,#925a2b) 7%,var(--vm-ui-bg,#fff)); border-top:2px solid color-mix(in srgb,var(--vm-ui-accent,#925a2b) 45%,var(--vm-ui-border,#ddd)); } .vm-helper-ui .vm-diagnostics-card.-requests { grid-column:span 4; } .vm-helper-ui :is(.vm-diagnostics-card,.vm-diagnostics-detail-box,.vm-diagnostics-list) { min-width:0; overflow-wrap:anywhere; } .vm-helper-ui .vm-diagnostics-card-value { color:var(--vm-ui-text,#333); margin:7px 0 4px; font-size:18px; font-weight:800; line-height:1.2; font-variant-numeric:tabular-nums; } .vm-helper-ui .vm-diagnostics-card-label { color:var(--vm-ui-text,#333); font-size:11px; font-weight:600; line-height:1.3; } .vm-helper-ui .vm-diagnostics-card-foot { margin-top:7px; padding-top:5px; border-top:1px solid var(--vm-ui-border,#ddd); } .vm-helper-ui .vm-diagnostics-list { max-height:180px; overflow:auto; } @media (max-width:900px) { .vm-helper-ui .vm-diagnostics-card { grid-column:span 6; } } @media (max-width:650px) { .vm-helper-ui :is(.vm-diagnostics-overview,.vm-diagnostics-detail-grid) { grid-template-columns:1fr; } .vm-helper-ui .vm-diagnostics-overview > .vm-diagnostics-card { grid-column:1 / -1; } } .vm-helper-ui .vm-diagnostics-event { padding:5px 0; border-top:1px solid var(--vm-ui-border,#ddd); } .vm-helper-ui .vm-diagnostics-empty { color:var(--vm-ui-text-muted,#666); } .vm-helper-ui .vm-diagnostics-privacy { margin-top:9px; } .vm-helper-ui .vm-settings-separator { height:1px; margin:8px 0; background:var(--vm-ui-border,#ddd); } .vm-helper-ui .vm-settings-small-label { font-size:12px; } .vm-helper-ui .vm-settings-single-ad-subsection-box, .vm-helper-ui .vm-settings-compact-inset-box { width:calc(100% - 22px); margin:4px 0 8px 22px; } .vm-helper-ui .vm-settings-compact-inset-box { margin-bottom:4px; } .vm-helper-ui .vm-settings-number-input { width:64px; } .vm-helper-ui .vm-settings-mini-input { min-width:0; } .vm-helper-ui .vm-settings-compact-info { margin-bottom:2px; } .vm-helper-ui .vm-settings-compact-info.-top { margin:4px 0 0; } .vm-helper-ui .vm-settings-compact-info.-spaced { margin-bottom:5px; } .vm-helper-ui .vm-settings-external-service-row, #vm-garso-settings-overlay .vm-settings-tag-group-row { display:grid; gap:8px; align-items:center; min-height:34px; padding:4px 0; } .vm-helper-ui .vm-settings-external-service-row { grid-template-columns:24px 140px minmax(0,1fr) 28px; } .vm-helper-ui .vm-settings-external-service-row .vm-settings-mini-input, #vm-garso-settings-overlay .vm-settings-tag-group-name { width:100%; height:24px; min-height:24px; padding:2px 6px; border:1px solid var(--vm-ui-input-border,#c9c9c9); border-radius:4px; background:var(--vm-ui-input-bg,#fff); box-shadow:none; font-size:12px; line-height:18px; } .vm-helper-ui .vm-settings-external-service-row .vm-settings-mini-remove, #vm-garso-settings-overlay .vm-settings-mini-remove { width:28px; height:28px; min-height:28px; padding:0; border:1px solid var(--vm-ui-input-border,#c9c9c9); border-radius:5px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-text,#222); font-size:17px; line-height:1; } .vm-helper-ui .vm-settings-mini-remove.-hidden { visibility:hidden; } .vm-helper-ui .vm-settings-tag-add-group { margin-top:3px; } #vm-garso-settings-overlay .vm-settings-tag-group-row { grid-template-columns:140px 36px minmax(0,1fr) 28px; } .vm-helper-ui .vm-settings-tag-group-row.-separated { border-top:1px solid var(--vm-ui-border,#eee); } #vm-garso-settings-overlay .vm-settings-tag-list { display:flex; flex-wrap:wrap; align-items:center; gap:4px 5px; min-width:0; min-height:28px; } .vm-helper-ui .vm-settings-tag-automatic { color:var(--vm-ui-text-muted,#777); font-size:9px; line-height:21px; } .vm-helper-ui .vm-settings-tag-chip { height:19px; min-height:19px; padding:0 5px; border:1px solid var(--vm-settings-tag-color); border-radius:10px; background:var(--vm-settings-tag-color); color:#fff; cursor:pointer; font-size:8.8px; line-height:17px; } #vm-garso-settings-overlay .vm-settings-tag-select { flex:0 0 190px; width:190px; max-width:190px; height:24px; min-height:24px; padding:2px 26px 2px 8px; border:1px solid var(--vm-ui-input-border,#c9c9c9); border-radius:5px; background:var(--vm-ui-input-bg,#fff); color:var(--vm-ui-input-text,var(--vm-ui-text,#222)); box-shadow:none; font-size:12px; line-height:1.2; } .vm-helper-ui .vm-settings-notes-actions { display:flex; flex-direction:column; align-items:flex-start; gap:6px; margin-left:25px; } .vm-helper-ui .vm-settings-notes-actions-row { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin:0; } .vm-helper-ui .vm-settings-notes-actions-row .vm-settings-cache-button { flex:0 0 auto; white-space:nowrap; } .vm-helper-ui .vm-settings-notes-actions-status { display:block; min-height:0; margin:0; } .vm-escort-list-init-error { box-sizing: border-box; margin: 10px 0; padding: 9px 11px; border: 1px solid var(--vm-ui-danger, #d9534f); border-radius: 7px; background: var(--vm-ui-bg-alt, rgba(217,83,79,.1)); color: var(--vm-ui-danger, #a52320); font:700 12px/1.35 var(--vm-ui-font-family,Arial,sans-serif); } `); } function makeSettingsTabGearButton( tabId, title='Ustawienia skryptu' ) { const button=makeButton('vm-settings-tab-gear'); setControlTitle(button,title); button.innerHTML= '<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" ' + 'style="display:block;margin:auto;fill:none;stroke:currentColor;' + 'stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round">' + '<circle cx="12" cy="12" r="3"/>' + '<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06' + 'a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.56V21h-4v-.09' + 'A1.7 1.7 0 0 0 9 19.36a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83' + '.06-.06A1.7 1.7 0 0 0 4.63 15a1.7 1.7 0 0 0-1.56-1.03H3v-4h.09' + 'A1.7 1.7 0 0 0 4.64 9a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83' + '.06.06A1.7 1.7 0 0 0 9 4.63a1.7 1.7 0 0 0 1.03-1.56V3h4v.09' + 'A1.7 1.7 0 0 0 15 4.64a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83' + '-.06.06A1.7 1.7 0 0 0 19.37 9a1.7 1.7 0 0 0 1.56 1.03H21v4h-.09' + 'A1.7 1.7 0 0 0 19.4 15z"/>' + '</svg>'; button.classList.add('vm-ui-button','vm-ui-icon','vm-ui-compact'); button.addEventListener('click',event => { stopUiEvent(event); const watchOverlay= document.getElementById(WATCH_SETTINGS_OVERLAY_ID); if (watchOverlay) { if (typeof watchOverlay._vmClose === 'function') { watchOverlay._vmClose(); } else { detachHelperNode(watchOverlay); } } openSettingsModal({tab:tabId}); }); return button; } function getSettingsCheckboxDefinitions(current,currentWatch={enabled:current.watchEnabled}) { return { quickAdPreview:['Pokazuj dymek z szybkim podglądem anonsu po najechaniu na link Escort.club (na Escort.club, Escorti.pl i Garsonierze)',current.quickAdPreview !== false], watchEnabled:['Włącz ulubione, ukrywanie, notatki i kategorie anonsów oraz profili',currentWatch.enabled,false], showWatchOnlineCount:['Stale pokazuj w lewym górnym rogu strony liczbę profili/anonsów online z włączonym alertem online/offline',current.showWatchOnlineCount,false], showWatchAlertBar:['Pokazuj alerty na górnym pasku',current.showWatchAlertBar !== false], watchSystemNotifications:['Włącz powiadomienia systemowe o nowych alertach',current.watchSystemNotifications], watchNotificationAnonymous:['Anonimizuj powiadomienia systemowe',current.watchNotificationAnonymous], autoGarsoCheck:['Automatycznie sprawdzaj Garsonierę po otwarciu strony z anonsem',current.autoGarsoCheck], garsoCheckEscortLinks:['Pokazuj przy linkach Escort.club czy strona z anonsem jest dostępna (online).',current.garsoCheckEscortLinks !== false], garsoShowSearchIcons:['Pokazuj lupkę przy linkach do Escort.club i numerach telefonu',current.garsoShowSearchIcons !== false], garsoShowTopicTools:['Pokazuj filtr postów oraz historię numerów i linków w temacie',current.garsoShowTopicTools !== false], garsoShowTopicSummary:['Pokazuj podsumowanie ocen i wzmianek w tematach',current.garsoShowTopicSummary !== false], garsoShowTopicHelperData:['W tematach na forum pokazuj notatki, kategorie i czy w ulubionych. Pokazuj w temacie listę odwiedzonych powiązanych anonsów',current.garsoShowTopicHelperData !== false], mergeEscortAds:['Agreguj powiązane anonse Escort.club',current.mergeEscortAds,false], showRelatedAdsTileLink:['Pokaż na kafelku link do profilu, czyli do listy ogłoszeń tej osoby (niezależne od agregowania)',current.showRelatedAdsTileLink !== false], openAllActiveRelatedAdsFromTile:['Zamiast otwierać stronę profilu otwórz w nowych kartach wszystkie aktywne ogłoszenia',!!current.openAllActiveRelatedAdsFromTile], showEscortiTileData:['Pokaż dane z escorti.pl na kafelkach (ilość powiązanych anonsów, datę założenia profilu, ilość tematów na Garso)',current.showEscortiTileData], showPricesInSearchResults:['Pokazuj cenę na kafelkach (za jednostkę czasu wybraną na stronie wyników)',current.showPricesInSearchResults,false], multiLocationEnabled:['Włącz multiwybór lokalizacji',current.multiLocationEnabled !== false], saveSearchLocationsEnabled:['Włącz możliwość dodawania lokalizacji do „ulubionych”',current.saveSearchLocationsEnabled !== false], showBottomLeftMenu:['Pokaż menu Escort Helper na górnym pasku',current.showBottomLeftMenu !== false], showMenuPagesAsFavicons:['Pokazuj linki do stron',current.showMenuPagesAsFavicons === true], showTopPhoneSearch:['Pokaż podręczną wyszukiwarkę w prawym dolnym rogu stron Escort.club, Escorti.pl i Garsoniera',current.showTopPhoneSearch !== false], hideRecommendedSearchSection:['Ukryj sekcję „Polecane” na stronie wyników',current.hideRecommendedSearchSection], hideRecommendedAdSection:['Ukryj sekcję „Polecane” na stronie pojedynczego anonsu',current.hideRecommendedAdSection], hideSingleAdContactButtons:['Ukryj przyciski „Daj napiwek” i „Wyślij wiadomość”',current.hideSingleAdContactButtons], hideSingleAdActions:['Ukryj sekcję z przyciskami „Dodaj do ulubionych”, „Ukryj w wynikach”, „Zgłoś naruszenie”, „Dodaj notatkę”',current.hideSingleAdActions], hideFeaturedSearchSection:['Ukryj sekcję „Wyróżnione” na stronie wyników',current.hideFeaturedSearchSection], hidePopularCitiesSection:['Ukryj sekcję „Popularne Miasta”',current.hidePopularCitiesSection], showImageSearchButtons:['Na zdjęciach pokazuj przyciski do wyszukiwania grafiki w serwisach zewnętrznych.',current.showImageSearchButtons], showPhoneReputationLinks:['Włącz wyświetlanie linków do sprawdzania numeru telefonu.',current.showPhoneReputationLinks], autoPhoneReputationSearch:['Automatycznie wyszukuj numer w obsługiwanych serwisach po wejściu na anons.',current.autoPhoneReputationSearch], showSummarySidePanel:['Pokazuj panel boczny „Podsumowanie”',current.showSummarySidePanel], showCompactSummaryAboveDescription:['Wyświetl skrócone podsumowanie nad opisem',current.showCompactSummaryAboveDescription], showTagHighlights:['Włącz prezentację usług / tagów',current.showTagHighlights !== false], showPhoneCopyButton:['Pokazuj przycisk do kopiowania numeru telefonu',current.showPhoneCopyButton !== false], escortFastStart:['Szybki start aktywnej karty: 30 zapytań z podwójnymi limitami',current.escortFastStart !== false], showLiveDiagnostics:['Pokazuj prędkość zapytań do Escort.club w lewym dolnym rogu stron',current.showLiveDiagnostics === true] }; } function createSettingsFields(current) { function fitSelectToLongestOption(select,minimumCharacters=0) { const longest=[...select.options].reduce( (length,option) => Math.max(length,option.textContent.length), 0 ); select.style.width=`${Math.max(minimumCharacters,longest) + 4}ch`; select.style.maxWidth='100%'; select.style.flex='0 1 auto'; select.style.textAlign='left'; } const settingsFieldControls={}; const watchSelect={rowTag:'label',labelTag:'span',rowClass:'vm-settings-inline-row -nowrap vm-settings-watch-interval', labelClass:'vm-settings-small-label',selectClass:'vm-settings-select -compact vm-ui-field'}; const settingsSelectDefinitions={ watchIntervalMinutes:['Sprawdzaj co:',WATCH_INTERVAL_SELECT_OPTIONS,String,{...watchSelect,minChars:8,read:Number}], watchProfileResolutionMode:['Profil osoby ustalaj przez:',WATCH_PROFILE_RESOLUTION_OPTIONS,normalizeWatchProfileResolutionMode,watchSelect], nonAlertChangeNewness:['Zmianę w oknie Ulubione (w kolumnie „Zmiany”) pokazuj wyróżnioną kolorem jako nową przez:', WATCH_NON_ALERT_NEWNESS_OPTIONS,normalizeWatchNonAlertChangeNewness,{...watchSelect, rowClass:watchSelect.rowClass+' -indented',read:normalizeWatchNonAlertChangeNewness}], escortAggregationMode:['Sposób agregowania kafelków:',ESCORT_AGGREGATION_MODE_OPTIONS,normalizeEscortAggregationMode], phoneClipboardFormat:['Format kopiowanego numeru:',PHONE_CLIPBOARD_FORMAT_OPTIONS,normalizePhoneClipboardFormat, {rowTag:'label',rowClass:'vm-settings-inline-row vm-settings-phone-copy-row',labelTag:'span',labelClass:''}], favoritesDefaultView:['Listę ulubionych otwieraj domyślnie w widoku:',[['table','tabeli'],['tiles','kafelków']],normalizeFavoritesDefaultView,{rowClass:'vm-settings-inline-row -indented',labelClass:'vm-settings-small-label',minChars:12}], compactSummaryComparisonMode:['Sposób znajdowania anonsów tej samej osoby w skróconym podsumowaniu:',ESCORT_COMPACT_SUMMARY_MODE_OPTIONS,normalizeEscortCompactSummaryMode], relatedAdsTileLinkMode:['Sposób znajdowania ogłoszeń:',ESCORT_RELATED_ADS_TILE_LINK_MODE_OPTIONS,normalizeEscortRelatedAdsTileLinkMode], topPhoneSearchEnterTarget:['Po naciśnięciu Enter wyszukuj domyślnie w:',TOP_PHONE_SEARCH_TARGET_OPTIONS,normalizeTopPhoneSearchEnterTarget,{ rowClass:'vm-settings-inline-row vm-settings-top-phone-option', labelClass:'', labelTag:'span', selectClass:"vm-settings-select -wide-arrow vm-ui-field", minChars:16 }], topPhoneSearchOpenMode:['Otwieraj wyniki:',TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS,normalizeTopPhoneSearchOpenMode,{ rowClass:'vm-settings-inline-row vm-settings-top-phone-option', labelClass:'', labelTag:'span', selectClass:"vm-settings-select -wide-arrow vm-ui-field" }] }; function makeSettingsSelectRow(key) { const [labelText,options,normalize,config={}]=settingsSelectDefinitions[key]; const {rowTag='div',rowClass='vm-settings-inline-row',labelClass='vm-settings-field-label', labelTag='label',selectClass="vm-settings-select vm-ui-field",minChars=0,read=String}=config; const value=normalize(current[key]); const row=makeElement(rowTag,rowClass); const label=makeElement(labelTag,labelClass,labelText); const select=makeElement('select'); appendSelectOptions(select,options,value); select.className=selectClass; fitSelectToLongestOption(select,minChars); row.append(label,select); settingsFieldControls[key]=[select,read]; return {row,label,select}; } const settingsNumberDefinitions={ maxRelatedAdsTabs:['Maksymalna liczba otwieranych anonsów:',1,null,'vm-settings-field-label', value => Math.max(1,Number(value) || DEFAULT_SETTINGS.maxRelatedAdsTabs), value => Math.max(1,Math.floor(Number(value) || DEFAULT_SETTINGS.maxRelatedAdsTabs))], alarmingTagCountLimit:['Alarmująca liczba oferowanych usług / tagów: powyżej',0,10000,'vm-settings-small-label', value => value,value => normalizeCustomFilterInteger(value,DEFAULT_SETTINGS.alarmingTagCountLimit,0,10000)] }; const requestRate=value => normalizeEscortRequestLimitPerMinute(value); settingsNumberDefinitions.escortLimitPerMinute=['Łączny limit zapytań do Escort.club na minutę:',1,840,'vm-settings-field-label',requestRate,requestRate]; const concurrent=value => normalizeEscortRequestMaxConcurrent(value,DEFAULT_SETTINGS.escortMaxConcurrent); settingsNumberDefinitions.escortMaxConcurrent=['Łącznie równoczesnych — GET / POST i HEAD:',1,24,'vm-settings-field-label',concurrent,concurrent]; function makeSettingsNumberRow(key) { const [text,min,max,labelClass,load,save]=settingsNumberDefinitions[key]; const row=makeElement('label','vm-settings-inline-row -nowrap'); const label=makeElement('span',labelClass,text); const input=makeElement('input',"vm-settings-number-input vm-ui-field",null,{type:'number',min:String(min)}); if (max != null) input.max=String(max); input.step='1'; input.inputMode='numeric'; input.value=String(load(current[key])); row.append(label,input); settingsFieldControls[key]=[input,save]; return {row,input}; } return {select:makeSettingsSelectRow,number:makeSettingsNumberRow,controls:settingsFieldControls}; } function createEscortRequestSettingsRows(makeNumberRow,fastStartCheck=null) { const box=makeElement('div','vm-settings-request-limits'); for (const key of ['escortLimitPerMinute','escortMaxConcurrent']) { const {row,input}=makeNumberRow(key); row.title=input.title=key === 'escortLimitPerMinute' ? 'Bazowy limit 1–840 zapytań/min, wspólny dla HEAD i GET / POST ze wszystkich kart.' : 'Bazowy limit 1–24 jednoczesnych zapytań GET / POST i HEAD razem, we wszystkich kartach.'; box.appendChild(row); } if (fastStartCheck) { box.append(fastStartCheck,makeElement('div','vm-settings-info -indented vm-ui-help', 'Podwaja tempo i równoległość dla oglądanej strony i ręcznych akcji. Ponownie dostępny po min. 5 min, jeśli w ostatnich 5 min wysłano łącznie najwyżej 90 zapytań.')); } return box; } function waitForSettingsPaint() { return new Promise(resolve => { let frame=0; const finish=() => {clearTimeout(timer); cancelAnimationFrame(frame); resolve();}; let timer=setTimeout(finish,100); if (!document.hidden) frame=requestAnimationFrame(() => { clearTimeout(timer); timer=setTimeout(finish,0); }); }); } function createDeferredSettingsTabLoader(overlay,panels,loaders) { let generation=0; return async tabId => { const ticket=++generation, panel=panels.get(tabId); for (const node of panels.values()) node.removeAttribute('aria-busy'); if (!loaders[tabId]) return; const current=() => overlay.isConnected && ticket === generation; panel.querySelector('.vm-settings-load-error')?.remove(); panel.setAttribute('aria-busy','true'); await waitForSettingsPaint(); if (!current()) return; try { await loaders[tabId](current); } catch (error) { if (current()) { const message=makeElement('div','vm-settings-load-error vm-ui-help vm-ui-error', 'Nie udało się wczytać tej sekcji. Otwórz zakładkę ponownie, aby spróbować jeszcze raz.'); panel.prepend(message); } log('Błąd wczytywania zakładki ustawień',error); } finally { if (current()) panel.removeAttribute('aria-busy'); } }; } async function openSettingsModal(options={}) { const existingOverlay= document.getElementById('vm-garso-settings-overlay'); if (existingOverlay) { const requestedTab=String(options.tab || '').trim(); if ( requestedTab && typeof existingOverlay._vmActivateTab === 'function' ) { existingOverlay._vmActivateTab(requestedTab); } existingOverlay.style.display='flex'; return; } ensureSettingsUiStyles(); const firstRun=!!options.firstRun; let requestedTab=String(options.tab || '').trim(); const overlay=makeElement('div'); overlay.id='vm-garso-settings-overlay'; prepareHelperStyleApi(overlay); overlay.classList.add('vm-ui-overlay'); overlay.style.setProperty( '--vm-settings-pink', `var(--vm-ui-accent, ${getEscortPagePinkColor()})` ); const box=makeElement('div',"vm-settings-box vm-ui-dialog"); const title=makeElement('div',"vm-settings-title vm-ui-title",'Ustawienia'); const subtitle=firstRun ? makeElement( 'div', "vm-settings-subtitle vm-ui-help", 'Dostosuj działanie skryptu do swoich potrzeb i zapisz ustawienia.' ) :null; const tabBar=makeElement('div','vm-settings-tabs'); tabBar.setAttribute('role','tablist'); const contentGrid=makeElement('div','vm-settings-grid'); contentGrid.setAttribute('aria-busy','true'); const note=makeElement('div','vm-settings-note','Wczytywanie ustawień…'); note.setAttribute('role','status'); const actions=makeElement('div',"vm-settings-actions vm-ui-actions"); const cancelBtn=makeButton("vm-settings-footer-button -secondary vm-ui-button",'Anuluj'); const saveBtn=makeButton("vm-settings-footer-button -primary vm-ui-button",'Zapisz'); saveBtn.disabled=true; const closeController=createDialogCloseController(overlay,{onClose:() => overlay._vmDispose?.()}); closeController.bindButton(cancelBtn); overlay._vmActivateTab=tabId => {requestedTab=tabId;}; actions.append(note,cancelBtn,saveBtn); box.append(title); if (subtitle) box.append(subtitle); box.append(tabBar,contentGrid,actions); overlay.appendChild(box); document.body.appendChild(overlay); await waitForSettingsPaint(); if (!overlay.isConnected) return; try { populateSettingsModal({overlay,tabBar,contentGrid,saveBtn,closeController,requestedTab}); note.textContent='Zmiany zostaną zastosowane po zapisaniu i odświeżeniu strony.'; saveBtn.disabled=false; } catch (error) { overlay._vmDispose?.(); note.textContent='Nie udało się wczytać ustawień. Zamknij okno i spróbuj ponownie.'; log('Błąd wczytywania ustawień',error); } finally {contentGrid.removeAttribute('aria-busy');} } function populateSettingsModal({overlay,tabBar,contentGrid,saveBtn,closeController,requestedTab}) { let diagnosticsStatusTimer=null; overlay._vmDispose=() => { clearInterval(diagnosticsStatusTimer); }; const current=getSettings(); const currentWatch=getWatchSettings(); function makeSettingsSection(sectionTitle,description='',fullWidth=false,...children) { const section=makeElement('section',"vm-settings-section vm-ui-panel"); section.classList.toggle('-full-width',fullWidth); const heading=makeElement('div','vm-settings-section-title',sectionTitle); heading.classList.toggle('-with-description',!!description); section.appendChild(heading); if (description) section.appendChild(makeElement('div',"vm-settings-section-description vm-ui-help",description)); if (children.length) section.append(...children.filter(Boolean)); return section; } function makeCheckbox(labelText,checked) { const label=makeElement('label',"vm-settings-choice vm-ui-choice"); const input=makeElement('input','',null,{type:'checkbox',checked}); const text=makeElement('span','',labelText); label.appendChild(input); label.appendChild(text); return {label,input}; } function makeRadioOption(name,value,labelText,checked) { const label=makeElement('label',"vm-settings-choice -radio vm-ui-choice"); const input=makeElement('input','',null,{type:'radio',name,value,checked}); const text=makeElement('div','',labelText); label.append(input,text); return {label,input,text}; } const {select:makeSettingsSelectRow,number:makeSettingsNumberRow,controls:settingsFieldControls}=createSettingsFields({ ...current,watchIntervalMinutes:currentWatch.intervalMinutes,nonAlertChangeNewness:currentWatch.nonAlertChangeNewness }); const setSettingsVisualEnabled=(element,enabled,strong=false) => { element.style.opacity=enabled ? '1':(strong ? '.42':'.5'); element.style.cursor=enabled ? '':'not-allowed'; }; function bindSettingsEnabled(input,container,controls,strong=false) { const sync=() => { const enabled=input.checked; for (const control of controls) control.disabled=!enabled; setSettingsVisualEnabled(container,enabled,strong); container.setAttribute('aria-disabled',String(!enabled)); }; input.addEventListener('change',sync); sync(); } const checkboxDefinitions=getSettingsCheckboxDefinitions(current,currentWatch); const checks=Object.fromEntries( Object.entries(checkboxDefinitions).map( ([key,[label,checked]]) => [key,makeCheckbox(label,checked)] ) ); const readDeclarativeCheckboxSettings=() => Object.fromEntries( Object.entries(checkboxDefinitions) .filter(([,definition]) => definition[2] !== false) .map(([key]) => [key,checks[key].input.checked]) ); const favoritesDefaultView=makeSettingsSelectRow('favoritesDefaultView'); favoritesDefaultView.select.id='vm-favorites-default-view';favoritesDefaultView.label.htmlFor=favoritesDefaultView.select.id; const watchFavoritesInfo=makeElement( 'div', "vm-settings-info -indented vm-ui-help", 'Skrypt używa własnych list Ulubione i ukryte. Po włączeniu ukrywa natywne, czyli standardowe przyciski Escort.club służące do dodawania do ulubionych i ukrywania anonsów. Wyłączenie wstrzymuje działanie obu list, zachowując zapisane pozycje.' ); const editFavoriteCategoriesRow=makeElement( 'div', 'vm-settings-notes-actions-row' ); const editFavoriteCategoriesBtn=makeButton( "vm-settings-cache-button vm-ui-button", 'Dodaj / edytuj kategorie' ); editFavoriteCategoriesBtn.addEventListener('click',event => { event.preventDefault(); openFavoriteCategoriesEditor(syncClearNotesAndCategoriesButtons); }); const clearNotesRow=makeElement('div','vm-settings-notes-actions-row'); const clearAllNotesBtn=makeButton( "vm-settings-cache-button -danger vm-ui-button", 'Usuń wszystkie notatki' ); const clearAllCategoriesBtn=makeButton( "vm-settings-cache-button -danger vm-ui-button", 'Usuń wszystkie kategorie' ); const clearNotesStatus=makeElement('span','vm-settings-info vm-settings-notes-actions-status vm-ui-help'); const clearCategoriesStatus=makeElement('span','vm-settings-info vm-settings-notes-actions-status vm-ui-help'); const syncClearNotesAndCategoriesButtons=() => { const items=getWatchedItems(); clearAllNotesBtn.disabled= !items.some( item => !!String( item?.note || '' ).trim() ); clearAllCategoriesBtn.disabled= !items.some( item => normalizeFavoriteAssignedCategories( item?.categories ).length > 0 ); }; clearAllNotesBtn.addEventListener( 'click', event => { event.preventDefault(); if (clearAllNotesBtn.disabled) return; const confirmed=window.confirm( 'Usunąć wszystkie zapisane notatki ze wszystkich anonsów i profili?\n\n' + 'Kategorie, Ulubione i ustawienia alertów pozostaną bez zmian. Po usunięciu przez 10 sekund będzie dostępne „Cofnij”.' ); if (!confirmed) return; const removed= clearAllFavoriteNotes(); clearNotesStatus.textContent= removed ? `Usunięto notatki z ${removed} pozycji.` :'Brak notatek do usunięcia.'; syncClearNotesAndCategoriesButtons(); setTimeout( syncClearNotesAndCategoriesButtons, 0 ); } ); clearAllCategoriesBtn.addEventListener( 'click', event => { event.preventDefault(); if (clearAllCategoriesBtn.disabled) return; const confirmed=window.confirm( 'Usunąć wszystkie przypisania kategorii ze wszystkich anonsów i profili?\n\n' + 'Lista dostępnych kategorii i ich kolory pozostaną bez zmian. Ulubione, notatki i alerty również pozostaną bez zmian. Przez 10 sekund będzie dostępne „Cofnij”.' ); if (!confirmed) return; const removed= clearAllFavoriteAssignedCategories(); clearCategoriesStatus.textContent= removed ? `Usunięto kategorie z ${removed} pozycji.` :'Brak przypisanych kategorii do usunięcia.'; syncClearNotesAndCategoriesButtons(); } ); clearNotesRow.appendChild(clearAllNotesBtn); editFavoriteCategoriesRow.append(editFavoriteCategoriesBtn,clearAllCategoriesBtn); const notesActionsBox=makeElement( 'div', 'vm-settings-notes-actions' ); notesActionsBox.append(clearNotesRow,clearNotesStatus); clearAllNotesBtn.disabled=clearAllCategoriesBtn.disabled=true; const watchOnlineSeparator=makeElement( 'div', 'vm-settings-separator' ); const {row:nonAlertChangeNewnessRow,select:nonAlertChangeNewnessSelect}=makeSettingsSelectRow('nonAlertChangeNewness'); const nonAlertChangeNewnessInfo=makeElement( 'div', "vm-settings-info -indented vm-ui-help", 'Poniższe dotyczy tylko zmian, które nie wywołały alertu. Zmiana związana z alertem pozostaje nowa do jego odczytania: przyciskiem ✓/× lub przez otwarcie strony alertu, także w tle. Samo otwarcie tabeli lub kafelków nie odczytuje alertów.' ); const clearWatchHistoryRow=makeElement( 'div', 'vm-settings-inline-row -indented' ); const clearWatchHistoryBtn=makeButton( "vm-settings-cache-button -danger vm-ui-button", 'Wyczyść historię zmian' ); const clearWatchHistoryStatus=makeElement( 'span', "vm-settings-info vm-ui-help" ); clearWatchHistoryBtn.disabled=true; clearWatchHistoryBtn.addEventListener( 'click', event => { event.preventDefault(); if (clearWatchHistoryBtn.disabled) return; const confirmed=window.confirm( 'Usunąć całą historię zmian?\n\n' + 'Lepiej tego nie robić, ponieważ po usunięciu nie będzie można podejrzeć tych zmian w inny sposób. Tej operacji nie można cofnąć.' ); if (!confirmed) return; saveWatchEvents([]); renderWatchNotificationBar(); clearWatchHistoryBtn.disabled=true; clearWatchHistoryStatus.textContent= 'Historia zmian została usunięta.'; } ); clearWatchHistoryRow.append( clearWatchHistoryBtn, clearWatchHistoryStatus ); const clearFavoritesRow=makeElement('div','vm-settings-inline-row -indented'); const clearAllFavoritesBtn=makeButton('vm-settings-cache-button -danger vm-ui-button','Usuń wszystkie ulubione'); clearAllFavoritesBtn.id='vm-settings-clear-all-favorites'; const clearFavoritesStatus=makeElement('span','vm-settings-info vm-ui-help'); clearFavoritesStatus.setAttribute('role','status'); const syncClearAllFavoritesButton=()=>{clearAllFavoritesBtn.disabled=!getWatchedItems().some(item=>item.favorite);}; clearAllFavoritesBtn.addEventListener('click',event=>{ stopUiEvent(event); const count=getWatchedItems().filter(item=>item.favorite).length; if(!count){syncClearAllFavoritesButton();return;} if(!window.confirm(`Usunąć wszystkie ulubione (${count}) i wyłączyć ich alerty?\n\n`+ 'Profile i anonse pozostaną zapisane, także te bez notatek. Notatki, kategorie, ukrywanie, powiązania i historia zmian nie zostaną usunięte.\n\n'+ 'Oczekujące alerty tych pozycji zostaną oznaczone jako odczytane. Operacja zadziała od razu, bez zapisywania ustawień.'))return; clearAllFavoritesBtn.disabled=true; try{ const removed=clearAllFavorites(); clearFavoritesStatus.textContent=`Usunięto z Ulubionych: ${removed}. Alerty wyłączone; profile, notatki i kategorie zachowane.`; }catch(error){clearFavoritesStatus.textContent='Nie udało się usunąć wszystkich ulubionych.';log('Błąd usuwania wszystkich ulubionych',error);} finally{syncClearAllFavoritesButton();} }); const clearFavoritesInfo=makeElement('div','vm-settings-info -indented vm-ui-help', 'Usuwa tylko z Ulubionych i wyłącza alerty tych pozycji. Nie usuwa profili, anonsów, notatek, kategorii, ukrywania ani historii. Działa od razu po potwierdzeniu.'); clearFavoritesRow.append(clearAllFavoritesBtn,clearFavoritesStatus); clearAllFavoritesBtn.disabled=true; let watchEnabledDraft=currentWatch.enabled === true; checks.watchEnabled.input.style.pointerEvents='none'; checks.watchEnabled.label.tabIndex=0; checks.watchEnabled.label.setAttribute('role','checkbox'); const {row:watchIntervalRow,select:watchIntervalSelect}=makeSettingsSelectRow('watchIntervalMinutes'); Object.assign(watchIntervalSelect.style,{width:'110px',minWidth:'110px',flex:'0 0 110px'}); const notificationTextRow=makeElement('label','vm-settings-inline-row'); const notificationText=makeElement('input','vm-ui-field',null,{type:'text',maxLength:240,value:current.watchNotificationText,placeholder:'Nowy alert'}); notificationText.style.flex='1'; notificationText.style.minWidth='120px'; notificationTextRow.append(makeElement('span','vm-settings-field-label','Treść anonimowego alertu:'),notificationText); settingsFieldControls.watchNotificationText=[notificationText,normalizeWatchNotificationText]; const notificationInfo=makeElement('div','vm-ui-help','Kliknięcie otwiera powiązaną stronę i odczytuje alert. Anonimizacja ukrywa dane profilu; system może nadal pokazywać nazwę rozszerzenia.'); const watchRuntimeInfo=makeElement('div',"vm-settings-info vm-settings-runtime-info vm-ui-help",'Automatyczne sprawdzanie działa przy otwartej karcie Escort.club, Escorti.pl lub Garsoniera.com.pl. Przy włączonych alertach częstotliwość poniżej dotyczy aktywności i nowych anonsów. Pełne dane oraz profile tylko z notatką są sprawdzane nie częściej niż co godzinę. Ukryte profile są sprawdzane pod kątem nowych anonsów co około godzinę.'); const {row:watchProfileModeRow,select:watchProfileModeSelect}=makeSettingsSelectRow('watchProfileResolutionMode'); watchProfileModeSelect.title='Wspólny sposób ustalania profili w Ulubionych i Ukrytych na podstawie anonsu Escort.club. Przy „Zawsze pytaj” wybór pojawia się przy dodawaniu lub edycji całego profilu, także zapisanego wcześniej. Automatyczne sprawdzanie używa zapisanego sposobu.'; watchProfileModeRow.title=watchProfileModeSelect.title; const syncWatchSettingsControls=() => { checks.watchEnabled.input.checked=watchEnabledDraft; checks.watchEnabled.label.setAttribute( 'aria-checked', watchEnabledDraft ? 'true':'false' ); const onlineCountEnabled=watchEnabledDraft; favoritesDefaultView.select.disabled=!onlineCountEnabled; setSettingsVisualEnabled(favoritesDefaultView.row,onlineCountEnabled); checks.showWatchOnlineCount.input.disabled=!onlineCountEnabled; setSettingsVisualEnabled( checks.showWatchOnlineCount.label, onlineCountEnabled ); const notificationsEnabled=onlineCountEnabled && checks.watchSystemNotifications.input.checked; for (const [key,enabled] of [['showWatchAlertBar',onlineCountEnabled],['watchSystemNotifications',onlineCountEnabled],['watchNotificationAnonymous',notificationsEnabled]]) { checks[key].input.disabled=!enabled; setSettingsVisualEnabled(checks[key].label,enabled); } notificationTextRow.hidden=!checks.watchNotificationAnonymous.input.checked; notificationTextRow.style.display=notificationTextRow.hidden ? 'none':''; notificationText.disabled=!notificationsEnabled || notificationTextRow.hidden; setSettingsVisualEnabled(notificationTextRow,!notificationText.disabled); watchIntervalSelect.disabled= !watchEnabledDraft; watchIntervalRow.style.opacity= watchEnabledDraft ? '1' :'.5'; const watchProfileModeEnabled= watchEnabledDraft; watchProfileModeSelect.disabled= !watchProfileModeEnabled; setSettingsVisualEnabled( watchProfileModeRow, watchProfileModeEnabled ); const nonAlertNewnessEnabled= watchEnabledDraft; nonAlertChangeNewnessSelect.disabled= !nonAlertNewnessEnabled; nonAlertChangeNewnessRow.style.opacity= nonAlertNewnessEnabled ? '1' :'.5'; }; for (const key of ['watchSystemNotifications','watchNotificationAnonymous']) checks[key].input.addEventListener('change',syncWatchSettingsControls); const toggleWatchEnabledDraft=() => { watchEnabledDraft=!watchEnabledDraft; syncWatchSettingsControls(); }; checks.watchEnabled.label.addEventListener('click',event => { stopUiEvent(event); toggleWatchEnabledDraft(); }); checks.watchEnabled.label.addEventListener('keydown',event => { if (event.key !== ' ' && event.key !== 'Enter') return; stopUiEvent(event); toggleWatchEnabledDraft(); }); syncWatchSettingsControls(); const selectedAutoGarsoAnalysisMode=normalizeAutoGarsoAnalysisMode( current.autoGarsoAnalysisMode ); const autoGarsoCountRadio=makeRadioOption( 'vm-auto-garso-analysis-mode', 'count', 'Sprawdzaj, czy są tematy (2 zapytania)', selectedAutoGarsoAnalysisMode === 'count' ); const autoGarsoContentRadio=makeRadioOption( 'vm-auto-garso-analysis-mode', 'content', 'Sprawdzaj treść tematów (analizuj posty) (>2 zapytania)', selectedAutoGarsoAnalysisMode === 'content' ); const autoGarsoModeWarning=makeElement('div',"vm-settings-auto-warning vm-ui-help vm-ui-error",'Uwaga: częste pełne analizy mogą uruchomić zabezpieczenie antyfloodowe i spowodować czasową blokadę na forum a może nawet bana.'); const autoGarsoModeBox=makeElement('div',"vm-settings-auto-box vm-ui-panel"); autoGarsoContentRadio.text.appendChild(autoGarsoModeWarning); autoGarsoModeBox.append( autoGarsoCountRadio.label, autoGarsoContentRadio.label ); const autoCheckInfo=makeElement('div',"vm-settings-info vm-ui-help vm-settings-compact-info -top",'Jeżeli odpowiedni cache dla wybranego trybu nie jest przeterminowany, skrypt korzysta z zapisanych danych i nie odpytuje garsoniera.com.pl.'); styleSingleAdSubsectionBox( autoGarsoModeBox ); autoGarsoModeBox.appendChild( autoCheckInfo ); const aggregationModeBox=makeElement('div',"vm-settings-options-box vm-ui-panel"); const {row:aggregationModeSelectRow,select:aggregationModeSelect}=makeSettingsSelectRow('escortAggregationMode'); const aggregationModeDescription=makeElement('div',"vm-settings-field-description vm-ui-help"); const aggregationModeWarning=makeElement('div',"vm-settings-warning vm-settings-field-description vm-ui-help vm-ui-error",'Uwaga: agregowanie z wykorzystaniem wyszukiwarki Escort.club może trwać dłużej.'); aggregationModeWarning.style.display='none'; function syncAggregationModeControls() { const enabled= checks.mergeEscortAds.input.checked; const mode=normalizeEscortAggregationMode(aggregationModeSelect.value); aggregationModeSelect.disabled=!enabled; aggregationModeBox.style.opacity=enabled ? '1':'.5'; aggregationModeDescription.textContent= ESCORT_AGGREGATION_MODE_DESCRIPTIONS[mode] || ''; aggregationModeWarning.style.display=mode === 'phone-escort-search' ? 'block' :'none'; } aggregationModeSelect.addEventListener('change',syncAggregationModeControls); checks.mergeEscortAds.input.addEventListener('change',syncAggregationModeControls); aggregationModeBox.append( aggregationModeSelectRow, aggregationModeDescription, aggregationModeWarning ); syncAggregationModeControls(); function styleSingleAdSubsectionBox(box) { box.classList.add("vm-settings-single-ad-subsection-box","vm-ui-panel"); } const compactSummaryComparisonModeBox=makeElement( 'div', "vm-settings-options-box vm-ui-panel" ); styleSingleAdSubsectionBox( compactSummaryComparisonModeBox ); const { row:compactSummaryComparisonModeRow, select:compactSummaryComparisonModeSelect }=makeSettingsSelectRow('compactSummaryComparisonMode'); const compactSummaryComparisonModeDescription=makeElement('div',"vm-settings-field-description vm-ui-help"); const syncCompactSummaryComparisonMode=() => { const mode=normalizeEscortCompactSummaryMode( compactSummaryComparisonModeSelect.value ); compactSummaryComparisonModeDescription.textContent= ESCORT_COMPACT_SUMMARY_MODE_DESCRIPTIONS[mode] || ''; }; compactSummaryComparisonModeSelect.addEventListener( 'change', syncCompactSummaryComparisonMode ); compactSummaryComparisonModeBox.append( compactSummaryComparisonModeRow, compactSummaryComparisonModeDescription ); syncCompactSummaryComparisonMode(); const {row:maxRelatedAdsTabsRow,input:maxRelatedAdsTabsInput}=makeSettingsNumberRow('maxRelatedAdsTabs'); const relatedAdsTileLinkBox=makeElement('div',"vm-settings-options-box vm-ui-panel"); const { row:relatedAdsTileLinkRow, select:relatedAdsTileLinkSelect }=makeSettingsSelectRow('relatedAdsTileLinkMode'); const relatedAdsTileLinkDescription=makeElement( 'div', "vm-settings-field-description vm-ui-help" ); const syncRelatedAdsTileLinkControls=() => { const enabled=checks.showRelatedAdsTileLink.input.checked; const mode=normalizeEscortRelatedAdsTileLinkMode( relatedAdsTileLinkSelect.value ); relatedAdsTileLinkSelect.disabled=!enabled; checks.openAllActiveRelatedAdsFromTile.input.disabled=!enabled; const openAllEnabled=enabled && checks.openAllActiveRelatedAdsFromTile.input.checked; maxRelatedAdsTabsInput.disabled=!openAllEnabled; maxRelatedAdsTabsRow.style.opacity=openAllEnabled ? '1':'.5'; relatedAdsTileLinkBox.style.opacity=enabled ? '1':'.5'; relatedAdsTileLinkDescription.textContent= ESCORT_AGGREGATION_MODE_DESCRIPTIONS[mode] || ''; }; checks.showRelatedAdsTileLink.input.addEventListener( 'change', syncRelatedAdsTileLinkControls ); relatedAdsTileLinkSelect.addEventListener( 'change', syncRelatedAdsTileLinkControls ); checks.openAllActiveRelatedAdsFromTile.input.addEventListener( 'change', syncRelatedAdsTileLinkControls ); relatedAdsTileLinkBox.append( relatedAdsTileLinkRow, relatedAdsTileLinkDescription, checks.openAllActiveRelatedAdsFromTile.label, maxRelatedAdsTabsRow ); syncRelatedAdsTileLinkControls(); const multiLocationInfo=makeElement( 'div', "vm-settings-info -indented vm-ui-help", 'Multiwybór łączy wyniki kilku miast na jednej liście; pozostałe natywne filtry są wspólne dla wszystkich lokalizacji.' ); Object.assign( checks.showMenuPagesAsFavicons.label.style, { marginLeft:'25px', marginTop:'4px' } ); const menuPagesDraft= normalizeMenuPages( current.menuPages ).map(item => ({...item})); const menuPagesEditor= createExternalServicesEditor({ masterCheck:null, items:menuPagesDraft, infoText: 'Linki są wyświetlane bezpośrednio na pasku menu. Nazwa pojawia się po najechaniu na ikonę.', addButtonText: '+ Dodaj link', makeNewItem:count => ({ id: `custom-menu-${Date.now()}-${count + 1}`, name:'Nowa strona', enabled:true, url:'https://' }), urlKey:'url', normalizeUrl: normalizeMenuPageUrl, urlPlaceholder: 'https://example.com/', enabledTitle: 'Pokaż ten link', removeTitle: 'Usuń link', itemNoun: 'linku' }); const syncBottomLeftMenuSettings=() => { const enabled= checks.showBottomLeftMenu.input.checked; checks.showMenuPagesAsFavicons.input.disabled= !enabled; checks.showMenuPagesAsFavicons.label.style.opacity= enabled ? '1':'.5'; menuPagesEditor.box.style.opacity= enabled ? '1':'.5'; }; checks.showBottomLeftMenu.input.addEventListener( 'change', syncBottomLeftMenuSettings ); checks.showMenuPagesAsFavicons.input.addEventListener( 'change', syncBottomLeftMenuSettings ); syncBottomLeftMenuSettings(); const { row:topPhoneSearchEnterTargetRow, select:topPhoneSearchEnterTargetSelect }=makeSettingsSelectRow('topPhoneSearchEnterTarget'); const { row:topPhoneSearchOpenModeRow, select:topPhoneSearchOpenModeSelect }=makeSettingsSelectRow('topPhoneSearchOpenMode'); const topPhoneSearchOptionsRow=makeElement('div','vm-settings-inline-row vm-settings-top-phone-options'); topPhoneSearchOptionsRow.append( topPhoneSearchEnterTargetRow, topPhoneSearchOpenModeRow ); bindSettingsEnabled(checks.showTopPhoneSearch.input,topPhoneSearchOptionsRow,[topPhoneSearchEnterTargetSelect,topPhoneSearchOpenModeSelect]); bindSettingsEnabled(checks.showTopPhoneSearch.input,checks.garsoShowSearchIcons.label,[checks.garsoShowSearchIcons.input],true); function createExternalServicesEditor({ masterCheck, items, infoText, addButtonText, makeNewItem, urlKey, normalizeUrl, urlPlaceholder, enabledTitle, removeTitle, itemNoun, beforeInfo=[] }) { const box=makeElement('div',"vm-settings-image-search-engines vm-settings-compact-inset-box vm-ui-panel"); const info=makeElement('div','',infoText); info.className="vm-settings-compact-info vm-ui-help"; const rows=makeElement('div'); const addRow=makeElement('div','vm-settings-inline-row'); const addButton=makeButton("vm-settings-action-button vm-ui-button -accent",addButtonText); let rendered=false; const sync=() => { const enabled= !masterCheck || masterCheck.input.checked; setSettingsVisualEnabled(box,enabled,true); box.setAttribute( 'aria-disabled', String(!enabled) ); for (const control of box.querySelectorAll( 'input, button' )) { control.disabled=!enabled; } }; const render=() => { rendered=true; rows.replaceChildren(); items.forEach((item,index) => { const row=makeElement('div','vm-settings-external-service-row'); const enabled=makeElement('input','',null,{type:'checkbox'}); enabled.checked=item.enabled !== false; enabled.title=enabledTitle; const name=makeElement('input','',null,{type:'text'}); name.value=item.name; name.placeholder='Nazwa'; name.maxLength=40; const url=makeElement('input','',null,{type:'text',value:item[urlKey],placeholder:urlPlaceholder}); const remove=makeButton("vm-settings-cache-button vm-ui-button",'×',{title:removeTitle}); name.className=url.className="vm-settings-mini-input vm-ui-field vm-ui-compact"; remove.classList.add("vm-settings-mini-remove","vm-ui-icon","vm-ui-compact"); enabled.addEventListener('change',() => {item.enabled=enabled.checked;}); name.addEventListener('input',() => {item.name=name.value;}); url.addEventListener('input',() => {item[urlKey]=url.value;}); remove.addEventListener('click',event => { event.preventDefault(); items.splice(index,1); render(); }); row.append(enabled,name,url,remove); rows.appendChild(row); }); sync(); }; addButton.addEventListener('click',event => { event.preventDefault(); items.push(makeNewItem(items.length)); render(); rows.lastElementChild?.querySelector('input[type="text"]')?.focus(); }); addRow.appendChild(addButton); box.append(...beforeInfo,info,rows,addRow); if (masterCheck?.input) { masterCheck.input.addEventListener( 'change', sync ); } sync(); const read=() => items.map((item,index) => { const name=normalizeEscortAdText(item.name).slice(0,40); const normalizedUrl=normalizeUrl(item[urlKey]); if (!name) throw new Error(`Podaj nazwę ${itemNoun} w wierszu ${index + 1}.`); if (!normalizedUrl) throw new Error(`Podaj prawidłowy adres http/https ${itemNoun} w wierszu ${index + 1}.`); return {...item,name,[urlKey]:normalizedUrl,enabled:item.enabled !== false}; }); return {box,read,mount:() => {if (!rendered) render();}}; } const imageSearchEnginesDraft=normalizeImageSearchEngines(current.imageSearchEngines) .map(item => ({...item})); const imageSearchEditor=createExternalServicesEditor({ masterCheck:checks.showImageSearchButtons, items:imageSearchEnginesDraft, infoText:'Adres zdjęcia zostanie dopisany do URL usługi. Google i Yandex: wyszukiwanie. PicDet i Hive AI: tylko wklejenie do pola, bez wysyłania formularza. Wynik Hive może być błędny i nie wyklucza retuszu.', addButtonText:'+ Dodaj wyszukiwarkę', makeNewItem:count => ({id:`custom-${Date.now()}-${count + 1}`,name:'Nowa',enabled:true,urlPrefix:''}), urlKey:'urlPrefix', normalizeUrl:normalizeImageSearchUrlPrefix, urlPlaceholder:'https://serwis.example/search?url=', enabledTitle:'Włącz / wyłącz tę wyszukiwarkę', removeTitle:'Usuń wyszukiwarkę', itemNoun:'wyszukiwarki obrazem' }); styleSingleAdSubsectionBox( imageSearchEditor.box ); checks.autoPhoneReputationSearch.label.style.margin='0 0 4px'; const phoneReputationServicesDraft=normalizePhoneReputationServices( current.phoneReputationServices ).map(item => ({...item})); const phoneReputationEditor=createExternalServicesEditor({ masterCheck:checks.showPhoneReputationLinks, items:phoneReputationServicesDraft, beforeInfo:[checks.autoPhoneReputationSearch.label], infoText:'W URL możesz użyć {phone} (9 cyfr), {phone48} (48 + 9 cyfr) lub {query} (kilka formatów numeru połączonych OR). Automatyczne odczytywanie ocen działa dla wbudowanych KTOTO, NumeryTelefonu i SprawdzenieNumeru; własne serwisy są dodawane jako linki.', addButtonText:'+ Dodaj serwis', makeNewItem:count => ({id:`custom-${Date.now()}-${count + 1}`,name:'Nowy serwis',enabled:true,urlTemplate:'https://'}), urlKey:'urlTemplate', normalizeUrl:normalizePhoneReputationUrlTemplate, urlPlaceholder:'https://serwis.example/numer/{phone}', enabledTitle:'Pokaż ten serwis w sekcji sprawdzania numeru', removeTitle:'Usuń serwis', itemNoun:'serwisu numerów' }); styleSingleAdSubsectionBox( phoneReputationEditor.box ); const {row:alarmingTagCountRow,input:alarmingTagCountInput}=makeSettingsNumberRow('alarmingTagCountLimit'); compactSummaryComparisonModeBox.appendChild( alarmingTagCountRow ); bindSettingsEnabled(checks.showCompactSummaryAboveDescription.input,compactSummaryComparisonModeBox,[compactSummaryComparisonModeSelect,alarmingTagCountInput]); const tagHighlightGroupsDraft=normalizeTagHighlightGroups(current.tagHighlightGroups) .map(group => ({...group,tags:[...group.tags]})); const tagHighlightBox=makeElement( 'div', 'vm-settings-tag-highlight-groups' ); styleSingleAdSubsectionBox( tagHighlightBox ); const tagHighlightInfo=makeElement( 'div', "vm-settings-compact-info -spaced vm-ui-help", 'Tagi wybierasz z listy odczytanej z aktualnej strony i cache. „pozostałe” obejmuje automatycznie wszystkie nieprzypisane tagi.' ); const tagHighlightRows=makeElement('div','','Wczytywanie listy tagów…'); let tagHighlightRendered=false; const tagHighlightAddGroup=makeButton( "vm-settings-action-button vm-settings-tag-add-group vm-ui-button -accent vm-ui-compact", '+ Dodaj grupę' ); const removeTagFromAllHighlightGroups=tag => { const wanted=normalizeEscortChangeComparisonValue(tag); for (const group of tagHighlightGroupsDraft) { if (group.locked) continue; group.tags=group.tags.filter(value => normalizeEscortChangeComparisonValue(value) !== wanted); } }; const renderTagHighlightGroups=() => { tagHighlightRows.replaceChildren(); const knownTagLabels=getKnownEscortTagLabels(tagHighlightGroupsDraft); tagHighlightGroupsDraft.forEach((group,index) => { const locked=group.locked === true || group.id === 'other'; const row=makeElement( 'div', `vm-settings-tag-group-row${index ? ' -separated':''}` ); const name=makeElement('input'); name.type='text'; name.value=locked ? 'pozostałe':group.name; name.disabled=locked; name.maxLength=40; name.className="vm-settings-mini-input vm-settings-tag-group-name vm-ui-field vm-ui-compact"; if (!locked) name.addEventListener('input',() => {group.name=name.value;}); const color=makeElement('input'); color.type='color'; color.value=normalizeFavoriteCategoryColor(group.color) || DEFAULT_TAG_HIGHLIGHT_GROUPS.at(-1).color; color.title='Kolor grupy'; color.className="vm-settings-tag-color vm-ui-field vm-ui-color vm-ui-compact"; color.addEventListener('input',() => {group.color=color.value;}); const tags=makeElement('div','vm-settings-tag-list'); if (locked) { const automatic=makeElement( 'span', 'vm-settings-tag-automatic', 'wszystkie nieprzypisane' ); tags.appendChild(automatic); } else { for (const tag of group.tags) { const chip=makeButton('vm-settings-tag-chip',`${tag} ×`,{title:'Usuń tag z tej grupy'}); chip.style.setProperty('--vm-settings-tag-color',group.color); chip.addEventListener('click',event => { event.preventDefault(); group.tags=group.tags.filter(value => normalizeEscortChangeComparisonValue(value) !== normalizeEscortChangeComparisonValue(tag)); renderTagHighlightGroups(); }); tags.appendChild(chip); } const assignedHere=new Set(group.tags.map(normalizeEscortChangeComparisonValue)); const choices=knownTagLabels .filter(tag => !assignedHere.has(normalizeEscortChangeComparisonValue(tag))); const addTag=makeElement('select'); appendSelectOptions(addTag,[['',choices.length ? '+ wybierz tag…':'brak tagów w cache'],...choices.map(tag => [tag,tag])],''); addTag.disabled=!choices.length; addTag.className="vm-settings-tag-select vm-ui-field vm-ui-compact"; addTag.addEventListener('change',() => { const tag=normalizeEscortAdText(addTag.value).slice(0,80); if (!tag) return; removeTagFromAllHighlightGroups(tag); group.tags.push(tag); renderTagHighlightGroups(); }); tags.appendChild(addTag); } const removeGroup=makeButton(`vm-settings-cache-button vm-ui-button vm-ui-icon vm-ui-compact vm-settings-mini-remove${locked ? ' -hidden':''}`,locked ? '':'×',{disabled:locked,title:locked ? 'Grupa „pozostałe” jest stała':'Usuń grupę'}); if (!locked) removeGroup.addEventListener('click',event => { event.preventDefault(); tagHighlightGroupsDraft.splice(index,1); renderTagHighlightGroups(); }); row.append(name,color,tags,removeGroup); tagHighlightRows.appendChild(row); }); tagHighlightRendered=true; syncTagHighlightControls(); }; tagHighlightAddGroup.addEventListener('click',event => { event.preventDefault(); const otherIndex=tagHighlightGroupsDraft.findIndex(group => group.locked || group.id === 'other'); const insertAt=otherIndex >= 0 ? otherIndex:tagHighlightGroupsDraft.length; tagHighlightGroupsDraft.splice(insertAt,0,{ id:`group-${Date.now()}`, name:`Grupa ${insertAt + 1}`, color:'#e6a0bf', tags:[] }); renderTagHighlightGroups(); }); tagHighlightBox.append( tagHighlightInfo, tagHighlightRows, tagHighlightAddGroup ); const syncTagHighlightControls=() => { const enabled= checks.showTagHighlights.input.checked; setSettingsVisualEnabled( tagHighlightBox, enabled, true ); tagHighlightBox.setAttribute( 'aria-disabled', String(!enabled) ); for (const control of tagHighlightBox.querySelectorAll( 'input, select, button' )) { control.disabled=!enabled || !tagHighlightRendered; } }; checks.showTagHighlights.input.addEventListener( 'change', syncTagHighlightControls ); syncTagHighlightControls(); const readTagHighlightGroupsDraft=() => normalizeTagHighlightGroups(tagHighlightGroupsDraft); const syncSummarySidePanelControls=() => { checks.autoGarsoCheck.input.disabled=false; checks.autoGarsoCheck.label.style.opacity='1'; checks.autoGarsoCheck.label.style.cursor='pointer'; checks.showCompactSummaryAboveDescription.input.disabled=false; checks.showCompactSummaryAboveDescription.label.style.opacity='1'; checks.showCompactSummaryAboveDescription.label.style.cursor='pointer'; const autoModeEnabled= checks.autoGarsoCheck.input.checked; for (const option of [autoGarsoCountRadio,autoGarsoContentRadio]) { option.input.disabled=!autoModeEnabled; option.label.style.cursor=autoModeEnabled ? 'pointer' :'not-allowed'; } autoGarsoCountRadio.label.style.opacity=checks.autoGarsoCheck.input.checked ? '1' :'.5'; autoGarsoContentRadio.label.style.opacity=autoModeEnabled ? '1' :'.5'; setSettingsVisualEnabled(autoGarsoModeBox,checks.autoGarsoCheck.input.checked,true); autoGarsoModeBox.setAttribute( 'aria-disabled', String(!checks.autoGarsoCheck.input.checked) ); }; checks.showSummarySidePanel.input.addEventListener( 'change', syncSummarySidePanelControls ); checks.autoGarsoCheck.input.addEventListener('change',syncSummarySidePanelControls); syncSummarySidePanelControls(); const phoneClipboardFormatBox=makeElement( 'div', 'vm-settings-phone-copy-box' ); styleSingleAdSubsectionBox( phoneClipboardFormatBox ); const {row:phoneClipboardFormatRow,label:phoneClipboardFormatLabel,select:phoneClipboardFormatSelect}=makeSettingsSelectRow('phoneClipboardFormat'); phoneClipboardFormatLabel.style.whiteSpace='nowrap'; phoneClipboardFormatBox.appendChild( phoneClipboardFormatRow ); bindSettingsEnabled(checks.showPhoneCopyButton.input,phoneClipboardFormatBox,[phoneClipboardFormatSelect],true); const persistentCacheInfo=makeElement('div','vm-settings-info vm-ui-help', 'Cache jest zawsze włączony. Ogranicza ponowne zapytania i pozwala otwierać zapisane anonse. Dane Garsoniery są aktualne przez 14 dni, pozostałe szczegóły przez 7 dni. Status online/offline pozostaje zapisany bezterminowo: po 30 minutach jest szary i może zostać odświeżony w tle. Cache IndexedDB jest osobny dla poddomen językowych; czyszczenie dotyczy bieżącej poddomeny.'); const clearCacheRow=makeElement( 'div', 'vm-settings-cache-controls-stack' ); const clearExpiredCacheRow=makeElement( 'div', 'vm-settings-cache-control-row' ); const clearExpiredCacheBtn=makeButton( "vm-settings-cache-button vm-ui-button", 'Wyczyść przeterminowany cache' ); const clearExpiredCacheSize=makeElement( 'span', "vm-settings-cache-size vm-ui-help" ); const clearSavedCacheRow=makeElement( 'div', 'vm-settings-cache-control-row' ); const clearCacheBtn=makeButton( "vm-settings-cache-button vm-ui-button", 'Wyczyść zapisany cache' ); const clearCacheSize=makeElement( 'span', "vm-settings-cache-size vm-ui-help" ); function refreshExpiredCacheSizeLabel() { const stats= getExpiredPersistentCacheStats(); if (!stats.available) { clearExpiredCacheSize.textContent='b/d'; clearExpiredCacheSize.title= 'Dokładny rozmiar przeterminowanego cache IndexedDB można policzyć na stronie Escort.club, na której go zapisano. Skrypt nie wykonuje dodatkowego zapytania tylko dla tej informacji.'; return; } clearExpiredCacheSize.textContent= formatCacheSize(stats.megabytes); clearExpiredCacheSize.title= `Przeterminowany cache możliwy do usunięcia: ${ formatPolishCount( stats.entries, 'wpis', 'wpisy', 'wpisów' ) }, około ${ formatCacheSize(stats.megabytes) }. Nie obejmuje przeterminowanych danych anonsów zachowywanych dlatego, że należą do obserwowanych pozycji.`; } function refreshCacheSizeLabel() { const stats=getPersistentCacheStats(); clearCacheSize.textContent= formatCacheSize(stats.megabytes); clearCacheSize.title= `Przybliżony rozmiar trwałego cache: ${ stats.entries } ${ stats.entries === 1 ? 'wpis' :'wpisów' } (${stats.garsoEntries} Garsoniera, ${ stats.listEntries } Escorti, ${ stats.escortAdDataEntries } Escort.club, ${stats.activityEntries || 0} statusów aktywności, ${stats.templateEntries || 0} szablonów anonsu). ` + 'To przybliżony rozmiar zserializowanych danych w IndexedDB i Violentmonkey.'; refreshExpiredCacheSizeLabel(); } clearCacheSize.textContent=clearExpiredCacheSize.textContent='Obliczanie…'; clearCacheBtn.disabled=clearExpiredCacheBtn.disabled=true; for (const [button,run,done,errorText] of [ [clearExpiredCacheBtn,clearExpiredPersistentCacheAcrossDomains, result => result.removed ? `Usunięto: ${result.removed}`:'Brak przeterminowanego cache','Nie udało się wyczyścić przeterminowanego cache'], [clearCacheBtn,clearPersistentCacheAcrossDomains,() => 'Cache wyczyszczony','Nie udało się wyczyścić zapisanego cache'] ]) { const label=button.textContent; button.addEventListener('click',async event => { stopUiEvent(event); button.disabled=true; button.textContent='Czyszczenie…'; try { const result=await run(); refreshCacheSizeLabel(); button.textContent=done(result); } catch (error) {button.textContent='Błąd czyszczenia'; log(errorText,error);} setTimeout(() => {button.disabled=false; button.textContent=label;},1800); }); } clearExpiredCacheRow.append( clearExpiredCacheBtn, clearExpiredCacheSize ); clearSavedCacheRow.append( clearCacheBtn, clearCacheSize ); const clearTechnicalDataRow=makeElement( 'div', 'vm-settings-cache-control-row' ); const clearTechnicalDataBtn=makeButton( "vm-settings-cache-button -danger vm-ui-button", 'Usuń dane techniczne skryptu' ); const clearTechnicalDataSize=makeElement( 'span', "vm-settings-cache-size vm-ui-help" ); function refreshTechnicalDataSizeLabel() { const stats=getTechnicalScriptDataStats(); clearTechnicalDataSize.textContent= formatCacheSize( stats.bytes / (1024 * 1024) ); clearTechnicalDataSize.title= `Techniczne dane skryptu: ${ formatPolishCount( stats.entries, 'wpis', 'wpisy', 'wpisów' ) }. Nie obejmuje ustawień, ulubionych, notatek, kategorii, historii ani cache IndexedDB.`; } clearTechnicalDataSize.textContent='Obliczanie…'; clearTechnicalDataBtn.disabled=true; clearTechnicalDataBtn.addEventListener( 'click', event => { stopUiEvent(event); const stats= getTechnicalScriptDataStats(); const confirmed=window.confirm( `Usunąć techniczne dane skryptu (${ formatCacheSize( stats.bytes / (1024 * 1024) ) })?\n\n` + 'Zostaną usunięte pomocnicze indeksy, mirrory, diagnostyka, snapshoty i dane tymczasowe.\n\n' + 'Ustawienia, ulubione i obserwowane, notatki, kategorie, historia, zapisane filtry i lokalizacje oraz cache IndexedDB pozostaną bez zmian.' ); if (!confirmed) return; const removed= clearTechnicalScriptDataStorage(); clearTechnicalDataBtn.disabled=true; clearTechnicalDataBtn.textContent= removed ? `Usunięto: ${removed}` :'Brak danych technicznych'; refreshTechnicalDataSizeLabel(); setTimeout( () => location.reload(), 900 ); } ); clearTechnicalDataRow.append( clearTechnicalDataBtn, clearTechnicalDataSize ); clearCacheRow.append( clearExpiredCacheRow, clearSavedCacheRow, clearTechnicalDataRow ); saveBtn.addEventListener('click',async event => { stopUiEvent(event); saveBtn.disabled=true; saveBtn.textContent='Zapisywanie…'; try { const nextSettings={ ...readDeclarativeCheckboxSettings(), ...Object.fromEntries(Object.entries(settingsFieldControls).map(([key,[input,read]]) => [key,read(input.value)])), autoGarsoAnalysisMode:autoGarsoContentRadio.input.checked ? 'content' :'count', mergeEscortAds:checks.mergeEscortAds.input.checked, showPricesInSearchResults:checks.showPricesInSearchResults.input.checked, searchResultPriceDuration:current.searchResultPriceDuration, searchResultExtraFields:normalizeSearchResultExtraFields( current.searchResultExtraFields ), imageSearchEngines:imageSearchEditor.read(), phoneReputationServices:phoneReputationEditor.read(), menuPages: menuPagesEditor.read(), savedLocationGroups:current.savedLocationGroups, tagHighlightGroups:readTagHighlightGroupsDraft(), usePersistentCache:true, notesEnabled:watchEnabledDraft, watchEnabled:watchEnabledDraft, showWatchOnlineCount: checks.showWatchOnlineCount.input.checked, favoritesDataColumns:current.favoritesDataColumns, favoriteCategories: getSettings().favoriteCategories, favoriteCategoryColors: getSettings().favoriteCategoryColors, historyCleanupKeywords:current.historyCleanupKeywords }; const savedSettings=await saveSettings({...getSettings(),...nextSettings},true); SETTINGS={...DEFAULT_SETTINGS,...savedSettings}; applyFavoriteSearchPageSettings(); closeController.close('save'); location.reload(); } catch (error) { saveBtn.disabled=false; saveBtn.textContent='Zapisz'; window.alert(error?.message || 'Nie udało się zapisać ustawień.'); } }); const watchSection=makeSettingsSection( 'Ulubione i zapisane anonse', 'Ulubione umożliwiają obserwowanie aktywności, lokalizacji i zmian danych. Ukryte usuwają wybrane anonse lub profile tylko z listy wyników. Strony anonsów i linki na Escorti pozostają dostępne.', true, checks.watchEnabled.label, watchFavoritesInfo, favoritesDefaultView.row, watchRuntimeInfo, watchIntervalRow, watchProfileModeRow, checks.showWatchOnlineCount.label, checks.showWatchAlertBar.label, checks.watchSystemNotifications.label, checks.watchNotificationAnonymous.label, notificationTextRow, notificationInfo, watchOnlineSeparator, nonAlertChangeNewnessInfo, nonAlertChangeNewnessRow, clearWatchHistoryRow, clearFavoritesRow, clearFavoritesInfo ); const notesSection=makeSettingsSection( 'Notatki', 'Wyłączenie funkcji zachowuje zapisane notatki i kategorie.', true, notesActionsBox ); const categoriesSection=makeSettingsSection( 'Kategorie','',true,editFavoriteCategoriesRow,clearCategoriesStatus ); const hidePageElementsSection=makeSettingsSection( 'Ukryj elementy strony Escort.club', 'Wybierz elementy natywnej strony Escort.club, które mają być ukrywane przez skrypt.', true, checks.hideRecommendedSearchSection.label, checks.hideRecommendedAdSection.label, checks.hideSingleAdContactButtons.label, checks.hideSingleAdActions.label, checks.hideFeaturedSearchSection.label, checks.hidePopularCitiesSection.label ); const searchButtonSection=makeSettingsSection( 'Przycisk wyszukiwarki', 'Ustawienia podręcznej wyszukiwarki dostępnej na obsługiwanych stronach.', true, checks.showTopPhoneSearch.label, topPhoneSearchOptionsRow ); const quickPreviewSection=makeSettingsSection( 'Dymki podglądu anonsów', 'Wspólne ustawienie dla linków prowadzących do anonsów Escort.club na Escort.club, Escorti.pl i Garsonierze.', true, checks.quickAdPreview.label ); const scriptMenuSection=makeSettingsSection( 'Menu skryptu', 'Ustawienia górnego paska Escort Helper', true, checks.showBottomLeftMenu.label, checks.showMenuPagesAsFavicons.label, menuPagesEditor.box ); const singleAdSection=makeSettingsSection( 'Widok pojedynczego anonsu escort.club', 'Narzędzia dostępne po otwarciu konkretnego anonsu Escort.club.', true, checks.showSummarySidePanel.label, checks.showCompactSummaryAboveDescription.label, compactSummaryComparisonModeBox, checks.showTagHighlights.label, tagHighlightBox, checks.autoGarsoCheck.label, autoGarsoModeBox, checks.showImageSearchButtons.label, imageSearchEditor.box, checks.showPhoneReputationLinks.label, phoneReputationEditor.box, checks.showPhoneCopyButton.label, phoneClipboardFormatBox ); const resultsTileAppearanceSection=makeSettingsSection( 'Wygląd kafelka w wynikach wyszukiwania Escort.club', 'Ustawienia informacji i dodatkowych elementów wyświetlanych bezpośrednio na kafelkach wyników.', true, checks.showEscortiTileData.label, checks.showPricesInSearchResults.label, checks.showRelatedAdsTileLink.label, relatedAdsTileLinkBox ); const resultsAggregationSection=makeSettingsSection( 'Agregowanie w wynikach wyszukiwania Escort.club', 'Sposób łączenia powiązanych anonsów i wykorzystywania zapisanych danych do agregowania wyników.', true, checks.mergeEscortAds.label, aggregationModeBox ); const resultsSearchToolsSection=makeSettingsSection( 'Narzędzia wyszukiwania', 'Opcje ułatwiające wyszukiwanie w wielu lokalizacjach i ponowne używanie zapisanych lokalizacji.', true, checks.multiLocationEnabled.label, multiLocationInfo, checks.saveSearchLocationsEnabled.label ); relatedAdsTileLinkBox.classList.add("vm-settings-compact-inset-box","vm-ui-panel"); aggregationModeBox.classList.add("vm-settings-compact-inset-box","vm-ui-panel"); const escortRequestLimits=createEscortRequestSettingsRows(makeSettingsNumberRow,checks.escortFastStart.label); const requestLimitsSection=makeSettingsSection('Limity zapytań do Escort.club', 'Wspólne limity dla HEAD i GET / POST we wszystkich kartach.',true,escortRequestLimits); const cacheSection=makeSettingsSection('Pamięć podręczna', 'Lokalnie zapisane dane Garsoniery, Escorti i anonsów Escort.club.',true,persistentCacheInfo,clearCacheRow); const liveDiagnosticsSection=makeSettingsSection('Panel na stronach','',true,checks.showLiveDiagnostics.label); const diagnosticsSection=makeSettingsSection('Zapis diagnostyczny', 'Zbiorcze liczniki i ostatnie błędy ze wszystkich kart. Przycisk „Odśwież” aktualizuje odczyt; zapisane zdarzenie nie oznacza trwającego problemu.',true); const diagnosticsHealth=makeElement( 'div', "vm-diagnostics-health vm-ui-panel" ); const diagnosticsOverview=makeElement( 'div', 'vm-diagnostics-overview' ); const diagnosticsDetailGrid=makeElement( 'div', 'vm-diagnostics-detail-grid' ); const diagnosticsActions=makeElement('div','vm-settings-inline-row vm-settings-transfer-row'); const diagnosticsRefreshBtn=makeButton("vm-settings-action-button vm-ui-button -accent",'Odśwież'); const diagnosticsExportBtn=makeButton("vm-settings-action-button vm-ui-button -accent",'Eksportuj diagnostykę'); const diagnosticsClearBtn=makeButton("vm-settings-cache-button vm-ui-button",'Wyczyść diagnostykę'); const diagnosticsStatus=makeElement('span',"vm-settings-transfer-status vm-ui-help",'Wczytywanie diagnostyki…'); diagnosticsExportBtn.disabled=diagnosticsClearBtn.disabled=true; const diagnosticsPrivacy=makeElement('div',"vm-diagnostics-privacy vm-ui-help",'Eksport zawiera źródła błędów, stan kolejek, krótki przebieg zapytań i zapis sprzed ostatniego bana. Nie zapisuje adresów anonsów, telefonów ani treści odpowiedzi. Wyczyszczenie diagnostyki zachowuje ustawienia, anonse i cache.'); const makeDiagnosticCard=(value,label,detail='',foot='') => { const card=makeElement('div',"vm-diagnostics-card vm-ui-panel"); const valueLine=makeElement('div','vm-diagnostics-card-value',String(value)); const labelLine=makeElement('div','vm-diagnostics-card-label',label); card.append(labelLine,valueLine); if (detail) card.append(makeElement('div','vm-ui-help',detail)); if (foot) { card.classList.add('-requests'); card.append(makeElement('div','vm-diagnostics-card-foot vm-ui-help',foot)); } return card; }; const makeDiagnosticList=(titleText,events) => { const list=makeElement('div',"vm-diagnostics-list vm-ui-panel"); const listTitle=makeElement('div','vm-diagnostics-list-title',titleText); list.appendChild(listTitle); const recent=(Array.isArray(events) ? events:[]).slice(-10).reverse(); if (!recent.length) { const empty=makeElement('div','vm-diagnostics-empty','Brak zapisanych zdarzeń.'); list.appendChild(empty); return list; } for (const event of recent) { const row=makeElement('div','vm-diagnostics-event'); const timestamp=getDiagnosticEventTimestamp(event); const time=timestamp ? new Date(timestamp).toLocaleString('pl-PL',{ day:'2-digit',month:'2-digit',year:'numeric', hour:'2-digit',minute:'2-digit',second:'2-digit' }):'Nieznany czas'; row.textContent=`${time}${event.scriptVersion ? ` • v${event.scriptVersion}`:''} - ${event.area}` + (event.message ? `: ${event.message}`:'') + (event.count > 1 ? ` · ×${event.count}`:''); list.appendChild(row); } return list; }; const formatDiagnosticTime=value => { const timestamp=Number(value) || 0; if (!timestamp) return 'brak danych'; return new Date(timestamp).toLocaleString( 'pl-PL', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' } ); }; const makeDiagnosticDetailBox=( titleText, lines ) => { const box=makeElement( 'div', "vm-diagnostics-detail-box vm-ui-panel" ); box.appendChild( makeElement( 'div', 'vm-diagnostics-detail-title', titleText ) ); for (const line of lines.filter(Boolean)) { box.appendChild( makeElement( 'div', 'vm-diagnostics-detail-line', line ) ); } return box; }; const diagnosticBackoffBox=makeDiagnosticDetailBox('Prędkość zapytań',[]); const diagnosticBackoffLines=[['Escort.club','escortClub'],['Escorti.pl','escorti'],['Garso','garso']].map(([label,key]) => { const node=makeElement('div','vm-diagnostics-detail-line'); diagnosticBackoffBox.appendChild(node); return {label,key,node}; }); const renderDiagnosticBackoff=(backoff=getAdaptiveBackoffDiagnostics()) => { for (const {label,key,node} of diagnosticBackoffLines) { const text=key === 'escorti' ? 'Escorti.pl: bez automatycznego spowalniania':formatDiagnosticBackoffStatus(label,backoff[key]); if (node.textContent !== text) node.textContent=text; } }; const renderDiagnostics=() => { const snapshot=getDiagnosticsSnapshot(),counters=snapshot.counters || {}; const cache=snapshot.cache || {},details=snapshot.cacheDetails || {},watch=snapshot.watch || {}; const network=snapshot.network?.escortClub || {},windows=snapshot.requestWindows || {}; diagnosticsHealth.className=`vm-diagnostics-health vm-ui-panel ${snapshot.health?.ok ? '-ok':'-warning'}`; diagnosticsHealth.replaceChildren( makeElement('span','vm-diagnostics-health-text',snapshot.health?.ok ? 'Brak ostrzeżeń w odczytanych pomiarach.' : `Ostrzeżenia w odczycie: ${snapshot.health?.text || 'brak podsumowania'}`), makeElement('a','vm-diagnostics-health-version vm-settings-action-button vm-ui-button -accent',`Wersja ${snapshot.scriptVersion || SCRIPT_VERSION}`, {href:'https://sleazyfork.org/pl/scripts/593145-escort-helper',target:'_blank',rel:'noopener noreferrer',title:'Strona aktualizacji skryptu'})); const lookups=Number(counters.cacheLookups) || 0,hits=Number(counters.cacheHits) || 0; diagnosticsOverview.replaceChildren( ...['escortClub','escorti','garso'].map((key,index) => makeDiagnosticCard( ['minute','fiveMinutes','halfHour'].map(window => windows[window]?.[key] ?? '—').join(' / '), `Zapytania do ${['Escort.club','Escorti.pl','Garsoniery'][index]}`,'1 min / 5 min / 30 min',`Ostatnie 24h: ${windows.day?.[key] ?? '—'}`)), makeDiagnosticCard(lookups ? `${Math.round(hits / lookups * 100)}%`:'—','Odczyty z cache',`${hits} trafień / ${lookups} odczytów`), makeDiagnosticCard(watch.overdue || 0,'Opóźnione odświeżenia','Względem harmonogramu obserwowania'), makeDiagnosticCard(counters.errors || 0,'Zapisane błędy'), makeDiagnosticCard(`${counters.parserEscortClub || 0} / ${counters.parserEscorti || 0} / ${counters.parserGarso || 0}`, 'Problemy odczytu stron','Escort.club / Escorti.pl / Garso'), makeDiagnosticCard(counters.garsoRetries || 0,'Ponowienia Garso',`Antyflood: ${counters.antiflood || 0}`), makeDiagnosticCard(cache.error ? '—':formatCacheSize((cache.bytes || 0) / (1024 * 1024)),'Pamięć CACHE', cache.error ? 'Nie udało się odczytać stanu cache':`${cache.entries || 0} wpisów`), ...[['violentmonkey','Pamięć skryptu łącznie'],['userData','Ustawienia i dane użytkownika']].map(([key,label]) => makeDiagnosticCard(formatCacheSize((snapshot.storage?.[key]?.bytes || 0) / (1024 * 1024)),label,`${snapshot.storage?.[key]?.entries || 0} wpisów`))); renderDiagnosticBackoff(snapshot.adaptiveBackoff || {}); const cacheLines=[ cache.error ? 'Nie udało się odczytać stanu cache.' : `Rozmiar: ${formatCacheSize((cache.bytes || 0) / (1024 * 1024))} · wpisów: ${cache.entries || 0}`, cache.backend ? `Sposób zapisu: ${cache.backend === 'Violentmonkey' ? 'pamięć skryptu (tryb zastępczy)':cache.backend}`:'', `Garso: ${details.garsoEntries || 0} · Escorti.pl: ${details.escortiEntries || 0} · anonse: ${details.escortAdEntries || 0}`, details.phoneReputationEntries ? `Opinie o numerach: ${details.phoneReputationEntries} wpisów`:'' ]; const networkLines=[429,503,403].map(status => `Ostatnie HTTP ${status}${status === 403 ? ' (ban)':''}: ${formatDiagnosticTime(network[`last${status}At`])}`); const watchLines=[ `Zapisane pozycje: ${watch.tracked || 0} · profile: ${watch.profiles || 0} · anonse: ${watch.ads || 0}`, `Ulubione: ${watch.favorites || 0} · opóźnione odświeżenia: ${watch.overdue || 0} · z ostatnim błędem: ${watch.withErrors || 0}`, `Ostatni cykl automatyczny: ${formatDiagnosticTime(watch.lastScheduledRunAt)}`, `Ostatnie pełne sprawdzenie: ${formatDiagnosticTime(watch.lastScheduledFullRunAt)}`, watch.nextScheduledRunAt ? `Najbliższe odświeżenie: ${formatDiagnosticTime(watch.nextScheduledRunAt)}` :watch.enabled ? 'Brak terminu automatycznego odświeżenia.':'Automatyczne sprawdzanie wyłączone.' ]; diagnosticsDetailGrid.replaceChildren(diagnosticBackoffBox, makeDiagnosticDetailBox('Blokady Escort.club',networkLines), makeDiagnosticDetailBox('Zapisane anonse i alerty',watchLines), makeDiagnosticList('Problemy odczytu stron (parsera) - ostatnie 10',snapshot.parserIssues), makeDiagnosticDetailBox('Pamięć CACHE',cacheLines), makeDiagnosticList('Historia błędów - ostatnie 10',snapshot.errors)); }; diagnosticsRefreshBtn.addEventListener('click',event => { event.preventDefault(); loadSettingsTab('diagnostics'); }); diagnosticsExportBtn.addEventListener('click',event => { event.preventDefault(); try { downloadDiagnosticsExport(); diagnosticsStatus.textContent='Zapisano eksport diagnostyczny.'; } catch (error) { diagnosticsStatus.textContent='Nie udało się wyeksportować diagnostyki.'; log('Błąd eksportu diagnostyki',error); } }); diagnosticsClearBtn.addEventListener('click',event => { event.preventDefault(); clearDiagnostics(); loadSettingsTab('diagnostics'); }); diagnosticsActions.append(diagnosticsRefreshBtn,diagnosticsExportBtn,diagnosticsClearBtn,diagnosticsStatus); diagnosticsSection.append(diagnosticsHealth,diagnosticsOverview,diagnosticsDetailGrid,diagnosticsPrivacy,diagnosticsActions); const transferSection=makeSettingsSection( 'Eksport i import ustawień', 'Eksport bez danych prywatnych zapisuje tylko przełączniki, limity i tryby działania. Pomija lokalizacje, ulubione, notatki, historię, diagnostykę, własne teksty, linki i listy. Eksport wszystkich danych obejmuje również dane prywatne, bez cache i danych tymczasowych. Eksportowane są zapisane ustawienia — zmiany najpierw zatwierdź przyciskiem „Zapisz”.', true ); const transferRow=makeElement('div','vm-settings-inline-row vm-settings-transfer-row'); const exportSettingsBtn=makeButton("vm-settings-action-button vm-ui-button -accent",'Eksportuj wszystkie dane'); const exportShareableSettingsBtn=makeButton("vm-settings-action-button vm-ui-button -accent",'Eksportuj ustawienia bez danych prywatnych'); const importSettingsBtn=makeButton("vm-settings-action-button vm-ui-button -accent",'Importuj'); const clearSettingsDataBtn=makeButton("vm-settings-cache-button -danger vm-ui-button",'Usuń dane ustawień'); const importSettingsInput=makeElement('input','',null,{type:'file',accept:'.json,application/json'}); importSettingsInput.style.display='none'; const transferStatus=makeElement('span',"vm-settings-transfer-status vm-ui-help"); for (const [button,settingsOnly] of [[exportSettingsBtn,false],[exportShareableSettingsBtn,true]]) button.addEventListener('click',event => { stopUiEvent(event); try { downloadScriptDataExport(settingsOnly); transferStatus.textContent='Zapisano plik eksportu.'; } catch (error) { transferStatus.textContent='Nie udało się wyeksportować danych.'; log('Błąd eksportu danych ustawień',error); } }); clearSettingsDataBtn.addEventListener('click',async event => { stopUiEvent(event); const stats=getScriptDataStats(); const confirmed=window.confirm( `Usunąć ${formatPolishCount(stats.entries,'wpis','wpisy','wpisów')} danych ustawień?\n\n` + 'Zostaną usunięte ustawienia główne, zapisane filtry i lokalizacje, ulubione i obserwowane, notatki i kategorie, historia zmian, diagnostyka oraz stan interfejsu.\n\n' + 'Cache pozostanie bez zmian.' ); if (!confirmed) return; clearSettingsDataBtn.disabled=true; try { await clearScriptDataStorage(); transferStatus.textContent='Dane ustawień usunięte. Odświeżam stronę…'; location.reload(); } catch (error) { transferStatus.textContent='Nie udało się usunąć danych ustawień.'; log('Błąd usuwania danych ustawień',error); } finally { clearSettingsDataBtn.disabled=false; } }); importSettingsBtn.addEventListener('click',event => { stopUiEvent(event); importSettingsInput.click(); }); importSettingsInput.addEventListener('change',async () => { const file=importSettingsInput.files?.[0]; if (!file) return; importSettingsBtn.disabled=true; transferStatus.textContent='Sprawdzam plik…'; try { const payload=JSON.parse(await file.text()); const checked=validateScriptDataImportPayload(payload); const confirmed=window.confirm(checked.settings ? 'Zaimportować ustawienia działania skryptu? Ulubione, notatki, lokalizacje, linki i własne listy pozostaną zachowane.' : `Zaimportować ${formatPolishCount(checked.data.length,'wpis','wpisy','wpisów')} danych ustawień?\n\n` + 'Całe obecne dane ustawień zostaną usunięte i zastąpione zawartością tej kopii. Cache pozostanie bez zmian.' ); if (!confirmed) { transferStatus.textContent='Import anulowany.'; return; } await restoreScriptDataExport(payload); transferStatus.textContent='Zaimportowano. Odświeżam stronę…'; location.reload(); } catch (error) { transferStatus.textContent=error?.message || 'Nie udało się zaimportować pliku.'; log('Błąd importu danych skryptu',error); } finally { importSettingsBtn.disabled=false; importSettingsInput.value=''; } }); transferRow.append( exportShareableSettingsBtn, exportSettingsBtn, importSettingsBtn, clearSettingsDataBtn, importSettingsInput, transferStatus ); transferSection.appendChild(transferRow); const garsoSection=makeSettingsSection( 'Garsoniera', 'Niezależne opcje dla stron forum Garsoniera.com.pl', true, checks.garsoCheckEscortLinks.label, checks.garsoShowSearchIcons.label, makeElement('div','vm-settings-info -indented vm-ui-help','Kliknięcie w lupkę wstawia link lub nr tel do podręcznej wyszukiwarki i można wybrać gdzie się chce wyszukać.'), checks.garsoShowTopicHelperData.label, makeElement('div','vm-settings-info -indented vm-ui-help','Jeżeli temat oznaczony jest numerem telefonu z "xxx" to anons oraz jego notatki i kategorie nie zostaną odszukane.'), checks.garsoShowTopicTools.label, makeElement('div','vm-settings-info -indented vm-ui-help','Filtr dotyczy bieżącej strony. Historia obejmuje cały temat i korzysta ze wspólnej analizy oraz cache.'), checks.garsoShowTopicSummary.label, makeElement('div','vm-settings-info -indented vm-ui-help','Podsumowanie pojawia się w tematach z numerem telefonu w adresie. Automatyczna ponowna analiza następuje po wzroście liczby postów.') ); const settingsTabs=[ [ 'page', 'Wygląd strony', [ hidePageElementsSection, searchButtonSection, quickPreviewSection, scriptMenuSection ] ], [ 'results', 'Lista wyników', [ resultsTileAppearanceSection, resultsAggregationSection, resultsSearchToolsSection ] ], ['ad','Pojedynczy anons',[singleAdSection]], ['garso','Garsoniera',[garsoSection]], ['watch','Ulubione i zapisane anonse',[watchSection,notesSection,categoriesSection]], ['data','Dane i pamięć podręczna',[requestLimitsSection,cacheSection,transferSection]], ['diagnostics','Diagnostyka skryptu',[liveDiagnosticsSection,diagnosticsSection]] ]; const tabButtons=new Map(); const tabPanels=new Map(); const loadSettingsCache=async current => { await persistentCacheInitialization; if (!current()) return false; await ensureFullPersistentCache(); return current(); }; const loadSettingsTab=createDeferredSettingsTabLoader(overlay,tabPanels,{ page:() => menuPagesEditor.mount(), ad:async current => { imageSearchEditor.mount(); phoneReputationEditor.mount(); if (!tagHighlightRendered && await loadSettingsCache(current)) renderTagHighlightGroups(); }, watch:() => { syncClearNotesAndCategoriesButtons(); syncClearAllFavoritesButton(); clearWatchHistoryBtn.disabled=!getWatchEvents().length; }, data:async current => { if (!await loadSettingsCache(current)) return; refreshCacheSizeLabel(); refreshTechnicalDataSizeLabel(); clearCacheBtn.disabled=clearExpiredCacheBtn.disabled=clearTechnicalDataBtn.disabled=false; }, diagnostics:async current => { clearInterval(diagnosticsStatusTimer); diagnosticsStatusTimer=setInterval(() => { if (!document.hidden && current()) renderDiagnosticBackoff(); },1000); diagnosticsRefreshBtn.disabled=true; try { if (!await loadSettingsCache(current)) return; renderDiagnostics(); diagnosticsExportBtn.disabled=diagnosticsClearBtn.disabled=false; diagnosticsStatus.textContent=''; } finally {diagnosticsRefreshBtn.disabled=false;} } }); const activateSettingsTab=tabId => { for (const [id,button] of tabButtons) { const active=id === tabId; button.classList.toggle('-active',active); button.setAttribute('aria-selected',active ? 'true':'false'); tabPanels.get(id)?.classList.toggle('-active',active); } contentGrid.scrollTop=0; clearInterval(diagnosticsStatusTimer); loadSettingsTab(tabId); }; for (const [id,label,sections] of settingsTabs) { const button=makeButton('vm-settings-tab-button',label); button.dataset.vmSettingsTab=id; button.setAttribute('role','tab'); button.addEventListener('click',() => activateSettingsTab(id)); const panel=makeElement('div','vm-settings-tab-panel'); panel.setAttribute('role','tabpanel'); panel.append(...sections); tabButtons.set(id,button); tabPanels.set(id,panel); tabBar.appendChild(button); contentGrid.appendChild(panel); } overlay._vmActivateTab=tabId => { if (tabButtons.has(tabId)) activateSettingsTab(tabId); }; activateSettingsTab(tabButtons.has(requestedTab) ? requestedTab:'page'); } function saveHistoryCleanupKeywords(value) { const keywords=normalizeHistoryCleanupKeywords(value); updateSettings({ historyCleanupKeywords:keywords }); return keywords; } function openBrowserHistorySearchTabs(sourceKeywords=null) { const keywords=normalizeHistoryCleanupKeywords( sourceKeywords ?? getSettings().historyCleanupKeywords ); if (!keywords.length) { window.alert('Lista słów kluczowych jest pusta. Uzupełnij ją w tym oknie.'); return; } keywords.forEach((keyword,index) => { const historyUrl= `edge://history/all?q=${encodeURIComponent(keyword)}`; GM_openInTab(historyUrl,{ active:index === keywords.length - 1 }); }); } function openBrowserHistoryCleanupModal() { const overlayId='vm-garso-history-cleanup-overlay'; if (document.getElementById(overlayId)) return; const overlay=makeElement('div'); overlay.id=overlayId; prepareHelperStyleApi(overlay); overlay.classList.add('vm-ui-overlay'); const closeController=createDialogCloseController(overlay); const box=makeElement('div',"vm-ui-dialog"); Object.assign(box.style,{width:'min(570px, calc(100vw - 32px))'}); const titleRow=makeElement('div',"vm-ui-header"); const title=makeElement('div',"vm-ui-title",'Historia przeglądarki'); const close=makeButton("vm-ui-button vm-ui-icon",'×'); close.addEventListener( 'click', () => closeController.close('button') ); titleRow.append(title,close); const keywordsLabel=makeElement('label',"vm-ui-label",'Frazy do wyszukania w historii przeglądarki:'); const keywordsInput=makeElement('textarea',"vm-ui-field vm-ui-textarea",null,{value:normalizeHistoryCleanupKeywords( getSettings().historyCleanupKeywords ).join('\n'),rows:8,placeholder:'Każde słowo kluczowe w osobnym wierszu'}); Object.assign(keywordsInput.style,{minHeight:'145px'}); const info=makeElement('div',"vm-ui-help",'Każdy wpis otworzy osobną kartę historii Edge. Na każdej karcie użyj Ctrl+A, a następnie przycisku „Usuń”.'); Object.assign(info.style,{marginTop:'7px'}); const actions=makeElement('div',"vm-ui-actions"); const save=makeButton('','Zapisz'); const open=makeButton(); for (const button of [save,open]) { button.classList.add('vm-ui-button'); } save.classList.add('-accent'); open.classList.add('-primary'); const updateOpenLabel=() => { const count=normalizeHistoryCleanupKeywords(keywordsInput.value).length; open.textContent=count === 1 ? 'Otwórz 1 kartę' :`Otwórz ${count} kart`; open.disabled=count === 0; open.style.opacity=count === 0 ? '.5':'1'; }; keywordsInput.addEventListener('input',updateOpenLabel); updateOpenLabel(); save.addEventListener('click',() => { saveHistoryCleanupKeywords(keywordsInput.value); closeController.close('button'); }); open.addEventListener('click',() => { const keywords=saveHistoryCleanupKeywords(keywordsInput.value); if (!keywords.length) return; closeController.close('button'); openBrowserHistorySearchTabs(keywords); }); actions.append(save,open); box.append(titleRow,keywordsLabel,keywordsInput,info,actions); overlay.appendChild(box); document.body.appendChild(overlay); } const vmMenuIcon=glyph => `data:image/svg+xml;charset=utf-8,${encodeURIComponent( typeof glyph === 'string' ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><text x="8" y="12.3" text-anchor="middle" font-family="Arial,sans-serif" font-size="13" fill="%23333">${glyph}</text></svg>` :new XMLSerializer().serializeToString(glyph.firstElementChild).replace(/currentColor/g,'#777') )}`; const HELPER_MENU_ID= 'vm-escort-helper-menu'; const HELPER_MENU_PANEL_ID= 'vm-escort-helper-menu-panel'; function detachHelperNode(node) { if (node?.parentNode) node.parentNode.removeChild(node); } function attachWatchCheckProgress(status,isLocalCheck) { const shared=makeElement('div',status.className); shared.setAttribute('role','status'); status.after(shared); let listener,closed=false; const update=() => { if (closed) return; const lock=GM_getValue(WATCH_CHECK_LOCK_KEY,null),progress=lock?.progress; const active=lock?.owner && lock.expiresAt > Date.now() && progress?.total > 0 && !isLocalCheck(); const text=active ? `${progress.checked || 0}/${progress.total} pozycji${progress.adTotal ? ` • anonse ${progress.adChecked || 0}/${progress.adTotal}`:''}`:''; if (shared.textContent !== text) shared.textContent=text; shared.hidden=!active; shared.style.display=active ? 'block':'none'; }; try {listener=GM_addValueChangeListener(WATCH_CHECK_LOCK_KEY,update);} catch {} const timer=setInterval(update,1000); update(); return () => { closed=true; clearInterval(timer); if (listener != null) try {GM_removeValueChangeListener(listener);} catch {} detachHelperNode(shared); }; } function readFavoriteViewPreferences(key,defaults={}) { try { const saved=GM_getValue(key,null); if (saved && typeof saved === 'object' && !Array.isArray(saved)) return {...defaults,...saved}; } catch {} return {...defaults}; } function saveFavoriteViewPreferences(key,patch) { const saved=readFavoriteViewPreferences(key); if (Object.keys(patch).some(name=>JSON.stringify(saved[name])!==JSON.stringify(patch[name]))) writeHelperStorageValue(key,{...saved,...patch}); } function switchFavoriteView(view) { if (view === 'table') return openWatchedSettingsModal(false,true); const options=getFavoriteTilesOptions(); return openFavoriteTiles(options.fields,options.extra); } function createFavoriteViewData(events=[]) { const histories=new Map(),entries=new Map(),now=new Date(); for (const event of events) { if (!histories.has(event.itemId)) histories.set(event.itemId,[]); histories.get(event.itemId).push(event); } return item => { if (entries.has(item)) return entries.get(item); const values=new Map(),history=histories.get(item.id) || []; const read=(key,load) => { if (!values.has(key)) values.set(key,load()); return values.get(key); }; const entry={ events:history, stateRank:item.state?.online === true ? 0:item.state?.online === false ? 1:2, get unread() {return read('unread',() => history.filter(event => event.alertTriggered && !event.acknowledged));}, get latestChange() {return read('latest',() => history.reduce((latest,event) => Math.max(latest,Number(event.timestamp) || 0),0));}, get active() {return read('active',() => getWatchedItemAdUrls(item,true));}, get all() {return read('all',() => getWatchedItemAdUrls(item,false));}, get details() {return read('details',() => getWatchedItemCachedAdDetails(item,true));}, get allDetails() {return read('allDetails',() => getWatchedItemCachedAdDetails(item,false));}, get escorti() {return read('escorti',() => getCachedEscortiResultsForFavoriteItem(item));}, get cities() {return read('cities',() => getFavoriteLocationCities(item,entry));}, get lastVisited() {return read('visited',() => getFavoriteLastVisitedAt(item));}, get availability() {return read('availability',() => getFavoriteAvailabilityNow(item,now,entry.details));}, get availabilityTooltip() {return read('hours',() => getFavoriteAvailabilityTodayTooltip(item,now,entry.details));}, column(field) {return read('column:'+field,() => formatFavoriteColumnValue(item,field,entry));}, sortValue(field) {return read('sort:'+field,() => favoriteDataSortValue(item,field,entry.column(field),entry));} }; entries.set(item,entry); return entry; }; } function createFavoriteListRenderer(host) { const rows=new Map(); return { get(id,signature) {const saved=rows.get(id);return saved?.signature===signature ? saved.node:null;}, set(id,signature,node) {rows.set(id,{signature,node});return node;}, render(nodes) { const wanted=new Set(nodes); for (const node of [...host.childNodes]) if (!wanted.has(node)) detachHelperNode(node); let cursor=host.firstChild; for (const node of nodes) { if (node===cursor) cursor=cursor.nextSibling; else host.insertBefore(node,cursor); } for (const [id,saved] of rows) if (!wanted.has(saved.node)) rows.delete(id); }, clear() {rows.clear();} }; } function fillFavoriteCategoryControls(host,item,settings,onChange,{pointerDown=false,disabled=false,decorate=null}={}) { const assigned=normalizeFavoriteAssignedCategories(item.categories); assigned.forEach((value,index) => { const chip=createFavoriteCategoryChip(value,settings.categoryColors[value] || '#b8b8bd', () => onChange(current => current.filter(entry => entry !== value))); const remove=chip.querySelector('button'); remove.disabled=disabled; decorate?.(remove,`category-${index}`); host.appendChild(chip); }); const available=settings.categories.filter(value => !assigned.includes(value)); if (available.length) { const add=createFavoriteCategoryAddButton(anchor => openEscortInlineCategoryPicker(anchor,available,settings, value => onChange(current => [...current,value]),String(item.id || '')),pointerDown); add.disabled=disabled; add.setAttribute('aria-label','Dodaj kategorię'); add.setAttribute('aria-haspopup','menu'); decorate?.(add,'categories'); host.appendChild(add); } return assigned; } const FAVORITE_TILES_PARAM='vm_favorite_tiles',FAVORITE_TILES_FIELDS_PARAM='vm_favorite_tiles_fields',FAVORITE_TILES_EXTRA_PARAM='vm_favorite_tiles_extra'; const FAVORITE_TILES_DEFAULT='image,profile,city,age,price,online,alerts,visited,note,categories,aggregate'; const FAVORITE_TILES_FIELDS=new Set((FAVORITE_TILES_DEFAULT+',height,weight,breasts').split(',')); const FAVORITE_TILES_UI_KEY='vm_favorite_tiles_ui_v1'; let favoriteTilesOpenGeneration=0; function getFavoritePageMode(){return IS_ESCORT_CLUB?new URL(location.href).searchParams.get(FAVORITE_TILES_PARAM):null;} function getFavoriteTilesPreferences(){return readFavoriteViewPreferences(FAVORITE_TILES_UI_KEY);} function saveFavoriteTilesPreferences(patch){saveFavoriteViewPreferences(FAVORITE_TILES_UI_KEY,patch);} function getFavoriteTilesOptions(url=location.href){ const q=new URL(url,location.href).searchParams,saved=getFavoriteTilesPreferences(); // Zapisane wybory, także puste, mają pierwszeństwo przed starym URL. const fields=new Set((Array.isArray(saved.fields)?saved.fields:(q.has(FAVORITE_TILES_FIELDS_PARAM)?q.get(FAVORITE_TILES_FIELDS_PARAM):FAVORITE_TILES_DEFAULT).split(',')).filter(v=>FAVORITE_TILES_FIELDS.has(v))); return {fields,extra:normalizeEscortDataFieldList(Array.isArray(saved.extra)?saved.extra:(q.get(FAVORITE_TILES_EXTRA_PARAM)||'').split(','),FAVORITES_MAX_DATA_COLUMNS)}; } function openFavoriteTiles(fields,extra=[]){ saveFavoriteTilesPreferences({fields:[...fields].filter(v=>FAVORITE_TILES_FIELDS.has(v)),extra:normalizeEscortDataFieldList(extra,FAVORITES_MAX_DATA_COLUMNS)}); closeHelperDialog(document.getElementById(WATCH_SETTINGS_OVERLAY_ID)); const host=document.getElementById('vm-favorite-tiles-test'); if(host?._vmApplyOptions)host._vmApplyOptions(getFavoriteTilesOptions()); else return renderFavoriteTilesView().catch(error=>{log('Błąd otwierania kafelków',error);showWatchToast('Nie udało się otworzyć kafelków.',true);}); } function getFavoriteTilesData(item,view=null){ const active=view?.active || getWatchedItemAdUrls(item,true),all=view?.all || getWatchedItemAdUrls(item,false),url=active[0]||all[0]||'',adId=parseAdIdFromUrl(url); const details=[...new Map((item.sourceType==='escort-ad'?getWatchedItemCachedAdDetails({adUrl:item.adUrl},true):view?.details || getWatchedItemCachedAdDetails(item,true)).map(detail=>[detail.adId,detail])).values()]; const data=details.find(detail=>detail.adId===adId)?.adData||(adId?(getStoredEscortAdData(adId)||getWatchedAdDataMirror(adId)):null); return {active,all,url,adId,data,details,activeIds:new Set(active.map(parseAdIdFromUrl))}; } function getFavoriteTileStat(data,label,unit=''){const value=normalizeEscortAdText(getEscortObjectEntryByNormalizedKey(data?.stats||{},label)?.[1]);return value&&unit&&/^\d+(?:[.,]\d+)?$/.test(value)?`${value} ${unit}`:value||'';} function getFavoriteTileBreastsText(data){ const shortType=value=>value.replace(/\b(naturaln[yae]|sztuczn[yae])\b/gi,word=>/^n/i.test(word)?'N':'S'); let breasts=shortType(getFavoriteTileStat(data,'biust')); const type=shortType(getFavoriteTileStat(data,'typ biustu')||getFavoriteTileStat(data,'rodzaj biustu')); if(breasts&&type&&!normalizeText(breasts).split(/[\s,;()/]+/).includes(normalizeText(type)))breasts+=` ${type}`; return breasts; } function getFavoriteTileDimensionsText(data){ const height=getFavoriteTileStat(data,'wzrost','cm'),weight=getFavoriteTileStat(data,'waga','kg'),breasts=getFavoriteTileBreastsText(data); return [height,weight,breasts?`biust ${breasts}`:''].filter(Boolean).join(', '); } function getFavoriteTileProfileFacts(item,entry,duration,view=null){ // Aktywne anonse profilu; przy braku ich listy ostatnie zapisane dane. const details=entry.details,profile=item.sourceType!=='escort-ad',active=profile&&entry.active.length>0; const unique=values=>[...new Map(values.map(normalizeEscortAdText).filter(Boolean).map(value=>[normalizeEscortChangeComparisonValue(value),value])).values()] .sort((a,b)=>a.localeCompare(b,'pl-PL',{numeric:true,sensitivity:'base'})); const range=(values,unit)=>{ const list=unique(values);if(!list.length)return ''; const suffix=unit==='lat'?/^(?:lat|lata|l\.?|rok|r\.?|y|years?)?$/i:new RegExp(`^(?:${unit})?$`,'i'); const numbers=list.map(value=>{const match=value.match(/^(\d+(?:[.,]\d+)?)\s*(.*)$/);return match&&suffix.test(match[2])?Number(match[1].replace(',','.')):NaN;}); if(!numbers.every(Number.isFinite))return list.join(' / '); const min=Math.min(...numbers),max=Math.max(...numbers),format=value=>value.toLocaleString('pl-PL',{maximumFractionDigits:2}); return `${format(min)}${min===max?'':`–${format(max)}`} ${unit}`; }; const stat=(label,unit)=>range(details.map(({adData})=>getFavoriteTileStat(adData,label)),unit); const age=stat('wiek','lat'),height=stat('wzrost','cm'),weight=stat('waga','kg'),breasts=unique(details.map(({adData})=>getFavoriteTileBreastsText(adData))).join(' / '); const dimensions=[height,weight,breasts?`biust ${breasts}`:''].filter(Boolean).join(', '),priceGroups=new Map(); for(const {adData} of details){ const price=adData.prices?.[duration];if(!formatEscortTilePrice(price))continue; const currency=String(price.currency||'').toUpperCase();if(!priceGroups.has(currency))priceGroups.set(currency,new Set()); priceGroups.get(currency).add(Number(price.amount)); } const price=[...priceGroups].sort(([a],[b])=>a.localeCompare(b)).map(([currency,values])=>{ const min=Math.min(...values),max=Math.max(...values); return min===max?formatEscortTilePrice({amount:min,currency}):`${formatEscortTilePrice({amount:min})}–${formatEscortTilePrice({amount:max,currency})}`; }).join(' / '); const savedCities=profile?unique((item.state?.cities||[]).map(normalizeEscortCity)):[],cachedCities=details.map(({adData})=>normalizeEscortCity(adData.location?.city)); // Pełny stan miast ma pierwszeństwo przed cache anonsów. const cities=savedCities.length&&!item.state?.locationPartial?savedCities:unique([...savedCities,...(profile&&!item.state?.cities?.length?(view?.cities || getFavoriteLocationCities(item)):cachedCities)]); const expected=profile?(active?entry.active.length:entry.all.length):(item.adUrl?1:0); const basis=`${profile?(active?'Aktywne anonse':'Ostatnie zapisane dane'):'Anons'} · cache ${details.length}/${expected}${item.state?.partial||item.state?.locationPartial?' · niepełne dane':''}`; return {age,height,weight,breasts,dimensions,price,cities,basis}; } async function renderFavoriteTilesView(generation=++favoriteTilesOpenGeneration){ await persistentCacheInitialization; await ensureFullPersistentCache(); if(generation!==favoriteTilesOpenGeneration)return false; if(document.getElementById('vm-favorite-tiles-test'))return true; if(document.readyState==='loading')await new Promise(resolve=>document.addEventListener('DOMContentLoaded',resolve,{once:true})); if(generation!==favoriteTilesOpenGeneration)return false; if(document.getElementById('vm-favorite-tiles-test'))return true; const returnFocus=document.activeElement,pageScroll={left:scrollX,top:scrollY,behavior:'instant'}; document.documentElement.classList.add('vm-favorite-tiles-test-mode');document.body.classList.add('vm-favorite-tiles-test-page'); let {fields,extra}=getFavoriteTilesOptions(); const host=prepareHelperStyleApi(makeElement('section','vm-favorite-tiles-test',null,{id:'vm-favorite-tiles-test'})); host.setAttribute('aria-label','Ulubione — widok kafelkowy');host.setAttribute('role','dialog');host.setAttribute('aria-modal','true'); const updateTop=() => { const bar=document.getElementById(WATCH_BAR_ID),menu=document.getElementById(HELPER_MENU_ID); const bottom=Math.max(0,...[bar,menu].filter(node => node && !node.hidden).map(node => node.getBoundingClientRect().bottom)); host.style.setProperty('--vm-favorite-top-offset',`${Math.ceil(bottom)}px`); }; const topObserver=new ResizeObserver(updateTop); for (const node of [document.body,document.getElementById(WATCH_BAR_ID),document.getElementById(HELPER_MENU_ID)].filter(Boolean)) topObserver.observe(node); updateTop(); const lifetime=new AbortController(),{signal}=lifetime,storageListeners=[]; window.addEventListener('resize',updateTop,{signal}); signal.addEventListener('abort',() => topObserver.disconnect(),{once:true}); // Skrót #vft rozwijamy do dotychczasowego ID; specyficzność CSS nie zmienia się. const style=makeElement('style');style.textContent=` html.vm-favorite-tiles-test-mode,body.vm-favorite-tiles-test-page{overflow:hidden!important} #vft{position:fixed;z-index:2147483500;inset:var(--vm-favorite-top-offset,0px) 0 0;margin:0!important;padding:0!important;overflow:auto;overscroll-behavior:contain;background:var(--vm-ui-bg-alt,#f4f5f8);color:var(--vm-ui-text,#292c35);font:400 13px/1.45 var(--vm-ui-font-family,Arial,sans-serif);text-align:left;isolation:isolate} #vft::before{content:'';position:fixed;z-index:11;inset:0 0 auto;height:var(--vm-favorite-top-offset,0px);background:var(--vm-ui-bg,#fff);pointer-events:auto} #vft *,#vft *::before,#vft *::after{box-sizing:border-box} #vft [hidden]{display:none!important} #vft a{color:inherit;text-decoration:none} #vft a:hover{text-decoration:underline} #vft button,#vft input,#vft select,#vft summary{font-family:inherit} #vft :is(button,a,input,select,summary):focus-visible{outline:2px solid var(--vm-ui-accent,#ce3986);outline-offset:2px} #vft .vm-ui-button,#vft .vm-ui-field{min-height:28px;height:28px;font-size:11px;line-height:1.2;border-radius:5px;padding:3px 7px} #vft select.vm-ui-field{padding-right:22px} #vft .vft-toolbar{margin:0!important;padding:0!important;position:sticky;top:0;z-index:10;background:var(--vm-ui-bg,#fff);border-bottom:1px solid var(--vm-ui-border,#e1e3e9);box-shadow:0 2px 6px #00000006} #vft .vft-toolbar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:1660px;margin:0 auto;padding:5px 20px} #vft .vft-toolbar .vm-ui-button,#vft .vft-toolbar .vm-ui-field{height:30px;min-height:30px;min-width:0;margin:0;padding:4px 8px;font-size:11px;line-height:1.3;border-radius:6px} #vft .vft-toolbar select.vm-ui-field{padding-right:24px} #vft .vft-heading{display:flex;flex:none;align-items:center;gap:6px;white-space:nowrap} #vft .vft-title{margin:0;font-size:14px;line-height:1.2;font-weight:750;color:inherit} #vft .vft-count{font-size:10px;line-height:1.2;padding:3px 5px;border-radius:5px;background:var(--vm-ui-bg-alt,#eef0f4);color:var(--vm-ui-text-muted,#697080);font-variant-numeric:tabular-nums} #vft .vft-search{position:relative;display:block;flex:1 1 220px;min-width:80px;margin:0} #vft .vft-search>.vft-icon{position:absolute;top:7px;left:9px;color:var(--vm-ui-text-muted,#697080);pointer-events:none} #vft .vft-search>.vm-ui-field{width:100%;padding-left:30px;padding-right:30px;background:var(--vm-ui-input-bg,#fff)} #vft .vft-search input::-webkit-search-cancel-button{display:none} #vft .vft-search-clear{position:absolute;right:3px;top:3px;display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin:0;padding:0;border:0;border-radius:4px;background:transparent;color:var(--vm-ui-text-muted,#697080);font-size:18px;line-height:1;cursor:pointer} #vft .vft-search-clear:hover{background:var(--vm-ui-bg-hover,#e9ecf1);color:var(--vm-ui-text,#292c35)} #vft .vft-toolbar-controls{display:flex;flex:none;align-items:center;gap:6px;min-width:0} #vft .vft-icon{display:block;flex:none;width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} #vft .vft-toolbar :is(.vft-tool-toggle,.vft-toolbar-action){display:inline-flex;align-items:center;justify-content:center;gap:5px;flex:none;white-space:nowrap;cursor:pointer} #vft .vft-toolbar .vft-close{flex:0 0 28px;width:28px;padding:0;border-color:transparent;background:transparent;color:var(--vm-ui-text-muted,#697080)} #vft .vft-tool{flex:none} #vft .vft-tool-toggle[aria-expanded=true],#vft .vft-filter-tool[data-active=true]>.vft-tool-toggle{border-color:var(--vm-ui-accent,#ce3986);background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 9%,var(--vm-ui-bg,#fff));color:var(--vm-ui-text,#292c35)} #vft .vft-filter-count{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 3px;border-radius:4px;background:var(--vm-ui-accent,#ce3986);color:var(--vm-ui-accent-contrast,#fff);font-size:10px} #vft .vft-sort{display:flex;align-items:center;gap:5px;flex:0 1 200px;min-width:0;width:200px;margin:0;color:var(--vm-ui-text-muted,#697080);font-size:10px;white-space:nowrap} #vft .vft-sort>.vm-ui-field{flex:1;min-width:0;width:100%;text-overflow:ellipsis;color:var(--vm-ui-text,#292c35)} #vft .vft-tool-panel{position:fixed;z-index:30;width:344px;max-width:calc(100vw - 16px);max-height:420px;overflow:auto;overscroll-behavior:contain;padding:10px;border:1px solid var(--vm-ui-border,#dfe2e9);border-radius:9px;background:var(--vm-ui-bg,#fff);color:var(--vm-ui-text,#292c35);box-shadow:var(--vm-ui-shadow,0 8px 24px #0003);text-align:left} #vft .vft-panel-head,#vft .vft-panel-footer{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0} #vft .vft-panel-head{margin-bottom:8px;font-size:12px;font-weight:700} #vft .vft-panel-result{color:var(--vm-ui-text-muted,#697080);font-size:10px;font-weight:400;text-align:right} #vft .vft-panel-footer{margin-top:10px;padding-top:8px;border-top:1px solid var(--vm-ui-border,#e1e3e9)} #vft .vft-filters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} #vft .vft-filters>.vft-field:first-child{grid-column:1/-1} #vft .vft-field{display:flex;flex-direction:column;gap:3px;min-width:0;margin:0;color:var(--vm-ui-text-muted,#697080);font-size:10px} #vft .vft-field>.vm-ui-field{width:100%;font-weight:400;color:var(--vm-ui-text,#292c35)} #vft .vft-filter-group{display:flex;flex:none;align-items:center;gap:4px;min-width:0;padding:2px;border:1px solid var(--vm-ui-border,#dfe2e9);border-radius:8px;background:var(--vm-ui-bg-alt,#f4f5f8)} #vft .vft-filter-group .vft-tool-toggle{height:28px;min-height:28px;border-color:transparent;background:transparent} #vft .vft-quick{display:flex;align-items:center;flex-wrap:nowrap;gap:3px;min-width:0;margin:0;padding-left:4px;border-left:1px solid var(--vm-ui-border,#dfe2e9)} #vft .vft-chip{min-height:28px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--vm-ui-text-muted,#697080);padding:4px 6px;font-family:inherit;font-size:11px;font-weight:600;line-height:1.3;cursor:pointer;white-space:nowrap} #vft .vft-chip:hover{background:var(--vm-ui-bg-hover,#e9ecf1);color:var(--vm-ui-text,#292c35)} #vft .vft-chip[aria-pressed=true]{background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 13%,var(--vm-ui-bg,#fff));border-color:var(--vm-ui-accent,#ce3986);color:var(--vm-ui-text,#292c35)} #vft .vft-view-tools{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} #vft .vft-extra-controls{grid-column:1/-1;min-width:0;padding-top:8px;border-top:1px solid var(--vm-ui-border,#e1e3e9)} #vft .vft-extra-controls>.vft-panel-head{margin-bottom:5px;font-size:11px} #vft .vft-extra-controls>.vm-ui-field{width:100%;max-width:100%;margin-bottom:5px} #vft .vft-field-chips{display:grid;gap:4px;min-width:0} #vft .vft-field-chip{display:flex;align-items:center;gap:5px;min-width:0;padding:1px 3px 1px 7px;border:1px solid var(--vm-ui-border,#dfe2e9);border-radius:5px;background:var(--vm-ui-bg-alt,#f4f5f8);font-size:11px} #vft .vft-field-chip>span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #vft .vft-field-chip>button{flex:none;width:24px;min-height:24px;padding:0;border:0;background:transparent;color:var(--vm-ui-text-muted,#697080);font-size:17px;cursor:pointer} @media(max-width:1400px){#vft .vft-toolbar-inner{padding-inline:12px;gap:6px}#vft .vft-toolbar-action>.vft-action-label{display:none}#vft .vft-sort{width:176px;flex-basis:176px}#vft .vft-toolbar .vft-toolbar-action{width:30px;padding:0}} @media(max-width:1100px){#vft .vft-toolbar-inner{flex-wrap:wrap;gap:4px 6px;padding:5px 10px}#vft .vft-search{flex-basis:100px;min-width:60px}#vft .vft-toolbar-controls{order:1;flex:1 0 100%;width:100%;gap:5px}#vft .vft-sort{flex:1 1 0;width:auto}#vft .vft-sort>span{display:none}#vft .vft-toolbar .vft-tool-toggle{padding-inline:6px}#vft .vft-tool-toggle>.vft-caret{display:none}} @media(max-width:760px){#vft .vft-toolbar-controls{flex-wrap:wrap}#vft .vft-filter-group{flex:1 0 100%;width:100%}#vft .vft-quick{flex:1;flex-wrap:wrap}#vft .vft-chip{flex:1;padding-inline:5px}#vft .vft-filter-group .vft-tool-toggle{padding-inline:6px}} @media(max-width:480px){#vft .vft-filter-group{gap:3px}#vft .vft-filter-group .vft-tool-toggle{position:relative;padding-inline:5px}#vft .vft-filter-group .vft-tool-toggle>.vft-icon{display:none}#vft .vft-filter-count{position:absolute;right:-3px;top:-6px;font-size:9px;height:14px;min-width:14px}#vft .vft-quick{gap:2px;padding-left:3px}#vft .vft-chip{font-size:10px;padding-inline:3px}} #vft .vft-content{max-width:1660px;margin:0 auto;padding:14px 20px} #vft .vft-status{margin-bottom:12px;padding:7px 10px;border:1px solid var(--vm-ui-border,#ddd);border-radius:6px;background:var(--vm-ui-bg,#fff);font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere} #vft .vft-status[data-error=true]{color:var(--vm-ui-danger,#b42335)} #vft .vft-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(280px,100%),1fr));gap:14px;align-items:start} #vft[data-density=compact] .vft-grid{grid-template-columns:repeat(auto-fill,minmax(min(225px,100%),1fr));gap:10px} #vft .vft-card{position:relative;min-width:0;border:1px solid var(--vm-ui-border,#dfe2e9);border-radius:10px;background:var(--vm-ui-bg,#fff);box-shadow:0 2px 6px #18263b07} #vft .vft-card:has(.vft-menu[open]){z-index:3} #vft .vft-card[data-unread=true]{border-color:var(--vm-ui-accent,#ce3986)} #vft .vft-hero{position:relative;display:flex;flex-direction:column;justify-content:space-between;min-height:330px;border-radius:9px 9px 0 0;background:#273242;color:#fff} #vft[data-density=compact] .vft-hero{min-height:295px} #vft .vft-hero.-text-only{min-height:0;background:linear-gradient(145deg,#354256,#202a39)} #vft .vft-hero:last-child,#vft .vft-hero:last-child .vft-media{border-radius:9px} #vft .vft-media{position:absolute;inset:0;z-index:0;border-radius:inherit;overflow:hidden} #vft .vft-media::after{content:'';position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,rgba(6,12,20,0) 52%,rgba(6,12,20,.08) 66%,rgba(6,12,20,.28) 82%,rgba(6,12,20,.62) 100%);pointer-events:none} #vft .vft-media:has(.vft-placeholder)::after{display:none} #vft .vft-photo-link,#vft .vft-img{display:block;width:100%;height:100%} #vft .vft-photo-link{position:relative} #vft .vft-img{object-fit:cover;object-position:center 25%} #vft .vft-placeholder{display:flex;flex-direction:column;gap:5px;align-items:center;justify-content:center;width:100%;height:100%;background:var(--vm-ui-bg-alt,#edf0f4);color:var(--vm-ui-text-muted,#697080);font-size:11px} #vft .vft-media .vft-placeholder{justify-content:flex-start;padding-top:58px;background:linear-gradient(145deg,#3b4a61,#202a39);color:#dbe3ee} #vft .vft-initials{font-size:36px;font-weight:700;opacity:.5;line-height:1.1} #vft .vft-photo-top{position:relative;z-index:2;display:flex;flex-shrink:0;gap:6px;align-items:flex-start;justify-content:space-between;padding:9px;border-radius:inherit;background:transparent;pointer-events:none} #vft .vft-photo-top button,#vft .vft-photo-top details{pointer-events:auto} #vft .vft-badges{display:flex;gap:4px;align-items:flex-start;flex-wrap:wrap;min-width:0} #vft .vft-badge{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:3px 6px;border:1px solid #ffffff28;border-radius:5px;background:#17202ddd;color:#fff;font-size:10px;font-weight:600;line-height:1.4;overflow-wrap:anywhere} #vft .vft-badge.on{background:#16513eeb;color:#deffeb;border-color:#8df5b84d} #vft .vft-badge.new{background:#7f2054ed;color:#fff;border:2px solid #ff3b30;padding:2px 5px;cursor:pointer} #vft .vft-status-mark{display:inline-block;flex:none;min-width:1em;text-align:center;font:800 14px/1 Arial,sans-serif;vertical-align:-1px;text-shadow:none} #vft .vft-status-mark.on{color:var(--vm-ui-success,#187747)} #vft .vft-status-mark.off{color:var(--vm-ui-danger,#b42335)} #vft .vft-status-mark.unknown{color:var(--vm-ui-text-muted,#697080)} #vft .vft-status-outside{color:var(--vm-ui-warning,#96631d);font-weight:600} #vft .vft-badge .vft-status-mark.on{color:#8df5b8} #vft .vft-badge .vft-status-mark.off{color:#ff9292} #vft .vft-badge .vft-status-mark.unknown{color:#cfd7e3} #vft .vft-photo-actions{display:flex;flex:none;gap:4px;align-items:center} #vft .vft-photo-action,#vft .vft-menu>summary{display:flex;align-items:center;justify-content:center;list-style:none;width:28px;height:28px;min-height:28px;padding:0;border:1px solid #ffffff40;border-radius:5px;background:#17202dc9;color:#fff;font-size:18px;line-height:1;cursor:pointer} #vft .vft-photo-action:hover,#vft .vft-menu>summary:hover{background:#17202df2} #vft .vft-photo-action:disabled{opacity:.55;cursor:wait} #vft .vft-photo-content{position:relative;z-index:1;flex-shrink:0;margin-top:auto;padding:8px 11px 9px;border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;background:transparent;text-shadow:0 1px 2px #000e,0 0 4px #0009;pointer-events:none} #vft .vft-photo-content a,#vft .vft-photo-content button{pointer-events:auto;text-shadow:inherit} #vft .vft-photo-content :is(.vft-name,.vft-source,.vft-city,.vft-price){padding:0;border-radius:0;background:transparent} #vft .vft-photo-content :is(.vft-name,.vft-source){width:fit-content;max-width:100%} #vft .vft-hero.-text-only .vft-photo-top{padding-bottom:8px} #vft .vft-hero.-text-only .vft-photo-content{padding-top:6px;background:transparent} #vft .vft-name,#vft .vft-city,#vft .vft-dimensions,#vft .vft-clamp{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden} #vft .vft-name{margin:0;font-size:17px;line-height:1.25;font-weight:700;overflow-wrap:anywhere;color:var(--vft-title-color,#ffb3d5)!important;text-shadow:inherit} #vft .vft-name>a{color:inherit!important} #vft :is(.vft-name,.vft-name>a,.vft-source){-webkit-text-fill-color:currentColor!important} #vft .vft-source{display:block;margin-top:3px;color:var(--vft-source-color,#a6dcff)!important;font-size:10px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #vft .vft-dimensions{margin-top:3px;font-size:11px;line-height:1.35;color:#f3f5f8;overflow-wrap:anywhere;} #vft :is(.vft-city,.vft-price,.vft-dimensions){pointer-events:auto} #vft .vft-keyfacts{display:flex;gap:8px;justify-content:space-between;align-items:baseline;margin-top:5px} #vft .vft-city{font-weight:600;font-size:12px;min-width:0;overflow-wrap:anywhere} #vft .vft-price{margin-left:auto;text-align:right;flex:0 0 auto;max-width:58%;font-size:17px;font-weight:750;line-height:1.25;overflow-wrap:anywhere} #vft .vft-price-period{font-size:10px;font-weight:400;color:#dbe1eb;white-space:nowrap} #vft .vft-caption{display:block;color:var(--vm-ui-text-muted,#697080);font-size:11px;font-weight:400;line-height:1.4} #vft .vft-clamp{white-space:pre-wrap} #vft .vft-body{padding:0;display:grid;grid-template-columns:minmax(0,1fr);gap:0;min-width:0;text-align:left} #vft .vft-personal{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;justify-items:stretch;gap:2px;min-width:0;width:100%;padding:4px 10px;text-align:left;background:var(--vm-ui-bg,#fff)} #vft .vft-bottom-row{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:8px;align-items:start;width:100%;min-width:0} #vft .vft-cats{grid-column:1;display:flex;gap:4px;align-items:center;align-content:flex-start;justify-content:flex-start;flex-wrap:wrap;min-width:0;min-height:26px} #vft .vft-cats .vm-favorites-category-chip{max-width:100%;height:20px;font-size:9px;line-height:18px} #vft .vft-cats .vm-favorites-category-add-button{width:20px;min-width:20px;height:20px;flex:0 0 20px} #vft .vft-cats:focus-within :is(.vm-favorites-category-remove,.vm-favorites-category-add-button){opacity:1!important} #vft .vft-cats button:disabled{cursor:not-allowed;opacity:.4!important} #vft .vft-bottom-icons{grid-column:2;display:flex;align-items:center;justify-content:flex-end;align-self:start;min-width:26px;height:26px} #vft .vft-bottom-icons .vm-escort-inline-alerts{margin:0} #vft .vft-bottom-icons svg{display:block;flex:none} #vft .vft-note{display:flex;align-items:flex-start;justify-content:flex-start;gap:5px;width:100%;min-width:0;max-width:100%;min-height:20px;margin:0;text-align:left;padding:2px 4px;border:0;border-radius:4px;background:var(--vm-ui-bg-alt,#f5f6f8);color:inherit;cursor:pointer;font-size:11px;line-height:1.3;white-space:normal;overflow-wrap:anywhere} #vft .vft-note .vft-clamp{min-width:0;flex:1;text-align:left} #vft .vft-note-icon{flex:none;color:var(--vm-ui-text-muted,#697080)} #vft .vft-note.-empty{min-height:20px;padding:2px 0;background:transparent;color:var(--vm-ui-text-muted,#697080);font-size:10px} #vft .vft-note:disabled{cursor:not-allowed;opacity:.6} #vft .vft-extra-wrap{width:100%;min-width:0;padding:4px 10px;border:0;border-radius:0;background:color-mix(in srgb,var(--vm-ui-text,#292c35) 8%,var(--vm-ui-bg,#fff))} #vft .vft-extra{display:grid;grid-template-columns:minmax(0,1fr);gap:0;margin:0;font-size:11px} #vft .vft-extra-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);column-gap:8px;align-items:center;padding:1px 0;line-height:16px} #vft .vft-extra dt{margin:0;text-align:left;color:var(--vm-ui-text-muted,#697080);font-size:10px} #vft .vft-extra dd{margin:0;text-align:right;font-weight:600} #vft .vft-extra :is(dt,dd){min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;overflow-wrap:normal} #vft .vft-error{padding:4px 10px;color:var(--vm-ui-danger,#b42335);font-size:11px;overflow-wrap:anywhere} #vft .vft-error summary{cursor:pointer;min-height:24px;display:flex;justify-content:space-between;gap:8px;align-items:center} #vft .vft-error summary span:first-child{color:var(--vm-ui-text-muted,#697080)} #vft .vft-menu{position:relative;flex:none;color:var(--vm-ui-text,#292c35)} #vft .vft-menu>summary::-webkit-details-marker{display:none} #vft .vft-menu-panel{position:fixed;z-index:20;width:230px;max-width:calc(100vw - 16px);max-height:360px;overflow:auto;overscroll-behavior:contain;padding:5px;border:1px solid var(--vm-ui-border,#ddd);border-radius:8px;background:var(--vm-ui-bg,#fff);color:var(--vm-ui-text,#292c35);box-shadow:0 8px 28px #0004;text-shadow:none} #vft .vft-menu-panel button{display:block;width:100%;border:0;border-radius:5px;padding:8px 10px;min-height:34px;background:transparent;color:inherit;text-align:left;font-size:12px;cursor:pointer} #vft .vft-menu-panel button:hover{background:var(--vm-ui-bg-alt,#f1f3f6)} #vft .vft-menu-panel button:disabled{opacity:.5;cursor:not-allowed} #vft .vft-menu-panel .vft-danger{color:var(--vm-ui-danger,#b42335);border-top:1px solid var(--vm-ui-border,#ddd);margin-top:4px} #vft .vft-extra-wrap,#vft .vft-extra-wrap+.vft-personal{border-top:1px solid var(--vm-ui-border-strong,#bec3cf)} #vft .vft-related{border-top:2px solid var(--vm-ui-accent,#ce3986);border-radius:0 0 9px 9px;background:var(--vm-ui-bg,#fff)} #vft .vft-related>summary{display:flex;align-items:center;gap:6px;list-style:none;padding:5px 10px;min-height:32px;font-size:11px;line-height:1.3;font-weight:700;cursor:pointer;color:var(--vm-ui-text,#292c35);background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 18%,var(--vm-ui-bg,#fff));border-radius:inherit} #vft .vft-related>summary:hover,#vft .vft-related>summary:focus-visible{background:color-mix(in srgb,var(--vm-ui-accent,#ce3986) 27%,var(--vm-ui-bg,#fff))} #vft .vft-related[open]>summary{border-radius:0;border-bottom:1px solid var(--vm-ui-border,#e2e4e9)} #vft .vft-related>summary::-webkit-details-marker{display:none} #vft .vft-related>summary::before{content:'▸';flex:none;font-size:16px;color:var(--vm-ui-accent,#ce3986)} #vft .vft-related[open]>summary::before{content:'▾'} #vft .vft-related-count{margin-left:auto;flex:none;padding:2px 6px;border:1px solid color-mix(in srgb,var(--vm-ui-accent,#ce3986) 35%,var(--vm-ui-border,#e2e4e9));border-radius:4px;text-align:right;color:var(--vm-ui-text,#292c35);background:var(--vm-ui-bg,#fff);font-size:10px;font-weight:750;white-space:nowrap} #vft .vft-children{max-height:340px;overflow:auto;overscroll-behavior:contain;padding:2px 8px 8px;display:grid;gap:6px} #vft .vft-child{display:flex;gap:8px;min-width:0;padding:6px;border:1px solid var(--vm-ui-border,#e2e4e9);border-radius:6px;font-size:11px} #vft .vft-child-photo{width:48px;height:60px;object-fit:cover;object-position:top;border-radius:4px;flex:none;overflow:hidden} #vft .vft-child-data{min-width:0;overflow-wrap:anywhere} #vft .vft-child-name{font-weight:600} #vft .vft-cache{margin-left:4px;font-size:10px;text-decoration:underline} #vft .vft-open-all{justify-self:start} #vft .vft-child .vft-initials{font-size:20px} #vft .vft-empty{grid-column:1/-1;padding:40px 20px;text-align:center;border:1px dashed var(--vm-ui-border,#ccc);border-radius:10px;background:var(--vm-ui-bg,#fff);color:var(--vm-ui-text-muted,#697080)} #vft .vft-empty strong{display:block;font-size:18px;margin-bottom:6px;color:var(--vm-ui-text,#292c35)} #vft .vft-empty .vm-ui-button{margin-top:12px} @media(max-width:700px){#vft .vft-content{padding:10px}#vft .vft-grid{grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:10px}#vft .vft-hero{min-height:330px}#vft .vft-hero.-text-only{min-height:0}} `.replaceAll('#vft','#vm-favorite-tiles-test');host.appendChild(style); const initialDuration=normalizeEscortPriceDuration(SETTINGS.searchResultPriceDuration); const state={q:'',category:'all',online:'all',alerts:'all',kind:'all',sort:'added-desc',density:'comfortable',duration:initialDuration==='hide'?'60':initialDuration}; try{const saved=JSON.parse(sessionStorage.getItem(FAVORITE_TILES_UI_KEY)||'null');if(saved&&typeof saved==='object')for(const key of Object.keys(state))if(typeof saved[key]==='string')state[key]=saved[key];}catch {} const savedView=getFavoriteTilesPreferences(); for(const key of ['sort','density','duration'])if(typeof savedView[key]==='string')state[key]=savedView[key]; const pending=new Set(),expanded=new Set(),toolbar=makeElement('header','vft-toolbar'),toolbarInner=makeElement('div','vft-toolbar-inner'),heading=makeElement('div','vft-heading'),count=makeElement('div','vft-count'); count.setAttribute('role','status');heading.append(makeElement('h1','vft-title','Ulubione'),count); const button=(label,action,className='vm-ui-button')=>{const b=makeButton(className,label);b.addEventListener('click',action);return b;}; const labeled=(label,control)=>{control.setAttribute('aria-label',label);const wrapper=makeElement('label','vft-field');wrapper.append(makeElement('span','',label),control);return wrapper;}; const select=(key,options=[])=>{const input=makeElement('select','vm-ui-field');if(options.length&&!options.some(([value])=>value===state[key]))state[key]=options[0][0];appendSelectOptions(input,options,state[key]);input.addEventListener('change',()=>{state[key]=input.value;render();if(['sort','density','duration'].includes(key))saveFavoriteTilesPreferences({[key]:state[key]});});return input;}; const icon=paths=>{const node=document.createElementNS('http://www.w3.org/2000/svg','svg');node.setAttribute('viewBox','0 0 24 24');node.setAttribute('class','vft-icon');node.setAttribute('aria-hidden','true');node.innerHTML=paths;return node;}; const toolPanels=[]; const closeToolPanels=(restoreFocus=false)=>{for(const tool of toolPanels)if(!tool.panel.hidden){tool.panel.hidden=true;tool.toggle.setAttribute('aria-expanded','false');if(restoreFocus)tool.toggle.focus({preventScroll:true});}}; const positionToolPanel=tool=>{ if(tool.panel.hidden||!tool.root.isConnected)return; const rect=tool.toggle.getBoundingClientRect(),top=Math.min(toolbar.getBoundingClientRect().bottom+4,innerHeight-56); tool.panel.style.maxHeight=`${Math.max(40,Math.min(420,innerHeight-top-8))}px`; tool.panel.style.left=`${Math.max(8,Math.min(rect.left,innerWidth-tool.panel.offsetWidth-8))}px`;tool.panel.style.top=`${Math.max(8,top)}px`; }; const makeToolPanel=(key,label,paths)=>{ const root=makeElement('div',`vft-tool vft-${key}-tool`),toggle=makeButton('vm-ui-button vft-tool-toggle'),panel=makeElement('div','vft-tool-panel',null,{id:`vft-${key}-panel`,hidden:true}); const tool={root,toggle,panel};toggle.id=`vft-${key}-toggle`;toggle.append(icon(paths),makeElement('span','',label),makeElement('span','vft-caret','▾'));toggle.setAttribute('aria-expanded','false');toggle.setAttribute('aria-controls',panel.id); panel.setAttribute('role','region');panel.setAttribute('aria-labelledby',toggle.id); toggle.addEventListener('click',()=>{const open=panel.hidden;closeToolPanels();if(open){for(const menu of host.querySelectorAll('.vft-menu[open]'))menu.open=false;panel.hidden=false;toggle.setAttribute('aria-expanded','true');positionToolPanel(tool);}}); root.addEventListener('keydown',event=>{if(event.key==='Escape'&&!panel.hidden){stopUiEvent(event);closeToolPanels(true);}else if(event.key==='ArrowDown'&&event.target===toggle){event.preventDefault();if(panel.hidden)toggle.click();panel.querySelector('button,input,select')?.focus({preventScroll:true});}}); root.addEventListener('focusout',event=>{if(event.relatedTarget&&!root.contains(event.relatedTarget)&&!panel.hidden)closeToolPanels();}); root.append(toggle,panel);toolPanels.push(tool);return tool; }; const filtersTool=makeToolPanel('filter','Filtry','<path d="M4 7h16M4 17h16M8 4v6M16 14v6"/>'),viewTool=makeToolPanel('view','Widok','<rect x="3" y="4" width="7" height="16" rx="1.5"/><rect x="14" y="4" width="7" height="16" rx="1.5"/>'); const filterCount=makeElement('span','vft-filter-count',null,{hidden:true}),filterResult=makeElement('span','vft-panel-result');filterCount.setAttribute('aria-hidden','true');filtersTool.toggle.insertBefore(filterCount,filtersTool.toggle.lastElementChild); const q=makeElement('input','vm-ui-field',null,{type:'search',value:state.q,placeholder:'Szukaj w Ulubionych…'}),searchField=makeElement('div','vft-search');q.setAttribute('aria-label','Szukaj w Ulubionych');q.title='Szukaj po nazwie, mieście, telefonie lub notatce'; const searchClear=button('×',()=>{q.value=state.q='';q.focus({preventScroll:true});render();},'vft-search-clear');setControlTitle(searchClear,'Wyczyść wyszukiwanie'); const syncSearchClear=()=>{searchClear.hidden=!q.value.length;};syncSearchClear(); searchField.append(icon('<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>'),q,searchClear); const category=select('category'),online=select('online',[['all','Wszystkie'],['online','Online'],['offline','Offline'],['unknown','Brak danych']]),alerts=select('alerts',[['all','Wszystkie'],['unread','Nieodczytane'],['active','Włączone'],['none','Wyłączone']]); const filters=makeElement('div','vft-filters');filters.append(labeled('Kategoria',category),labeled('Stan',online),labeled('Alerty',alerts)); const quick=makeElement('div','vft-quick'),quickButtons=new Map();quick.setAttribute('role','group');quick.setAttribute('aria-label','Szybki wybór stanu, alertów i wyszukiwań'); for(const [key,label] of [['all','Wszystkie'],['online','Online'],['unread','Nowe alerty'],['search','Wyszukiwania']]){ const b=button(label,()=>{closeToolPanels();state.kind=key==='search'?'search':'all';state.online=key==='online'?'online':'all';state.alerts=key==='unread'?'unread':'all';online.value=state.online;alerts.value=state.alerts;render();},'vft-chip');b.title=key==='search'?'Pokaż zapisane wyszukiwania; pozostaw wpisany tekst i kategorię':'Zmień stan i alerty; pozostaw wpisany tekst i kategorię';quickButtons.set(key,b);quick.appendChild(b); } const clear=button('Wyczyść filtry',()=>{resetFilters();filtersTool.toggle.focus({preventScroll:true});}),filterHead=makeElement('div','vft-panel-head'),filterFooter=makeElement('div','vft-panel-footer'); filterHead.append(makeElement('span','','Filtruj Ulubione'),filterResult);filterFooter.append(clear,button('Gotowe',()=>closeToolPanels(true),'vm-ui-button -primary'));filtersTool.panel.append(filterHead,filters,filterFooter); const sort=select('sort'),sortField=labeled('Sortuj',sort),sortGroup=makeElement('optgroup','',null,{label:'Dane z profilu'});sortField.className='vft-sort'; appendSelectOptions(sort,[['added-desc','Ostatnio dodane'],['added-asc','Najstarsze'],['name','Nazwa A–Z'],['online','Online najpierw'],['active','Najwięcej aktywnych'],['checked','Ostatnio sprawdzone'],['changes','Ostatnia zmiana']]);sort.appendChild(sortGroup); let sortFields=new Map(),sortSignature=''; const refreshSortOptions=()=>{ const options=[['profile:cities','Miasto','cities',fields.has('city')],['ad:stats:wiek','Wiek','age',fields.has('age')],['ad:stats:wzrost','Wzrost','height',true],['ad:stats:waga','Waga','weight',true],['ad:stats:biust','Biust','breasts',true],['price',`Cena ${getEscortPriceDurationLabel(state.duration)}`,'price',fields.has('price')],...extra.map(field=>[field,getFavoriteColumnLabel(field),'',true])].filter(v=>v[3]); const signature=JSON.stringify(options);if(signature!==sortSignature){ sortSignature=signature;sortFields.clear();sortGroup.replaceChildren(); for(const [field,label,fact] of options){const key=field===`ad:prices:${state.duration}`&&fields.has('price')?'price':decodeEscortDataFieldSegment(field).toLowerCase();if(sortFields.has(key))continue; sortFields.set(key,{field,fact});appendSelectOptions(sortGroup,[['data:+:'+key,label+' ↑'],['data:-:'+key,label+' ↓']]);} } sort.value=state.sort;if(!sort.value)sort.value=state.sort='added-desc'; }; const duration=select('duration',ESCORT_PRICE_DURATION_OPTIONS),density=select('density',[['comfortable','Wygodny'],['compact','Zwarty']]),priceField=labeled('Cena za',duration),tools=makeElement('div','vft-view-tools'); const extraControls=makeElement('div','vft-extra-controls'),extraHead=makeElement('div','vft-panel-head'),extraCount=makeElement('span','vft-panel-result'); const extraSelect=makeElement('select','vm-ui-field',null,{id:'vft-extra-select',hidden:true}),extraChips=makeElement('div','vft-field-chips'); extraSelect.setAttribute('aria-label','Dodaj informację');extraSelect.title='Dane z cache; bez pobierania.'; extraCount.setAttribute('role','status'); const extraAdd=button('Dodaj',()=>{refreshViewOptions();extraSelect.hidden=!extraSelect.hidden;extraAdd.setAttribute('aria-expanded',String(!extraSelect.hidden));positionToolPanel(viewTool);if(!extraSelect.hidden)extraSelect.focus({preventScroll:true});}); extraAdd.setAttribute('aria-controls',extraSelect.id);extraAdd.setAttribute('aria-expanded','false'); extraHead.append(makeElement('span','','Wyświetl dodatkowo:'),extraCount,extraAdd);extraControls.append(extraHead,extraSelect,extraChips); const refreshViewOptions=()=>{ const available=getEscortDataFieldOptions('none',true).filter(([field])=>field!=='none'&&!extra.includes(field)); extraSelect.replaceChildren();appendSelectOptions(extraSelect,[['','wybierz…'],...available],'');extraAdd.disabled=!available.length||extra.length>=FAVORITES_MAX_DATA_COLUMNS; extraAdd.title=extraAdd.disabled?`Maksymalnie ${FAVORITES_MAX_DATA_COLUMNS} pól`:'Dodaj informację';extraCount.textContent=`${extra.length}/${FAVORITES_MAX_DATA_COLUMNS}`; if(extraAdd.disabled)extraSelect.hidden=true;extraAdd.setAttribute('aria-expanded',String(!extraSelect.hidden));extraChips.replaceChildren(); for(let index=0;index<extra.length;index++){ const field=extra[index]; const label=getFavoriteColumnLabel(field),chip=makeElement('span','vft-field-chip'); const remove=button('×',()=>{openFavoriteTiles(fields,extra.filter(value=>value!==field));(extraChips.querySelectorAll('button')[Math.min(index,extra.length-1)]||extraAdd).focus({preventScroll:true});},''); setControlTitle(remove,`Usuń informację: ${label}`);chip.append(makeElement('span','',label,{title:label}),remove);extraChips.appendChild(chip); } positionToolPanel(viewTool); }; extraSelect.addEventListener('change',()=>{ const field=normalizeEscortDataFieldId(extraSelect.value);if(field==='none'||extra.includes(field)||extraAdd.disabled)return; extraSelect.hidden=true;openFavoriteTiles(fields,normalizeEscortDataFieldList([...extra,field],FAVORITES_MAX_DATA_COLUMNS)); (extraAdd.disabled?extraChips.lastElementChild?.querySelector('button'):extraAdd)?.focus({preventScroll:true}); }); tools.append(priceField,labeled('Układ kafelków',density),extraControls);viewTool.panel.append(makeElement('div','vft-panel-head','Widok kafelków'),tools); window.addEventListener('vmEscortDataCatalogChanged',()=>{if(host.isConnected)refreshViewOptions();},{signal}); const refresh=button('',()=>{closeToolPanels();runChecks(visibleIds);},'vm-ui-button vft-toolbar-action -primary'),refreshLabel=makeElement('span','vft-action-label','Sprawdź'); refresh.append(icon('<path d="M20 7v5h-5M4 17v-5h5"/><path d="M6.1 6.1A8 8 0 0 1 19.8 11M4.2 13a8 8 0 0 0 13.7 4.9"/>'),refreshLabel); const tableView=makeFavoriteViewButton('table',()=>{closeToolPanels(true);switchFavoriteView('table');});tableView.classList.add('vft-toolbar-action'); const closeView=button('',()=>closeHelperDialog(host,'button'),'vm-ui-button vft-close');closeView.append(icon('<path d="m6 6 12 12M6 18 18 6"/>'));setControlTitle(closeView,'Zamknij'); const filterGroup=makeElement('div','vft-filter-group');filterGroup.setAttribute('role','group');filterGroup.setAttribute('aria-label','Filtry i szybki wybór');filterGroup.append(filtersTool.root,quick); const controls=makeElement('div','vft-toolbar-controls');controls.append(filterGroup,sortField,viewTool.root,refresh,tableView);toolbarInner.append(heading,searchField,controls,closeView);toolbar.appendChild(toolbarInner); document.addEventListener('pointerdown',event=>{if(!toolPanels.some(tool=>tool.root.contains(event.target)))closeToolPanels();},{capture:true,signal}); const content=makeElement('main','vft-content'),status=makeElement('div','vft-status'),grid=makeElement('div','vft-grid');status.hidden=true;status.setAttribute('role','status');grid.setAttribute('aria-label','Lista Ulubionych');content.append(status,grid);host.append(toolbar,content); const stopCheckProgress=attachWatchCheckProgress(status,() => pending.size > 0); const tileRows=createFavoriteListRenderer(grid); let visibleIds=[]; const updateCategoryOptions=items=>{const selected=state.category,settings=getFavoriteSettings(),values=[...new Set([...settings.categories,...items.flatMap(item=>normalizeFavoriteAssignedCategories(item.categories))])].sort((a,b)=>a.localeCompare(b,'pl-PL'));category.replaceChildren();appendSelectOptions(category,[['all','Wszystkie'],['__none__','Bez kategorii'],...values.map(value=>[value,value])],values.includes(selected)||selected==='__none__'?selected:'all');state.category=category.value;}; const fullDate=timestamp=>new Date(Number(timestamp)).toLocaleString('pl-PL'); const relative=timestamp=>formatEscortLastVisit(timestamp)||'przed chwilą'; const period=()=>getEscortPriceDurationLabel(state.duration); const link=(url,text,className='')=>makeElement('a',className,text,{href:url,target:'_blank',rel:'noopener noreferrer'}); const keyed=(node,item,key)=>{node.dataset.vftFocus=`${item.id}:${key}`;return node;}; const statusMark=(value,label='')=>{ const mark=makeElement('span',`vft-status-mark ${value===true?'on':value===false?'off':'unknown'}`,value===true?'✓':value===false?'×':'?'); if(label){setControlTitle(mark,label);mark.setAttribute('role','img');}else mark.setAttribute('aria-hidden','true');return mark; }; const runChecks=async ids=>{ if(pending.size)return;ids=[...new Set(ids)];if(!ids.length)return;ids.forEach(id=>pending.add(id));status.hidden=false;status.dataset.error='false';status.textContent=`Sprawdzanie ${ids.length} pozycji…`;render(); try{const result=await runWatchedChecks({force:true,itemIds:ids,onProgress:progress=>status.textContent=progress.message||`Sprawdzanie ${progress.itemIndex||progress.checked||0}/${progress.total||ids.length}: ${progress.itemName||''}`});status.dataset.error=String(!!result.failed);status.textContent=result.message||`Sprawdzono: ${result.checked||0}/${result.total||ids.length}.${result.failed?` Błędy: ${result.failed}. Szczegóły na kafelkach.`:''}`;} catch(error){status.textContent=error?.message||'Nie udało się sprawdzić danych.';status.dataset.error='true';} finally{pending.clear();render();} }; const editNote=async item=>{await changeTrackedEntry(item.id,'note');render();}; const editAlerts=async item=>{await changeTrackedEntry(item.id,'alerts');render();}; const placeholder=(name,small=false)=>{const box=makeElement('div','vft-placeholder');box.appendChild(makeElement('span','vft-initials',String(name||'?').trim().slice(0,1).toUpperCase()));if(!small)box.appendChild(makeElement('span','','Brak zdjęcia'));return box;}; const photo=(src,name,small=false)=>{if(!src)return placeholder(name,small);const img=makeElement('img',small?'vft-child-photo':'vft-img',null,{src,alt:small?'':`Zdjęcie — ${name}`,loading:'lazy',decoding:'async',referrerPolicy:'no-referrer'});img.addEventListener('error',()=>{const replacement=placeholder(name,small);if(small)replacement.classList.add('vft-child-photo');img.replaceWith(replacement);},{once:true});return img;}; const makeChild=(item,entry,url)=>{ const id=parseAdIdFromUrl(url),data=(id&&(getStoredEscortAdData(id)||getWatchedAdDataMirror(id)))||{},name=normalizeEscortAdText(data.title)||item.displayName||'Anons'; const row=link(url,null,'vft-child'),info=makeElement('div','vft-child-data'),active=entry.activeIds.has(id),availability=active?(item.sourceType==='escort-search'?'W bieżących wynikach':'Aktywny'):item.lastCheckedAt?(item.sourceType==='escort-search'?'Poza bieżącymi wynikami':'Nieaktywny'):'Stan nieznany'; if(fields.has('image')){const image=photo(data.imageUrl,name,true);image.classList.add('vft-child-photo');row.appendChild(image);} const caption=makeElement('span','vft-caption',`Anons ${id||'—'} · `); if(!active&&item.lastCheckedAt&&item.sourceType==='escort-search')caption.appendChild(makeElement('span','vft-status-outside','Poza wynikami',{title:'Poza wynikami; anons może nadal być dostępny.'})); else caption.appendChild(statusMark(active?true:item.lastCheckedAt?false:null,availability)); const cacheUrl=!active&&getAvailableEscortAdUrls([[url,{active:false}]])[0]; if(cacheUrl){row.href=cacheUrl;caption.appendChild(makeElement('span','vft-cache','cache',{title:'Otwórz zapisaną wersję anonsu'}));} info.append(makeElement('div','vft-child-name',name),caption); const details=[];if(fields.has('city'))details.push(normalizeEscortCity(data.location?.city)||'Lokalizacja: b/d'); const age=fields.has('age')?getFavoriteTileStat(data,'wiek','lat'):'';if(age)details.push(age); if(details.length)info.appendChild(makeElement('div','',details.join(', '))); info.appendChild(makeElement('div','',getFavoriteTileDimensionsText(data)||'Wymiary: b/d')); if(fields.has('price'))info.appendChild(makeElement('div','',`${formatEscortTilePrice(data.prices?.[state.duration])||'Cena: b/d'} / ${period()}`)); row.appendChild(info);return row; }; const positionMenu=menu=>{ if(!menu?.open||!menu.isConnected)return; const toggle=menu.querySelector('summary'),panel=menu.querySelector('.vft-menu-panel'),rect=toggle.getBoundingClientRect(),limitTop=Math.max(8,toolbar.getBoundingClientRect().bottom+4); if(rect.bottom<limitTop||rect.top>innerHeight){menu.open=false;return;} const below=Math.max(0,innerHeight-rect.bottom-10),above=Math.max(0,rect.top-limitTop-6),up=below<Math.min(260,panel.scrollHeight)&&above>below; panel.style.maxHeight=`${Math.min(360,up?above:below)}px`; panel.style.left=`${Math.max(8,Math.min(rect.right-panel.offsetWidth,innerWidth-panel.offsetWidth-8))}px`; panel.style.top=`${up?Math.max(limitTop,rect.top-panel.offsetHeight-6):rect.bottom+6}px`; }; const scheduleMenuPosition=createAnimationFrameScheduler(()=>{if(!host.isConnected)return;for(const menu of host.querySelectorAll('.vft-menu[open]'))positionMenu(menu);for(const tool of toolPanels)positionToolPanel(tool);}); host.addEventListener('scroll',scheduleMenuPosition,{passive:true});window.addEventListener('resize',scheduleMenuPosition,{signal}); let viewData; const makeCard=(item,itemEvents,entry,settings)=>{ const data=entry.data||{},isSearch=item.sourceType==='escort-search',url=isSearch?watchItemSourceUrl(item):entry.url||watchItemSourceUrl(item),name=item.displayName||normalizeEscortAdText(data.title)||'Ulubione',unread=viewData(item).unread; const card=makeElement('article','vft-card'),body=makeElement('div','vft-body'),hero=makeElement('div',`vft-hero${fields.has('image')?'':' -text-only'}`); card.dataset.itemId=item.id;card.dataset.unread=String(unread.length>0);card.setAttribute('aria-label',name); if(fields.has('image')){const media=makeElement('div','vft-media'),a=url?link(url,null,'vft-photo-link'):makeElement('div','vft-photo-link');if(url)a.setAttribute('aria-label',`Otwórz ${isSearch?'wyszukiwanie':entry.url?'anons':'profil'}: ${name}`);a.appendChild(photo(data.imageUrl||item.imageUrl,name));media.appendChild(a);hero.appendChild(media);} const top=makeElement('div','vft-photo-top'),badges=makeElement('div','vft-badges'),photoActions=makeElement('div','vft-photo-actions'),photoContent=makeElement('div','vft-photo-content'); if(fields.has('online')){const online=item.state?.online,badge=makeElement('span',`vft-badge ${online===true?'on':''}`);badge.append(statusMark(online),document.createTextNode(online===true?'Online':online===false?'Offline':'Brak danych'));badge.title=item.lastCheckedAt?`Sprawdzono: ${fullDate(item.lastCheckedAt)}`:'Jeszcze niesprawdzone';badges.appendChild(badge);} if(fields.has('alerts')&&unread.length){const alert=keyed(button(formatPolishCount(unread.length,'nowy alert','nowe alerty','nowych alertów'),()=>openWatchChangesTimeline(item,itemEvents),'vft-badge new'),item,'unread');setControlTitle(alert,`${unread.length} nowych alertów — historia`);badges.appendChild(alert);} if(item.hidden)badges.appendChild(makeElement('span','vft-badge','Ukryty w wynikach')); const title=makeElement('h2','vft-name');title.title=name;title.appendChild(url?link(url,name):makeElement('span','',name));photoContent.appendChild(title); if(fields.has('profile')){const source=watchItemSourceUrl(item),text=watchItemSourceText(item),node=source?link(source,text,'vft-source'):makeElement('div','vft-source',text);node.title=`Źródło profilu: ${text}`;photoContent.appendChild(node);} const facts=entry.facts||=getFavoriteTileProfileFacts(item,entry,state.duration,viewData(item)),keyfacts=makeElement('div','vft-keyfacts'),ageText=fields.has('age')?facts.age:''; if(fields.has('city')||ageText){const cityText=[fields.has('city')?(facts.cities.join(', ')||'Lokalizacja: b/d'):'',ageText].filter(Boolean).join(', ');keyfacts.appendChild(makeElement('div','vft-city',cityText,{title:`${cityText}\n${facts.basis}`}));} if(fields.has('price')){const price=makeElement('div','vft-price',facts.price||'b/d',{title:`${facts.price||'b/d'} / ${period()}\n${facts.basis}`});price.appendChild(makeElement('span','vft-price-period',` / ${period()}`));keyfacts.appendChild(price);} if(keyfacts.children.length)photoContent.appendChild(keyfacts); photoContent.appendChild(makeElement('div','vft-dimensions',facts.dimensions||'Wymiary: b/d',{title:`${facts.dimensions||'b/d'}\n${facts.basis}`})); const notesEnabled=getWatchSettings().enabled,latest=fields.has('alerts')?(unread[0]||itemEvents[0]):null,personal=makeElement('div','vft-personal'); if(extra.length){ const wrap=makeElement('div','vft-extra-wrap'),list=makeElement('dl','vft-extra'); for(const field of extra){const row=makeElement('div','vft-extra-row'),value=viewData(item).column(field)||'b/d',label=getFavoriteColumnLabel(field);row.append(makeElement('dt','',label,{title:label}),makeElement('dd','',String(value).replace(/\s+/g,' ').trim(),{title:value}));list.appendChild(row);} wrap.appendChild(list);body.appendChild(wrap); } if(fields.has('categories')||fields.has('alerts')){const bottom=makeElement('div','vft-bottom-row'); if(fields.has('categories')){ const categoryCell=makeElement('div','vft-cats vm-favorites-category-cell'); const assignedCategories=fillFavoriteCategoryControls(categoryCell,item,settings,async change => { await changeTrackedEntry(item.id,'categories',change);render(); },{disabled:!notesEnabled,decorate:(control,key) => keyed(control,item,key)}); categoryCell.title=assignedCategories.length?assignedCategories.join(', '):'Dodaj kategorię';bottom.appendChild(categoryCell); } if(fields.has('alerts')){ const icons=makeElement('div','vft-bottom-icons'),alertButton=createTrackedAlertsControl({ alertItems:[item],onEditAlerts:()=>editAlerts(item),labelText:item.sourceType==='escort-ad'?'Anons':'Profil',alertsDisabled:!item.favorite }); icons.appendChild(keyed(alertButton,item,'alerts'));bottom.appendChild(icons); } personal.appendChild(bottom);} if(fields.has('note')){const note=keyed(button('',()=>editNote(item),`vft-note${item.note?'':' -empty'}`),item,'note');note.disabled=!notesEnabled;note.title=item.note||'Dodaj notatkę';note.setAttribute('aria-label',`Edytuj notatkę: ${name}`);if(item.note){const icon=makeElement('span','vft-note-icon','✎');icon.setAttribute('aria-hidden','true');note.append(icon,makeElement('span','vft-clamp',item.note));}else note.textContent='+ Notatka';personal.appendChild(note);} if(personal.children.length)body.appendChild(personal); if(item.lastError){const error=makeElement('details','vft-error'),summary=makeElement('summary');summary.title='Szczegóły błędu';summary.append(makeElement('span','','Sprawdzenie'),makeElement('span','','⚠ Błąd · rozwiń'));error.append(summary,makeElement('div','',item.lastError));body.appendChild(error);} const history=keyed(button('◷',()=>openWatchChangesTimeline(item,itemEvents),'vft-photo-action'),item,'history'),check=keyed(button(pending.has(item.id)?'…':'↻',()=>runChecks([item.id]),'vft-photo-action'),item,'check'); const historyTitle=latest?`Ostatnia zmiana: ${relative(latest.timestamp)}\n${latest.message||latest.description||''}`:`Historia zmian (${itemEvents.length})`; setControlTitle(history,historyTitle);setControlTitle(check,`Sprawdź dane: ${name}`);check.disabled=pending.size>0; const menu=makeElement('details','vft-menu'),menuToggle=keyed(makeElement('summary','','⋯'),item,'menu'),menuPanel=makeElement('div','vft-menu-panel');setControlTitle(menuToggle,`Więcej działań: ${name}`); const menuAction=(label,action,danger=false)=>{const b=button(label,()=>{menu.open=false;menuToggle.focus({preventScroll:true});action();},danger?'vft-danger':'');keyed(b,item,`menu-${menuPanel.children.length}`);menuPanel.appendChild(b);return b;}; if(unread.length)menuAction('Oznacz alerty jako przeczytane',()=>{const ids=new Set(unread.map(event=>event.id));saveWatchEvents(getWatchEvents().map(event=>ids.has(event.id)?{...event,acknowledged:true}:event));renderWatchNotificationBar();render();}); menuAction('Zmień nazwę',async()=>{await changeTrackedEntry(item.id,'rename');render();}); menuAction(item.hidden?'Pokaż w wynikach':'Ukryj w wynikach',()=>{changeTrackedItemStatus(item.id,'hidden');render();}); menuAction('Usuń z Ulubionych',()=>{changeTrackedItemStatus(item.id,'favorite');render();}).title=trackedRemovalHint('favorite'); menuAction('Usuń cały wpis…',()=>{changeTrackedItemStatus(item.id,'remove');render();},true).title=trackedRemovalHint('profile'); menu.append(menuToggle,menuPanel);menu.addEventListener('toggle',()=>{if(menu.open&&menu.isConnected){for(const other of host.querySelectorAll('.vft-menu[open]'))if(other!==menu)other.open=false;positionMenu(menu);}}); photoActions.append(history,check,menu);top.append(badges,photoActions);hero.append(top,photoContent);card.appendChild(hero);if(body.children.length)card.appendChild(body); if(fields.has('aggregate')&&item.sourceType!=='escort-ad'){ const all=isSearch?getWatchedItemHistoricalAdUrls(item,itemEvents):entry.all,active=Number.isFinite(item.state?.activeCount)?item.state.activeCount:entry.active.length,total=isSearch?all.length:Number.isFinite(item.state?.totalAds)?item.state.totalAds:all.length; const related=makeElement('details','vft-related'),summary=keyed(makeElement('summary'),item,'related'),children=makeElement('div','vft-children');let built=false; const relatedCount=makeElement('span','vft-related-count'); if(item.lastCheckedAt)relatedCount.append(`${active} `,statusMark(true,isSearch?'W bieżących wynikach':'Aktywne anonse'),` / ${total}`);else relatedCount.textContent=`${all.length} zapisanych · stan b/d`; summary.append(makeElement('span','','Anonse profilu'),relatedCount);summary.title=isSearch?'Bieżące i wszystkie dotąd znalezione anonse':'Aktywne i wszystkie zapisane anonse'; const build=()=>{ if(built)return;built=true; const urls=[...all].sort((a,b)=>Number(entry.activeIds.has(parseAdIdFromUrl(b)))-Number(entry.activeIds.has(parseAdIdFromUrl(a)))),results=new Map(urls.map(url=>[url,{active:entry.activeIds.has(parseAdIdFromUrl(url))}])); const openAll=keyed(button('',event=>openAvailableEscortAds(results,event),'vm-ui-button vm-ui-compact vft-open-all'),item,'open-all'); updateOpenAvailableEscortAdsButton(openAll,results);children.appendChild(openAll); if(!all.length)children.appendChild(makeElement('div','vft-caption','Brak adresów anonsów. Sprawdź profil.')); else if(total!==all.length)children.appendChild(makeElement('div','vft-caption',`Zapisane adresy: ${all.length}. Licznik dotyczy ostatniego sprawdzenia.`)); urls.forEach(value=>children.appendChild(makeChild(item,entry,value))); }; related.append(summary,children);related.open=expanded.has(item.id);if(related.open)build();related.addEventListener('toggle',()=>{if(!related.isConnected)return;if(related.open){expanded.add(item.id);build();}else expanded.delete(item.id);});card.appendChild(related); } return card; }; const resetFilters=()=>{state.q='';state.category=state.online=state.alerts=state.kind='all';q.value='';online.value=alerts.value='all';render();}; const render=()=>{ if(!host.isConnected)return; const focusKey=document.activeElement?.dataset.vftFocus,openMenuId=host.querySelector('.vft-menu[open]')?.closest('.vft-card')?.dataset.itemId,scrollTop=host.scrollTop,items=getWatchedItems().filter(item=>item.favorite),events=getWatchEvents(),settings=getFavoriteSettings(); viewData=createFavoriteViewData(events); const itemEvents=item=>viewData(item).events,hasUnread=item=>viewData(item).unread.length>0,entries=new Map(items.map(item=>[item.id,getFavoriteTilesData(item,viewData(item))])); const searchCount=items.filter(item=>item.sourceType==='escort-search').length; if(state.kind!=='search'||!searchCount)state.kind='all'; const searchButton=quickButtons.get('search');searchButton.textContent=`Wyszukiwania (${searchCount})`; if(!searchCount&&document.activeElement===searchButton)quickButtons.get('all').focus({preventScroll:true});searchButton.hidden=!searchCount; updateCategoryOptions(items);host.dataset.density=state.density;priceField.hidden=!fields.has('price');syncSearchClear(); const terms=normalizeText(state.q).split(/\s+/).filter(Boolean),filtered=items.filter(item=>{ if(state.kind==='search'&&item.sourceType!=='escort-search')return false; if(terms.length){const entry=entries.get(item.id),text=normalizeText([item.displayName,watchItemSourceText(item),item.phoneDigits,item.note,(item.categories||[]).join(' '),watchCitiesText(viewData(item).cities),...entry.details.map(({adData})=>adData.location?.city),...entry.all].filter(Boolean).join(' '));if(!terms.every(term=>text.includes(term)))return false;} const cats=normalizeFavoriteAssignedCategories(item.categories);if(state.category==='__none__'&&cats.length)return false;if(state.category!=='all'&&state.category!=='__none__'&&!cats.includes(state.category))return false; const status=item.state?.online===true?'online':item.state?.online===false?'offline':'unknown';if(state.online!=='all'&&state.online!==status)return false; const active=watchedItemHasActiveAlerts(item);return !((state.alerts==='active'&&!active)||(state.alerts==='none'&&active)||(state.alerts==='unread'&&!hasUnread(item))); }); refreshSortOptions(); const dataSort=state.sort.match(/^data:([+-]):(.+)$/),sortValues=new Map(); if(dataSort){const {field,fact}=sortFields.get(dataSort[2]);for(const item of filtered){ const entry=entries.get(item.id),text=fact?(entry.facts||=getFavoriteTileProfileFacts(item,entry,state.duration,viewData(item)))[fact]:undefined; sortValues.set(item.id,fact ? favoriteDataSortValue(item,field==='price'?`ad:prices:${state.duration}`:field,text,viewData(item)):viewData(item).sortValue(field==='price'?`ad:prices:${state.duration}`:field));}} const cmp=(a,b)=>compareFavoriteValues(a.displayName||'',b.displayName||''),rank=item=>viewData(item).stateRank; filtered.sort((a,b)=>(dataSort?compareFavoriteValues(sortValues.get(a.id),sortValues.get(b.id),dataSort[1]==='-'?-1:1):state.sort==='name'?cmp(a,b):state.sort==='added-asc'?(Number(a.createdAt)||0)-(Number(b.createdAt)||0):state.sort==='online'?rank(a)-rank(b):state.sort==='active'?(Number(b.state?.activeCount)||0)-(Number(a.state?.activeCount)||0):state.sort==='checked'?(Number(b.lastCheckedAt)||0)-(Number(a.lastCheckedAt)||0):state.sort==='changes'?(Number(itemEvents(b)[0]?.timestamp)||0)-(Number(itemEvents(a)[0]?.timestamp)||0):(Number(b.createdAt)||0)-(Number(a.createdAt)||0))||cmp(a,b)); const nodes=[]; for(const item of filtered){ const entry=entries.get(item.id),history=itemEvents(item),signature=JSON.stringify([item,history,entry,viewData(item).allDetails,settings,[...fields],extra,state.duration,pending.size>0,getWatchSettings().enabled,Math.floor(Date.now()/60000)]); const card=tileRows.get(item.id,signature)||tileRows.set(item.id,signature,makeCard(item,history,entry,settings));nodes.push(card); } if(!filtered.length){const empty=makeElement('div','vft-empty');empty.append(makeElement('strong','',items.length?'Brak pasujących pozycji':'Nie masz jeszcze Ulubionych'),makeElement('div','',items.length?'Zmień lub wyczyść filtry.':'Dodaj anons lub profil gwiazdką.'));if(items.length)empty.appendChild(button('Wyczyść filtry',resetFilters));nodes.push(empty);}tileRows.render(nodes);if(openMenuId){const menu=[...grid.querySelectorAll('.vft-card')].find(node=>node.dataset.itemId===openMenuId)?.querySelector('.vft-menu');if(menu)menu.open=true;} visibleIds=filtered.map(item=>item.id);const onlineCount=items.filter(item=>item.state?.online===true).length,unreadCount=items.filter(hasUnread).length; count.textContent=`${filtered.length}/${items.length}`;setControlTitle(count,`Widoczne: ${filtered.length}/${items.length}`);quickButtons.get('all').textContent=`Wszystkie (${items.length})`;quickButtons.get('online').textContent=`Online (${onlineCount})`;quickButtons.get('unread').textContent=`Nowe alerty (${unreadCount})`; for(const [key,b] of quickButtons)b.setAttribute('aria-pressed',String(key==='search'?state.kind==='search':state.kind==='all'&&(key==='all'?state.online==='all'&&state.alerts==='all':key==='online'?state.online==='online'&&state.alerts==='all':state.online==='all'&&state.alerts==='unread'))); const activeFilters=[!!state.q.trim(),state.category!=='all',state.online!=='all',state.alerts!=='all',state.kind==='search'].filter(Boolean).length; filterCount.hidden=!activeFilters;filterCount.textContent=String(activeFilters);filtersTool.root.dataset.active=String(activeFilters>0);filterResult.textContent=`${filtered.length} z ${items.length} pozycji`; const filterDescription=[state.q.trim()?`Szukaj: ${state.q.trim()}`:'',state.kind==='search'?'Typ: zapisane wyszukiwania':'',...[['Kategoria',category],['Stan',online],['Alerty',alerts]].filter(([,control])=>control.value!=='all').map(([label,control])=>`${label}: ${control.selectedOptions[0]?.textContent||control.value}`)].filter(Boolean).join('; '); setControlTitle(filtersTool.toggle,activeFilters?`Filtry (${activeFilters}) — ${filterDescription}`:'Filtry — kategorie, stan i alerty');clear.disabled=!activeFilters; sort.title=`Sortuj: ${sort.selectedOptions[0]?.textContent||''}`;setControlTitle(viewTool.toggle,`Widok — ${density.selectedOptions[0]?.textContent||''}${fields.has('price')?`; cena za ${period()}`:''}`); refresh.disabled=pending.size>0||!filtered.length;refreshLabel.textContent=pending.size?'Sprawdzanie…':'Sprawdź';setControlTitle(refresh,pending.size?'Sprawdzanie widocznych pozycji…':`Sprawdź dane widocznych pozycji (${filtered.length})`);refresh.setAttribute('aria-busy',String(pending.size>0)); try{sessionStorage.setItem(FAVORITE_TILES_UI_KEY,JSON.stringify(state));}catch {} if(focusKey){const control=[...host.querySelectorAll('[data-vft-focus]')].find(node=>node.dataset.vftFocus===focusKey);control?.focus({preventScroll:true});}host.scrollTop=scrollTop;scheduleMenuPosition(); }; const scheduleRender=createAnimationFrameScheduler(()=>{if(host.isConnected)render();}); q.addEventListener('input',()=>{state.q=q.value;syncSearchClear();scheduleRender();}); host.addEventListener('click',event=>{for(const menu of host.querySelectorAll('.vft-menu[open]'))if(!menu.contains(event.target))menu.open=false;}); host._vmRender=scheduleRender;host._vmApplyOptions=options=>{fields=options.fields;extra=options.extra;refreshViewOptions();render();}; const closeController=createDialogCloseController(host,{closeOnBackdrop:false,beforeClose:reason=>{ if(reason!=='escape')return true; const menu=host.querySelector('.vft-menu[open]');if(menu){menu.open=false;menu.querySelector('summary').focus();return false;} if(toolPanels.some(tool=>!tool.panel.hidden)){closeToolPanels(true);return false;}return true; },onClose:()=>{ if(signal.aborted)return; stopCheckProgress(); favoriteTilesOpenGeneration++; lifetime.abort();watchPageButtonRefreshers.delete(scheduleRender); for(const id of storageListeners)try{GM_removeValueChangeListener(id);}catch {} try{sessionStorage.setItem(FAVORITE_TILES_UI_KEY,JSON.stringify(state));}catch {} const picker=document.querySelector('.vm-favorites-category-picker');if(host.contains(picker?._vmAnchor))closeHelperDialog(picker); host._vmRender=host._vmApplyOptions=null;tileRows.clear();host.replaceChildren(); document.documentElement.classList.remove('vm-favorite-tiles-test-mode');document.body.classList.remove('vm-favorite-tiles-test-page'); window.scrollTo(pageScroll);if(returnFocus?.isConnected)returnFocus.focus({preventScroll:true}); }}); host._vmDestroy=()=>closeController.close('destroy'); document.body.appendChild(host);refreshViewOptions();render();q.focus({preventScroll:true}); // Migracja sesji i URL; potem zapis tylko po zmianie wyboru. saveFavoriteTilesPreferences({fields:[...fields],extra,sort:state.sort,density:state.density,duration:state.duration}); watchPageButtonRefreshers.add(scheduleRender); try{for(const key of [WATCH_EVENTS_STORAGE_KEY,WATCH_ITEMS_STORAGE_KEY,WATCH_AD_DATA_MIRROR_STORAGE_KEY,SETTINGS_STORAGE_KEY])storageListeners.push(GM_addValueChangeListener(key,scheduleRender));}catch {} window.addEventListener('focus',scheduleRender,{signal});document.addEventListener('visibilitychange',()=>{if(!document.hidden)scheduleRender();},{signal}); window.addEventListener('pagehide',event=>{if(!event.persisted)host._vmDestroy();},{signal}); return true; } function openScriptMenuPage(url) { const normalized= normalizeMenuPageUrl(url); if (!normalized) return; try { GM_openInTab( normalized, { active:true, insert:true } ); } catch { window.open( normalized, '_blank', 'noopener' ); } } function getEnabledMenuPages() { return normalizeMenuPages( SETTINGS.menuPages ).filter( item => item.enabled !== false ); } function getMenuPageFaviconUrl(url) { try { const parsed=new URL(url); if (/^(?:www\.)?sleazyfork\.org$/i.test(parsed.hostname)) return 'https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/app/javascript/images/blacklogo32.png'; return `${parsed.origin}/favicon.ico`; } catch { return ''; } } function getHelperMenuActions() { let enabled=false; try { enabled=getWatchSettings().enabled; } catch {} return [ { icon:'★', label:'Ulubione', visible:enabled === true, action:() => openWatchedSettingsModal(false) }, { icon:'✎', label:TRACKED_ITEMS_LABEL, visible:enabled === true, action:() => openWatchedSettingsModal(true) }, { icon:'⚙︎', label:'Ustawienia', visible:true, action:() => openSettingsModal() }, { icon:'⌫', label:'Otwórz historię do usunięcia', visible:true, action:() => openBrowserHistoryCleanupModal() } ].filter(item => item.visible); } function initHelperToolbar() { if (window.top !== window.self || (!GARSO_MENU_ONLY_MODE && !IS_ESCORT_CLUB && !['escorti.pl','www.escorti.pl'].includes(CURRENT_HOST))) return; if (!document.body) {setTimeout(initHelperToolbar,100); return;} if (document.getElementById(HELPER_MENU_ID)) return; const root=prepareHelperStyleApi(makeElement('div')); root.id=HELPER_MENU_ID; root.setAttribute('role','toolbar'); root.setAttribute('aria-label','Escort Helper'); const style=makeElement('style'); style.textContent=` #${HELPER_MENU_ID} { display:flex; flex:0 0 auto; flex-wrap:wrap; min-width:0; max-width:calc(100vw - 16px); align-items:center; gap:2px; } #${HELPER_MENU_ID}[hidden], #${HELPER_MENU_ID} [hidden] { display:none !important; } #${HELPER_MENU_ID} > .vm-ui-button, #${HELPER_MENU_ID} .vm-helper-inline-link { flex:0 0 auto; text-decoration:none; min-height:26px; height:26px; min-width:28px; padding:3px 5px; border-color:transparent; border-radius:5px; background:transparent; color:var(--vm-ui-accent-contrast,#fff); font-size:16px; line-height:1; } #${HELPER_MENU_ID} > .vm-ui-button:hover, #${HELPER_MENU_ID} > .vm-ui-button[aria-expanded="true"], #${HELPER_MENU_ID} .vm-helper-inline-link:hover { background:color-mix(in srgb,var(--vm-ui-accent-contrast,#fff) 18%,transparent); } #${HELPER_MENU_ID} > .vm-ui-button:focus-visible, #${HELPER_MENU_ID} .vm-helper-inline-link:focus-visible { outline-color:var(--vm-ui-accent-contrast,#fff); outline-offset:0; } #${HELPER_MENU_ID} .vm-helper-menu-separator { flex:0 0 1px; width:1px; height:18px; margin:0 5px; background:var(--vm-ui-accent-contrast,#fff); opacity:.55; } #${HELPER_MENU_ID} .vm-helper-inline-pages { display:flex; flex:0 1 auto; flex-wrap:wrap; align-items:center; gap:2px; min-width:0; overflow:visible; } #${HELPER_MENU_ID} .vm-helper-inline-link { width:28px; padding:2px; } #${HELPER_MENU_ID} .vm-helper-inline-link img { display:block; width:22px; height:22px; object-fit:contain; } #${HELPER_MENU_ID} .vm-helper-inline-link .vm-helper-page-initial { display:flex; flex:0 0 auto; align-items:center; justify-content:center; width:22px; height:22px; color:inherit; font-size:22px; font-weight:800; line-height:1; } #${HELPER_MENU_ID} .vm-helper-menu-badge { min-width:14px; padding:2px 4px; border-radius:999px; background:color-mix(in srgb,var(--vm-ui-accent-contrast,#fff) 22%,transparent); font:700 10px/1.2 var(--vm-ui-font-family,Arial,sans-serif); } #${HELPER_MENU_PANEL_ID} { position:fixed; z-index:1; width:min(290px,calc(100vw - 16px)); overflow:auto; box-sizing:border-box; padding:5px; border:1px solid var(--vm-ui-border,#ddd); border-radius:8px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-text,#222); box-shadow:var(--vm-ui-shadow,0 7px 24px rgba(0,0,0,.28)); } #${HELPER_MENU_PANEL_ID} .vm-helper-menu-item { display:flex; width:100%; gap:8px; padding:8px; border:0; background:transparent; color:inherit; font-weight:400; text-align:left; justify-content:flex-start; white-space:normal; text-decoration:none; } #${HELPER_MENU_PANEL_ID} .vm-helper-menu-item:hover, #${HELPER_MENU_PANEL_ID} .vm-helper-menu-item:focus-visible { background:var(--vm-ui-bg-hover,#f3f3f3); } #${HELPER_MENU_PANEL_ID} .vm-helper-menu-icon { flex:0 0 18px; color:var(--vm-ui-accent,#f54da3); text-align:center; font-weight:700; } `; const favorite=makeButton('vm-ui-button vm-helper-menu-favorite','★',{title:'Ulubione'}); favorite.setAttribute('aria-label','Ulubione'); const toggle=makeButton('vm-ui-button vm-helper-menu-toggle','☰'); const separator=makeElement('span','vm-helper-menu-separator'); separator.setAttribute('aria-hidden','true'); const inlinePages=makeElement('div','vm-helper-inline-pages'); inlinePages.setAttribute('role','group'); inlinePages.setAttribute('aria-label','Linki do stron'); setControlTitle(toggle,'Menu Escort Helper'); toggle.setAttribute('aria-haspopup','menu'); toggle.setAttribute('aria-controls',HELPER_MENU_PANEL_ID); toggle.setAttribute('aria-expanded','false'); const badge=makeElement('span','vm-helper-menu-badge',null,{hidden:true}); badge.setAttribute('aria-hidden','true'); favorite.appendChild(badge); root._vmSetAlertCount=count => { badge.textContent=String(count); badge.hidden=!count; favorite.setAttribute('aria-label',count ? `Ulubione - nowe alerty: ${count}`:'Ulubione'); }; const panel=makeElement('div','',null,{id:HELPER_MENU_PANEL_ID,hidden:true}); panel.setAttribute('role','menu'); root.append(style,favorite,toggle,separator,inlinePages,panel); document.body.appendChild(root); let positionFrame=0; const closeMenu=() => { panel.hidden=true; toggle.setAttribute('aria-expanded','false'); }; const makePageLink=page => { const link=makeElement('a','vm-ui-button vm-helper-inline-link',null,{title:page.name}); link.setAttribute('aria-label',page.name); link.href=page.url; link.target='_blank'; link.rel='noopener noreferrer'; const icon=makeElement('img','',null,{alt:'',referrerPolicy:'no-referrer',src:getMenuPageFaviconUrl(page.url)}); icon.addEventListener('error',() => { const initial=makeElement('span','vm-helper-page-initial',(page.name.trim().match(/\p{L}/u)?.[0] || [...page.name.trim()][0] || '?').toLocaleUpperCase('pl-PL')); initial.setAttribute('aria-hidden','true'); icon.replaceWith(initial); },{once:true}); link.appendChild(icon); link.addEventListener('click',event => { if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; event.preventDefault(); closeMenu(); openScriptMenuPage(page.url); }); return link; }; const renderItems=() => { panel.replaceChildren(); panel.setAttribute('aria-label','Menu Escort Helper'); for (const item of getHelperMenuActions()) { const button=makeButton('vm-ui-button vm-helper-menu-item'); button.setAttribute('role','menuitem'); const icon=makeElement('span','vm-helper-menu-icon'); icon.setAttribute('aria-hidden','true'); icon.append(item.icon); button.append(icon,makeElement('span','',item.label)); button.addEventListener('click',() => {closeMenu(); item.action();}); panel.appendChild(button); } }; const openMenu=() => { if (!panel.hidden) return; renderItems(); panel.hidden=false; toggle.setAttribute('aria-expanded','true'); syncPosition(); }; favorite.addEventListener('click',() => {closeMenu(); openWatchedSettingsModal(false);}); toggle.addEventListener('click',() => panel.hidden ? openMenu():closeMenu()); root.addEventListener('keydown',event => { if (event.key === 'Escape' && !panel.hidden) { stopUiEvent(event); closeMenu(); toggle.focus(); return; } if (!['ArrowDown','ArrowUp','Home','End'].includes(event.key) || (event.target !== toggle && !panel.contains(event.target))) return; stopUiEvent(event); openMenu(); const items=[...panel.querySelectorAll('[role="menuitem"]')],index=items.indexOf(document.activeElement); const next=event.key === 'Home' ? 0:event.key === 'End' ? items.length - 1 :index < 0 ? (event.key === 'ArrowUp' ? items.length - 1:0):(index + (event.key === 'ArrowUp' ? -1:1) + items.length) % items.length; items[next]?.focus(); }); document.addEventListener('click',event => {if (!root.contains(event.target)) closeMenu();},true); root.addEventListener('focusout',event => {if (!root._vmMoving && !root.contains(event.relatedTarget)) closeMenu();}); function syncPosition() { positionFrame=0; if (panel.hidden || root.hidden) return; const rect=toggle.getBoundingClientRect(),top=Math.round(rect.bottom + 4); panel.style.left=`${Math.max(8,Math.min(Math.round(rect.left),innerWidth - panel.offsetWidth - 8))}px`; panel.style.top=`${top}px`; panel.style.maxHeight=`${Math.max(40,innerHeight - top - 8)}px`; } const schedulePosition=() => {if (!positionFrame) positionFrame=requestAnimationFrame(syncPosition);}; root._vmPosition=schedulePosition; window.addEventListener('resize',schedulePosition); const render=() => { root.hidden=SETTINGS.showBottomLeftMenu === false; favorite.hidden=!getWatchSettings().enabled; const pages=getEnabledMenuPages(); const showLinks=!root.hidden && SETTINGS.showMenuPagesAsFavicons && pages.length > 0; separator.hidden=!showLinks; inlinePages.hidden=!showLinks; inlinePages.replaceChildren(...(inlinePages.hidden ? []:pages.map(makePageLink))); closeMenu(); renderWatchNotificationBar(); }; try { GM_addValueChangeListener(SETTINGS_STORAGE_KEY,() => {SETTINGS=getSettings(); applyFavoriteSearchPageSettings(); queueMicrotask(render);}); } catch {} render(); } const watchedMenuCommands=[ {label:'Ulubione',icon:'★',notesOnly:false,id:null}, {label:TRACKED_ITEMS_LABEL,icon:'✎',notesOnly:true,id:null} ]; if ((GARSO_MENU_ONLY_MODE || IS_ESCORT_CLUB || ['escorti.pl','www.escorti.pl'].includes(CURRENT_HOST)) && typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('Ustawienia',() => openSettingsModal(),{icon:vmMenuIcon('⚙︎')}); const syncWatchedMenuCommand=() => { try { const enabled=getWatchSettings().enabled; for (const command of watchedMenuCommands) { if (enabled && command.id == null) { command.id=GM_registerMenuCommand(command.label,() => openWatchedSettingsModal(command.notesOnly),{icon:vmMenuIcon(command.icon)}); } else if (!enabled && command.id != null && typeof GM_unregisterMenuCommand === 'function') { GM_unregisterMenuCommand(command.id); command.id=null; } } } catch (error) { log('Nie udało się zsynchronizować menu Ulubionych i Zapisanych anonsów',error); } }; syncWatchedMenuCommand(); try { GM_addValueChangeListener( SETTINGS_STORAGE_KEY, syncWatchedMenuCommand ); } catch {} GM_registerMenuCommand( 'Otwórz historię do usunięcia', () => openBrowserHistoryCleanupModal(), {icon:vmMenuIcon('⌫')} ); } function maybeOpenSettingsOnFirstRun() { if (!IS_ESCORT_CLUB) return; try { if (GM_getValue(SETTINGS_ONBOARDING_SEEN_KEY,false)) return; const cacheIsEmpty=getPersistentCacheStats().entries === 0; writeHelperStorageValue(SETTINGS_ONBOARDING_SEEN_KEY,true); if (cacheIsEmpty) { openSettingsModal({firstRun:true}); } } catch (error) { log('Nie udało się sprawdzić pierwszego uruchomienia ustawień',error); } } const ESCORT_REQUEST_QUEUE_PREFIX='vm_escort_request_queue_'; const ESCORT_REQUEST_START_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'start'; const ESCORT_REQUEST_TURN_MS=15000; const ESCORT_REQUEST_COOLDOWN_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'cooldown'; const ESCORT_REQUEST_TICKET_PREFIX=ESCORT_REQUEST_QUEUE_PREFIX + 'ticket_'; const ESCORT_REQUEST_FAILURE_PREFIX=ESCORT_REQUEST_QUEUE_PREFIX + 'failure503_'; let escort503FailureSequence=0; const ESCORT_REQUEST_PERMIT_PREFIX=ESCORT_REQUEST_QUEUE_PREFIX + 'permit_'; const ESCORT_REQUEST_PERMITS_CHANGED_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'permits_changed'; const ESCORT_REQUEST_PERMIT_TTL_MS=5 * 60 * 1000; const escortRequestLocalPermits=new Map(); let escortRequestPermitTimer=null; const ESCORT_REQUEST_FOREGROUND_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'foreground'; const ESCORT_REQUEST_BACKGROUND_WAIT_MS=30000; const ESCORT_FAST_START_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'fast_start'; const ESCORT_FAST_START_WINDOW_MS=5 * 60 * 1000; const ESCORT_REQUEST_INSTANCE_ID=`${Date.now()}-${Math.random().toString(36).slice(2)}`; const escortRequestQueues={get:{jobs:[],pumping:false,active:0,lastStartedAt:0},head:{jobs:[],pumping:false,active:0,lastStartedAt:0}}; const escortRequestQueueDiagnostics=Object.fromEntries(['get','head'].map(lane => [lane,{ waitReason:'none',nextEligibleAt:0,wakeAt:0 }])); let escortQueueEvidenceState; function getEscortQueueEvidenceState() { const epoch=String(GM_getValue('vm_garso_diagnostics_epoch','0')); if (!escortQueueEvidenceState || escortQueueEvidenceState.epoch !== epoch) { const now=Date.now(); escortQueueEvidenceState={epoch,since:now,events:[],admissions:[],lanes:Object.fromEntries(['get','head'].map(lane => [lane,{ reason:escortRequestQueueDiagnostics[lane].waitReason,since:now,waitMs:{},leaseConflicts:0, expiredReservations:0,expiredStartLeases:0,permitReschedules:0,admissionRetries:0,lateWakes:0,maxWakeLatenessMs:0}]))}; } return escortQueueEvidenceState; } function recordEscortQueueEvidence(lane,details={},counter='',admission=null) { try { const evidence=getEscortQueueEvidenceState(),state=evidence.lanes[lane],now=Date.now(),queue=escortRequestQueues[lane]; if (!state || !queue) return; if (admission) { evidence.admissions.push(admission); evidence.admissions=evidence.admissions.filter(row => row[1] >= now - 1800000).slice(-40); } const event=(kind,reason,elapsed) => evidence.events.push([now,lane,kind,reason,elapsed,queue.jobs.length,queue.active]); if (counter && ['leaseConflicts','expiredReservations','expiredStartLeases','permitReschedules','admissionRetries'].includes(counter)) state[counter]++; if (details.waitReason && details.waitReason !== state.reason) { const elapsed=Math.max(0,now - state.since); if (state.reason !== 'none') { state.waitMs[state.reason]=(state.waitMs[state.reason] || 0) + elapsed; if (elapsed >= 1000) event('wait',state.reason,elapsed); } state.reason=details.waitReason; state.since=now; if (['reload-required','cooldown','recovery-probe'].includes(state.reason)) event('block',state.reason,0); } if (details.wakeLatenessMs >= 1000) { state.lateWakes++; state.maxWakeLatenessMs=Math.max(state.maxWakeLatenessMs,details.wakeLatenessMs); event('timer',state.reason,details.wakeLatenessMs); } evidence.events=evidence.events.filter(row => row[0] >= now - 1800000).slice(-40); } catch {} } function getEscortQueueEvidence(now=Date.now()) { try { const value=getEscortQueueEvidenceState(),lanes={}; for (const [lane,state] of Object.entries(value.lanes)) { const {reason,since,waitMs,...counters}=state,totals={...waitMs}; if (reason !== 'none') totals[reason]=(totals[reason] || 0) + Math.max(0,now - since); lanes[lane]={...counters,waitMs:totals}; } return {since:value.since,lanes,events:value.events.filter(row => row[0] >= now - 1800000), admissions:value.admissions.filter(row => row[1] >= now - 1800000).map(row => row.slice())}; } catch {return null;} } let escortRequestSequence=0; function setEscortQueueWait(lane,details,admission=null) { recordEscortQueueEvidence(lane,details,'',admission); const state=escortRequestQueueDiagnostics[lane]; if (details.waitReason && details.waitReason !== state.waitReason) state.waitSince=Date.now(); Object.assign(state,details); } const escortRequestWakeListeners=new Set(); const escortRequestStatusListeners=new Set(); let escortRequestPageClosed=false; let escortRequestCooldownMemory={until:0,revision:0,lastProblemAt:0,level:0,reason:'',reloadRequired:false}; function isEscortQueuedRequestUrl(url) { try {return /(^|\.)escort\.club$/i.test(new URL(url,location.href).hostname);} catch {return false;} } function normalizeEscortQueueTimestamp(value) { const number=Number(value); return Number.isFinite(number) && number > 0 && number <= 8640000000000000 ? number:0; } function readEscortRequestStart() { const value=GM_getValue(ESCORT_REQUEST_START_KEY,null) || {}; const legacy=['get','head'].map(name => GM_getValue(ESCORT_REQUEST_START_KEY + '_' + name,null) || {}); return {owner:String(value.owner || ''),leaseUntil:normalizeEscortQueueTimestamp(value.leaseUntil), lastStartedAt:Math.max(normalizeEscortQueueTimestamp(value.lastStartedAt),...legacy.map(item => normalizeEscortQueueTimestamp(item.lastStartedAt))), legacyLeaseUntil:Math.max(0,...legacy.map(item => item.owner ? normalizeEscortQueueTimestamp(item.leaseUntil):0))}; } function readEscortRequestCooldown() { let value=null; try {value=GM_getValue(ESCORT_REQUEST_COOLDOWN_KEY,null);} catch {} const revision=normalizeEscortQueueTimestamp(value?.revision) || normalizeEscortQueueTimestamp(value?.lastProblemAt); if (value && (revision > escortRequestCooldownMemory.revision || (revision === escortRequestCooldownMemory.revision && normalizeEscortQueueTimestamp(value.until) >= escortRequestCooldownMemory.until))) { const legacy403=!('reloadRequired' in value) && value.reason === 'HTTP 403' && value.until > Date.now(); const reloadRequired=value.reloadRequired === true || legacy403; escortRequestCooldownMemory={until:reloadRequired ? 0:normalizeEscortQueueTimestamp(value.until),revision,reloadRequired, lastProblemAt:normalizeEscortQueueTimestamp(value.lastProblemAt),ramp503At:normalizeEscortQueueTimestamp(value.ramp503At), level:Math.max(0,Math.min(4,Number(value.level) || 0)),reason:String(value.reason || ''), recovery503:!reloadRequired && value.recovery503 === true,recoveryStep:Math.max(0,Math.min(3,Number(value.recoveryStep) || 0)), recoveryProbeId:String(value.recoveryProbeId || ''),recoverySince:normalizeEscortQueueTimestamp(value.recoverySince), failures503:Array.isArray(value.failures503) ? value.failures503.filter(at => Number.isFinite(at) && at > Date.now() - 30000).slice(-3):[]}; if (legacy403) {try {writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory);} catch {}} } return escortRequestCooldownMemory; } function getEscort503RateFactor(cooldown,now=Date.now()) { if (!cooldown.ramp503At || cooldown.reloadRequired) return 1; const elapsed=now - cooldown.ramp503At; return cooldown.recovery503 || elapsed < 30000 ? 0.5:elapsed < 150000 ? 0.75:1; } function readEscortFastStart(now=Date.now()) { const value=GM_getValue(ESCORT_FAST_START_KEY,null) || {}; return {startedAt:normalizeEscortQueueTimestamp(value.startedAt),used:Math.max(0,Math.min(30,Number(value.used) || 0)), starts:(Array.isArray(value.starts) ? value.starts:[]).filter(at => Number.isFinite(at) && at > now - ESCORT_FAST_START_WINDOW_MS && at <= now).slice(-91)}; } function getEscortFastStartState(settings,cooldown,now=Date.now()) { const data=readEscortFastStart(now),expired=!data.startedAt || now - data.startedAt >= ESCORT_FAST_START_WINDOW_MS; const allowed=settings.escortFastStart !== false && getEscort503RateFactor(cooldown,now) === 1 && !cooldown.reloadRequired && !cooldown.recovery503 && cooldown.until <= now && (!cooldown.lastProblemAt || now - cooldown.lastProblemAt >= ESCORT_FAST_START_WINDOW_MS); return {...data,remaining:allowed ? expired ? data.starts.length <= 90 ? 30:0:30 - data.used:0}; } function isEscortRequestForeground() { return !escortRequestPageClosed && typeof document === 'object' && !document.hidden && document.visibilityState !== 'hidden' && document.hasFocus?.() === true && GM_getValue(ESCORT_REQUEST_FOREGROUND_KEY,'') === ESCORT_REQUEST_INSTANCE_ID; } function recordEscortFastStartDispatch(job,now) { const data=readEscortFastStart(now); if (job.permit.fastStart) { if (!data.startedAt || now - data.startedAt >= ESCORT_FAST_START_WINDOW_MS) {data.startedAt=now; data.used=0;} data.used=Math.min(30,data.used + 1); } data.starts.push(now); data.starts=data.starts.slice(-91); writeHelperStorageValue(ESCORT_FAST_START_KEY,data); } function getEscortRequestQueueStatus(job=null) { const cooldown=readEscortRequestCooldown(),raw=readHelperSettingsSnapshot(),settings=normalizeEscortQueueSettings(raw),queues={}; const fast=getEscortFastStartState(raw,cooldown),foreground=job ? job.foregroundWork && isEscortRequestForeground():!!GM_getValue(ESCORT_REQUEST_FOREGROUND_KEY,''); const multiplier=fast.remaining && foreground ? 2:1,rateFactor=getEscort503RateFactor(cooldown); const permits=getEscortActivePermits(),configuredLimitPerMinute=settings.escortLimitPerMinute,limitPerMinute=Math.max(1,Math.floor(configuredLimitPerMinute * multiplier * rateFactor)); for (const [lane,queue] of Object.entries(escortRequestQueues)) { queues[lane]={configuredLimitPerMinute,limitPerMinute,sharedConcurrencyLimit:true,sharedActive:permits.length,configuredMaxConcurrent:settings.escortMaxConcurrent,maxConcurrent:settings.escortMaxConcurrent * multiplier, queued:queue.jobs.length,active:queue.active,globalActive:permits.filter(permit => permit.lane === lane).length}; } const maxConcurrent=settings.escortMaxConcurrent * multiplier; return {configuredLimitPerMinute,limitPerMinute,rateFactor,ramp503At:cooldown.ramp503At || 0,sharedRateLimit:true,sharedConcurrencyLimit:true,concurrencyScope:'all-tabs', fastStartActive:multiplier === 2,fastStartRemaining:fast.remaining,fastStartRecentCount:fast.starts.length, level:cooldown.level,lastProblemAt:cooldown.lastProblemAt,configuredMaxConcurrent:maxConcurrent / multiplier,queues,maxConcurrent, queued:queues.get.queued + queues.head.queued,active:queues.get.active + queues.head.active,globalActive:permits.length, blockedUntil:cooldown.until,reloadRequired:cooldown.reloadRequired,reason:cooldown.reason, recovery503:cooldown.recovery503 === true,recoveryStep:cooldown.recoveryStep || 0, probePending:!!cooldown.recovery503 && permits.some(permit => permit.id === cooldown.recoveryProbeId)}; } function wakeEscortRequestQueue() { updateEscortCoordination(); for (const wake of [...escortRequestWakeListeners]) wake(); } function getEscortActivityDetailConcurrency() { return Math.max(ESCORT_ACTIVE_CHECK_CONCURRENT,getEscortRequestQueueStatus().queues.get.maxConcurrent); } function subscribeEscortRequestQueueStatus(listener) { escortRequestStatusListeners.add(listener); return () => escortRequestStatusListeners.delete(listener); } function notifyEscortRequestQueueStatus() { if (!escortRequestStatusListeners.size) return; const status=getEscortRequestQueueStatus(); for (const listener of escortRequestStatusListeners) {try {listener(status);} catch {}} } function reportEscortRequestQueue() { notifyEscortRequestQueueStatus(); publishLiveDiagnosticsReport(); } function waitForEscortRequestQueue(delayMs,cancelToken,lane=null,notBefore=0) { cancelToken?.throwIfCancelled(); return new Promise((resolve,reject) => { let timer=null,unsubscribe=() => {},finished=false; const waitMs=Math.max(1,Math.min(1000,Number(delayMs) || 1)),expectedAt=delayMs === null ? 0:Date.now() + waitMs; if (lane) setEscortQueueWait(lane,{wakeAt:expectedAt}); const finish=error => { if (finished) return; finished=true; clearTimeout(timer); unsubscribe(); escortRequestWakeListeners.delete(wake); if (lane) setEscortQueueWait(lane,{wakeAt:0,lastWakeAt:Date.now(),wakeLatenessMs:expectedAt ? Math.max(0,Date.now() - expectedAt):0}); if (error) reject(error); else resolve(); }; const wake=() => { if (Date.now() >= notBefore) finish(); else if (!finished && delayMs !== null) { clearTimeout(timer); timer=setTimeout(wake,Math.max(1,notBefore - Date.now())); } }; escortRequestWakeListeners.add(wake); if (delayMs !== null) timer=setTimeout(wake,waitMs); unsubscribe=cancelToken?.onCancel(reason => finish(createOperationCancelledError(reason))) || (() => {}); }); } function getEscortActivePermits(now=Date.now(),prune=false) { const result=[]; for (const key of GM_listValues()) { if (!key.startsWith(ESCORT_REQUEST_PERMIT_PREFIX)) continue; const value=GM_getValue(key,null); if (!value || !['get','head'].includes(value.lane) || !normalizeEscortQueueTimestamp(value.expiresAt) || value.expiresAt <= now) { if (prune) deleteHelperStorageValue(key); continue; } result.push({...value,key}); } return result; } function refreshEscortRequestPermits() { clearTimeout(escortRequestPermitTimer); escortRequestPermitTimer=null; for (const permit of escortRequestLocalPermits.values()) { try { const key=ESCORT_REQUEST_PERMIT_PREFIX + permit.id,shared=GM_getValue(key,null); if (!shared || shared.owner !== permit.owner || shared.expiresAt <= Date.now()) { if (shared?.expiresAt <= Date.now()) recordEscortQueueEvidence(permit.lane,{},'expiredReservations'); escortRequestLocalPermits.delete(permit.id); continue; } // Niewysłane rezerwacje też wygasają. if (!permit.startedAt) continue; permit.expiresAt=Date.now() + ESCORT_REQUEST_PERMIT_TTL_MS; writeHelperStorageValue(key,permit); } catch {} } if (escortRequestLocalPermits.size) escortRequestPermitTimer=setTimeout(refreshEscortRequestPermits,15000); } function releaseEscortSharedPermit(permit) { if (!permit) return; escortRequestLocalPermits.delete(permit.id); try { deleteHelperStorageValue(ESCORT_REQUEST_PERMIT_PREFIX + permit.id); writeHelperStorageValue(ESCORT_REQUEST_PERMITS_CHANGED_KEY,`${Date.now()}-${permit.id}`); } catch {} if (!escortRequestLocalPermits.size) {clearTimeout(escortRequestPermitTimer); escortRequestPermitTimer=null;} } function getEscortQueueJobPriority(job,now=Date.now()) { return job.probe ? 3:job.manual || now - job.queuedAt >= ESCORT_REQUEST_BACKGROUND_WAIT_MS ? 2:0; } function sortEscortQueueJobs(queue) { const now=Date.now(); queue.jobs.sort((a,b) => getEscortQueueJobPriority(b,now) - getEscortQueueJobPriority(a,now) || a.queuedAt - b.queuedAt); } function readEscort503Failures(now=Date.now(),prune=false) { const result=[]; for (const key of GM_listValues()) { if (!key.startsWith(ESCORT_REQUEST_FAILURE_PREFIX)) continue; const at=Number(GM_getValue(key,0)); if (at > now - 30000 && at <= now) result.push(at); else if (prune) deleteHelperStorageValue(key); } return result.sort((a,b) => a - b).slice(-3); } function clearEscort503Failures() { for (const key of GM_listValues()) if (key.startsWith(ESCORT_REQUEST_FAILURE_PREFIX)) deleteHelperStorageValue(key); } function synchronizeEscort503Failures() { const previous=readEscortRequestCooldown(),now=Date.now(); if (previous.reloadRequired || previous.recovery503) return previous; const failures=readEscort503Failures(now,true); if (failures.length < 3) return previous; escortRequestCooldownMemory={...previous,until:Math.max(previous.until,now + 10000),revision:Math.max(now,previous.revision + 1), reason:'HTTP 503',lastProblemAt:failures[2],ramp503At:Math.max(previous.until,now + 10000),recovery503:true,recoveryStep:1,recoverySince:now,recoveryProbeId:'',failures503:failures}; writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory); return escortRequestCooldownMemory; } function getEscortRequestTickets(lane,now=Date.now()) { const foreground=GM_getValue(ESCORT_REQUEST_FOREGROUND_KEY,''),raw=readHelperSettingsSnapshot(),settings=normalizeEscortQueueSettings(raw); const fast=getEscortFastStartState(raw,readEscortRequestCooldown(),now); const permits=getEscortActivePermits(now); return GM_listValues().filter(key => key.startsWith(ESCORT_REQUEST_TICKET_PREFIX)).map(key => { const value=GM_getValue(key,null) || {},type=value.lane || (key.startsWith(ESCORT_REQUEST_TICKET_PREFIX + 'head_') ? 'head':'get'); const queuedAt=normalizeEscortQueueTimestamp(value.queuedAt),updatedAt=normalizeEscortQueueTimestamp(value.updatedAt); if (updatedAt + 60000 < now) deleteHelperStorageValue(key); const active=key === ESCORT_REQUEST_TICKET_PREFIX + type + '_' + foreground; const cap=settings.escortMaxConcurrent * (fast.remaining && value.foregroundWork && active ? 2:1); return {key,claim:String(value.claim || ''),claimAt:normalizeEscortQueueTimestamp(value.claimAt),servedAt:normalizeEscortQueueTimestamp(value.servedAt),lane:type,probe:value.probe === true,queuedAt,updatedAt, available:permits.length < cap, priority:value.manual || now - queuedAt >= ESCORT_REQUEST_BACKGROUND_WAIT_MS ? 2: key === ESCORT_REQUEST_TICKET_PREFIX + type + '_' + foreground ? 1:0}; }).filter(ticket => ticket.queuedAt > 0 && ticket.updatedAt + ESCORT_REQUEST_TURN_MS > now && ticket.available) .sort((a,b) => Number(b.probe) - Number(a.probe) || b.priority - a.priority || a.servedAt - b.servedAt || a.queuedAt - b.queuedAt || a.key.localeCompare(b.key)); } function updateEscortRequestForeground() { try { const owner=GM_getValue(ESCORT_REQUEST_FOREGROUND_KEY,''); const focused=!escortRequestPageClosed && typeof document === 'object' && !document.hidden && document.visibilityState !== 'hidden' && document.hasFocus?.() === true; if (focused && owner !== ESCORT_REQUEST_INSTANCE_ID) writeHelperStorageValue(ESCORT_REQUEST_FOREGROUND_KEY,ESCORT_REQUEST_INSTANCE_ID); else if (!focused && owner === ESCORT_REQUEST_INSTANCE_ID) deleteHelperStorageValue(ESCORT_REQUEST_FOREGROUND_KEY); } catch {} } const ESCORT_COORD_PREFIX=ESCORT_REQUEST_QUEUE_PREFIX + 'peer_'; const ESCORT_COORD_SIGNAL=ESCORT_REQUEST_QUEUE_PREFIX + 'coordination'; const ESCORT_COORD_JOIN_MS=2500; let escortCoordPeer=null,escortCoordUpdating=false; function readEscortCoordPeers(now=Date.now()) { const peers=[]; for (const key of GM_listValues()) { if (!key.startsWith(ESCORT_COORD_PREFIX)) continue; const peer=GM_getValue(key,{}); if (Math.max(peer.until || 0,peer.lastStartedAt || 0) < now - ESCORT_REQUEST_PERMIT_TTL_MS) deleteHelperStorageValue(key); else if (peer.until > now && peer.until <= now + ESCORT_REQUEST_TURN_MS) peers.push(peer); } return peers; } // Głosy należą do pojedynczych kart; rezerwacja wymaga zgody wszystkich aktywnych uczestników. function updateEscortCoordination(join=false) { if (escortCoordUpdating || escortRequestPageClosed) return; join=join || escortRequestLocalPermits.size > 0 || Object.values(escortRequestQueues).some(queue => queue.jobs.length && (queue.jobs[0].probe || !readEscortRequestCooldown().reloadRequired)); escortCoordUpdating=true; try { const now=Date.now(),key=ESCORT_COORD_PREFIX + ESCORT_REQUEST_INSTANCE_ID; if (!escortCoordPeer || escortCoordPeer.until <= now) { if (!join) return; if (escortCoordPeer) for (const [lane,queue] of Object.entries(escortRequestQueues)) { if (queue.jobs.length) recordEscortQueueEvidence(lane,{},'expiredStartLeases'); } escortCoordPeer={until:now + ESCORT_REQUEST_TURN_MS,readyAt:now + ESCORT_COORD_JOIN_MS,vote:'',voteUntil:0,lastStartedAt:escortCoordPeer?.lastStartedAt || 0}; } const peer=escortCoordPeer,before=JSON.stringify(GM_getValue(key,null)); if (join && peer.until < now + ESCORT_REQUEST_TURN_MS / 2) peer.until=now + ESCORT_REQUEST_TURN_MS; const tickets=getEscortRequestTickets(null,now),permits=getEscortActivePermits(now); peer.permits=[...escortRequestLocalPermits.values()].map(({id,lane,startedAt,expiresAt}) => ({id,lane,startedAt,expiresAt})); const retained=peer.voteUntil > now && (tickets.some(ticket => ticket.claim === peer.vote) || permits.some(permit => permit.turn === peer.vote && !permit.startedAt)); if (!retained) { peer.vote=now >= peer.readyAt ? tickets.filter(ticket => ticket.claim).sort((a,b) => a.claimAt - b.claimAt || a.claim.localeCompare(b.claim))[0]?.claim || '':''; peer.voteUntil=peer.vote ? Math.min(peer.until,now + ESCORT_REQUEST_TURN_MS):0; } if (before !== JSON.stringify(peer)) { writeHelperStorageValue(key,peer); writeHelperStorageValue(ESCORT_COORD_SIGNAL,`${now}-${Math.random()}`); } } catch {} finally {escortCoordUpdating=false;} } function getEscortCoordinationTurn(claim,now=Date.now()) { updateEscortCoordination(true); const peers=readEscortCoordPeers(now); return {readyAt:Math.max(now,...peers.map(peer => peer.readyAt || 0)), until:peers.length && peers.every(peer => peer.vote === claim && peer.voteUntil > now) ? Math.min(...peers.map(peer => Math.min(peer.until,peer.voteUntil))):0, lastStartedAt:Math.max(0,...peers.map(peer => peer.lastStartedAt || 0)), permits:[...new Map([...getEscortActivePermits(now,true),...peers.flatMap(peer => peer.permits || [])].filter(permit => permit.expiresAt > now).map(permit => [permit.id,permit])).values()]}; } async function acquireEscortSharedRequestStart(cancelToken,job,lane) { const queue=escortRequestQueues[lane],ticketKey=ESCORT_REQUEST_TICKET_PREFIX + lane + '_' + ESCORT_REQUEST_INSTANCE_ID; let claim='',claimAt=0,attempt=0; const publish=() => { const previous=GM_getValue(ticketKey,null); writeHelperStorageValue(ticketKey,{claim,claimAt,queuedAt:job.queuedAt,servedAt:Math.max(escortRequestQueues.get.lastStartedAt,escortRequestQueues.head.lastStartedAt),updatedAt:Date.now(),probe:job.probe,manual:job.manual,foregroundWork:job.foregroundWork && isEscortRequestForeground(),lane}); if (!previous || previous.claim !== claim) writeHelperStorageValue(ESCORT_COORD_SIGNAL,`${Date.now()}-${Math.random()}`); }; try { while (true) { cancelToken.throwIfCancelled(); if (escortRequestPageClosed) throw createOperationCancelledError('Zamknięto stronę'); sortEscortQueueJobs(queue); if (queue.jobs[0] !== job) return false; let now=Date.now(),cooldown=synchronizeEscort503Failures(); if (!job.probe && (cooldown.reloadRequired || cooldown.until > now)) { deleteHelperStorageValue(ticketKey); updateEscortCoordination(); claim=''; claimAt=0; setEscortQueueWait(lane,{waitReason:cooldown.reloadRequired ? 'reload-required':'cooldown',nextEligibleAt:cooldown.until}); await waitForEscortRequestQueue(cooldown.reloadRequired ? null:cooldown.until - now,cancelToken,lane); continue; } publish(); const tickets=getEscortRequestTickets(null,now),claimant=tickets.filter(ticket => ticket.claim).sort((a,b) => a.claimAt - b.claimAt || a.claim.localeCompare(b.claim))[0]; if (claim && claimant?.claim !== claim) { recordEscortQueueEvidence(lane,{},'leaseConflicts'); recordEscortQueueEvidence(lane,{},'admissionRetries'); claim=''; claimAt=0; publish(); } else if (!claim && !claimant && tickets[0]?.key === ticketKey) {claim=ticketKey + '_' + job.id + '_' + ++attempt; claimAt=now; publish();} const turn=getEscortCoordinationTurn(claim,now),state=readEscortRequestStart(),status=getEscortRequestQueueStatus(job),permits=turn.permits; const lastStartedAt=Math.max(state.lastStartedAt,turn.lastStartedAt),gap=Math.ceil(60000 / status.limitPerMinute); const readyAt=Math.max(job.probe ? 0:cooldown.until,lastStartedAt ? lastStartedAt + gap:0,state.legacyLeaseUntil,state.owner ? state.leaseUntil:0,turn.readyAt); const saturated=permits.length >= status.maxConcurrent,dispatchWaiting=permits.some(permit => !permit.startedAt); const probeWaiting=!job.probe && cooldown.recovery503 && permits.length > 0; if (readyAt > now || saturated || probeWaiting || dispatchWaiting || !claim || !turn.until) { const waitReason=dispatchWaiting ? 'pending-dispatch':saturated ? 'global-concurrency-limit':probeWaiting ? 'recovery-probe': state.owner && readyAt === state.leaseUntil ? 'start-lease':readyAt === state.legacyLeaseUntil ? 'legacy-lease': turn.readyAt > now && readyAt === turn.readyAt ? 'verifying-start':readyAt > now ? 'rate-limit':'other-tab-ticket'; setEscortQueueWait(lane,{waitReason,nextEligibleAt:readyAt > now ? readyAt:0}); await waitForEscortRequestQueue(readyAt > now ? readyAt - now:1000,cancelToken,lane); if (readyAt > now && Date.now() >= readyAt) writeHelperStorageValue(ESCORT_COORD_SIGNAL,'wake:' + Date.now()); continue; } cancelToken.throwIfCancelled(); job.permit={id:ESCORT_REQUEST_INSTANCE_ID + '_' + job.id,lane,owner:ESCORT_REQUEST_INSTANCE_ID, admittedAt:now,startedAt:0,expiresAt:turn.until,turn:claim,turnUntil:turn.until, fastStart:status.fastStartActive,limitPerMinute:status.limitPerMinute,recovery503:!job.probe && cooldown.recovery503 === true}; writeHelperStorageValue(ESCORT_REQUEST_PERMIT_PREFIX + job.permit.id,job.permit); escortRequestLocalPermits.set(job.permit.id,job.permit); refreshEscortRequestPermits(); if (job.permit.recovery503) { escortRequestCooldownMemory={...cooldown,recoveryProbeId:job.permit.id,revision:Math.max(now,cooldown.revision + 1)}; writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory); } job.admission=[job.queuedAt,now,0,lane,permits.length,lastStartedAt,status.limitPerMinute,status.maxConcurrent,Number(status.fastStartActive)]; queue.lastStartedAt=now; writeHelperStorageValue(ESCORT_REQUEST_START_KEY,{owner:'',leaseUntil:0,lastStartedAt:now}); return true; } } finally {deleteHelperStorageValue(ticketKey); updateEscortCoordination(); writeHelperStorageValue(ESCORT_COORD_SIGNAL,`${Date.now()}-${Math.random()}`);} } async function pumpEscortRequestQueue(lane) { const queue=escortRequestQueues[lane]; if (queue.pumping) return; queue.pumping=true; try { while (queue.jobs.length) { sortEscortQueueJobs(queue); const job=queue.jobs[0]; try { if (!job.probe && readEscortRequestCooldown().reloadRequired) { setEscortQueueWait(lane,{waitReason:'reload-required',nextEligibleAt:0}); await waitForEscortRequestQueue(null,job.token,lane); continue; } if (queue.active >= getEscortRequestQueueStatus(job).queues[lane].maxConcurrent) { setEscortQueueWait(lane,{waitReason:'concurrency-limit',nextEligibleAt:0}); await waitForEscortRequestQueue(1000,job.token,lane); continue; } if (!await acquireEscortSharedRequestStart(job.token,job,lane)) continue; job.token.throwIfCancelled(); if (queue.jobs[0] !== job) {releaseEscortSharedPermit(job.permit); continue;} queue.jobs.shift(); job.off(); job.settled=true; queue.active++; setEscortQueueWait(lane,{waitReason:'none',nextEligibleAt:0,wakeAt:0},job.admission); reportEscortRequestQueue(); let released=false,started=false; const release=() => { if (released) return; released=true; releaseEscortSharedPermit(job.permit); queue.active--; try {reportEscortRequestQueue();} catch {} wakeEscortRequestQueue(); }; let observed503=false; release.observeResponse=(response,complete=true) => { if (Number(response?.status) === 503) {if (observed503) return; observed503=true;} return observeEscortRequestResponse(job.url,response,{permitId:job.permit.id,startedAt:job.permit.startedAt,complete}); }; release.beforeDispatch=() => { const state=readEscortRequestCooldown(); if (job.probe && state.revision !== job.probeRevision) throw createOperationCancelledError('Zmienił się stan blokady Escort.club'); if (!job.probe && state.reloadRequired) throw createEscortRequestBlockedError(state,403); if (!job.probe && state.until > Date.now()) throw createEscortRequestBlockedError(state,state.recovery503 ? 503:429); const shared=GM_getValue(ESCORT_REQUEST_PERMIT_PREFIX + job.permit.id,null); const limits=getEscortRequestQueueStatus(job); if (job.permit.turnUntil <= Date.now() || released || !escortRequestLocalPermits.has(job.permit.id) || !shared || shared.owner !== job.permit.owner || shared.expiresAt <= Date.now() || job.permit.limitPerMinute > limits.limitPerMinute || (job.permit.fastStart && !limits.fastStartActive)) { recordEscortQueueEvidence(lane,{},'permitReschedules'); if (shared?.expiresAt <= Date.now() && escortRequestLocalPermits.has(job.permit.id)) recordEscortQueueEvidence(lane,{},'expiredReservations'); escortRequestLocalPermits.delete(job.permit.id); const error=new Error('Rezerwacja lub limity zapytania zmieniły się. Ponowienie przez wspólną kolejkę.'); error.code='ESCORT_QUEUE_RESCHEDULE'; error.status=0; error.retryable=true; throw error; } }; release.markStarted=() => { if (started) return; if (released || job.permit.turnUntil <= Date.now()) release.beforeDispatch(); started=true; const now=Date.now(); try {recordEscortFastStartDispatch(job,now);} catch (error) {if (job.permit.fastStart) throw error;} if (escortQueueEvidenceState?.admissions.includes(job.admission)) job.admission[2]=now; escortCoordPeer.lastStartedAt=now; job.permit.startedAt=now; refreshEscortRequestPermits(); queue.lastStartedAt=Math.max(queue.lastStartedAt,now); try { const state=readEscortRequestStart(); writeHelperStorageValue(ESCORT_REQUEST_START_KEY,{...state,lastStartedAt:Math.max(state.lastStartedAt,now)}); } catch {} wakeEscortRequestQueue(); }; job.resolve(release); } catch (error) { releaseEscortSharedPermit(job.permit); const index=queue.jobs.indexOf(job); if (index >= 0) queue.jobs.splice(index,1); job.off(); job.settled=true; job.reject(error); setEscortQueueWait(lane,{wakeAt:0}); } } } finally {queue.pumping=false; setEscortQueueWait(lane,{wakeAt:0,waitReason:queue.jobs.length ? escortRequestQueueDiagnostics[lane].waitReason:'none'});} } async function acquireEscortRequestSlot(url,cancelToken=null,probe=false,method='GET',requestContext=null) { cancelToken?.throwIfCancelled(); if (!isEscortQueuedRequestUrl(url)) return () => {}; if (escortRequestPageClosed) throw createOperationCancelledError('Zamknięto stronę'); const lane=String(method).toUpperCase() === 'HEAD' ? 'head':'get',queue=escortRequestQueues[lane]; return new Promise((resolve,reject) => { const job={resolve,reject,token:createOperationCancelToken('Zapytanie Escort.club'),off:() => {},settled:false,probe:probe === true, probeRevision:probe ? readEscortRequestCooldown().revision:0,id:'q' + ++escortRequestSequence,queuedAt:Date.now()}; job.url=url; job.manual=requestContext?.manual === true || /manual|^tile\.related$/.test(requestContext?.source || ''); job.foregroundWork=!job.probe && (job.manual || !/^(?:watch[.-]|cache-bridge$)/.test(requestContext?.source || '')); if (job.probe) queue.jobs.unshift(job); else queue.jobs.push(job); sortEscortQueueJobs(queue); reportEscortRequestQueue(); job.off=cancelToken?.onCancel(reason => { if (job.settled) return; job.settled=true; job.token.cancel(reason); const index=queue.jobs.indexOf(job); if (index >= 0) queue.jobs.splice(index,1); job.off(); reject(createOperationCancelledError(reason)); reportEscortRequestQueue(); wakeEscortRequestQueue(); }) || (() => {}); wakeEscortRequestQueue(); pumpEscortRequestQueue(lane); }); } function isEscortRequestBlockedError(error) { return error?.code === 'ESCORT_REQUEST_BLOCKED'; } function isEscortAntifloodResponse(html) { const source=String(html || '').replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi,''); const marker=/\b(?:anti|anty)[\s-]*flood(?:ow\w*)?\b|\bflood control\b|\btoo many requests\b|zbyt (?:wiele|du[zż]o) zapyta[nń]/i; if (/^\s*\{/.test(source)) { try { const value=JSON.parse(source); return !value.phone && !value.number && [value.error,value.message].some(text => typeof text === 'string' && marker.test(text)); } catch {return false;} } const notices=source.match(/<(?:title|h[12])\b[^>]*>[\s\S]*?<\/(?:title|h[12])>|<(?:p|div)\b[^>]*class\s*=\s*["'][^"']*\b(?:error|alert|warning)\b[^"']*["'][^>]*>[\s\S]*?<\/(?:p|div)>/gi) || []; if (notices.some(value => marker.test(value.replace(/<[^>]*>/g,' ')))) return true; const text=source.replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim(); return text.length < 1200 && /^(?:system |kontrola |ochrona |error[: ]*|błąd[: ]*)?(?:(?:anti|anty)[\s-]*flood(?:ow\w*)?|flood control|too many requests|zbyt (?:wiele|du[zż]o) zapyta[nń])\b/i.test(text); } function createEscortRequestBlockedError(state,status) { const message=state.reloadRequired ? 'Wstrzymano zapytania do odświeżenia strony.':state.until > Date.now() ? `Wstrzymano zapytania do ${new Date(state.until).toLocaleTimeString('pl-PL')}.`:'Ponowienie przez wspólną kolejkę.'; const error=new Error(`Escort.club: ${state.reason}. ${message}`); error.code='ESCORT_REQUEST_BLOCKED'; error.status=status; error.retryable=status === 503 && !state.reloadRequired && state.reason === 'HTTP 503'; error.blockedUntil=state.until; error.reloadRequired=state.reloadRequired; return error; } function refineEscortRequestBlockedError(url,response,error) { if (error?.reloadRequired || !isEscortQueuedRequestUrl(url) || !isEscortRequestBlockedError(error)) return error; const html=response?.responseText || ''; if (error.status === 503 && (isEscortAntifloodResponse(html) || isEscortAccessChallengeResponse(html,response?.headers || response?.responseHeaders))) { try {observeEscortRequestResponse(url,{...response,status:503});} catch (refined) {return refined;} } if (!isEscortAntifloodResponse(html)) return error; const state=readEscortRequestCooldown(); if (state.until === error.blockedUntil && state.reason !== 'Kontrola antyfloodowa Escort.club') { escortRequestCooldownMemory={...state,reason:'Kontrola antyfloodowa Escort.club'}; try {writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory);} catch {} reportEscortRequestQueue(); } error.retryable=false; error.message=`Escort.club: kontrola antyfloodowa. Wstrzymano zapytania do ${new Date(error.blockedUntil).toLocaleTimeString('pl-PL')}.`; return error; } function isEscortAccessChallengeResponse(html,headers) { return getResponseHeader(headers,'cf-mitigated').toLowerCase() === 'challenge' || (/<title[^>]*>\s*(?:just a moment|attention required|checking your browser)/i.test(html) && /cf-chl-|challenge-platform|cloudflare/i.test(html)) || (/<title[^>]*>\s*(?:access denied|forbidden|request blocked|site unavailable)/i.test(html) && /<h[12][^>]*>\s*(?:access denied|forbidden|request blocked)|your (?:ip|access).{0,50}(?:blocked|banned)|cloudflare/i.test(html)); } function observeEscortRequestResponse(url,response={},context={}) { if (!isEscortQueuedRequestUrl(url)) return; const status=Number(response.status) || 0,headers=response.headers || response.responseHeaders; const retryAfter=getResponseHeader(headers,'retry-after'),now=Date.now(); let retryUntil=0; if (/^\d+(?:\.\d+)?$/.test(retryAfter)) retryUntil=now + Number(retryAfter) * 1000; else if (retryAfter && Number.isFinite(Date.parse(retryAfter))) retryUntil=Date.parse(retryAfter); retryUntil=normalizeEscortQueueTimestamp(retryUntil); const html=typeof response.responseText === 'string' ? response.responseText.slice(0,120000):''; const antiflood=isEscortAntifloodResponse(html); const challenge=isEscortAccessChallengeResponse(html,headers); const previous=readEscortRequestCooldown(); if (!previous.reloadRequired && status === 503 && !challenge && !antiflood) { writeHelperStorageValue(ESCORT_REQUEST_FAILURE_PREFIX + (context.permitId || ESCORT_REQUEST_INSTANCE_ID + '_' + ++escort503FailureSequence),now); const failures=readEscort503Failures(now,true); const probeFailed=previous.recovery503 && context.permitId === previous.recoveryProbeId; const recovery503=previous.recovery503 || failures.length >= 3 || retryUntil > now; const recoveryStep=recovery503 ? Math.min(3,Math.max(1,(previous.recoveryStep || 0) + (probeFailed ? 1:0))):0; const until=recovery503 ? Math.max(previous.until,retryUntil, !previous.recovery503 || probeFailed ? now + recoveryStep * 10000:0):previous.until; escortRequestCooldownMemory={...previous,until,revision:Math.max(now,previous.revision + 1),reason:'HTTP 503', lastProblemAt:now,ramp503At:Math.max(now,until),recovery503,recoveryStep,recoveryProbeId:probeFailed ? '':previous.recoveryProbeId || '', recoverySince:previous.recovery503 ? previous.recoverySince:recovery503 ? now:0,failures503:failures}; writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory); reportEscortRequestQueue(); wakeEscortRequestQueue(); throw createEscortRequestBlockedError(escortRequestCooldownMemory,503); } if (status !== 403 && status !== 429 && !challenge && !antiflood) { if (!previous.reloadRequired && previous.recovery503 && context.complete !== false && status >= 200 && status < 400 && context.permitId && context.permitId === previous.recoveryProbeId && context.startedAt >= previous.recoverySince) { escortRequestCooldownMemory={...previous,until:0,reason:'',level:0,recovery503:false,recoveryStep:0, recoveryProbeId:'',recoverySince:0,failures503:[],ramp503At:now,revision:Math.max(now,previous.revision + 1)}; clearEscort503Failures(); writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory); reportEscortRequestQueue(); wakeEscortRequestQueue(); } return; } const reloadRequired=status === 403 || previous.reloadRequired,samePause=previous.reloadRequired || (previous.until > now && !previous.recovery503); const level=reloadRequired ? 1:samePause ? Math.max(1,previous.level):previous.lastProblemAt && now <= previous.until + 15 * 60 * 1000 ? Math.min(4,previous.level + 1):1; const reason=reloadRequired ? 'HTTP 403':antiflood ? 'Kontrola antyfloodowa Escort.club':challenge ? 'Kontrola dostępu Escort.club':`HTTP ${status}`; const until=reloadRequired ? 0:Math.max(previous.until,retryUntil,samePause ? 0:now + Math.min(30 * 60 * 1000,5 * 60 * 1000 * 2 ** (level - 1))); escortRequestCooldownMemory={until,reloadRequired,revision:Math.max(now,previous.revision + 1),lastProblemAt:samePause ? previous.lastProblemAt || now:now, level,reason,ramp503At:reloadRequired ? 0:status === 503 || getEscort503RateFactor(previous,now) < 1 ? until:0,recovery503:false,recoveryStep:0,recoveryProbeId:'',recoverySince:0,failures503:[]}; try {clearEscort503Failures(); writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory);} catch {} reportEscortRequestQueue(); wakeEscortRequestQueue(); throw createEscortRequestBlockedError(escortRequestCooldownMemory,status); } try { for (const key of [ESCORT_REQUEST_START_KEY,ESCORT_REQUEST_START_KEY + '_get',ESCORT_REQUEST_START_KEY + '_head',ESCORT_REQUEST_FOREGROUND_KEY,ESCORT_REQUEST_PERMITS_CHANGED_KEY,ESCORT_COORD_SIGNAL]) { GM_addValueChangeListener(key,wakeEscortRequestQueue); } GM_addValueChangeListener(ESCORT_REQUEST_COOLDOWN_KEY,(_key,previous,next) => { reportEscortRequestQueue(); wakeEscortRequestQueue(); if (next?.reloadRequired && !previous?.reloadRequired) publishLiveDiagnosticsReport(true); }); } catch {} window.addEventListener('pagehide',() => { escortRequestPageClosed=true; try {if (escortCoordPeer) {escortCoordPeer.until=0; writeHelperStorageValue(ESCORT_COORD_PREFIX + ESCORT_REQUEST_INSTANCE_ID,escortCoordPeer);}} catch {} updateEscortRequestForeground(); clearTimeout(escortRequestPermitTimer); escortRequestPermitTimer=null; for (const [lane,queue] of Object.entries(escortRequestQueues)) { for (const job of [...queue.jobs]) job.token.cancel('Zamknięto stronę'); try {deleteHelperStorageValue(ESCORT_REQUEST_TICKET_PREFIX + lane + '_' + ESCORT_REQUEST_INSTANCE_ID);} catch {} } wakeEscortRequestQueue(); }); function resumeEscortRequestQueue() { if (escortRequestPageClosed) return; updateEscortRequestForeground(); refreshEscortRequestPermits(); wakeEscortRequestQueue(); for (const lane of ['get','head']) if (escortRequestQueues[lane].jobs.length) void pumpEscortRequestQueue(lane); notifyEscortRequestQueueStatus(); } window.addEventListener('pageshow',() => {escortRequestPageClosed=false; resumeEscortRequestQueue();}); window.addEventListener('focus',resumeEscortRequestQueue); window.addEventListener('blur',updateEscortRequestForeground); if (typeof document === 'object') { document.addEventListener('visibilitychange',() => {if (document.hidden) updateEscortRequestForeground(); else resumeEscortRequestQueue();}); document.addEventListener('resume',resumeEscortRequestQueue); } updateEscortRequestForeground(); const ESCORT_COOLDOWN_PROBE_KEY=ESCORT_REQUEST_QUEUE_PREFIX + 'probe'; const ESCORT_COOLDOWN_PROBE_TIMEOUT_MS=15000; let escortCooldownProbeStarted=false; const escortCooldownStartupState={...readEscortRequestCooldown()}; function canProbeEscortCooldownOnStartup() { try { if (window.top !== window.self || escortRequestPageClosed) return false; const page=new URL(location.href),hash=new URLSearchParams(page.hash.slice(1)); if (!isEscortClubHost(page.hostname) && !/^(?:www\.)?(?:escorti\.pl|garsoniera\.com\.pl)$/.test(page.hostname)) return false; if (['vm_mode',PERSISTENT_CACHE_BRIDGE_PARAM,FAVORITE_SEARCH_EDIT_PARAM] .some(key => page.searchParams.has(key)) || hash.has(GARSO_OPEN_BRIDGE_PARAM) || hash.has('vm_garso_topic_worker')) return false; if (hash.has('vm_cached_template') || hash.get('vm_cached_archive_view') === '1') return false; if (getEscortCachedArchiveMarkerFromHash() && !getEscortAdIdFromPath(page.pathname)) return false; if (isEscortClubHost(page.hostname) && page.pathname === '/' && readRecentEscortCachedAdNavigation()) return false; return !sessionStorage.getItem(FAVORITE_SEARCH_EDIT_KEY); } catch {return false;} } function isConfirmedEscortCooldownProbeResponse(response) { if (Number(response?.status) !== 200 || typeof response.responseText !== 'string') return false; try { const finalUrl=new URL(response.finalUrl); if (finalUrl.protocol !== 'https:' || !isEscortClubHost(finalUrl.hostname)) return false; const adId=getEscortAdIdFromPath(finalUrl.pathname); if (!adId && finalUrl.pathname !== '/' && !isEscortSearchPath(finalUrl.pathname)) return false; const html=response.responseText,headers=response.responseHeaders || response.headers; const type=getResponseHeader(headers,'content-type'); if (type && !/^(?:text\/html|application\/xhtml\+xml)\b/i.test(type)) return false; if (/<title\b[^>]*>[^<]*\b(?:captcha|log[\s-]*in|sign[\s-]*in|logowanie|zaloguj)\b/i.test(html) || getResponseHeader(headers,'cf-mitigated').toLowerCase() === 'challenge') return false; if (isEscortAntifloodResponse(html) || classifyResponseProtection(200,html,headers).blockSignals.some(signal => signal !== 'captcha')) return false; const doc=new DOMParser().parseFromString(html,'text/html'); if (adId) return !!doc.querySelector('.content-info-col .content-name h1, .content-name.-desc-name h1') && !!doc.querySelector('.content-desc, .content-contact, .content-prices, .galleryContainer'); return !!findEscortSearchLocationControls(doc) || [...doc.querySelectorAll('.item-col.col > a[href]')].some(link => { const url=new URL(link.getAttribute('href'),finalUrl); return isEscortClubHost(url.hostname) && /^\/(?:anons|personal-ads)\/\d+\.html\/?$/.test(url.pathname); }); } catch {return false;} } function clearEscortCooldownAfterProbe(target) { const current=readEscortRequestCooldown(); if (current.revision !== target.revision || current.until !== target.until || current.lastProblemAt !== target.lastProblemAt) return false; const next={...current,until:0,reloadRequired:false,recovery503:false,recoveryStep:0,recoveryProbeId:'',recoverySince:0,failures503:[],ramp503At:current.reloadRequired ? 0:current.ramp503At ? Date.now():0, revision:Math.max(Date.now(),current.revision + 1)}; try {writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,next);} catch {return false;} clearEscort503Failures(); escortRequestCooldownMemory=next; reportEscortRequestQueue(); wakeEscortRequestQueue(); return true; } async function probeEscortCooldownOnStartup() { if (escortCooldownProbeStarted || !canProbeEscortCooldownOnStartup()) return; escortCooldownProbeStarted=true; const navigation=typeof performance === 'object' ? performance.getEntriesByType?.('navigation')?.[0]:null; if (isEscortClubHost(location.hostname) && navigation?.responseStatus === 403) { try {recordDiagnosticResponse(location.href,403); observeEscortRequestResponse(location.href,{status:403});} catch {} return; } const target={...readEscortRequestCooldown()}; if (target.recovery503 && !target.reloadRequired) return; if (target.reloadRequired) { if (!escortCooldownStartupState.reloadRequired || target.revision !== escortCooldownStartupState.revision) return; } else if (target.until <= Date.now()) return; // Świeża odpowiedź dokumentu może potwierdzić dostęp bez dodatkowego GET-a. if (isEscortClubHost(location.hostname) && ['reload','navigate'].includes(navigation?.type) && navigation.responseStatus === 200 && navigation.transferSize > 0 && !navigation.workerStart && Number(performance.timeOrigin) + Number(navigation.requestStart) >= target.revision && isConfirmedEscortCooldownProbeResponse({status:200,finalUrl:location.href,responseText:document.documentElement?.outerHTML})) { clearEscortCooldownAfterProbe(target); return; } if (target.reloadRequired && navigation?.type !== 'reload') return; const token=createOperationCancelToken('Sprawdzenie dostępu do Escort.club'),owner=ESCORT_REQUEST_INSTANCE_ID; const timeoutMs=ESCORT_COOLDOWN_PROBE_TIMEOUT_MS + Math.ceil(60000 / getEscortRequestQueueStatus().queues.get.limitPerMinute); let lease=null,timer=null,handle=null,release=() => {}; const pagehide=() => token.cancel('Zamknięto stronę'); try { const now=Date.now(),previous=GM_getValue(ESCORT_COOLDOWN_PROBE_KEY,null); if (normalizeEscortQueueTimestamp(previous?.until) > now) return; lease={owner,until:now + timeoutMs + 5000}; writeHelperStorageValue(ESCORT_COOLDOWN_PROBE_KEY,lease); await waitWithCancellation(40,token); if (GM_getValue(ESCORT_COOLDOWN_PROBE_KEY,null)?.owner !== owner || lease.until <= Date.now()) return; window.addEventListener('pagehide',pagehide,{once:true}); timer=setTimeout(() => token.cancel('Upłynął czas sprawdzania dostępu'),timeoutMs); const origin=isEscortClubHost(location.hostname) ? location.origin:'https://pl.escort.club'; const url=`${origin}/?vm_access_probe=${Date.now()}-${owner}`; release=await acquireEscortRequestSlot(url,token,true,'GET'); token.throwIfCancelled(); const current=readEscortRequestCooldown(); if ((!current.reloadRequired && current.until <= Date.now()) || current.revision !== target.revision || current.until !== target.until) { return; } await new Promise(resolve => { let finished=false,trace=null,off=() => {}; const finish=(response,outcome,error=null) => { if (finished) return; finished=true; off(); trace?.(response?.status || error?.status,outcome,response || error); try { if (outcome === 'response') recordDiagnosticResponse(url,response?.status); else recordDiagnosticRequestFailure(url,error || response); observeEscortRequestResponse(url,response || {}); if (outcome === 'response' && isConfirmedEscortCooldownProbeResponse(response)) clearEscortCooldownAfterProbe(target); } catch {} resolve(); }; off=token.onCancel(reason => { finish(null,'cancelled',createOperationCancelledError(reason)); try {handle?.abort?.();} catch {} }); try { release.beforeDispatch?.(); trace=recordDiagnosticRequest(url,'GET',{source:'access.probe'}); handle=GM_xmlhttpRequest({url,method:'GET',timeout:ESCORT_COOLDOWN_PROBE_TIMEOUT_MS,nocache:true, headers:{Accept:'text/html,application/xhtml+xml','Cache-Control':'no-cache','Pragma':'no-cache'}, onload:response => finish(response,'response'),onerror:response => finish(response,'error',response), ontimeout:response => finish(response,'timeout',response), onabort:() => finish(null,'cancelled',createOperationCancelledError('Przerwano sprawdzanie dostępu'))}); release.markStarted?.(); } catch (error) {finish(null,'error',error);} }); } catch {} finally { clearTimeout(timer); window.removeEventListener('pagehide',pagehide); release(); try { if (lease && GM_getValue(ESCORT_COOLDOWN_PROBE_KEY,null)?.owner === owner) { writeHelperStorageValue(ESCORT_COOLDOWN_PROBE_KEY,{owner:'',until:target.reloadRequired ? 0:Date.now() + 2000}); } } catch {} } } function migrateEscortAdaptiveBackoff() { try { const state=GM_getValue(ADAPTIVE_BACKOFF_STORAGE_KEY,null); if (!state) return; const previous=state.services?.escortClub,until=normalizeEscortQueueTimestamp(previous?.until); if (until > Date.now()) { const cooldown=readEscortRequestCooldown(),changed=!cooldown.reloadRequired && until > cooldown.until; if (changed) escortRequestCooldownMemory={...cooldown,until, revision:Math.max(Date.now(),cooldown.revision + 1),level:Math.max(1,cooldown.level), lastProblemAt:Math.max(cooldown.lastProblemAt,normalizeEscortQueueTimestamp(previous.lastProblemAt)), reason:String(previous.reason || 'HTTP 429')}; writeHelperStorageValue(ESCORT_REQUEST_COOLDOWN_KEY,escortRequestCooldownMemory); if (changed) {reportEscortRequestQueue(); wakeEscortRequestQueue();} } deleteHelperStorageValue(ADAPTIVE_BACKOFF_STORAGE_KEY); } catch {} } function getAdaptiveBackoffDiagnostics() { const now=Date.now(),queue=getEscortRequestQueueStatus(),search=readGarsoGlobalSearchSlot(); const until=search.serverRetryAt,waitMs=Math.max(0,until - now),topicGapMs=garsoTopicGapMs; return { escortClub:{active:queue.reloadRequired || queue.blockedUntil > now || queue.recovery503 === true || queue.rateFactor < 1,adaptiveEnabled:true,reloadRequired:queue.reloadRequired, level:queue.level,rateFactor:queue.rateFactor,ramp503At:queue.ramp503At,until:queue.blockedUntil,waitMs:Math.max(0,queue.blockedUntil - now), reason:queue.reason,lastProblemAt:queue.lastProblemAt, sharedRateLimit:queue.sharedRateLimit,concurrencyScope:queue.concurrencyScope, recovery503:queue.recovery503,recoveryStep:queue.recoveryStep,probePending:queue.probePending, limitPerMinute:queue.limitPerMinute,configuredLimitPerMinute:queue.configuredLimitPerMinute,queues:queue.queues}, escorti:{active:false,adaptiveEnabled:false}, garso:{active:waitMs > 0 || topicGapMs > GARSO_TOPIC_MIN_GAP_MS,adaptiveEnabled:true,until,waitMs, reason:waitMs > 0 ? 'Kontrola antyfloodowa Garso':topicGapMs > GARSO_TOPIC_MIN_GAP_MS ? 'Zwiększony odstęp pobierania tematów':'', searchGapMs:GARSO_SEARCH_MIN_GAP_MS,topicGapMs} }; } migrateEscortAdaptiveBackoff(); try {GM_addValueChangeListener(ADAPTIVE_BACKOFF_STORAGE_KEY,migrateEscortAdaptiveBackoff);} catch {} async function gmRequest(options) { while (true) { try { const { cancelToken=null, requestContext=null, beforeDispatch=null, ...requestOptions }=options || {}; let releaseSlot=() => {}; const observeResponse=(response,complete=true) => releaseSlot.observeResponse ? releaseSlot.observeResponse(response,complete):observeEscortRequestResponse(requestOptions.url,response); try { cancelToken?.throwIfCancelled(); releaseSlot=await acquireEscortRequestSlot(requestOptions.url,cancelToken,false,requestOptions.method,requestContext); cancelToken?.throwIfCancelled(); const skipped=beforeDispatch?.(); if (skipped) { releaseSlot(); return skipped.value; } } catch (error) { releaseSlot(); throw error; } return await new Promise((resolve,reject) => { let settled=false; let requestHandle=null,trace=null; let unsubscribe=() => {}; const finish=(callback,value) => { if (settled) return; settled=true; if (callback === reject) trace?.(value?.status,isOperationCancelledError(value) ? 'cancelled':'error',value); unsubscribe(); callback(value); }; const fail=(error,outcome) => { if (settled) return; trace?.(error?.status,outcome,error); recordDiagnosticRequestFailure(requestOptions.url,error); try {observeResponse(error || {},false);} catch (blockedError) {finish(reject,blockedError); return;} const failure=new Error(outcome === 'timeout' ? `Przekroczono czas oczekiwania na odpowiedź (limit ${(requestOptions.timeout ?? 20000) / 1000} s)` :Number(error?.status) > 0 ? `Błąd żądania HTTP ${Number(error.status)}`:'Błąd połączenia — nie otrzymano poprawnej odpowiedzi HTTP'); failure.code=outcome === 'timeout' ? 'REQUEST_TIMEOUT':'REQUEST_NETWORK_ERROR'; failure.status=Number(error?.status) || 0; failure.readyState=Number(error?.readyState) || 0; finish(reject,failure); }; try { releaseSlot.beforeDispatch?.(); trace=recordDiagnosticRequest(requestOptions.url,requestOptions.method,requestContext); requestHandle=GM_xmlhttpRequest({ timeout:20000, ...requestOptions, onload:response => { if (settled) return; trace?.(response?.status,'response',response); recordDiagnosticResponse(requestOptions.url,response?.status); try { observeResponse(response); finish(resolve,response); } catch (error) {finish(reject,error);} }, onerror:error => fail(error,'error'), ontimeout:error => fail(error,'timeout'), onabort:() => finish(reject, createOperationCancelledError(cancelToken?.reason || 'Żądanie przerwane')) }); releaseSlot.markStarted?.(); } catch (error) {finish(reject,error);} if (cancelToken && !settled) { unsubscribe=cancelToken.onCancel(reason => { try {requestHandle?.abort?.();} catch {} finish(reject,createOperationCancelledError(reason)); }); } }).finally(() => releaseSlot()); } catch (error) { if (options?.cancelToken?.cancelled && !isOperationCancelledError(error)) throw createOperationCancelledError(options.cancelToken.reason); if (error?.retryable && (error.status === 503 || error.code === 'ESCORT_QUEUE_RESCHEDULE') && !options?.cancelToken?.cancelled) continue; throw error; } } } function isGarsoRequestUrl(value) { try { const host=new URL(value || '',GARSO_BASE_URL).hostname.toLowerCase(); return host === 'garsoniera.com.pl' || host === 'www.garsoniera.com.pl'; } catch { return false; } } function updateBoundedPacingState( state, problem, { minGapMs, maxGapMs, stepUpMs, stepDownMs, recoverySuccesses } ) { let gapMs=Math.max( minGapMs, Math.min(maxGapMs,Number(state?.gapMs) || minGapMs) ); let successStreak=Math.max(0,Number(state?.successStreak) || 0); if (problem) { return { gapMs:Math.min(maxGapMs,gapMs + stepUpMs), successStreak:0 }; } successStreak++; if (successStreak >= recoverySuccesses) { gapMs=Math.max(minGapMs,gapMs - stepDownMs); successStreak=0; } return {gapMs,successStreak}; } function normalizeGarsoGlobalSearchSlot(value={}) { const gapMs=GARSO_SEARCH_MIN_GAP_MS; const lastStartedAt=Math.max(0,Number(value?.lastStartedAt) || 0); const lastFinishedAt=Math.max(0,Number(value?.lastFinishedAt) || 0); const serverRetryAt=Math.max(0,Number(value?.serverRetryAt) || 0); return {owner:String(value?.owner || ''),leaseUntil:Math.max(0,Number(value?.leaseUntil) || 0), lastStartedAt,lastFinishedAt,gapMs,serverRetryAt, nextAllowedAt:serverRetryAt || (lastFinishedAt ? lastFinishedAt + gapMs:lastStartedAt + gapMs), lastAntifloodAt:Math.max(0,Number(value?.lastAntifloodAt) || 0)}; } function readGarsoGlobalSearchSlot() { return normalizeGarsoGlobalSearchSlot(GM_getValue(GARSO_SEARCH_GLOBAL_SLOT_KEY,null)); } function writeGarsoGlobalSearchSlot(value) { writeHelperStorageValue(GARSO_SEARCH_GLOBAL_SLOT_KEY,normalizeGarsoGlobalSearchSlot(value)); } function getGarsoResponseWait(response,antiflood=false) { const header=String(response?.responseHeaders || '').match(/^retry-after:\s*([^\r\n]+)/im)?.[1]?.trim(); const retryAfterMs=!header ? null:/^\d+(?:\.\d+)?$/.test(header) ? Number(header) * 1000:Math.max(0,Date.parse(header) - Date.now()) || null; const text=String(response?.responseText || '').replace(/<[^>]*>/g,' ').replace(/ | | /gi,' '); const suggestedWaitMs=antiflood ? Number(text.match(/(?:odczekaj|poczekaj|zaczekaj|odczeka[ćc]|wait)[^\d]{0,80}(\d+(?:[.,]\d+)?)\s*(?:sek|sec)/i)?.[1]?.replace(',','.')) * 1000 || null:null; return {retryAfterMs,suggestedWaitMs}; } function syncGarsoNativeSearchCooldown() { if (!isGarsoRequestUrl(location.href)) return; const url=new URL(location.href); if (!/\/search\b/.test(url.pathname) && url.searchParams.get('module') !== 'search') return; const responseText=document.body?.innerText || '',status=performance.getEntriesByType('navigation')[0]?.responseStatus; const antiflood=Number(status) === 429 || (/(?:anti?[-\s]*flood|anty|odczekaj|poczekaj|zaczekaj|flood control)/i.test(responseText) && isGarsoAntifloodResponse(responseText)); const waits=getGarsoResponseWait({responseText},antiflood); noteGarsoNativeSearch(antiflood ? waits.suggestedWaitMs || GARSO_SEARCH_MIN_GAP_MS:GARSO_SEARCH_MIN_GAP_MS,!!waits.suggestedWaitMs); } function noteGarsoNativeSearch(waitMs=GARSO_SEARCH_MIN_GAP_MS,serverWait=false) { writeHelperStorageValue(GARSO_SEARCH_NATIVE_UNTIL_KEY,Math.max(serverWait ? 0:Number(GM_getValue(GARSO_SEARCH_NATIVE_UNTIL_KEY,0)) || 0,Date.now() + waitMs)); } function getGarsoSearchTickets() { const now=Date.now(),tickets=[]; for (const key of GM_listValues().filter(key => key.startsWith(GARSO_SEARCH_TICKET_PREFIX))) { const ticket=GM_getValue(key,null); // Starsze wersje zapisywały rezerwację na 120 s, bez znacznika odnowienia. const renewedAt=Number(ticket?.updatedAt ?? (ticket?.expiresAt - 120000)); if (ticket?.expiresAt > now && renewedAt + GARSO_SEARCH_TICKET_TTL_MS > now) tickets.push({...ticket,key}); } return tickets.sort((a,b) => a.queuedAt - b.queuedAt || a.key.localeCompare(b.key)); } async function waitForGarsoGlobalSearchSlotSignal( requestedWaitMs, cancelToken=null ) { const waitMs=Math.max( 1, Math.min( GARSO_SEARCH_GLOBAL_SLOT_SIGNAL_TIMEOUT_MS, Number(requestedWaitMs) || GARSO_SEARCH_GLOBAL_SLOT_SIGNAL_TIMEOUT_MS ) ); if ( typeof GM_addValueChangeListener !== 'function' || typeof GM_removeValueChangeListener !== 'function' ) { await waitWithCancellation( waitMs, cancelToken ); return; } await new Promise((resolve,reject) => { let settled=false; let listenerId=null; let timer=null; let unsubscribe=() => {}; const cleanup=() => { if (listenerId != null) { try { GM_removeValueChangeListener( listenerId ); } catch {} } if (timer) clearTimeout(timer); unsubscribe(); }; const finish=error => { if (settled) return; settled=true; cleanup(); if (error) reject(error); else resolve(); }; try { listenerId= GM_addValueChangeListener( GARSO_SEARCH_GLOBAL_SLOT_KEY, () => finish() ); } catch { listenerId=null; } timer=setTimeout( () => finish(), waitMs ); if (cancelToken) { unsubscribe= cancelToken.onCancel( reason => finish( createOperationCancelledError( reason ) ) ); } }); } function releaseGarsoGlobalSearchSlot(sent=false,problem=null,response=null) { const current=readGarsoGlobalSearchSlot(); if (current.owner !== GARSO_SEARCH_INSTANCE_ID) return; let next=current; if (sent) { const now=Date.now(),waits=getGarsoResponseWait(response,problem === true); const serverWaitMs=Math.max(waits.retryAfterMs || 0,waits.suggestedWaitMs || 0); next={...current, serverRetryAt:serverWaitMs ? now + serverWaitMs:0, lastFinishedAt:now,lastAntifloodAt:problem ? now:current.lastAntifloodAt}; } writeGarsoGlobalSearchSlot({...next,owner:'',leaseUntil:0}); } async function acquireGarsoGlobalSearchSlot(cancelToken=null) { const key=GARSO_SEARCH_TICKET_PREFIX + GARSO_SEARCH_INSTANCE_ID,queuedAt=Date.now(); try { while (true) { cancelToken?.throwIfCancelled(); if (garsoSearchPageClosed) throw createOperationCancelledError('Karta została zamknięta.'); // Opóźnione odnowienie nie zmienia pierwotnej kolejności zgłoszenia. const now=Date.now(); writeHelperStorageValue(key,{queuedAt,updatedAt:now,expiresAt:now + GARSO_SEARCH_TICKET_TTL_MS}); const current=readGarsoGlobalSearchSlot(); const readyAt=Math.max(current.nextAllowedAt,Number(GM_getValue(GARSO_SEARCH_NATIVE_UNTIL_KEY,0)) || 0, current.owner ? current.leaseUntil:0); if (readyAt > now || getGarsoSearchTickets()[0]?.key !== key) { await waitForGarsoGlobalSearchSlotSignal(readyAt > now ? readyAt - now:GARSO_SEARCH_GLOBAL_SLOT_SIGNAL_TIMEOUT_MS,cancelToken); continue; } writeGarsoGlobalSearchSlot({...current,owner:GARSO_SEARCH_INSTANCE_ID,leaseUntil:now + GARSO_SEARCH_TICKET_TTL_MS}); await waitWithCancellation(GARSO_SEARCH_GLOBAL_SLOT_VERIFY_MS,cancelToken); cancelToken?.throwIfCancelled(); const verified=readGarsoGlobalSearchSlot(); if (verified.owner !== GARSO_SEARCH_INSTANCE_ID) continue; const head=getGarsoSearchTickets()[0]?.key; if (garsoSearchPageClosed || (head && head !== key) || Math.max(verified.nextAllowedAt,Number(GM_getValue(GARSO_SEARCH_NATIVE_UNTIL_KEY,0)) || 0) > Date.now()) { releaseGarsoGlobalSearchSlot(); continue; } const startedAt=Date.now(); const leaseUntil=startedAt + GARSO_SEARCH_GLOBAL_SLOT_LEASE_MS; writeGarsoGlobalSearchSlot({...verified,lastStartedAt:startedAt,leaseUntil}); return startedAt; } } catch (error) { releaseGarsoGlobalSearchSlot(); throw error; } finally { deleteHelperStorageValue(key); } } function enqueueGarsoRequest(options) { const cancelToken=options?.cancelToken || null; const scheduled=garsoRequestQueue.catch(() => {}).then(async () => { cancelToken?.throwIfCancelled(); await acquireGarsoGlobalSearchSlot(cancelToken); let response=null,problem=null; try { cancelToken?.throwIfCancelled(); if (garsoSearchPageClosed) throw createOperationCancelledError('Karta została zamknięta.'); response=await gmRequest({...options,timeout:GARSO_SEARCH_REQUEST_TIMEOUT_MS}); if (Number(response?.status) === 429 || isGarsoAntifloodResponse(response?.responseText || '')) problem=true; else if (response?.status >= 200 && response.status < 400) problem=false; return response; } finally { releaseGarsoGlobalSearchSlot(true,problem,response); } }); garsoRequestQueue=scheduled.catch(() => {}); return scheduled; } async function garsoRequest( options, maxAttempts=GARSO_REQUEST_MAX_ATTEMPTS ) { const attempts=Math.max(1,Number(maxAttempts) || 1); let lastResponse=null; const cancelToken=options?.cancelToken || null; for (let attempt=1; attempt <= attempts; attempt++) { cancelToken?.throwIfCancelled(); const response=await enqueueGarsoRequest(options); lastResponse=response; const http429=Number(response?.status) === 429; const antiflood=http429 || isGarsoAntifloodResponse( response?.responseText || '' ); if (!antiflood) { return response; } if (http429) { incrementDiagnosticCounter('http429'); } else { incrementDiagnosticCounter('antiflood'); } if (attempt < attempts) incrementDiagnosticCounter('garsoRetries'); } const error=new Error('Garso: przekroczono limit ponowień po antyfloodzie'); error.code='GARSO_ANTIFLOOD'; error.response=lastResponse; throw error; } function updateGarsoTopicPacing(problem) { const next=updateBoundedPacingState( { gapMs:garsoTopicGapMs, successStreak:garsoTopicSuccessStreak }, problem, { minGapMs:GARSO_TOPIC_MIN_GAP_MS, maxGapMs:GARSO_TOPIC_MAX_GAP_MS, stepUpMs:GARSO_TOPIC_GAP_STEP_UP_MS, stepDownMs:GARSO_TOPIC_GAP_STEP_DOWN_MS, recoverySuccesses:GARSO_TOPIC_RECOVERY_SUCCESSES } ); garsoTopicGapMs=next.gapMs; garsoTopicSuccessStreak=next.successStreak; return garsoTopicGapMs; } function waitForGarsoTopicStartSlot(cancelToken=null) { const scheduled=garsoTopicStartQueue .catch(() => {}) .then(async () => { cancelToken?.throwIfCancelled(); const waitMs=Math.max( 0, garsoTopicGapMs - (Date.now() - garsoTopicLastStartedAt) ); if (waitMs > 0) { await waitWithCancellation(waitMs,cancelToken); } cancelToken?.throwIfCancelled(); garsoTopicLastStartedAt=Date.now(); }); garsoTopicStartQueue=scheduled.catch(() => {}); return scheduled; } async function acquireGarsoTopicRequestSlot(cancelToken=null) { cancelToken?.throwIfCancelled(); if (garsoTopicActiveRequests < GARSO_TOPIC_FETCH_CONCURRENT) { garsoTopicActiveRequests++; return; } await new Promise((resolve,reject) => { let unsubscribe=() => {}; const waiter={ resolve:() => { unsubscribe(); garsoTopicActiveRequests++; resolve(); }, reject:error => { unsubscribe(); reject(error); } }; garsoTopicRequestWaiters.push(waiter); if (cancelToken) { unsubscribe=cancelToken.onCancel(reason => { const index=garsoTopicRequestWaiters.indexOf(waiter); if (index >= 0) garsoTopicRequestWaiters.splice(index,1); waiter.reject(createOperationCancelledError(reason)); }); } }); } function releaseGarsoTopicRequestSlot() { garsoTopicActiveRequests=Math.max(0,garsoTopicActiveRequests - 1); const next=garsoTopicRequestWaiters.shift(); if (next) next.resolve(); } async function garsoTopicRequest(options) { const cancelToken=options?.cancelToken || null; let acquired=false; try { await acquireGarsoTopicRequestSlot(cancelToken); acquired=true; await waitForGarsoTopicStartSlot(cancelToken); cancelToken?.throwIfCancelled(); const response=await gmRequest(options); const http429=Number(response?.status) === 429; const responseText=String(response?.responseText || ''); const maybeAntiflood=http429 || /(?:anti?[-\s]*flood|flood control|odczekaj|poczekaj|zaczekaj)/i .test(responseText); const antiflood=http429 || ( maybeAntiflood && isGarsoAntifloodResponse(responseText) ); if (antiflood) { updateGarsoTopicPacing(true); if (http429) incrementDiagnosticCounter('http429'); else incrementDiagnosticCounter('antiflood'); const error=new Error('Garso: antyflood podczas pobierania tematu'); error.code='GARSO_ANTIFLOOD'; error.response=response; throw error; } updateGarsoTopicPacing(false); return response; } finally { if (acquired) releaseGarsoTopicRequestSlot(); } } async function gmGetText( url, cancelToken=null, {searchRequest=false}={} ) { let r; if (!isGarsoRequestUrl(url)) { r=await gmRequest({method:'GET',url,cancelToken}); } else if (searchRequest) { r=await garsoRequest({method:'GET',url,cancelToken}); } else { r=await garsoTopicRequest({method:'GET',url,cancelToken}); } if (r.status && (r.status < 200 || r.status >= 400)) { throw new Error(`HTTP ${r.status}`); } return r.responseText || ''; } let watchedCheckPromise=null; let watchedPollTimer=null; let watchedPollIntervalMs=0; let watchStorageListenersInitialized=false; let watchedInitialCheckScheduled=false; const watchPageButtonRefreshers=new Set(); const watchBarShiftedTopElements=new Map(); const hiddenUiRefreshers=new Set(); function getHiddenWatchIndex() { const enabled=SETTINGS.watchEnabled; if (hiddenWatchIndex?.enabled === enabled) return hiddenWatchIndex; const byAdId=new Map(); const byProfileId=new Map(); const add=(map,key,item) => { if (!key) return; key=String(key); if (!map.has(key)) map.set(key,[]); if (!map.get(key).includes(item)) map.get(key).push(item); }; const items=enabled ? getWatchedItems().filter(entry => entry.hidden):[]; for (const item of items) { const urls=item.sourceType === 'escort-ad' ? [item.adUrl]:getWatchedItemAdUrls(item); for (const url of urls) add(byAdId,parseAdIdFromUrl(url),item); if (item.sourceType === 'escort-ad') continue; for (const url of item.profileUrls) add(byProfileId,getEscortiProfileId(url),item); if (item.watchKey.startsWith('profiles:')) { for (const id of item.watchKey.slice(9).split(',')) { if (/^\d+$/.test(id)) add(byProfileId,id,item); } } } return hiddenWatchIndex={byAdId,byProfileId,items,enabled}; } function getHiddenResultProfileUrls(result) { return [...(result?.profileUrls || []),result?.profileUrl, ...(result?.profileSummaries || []).map(profile => profile?.profileUrl)].filter(Boolean); } function getHiddenAdMatches(adId,profileUrls=[]) { const index=getHiddenWatchIndex(); const matches=new Set(index.byAdId.get(String(adId)) || []); for (const url of profileUrls) { for (const item of index.byProfileId.get(String(getEscortiProfileId(url))) || []) matches.add(item); } return [...matches]; } function isWatchedItemHidden(item) { if (item.sourceType !== 'escort-ad') { return getHiddenWatchIndex().items.some(hidden => hidden.sourceType !== 'escort-ad' && watchedItemsOverlap(hidden,item)); } const adId=parseAdIdFromUrl(item.adUrl); return getHiddenAdMatches(adId,[...item.profileUrls,...getHiddenResultProfileUrls(getListCache(adId))]).length > 0; } function refreshHiddenItemsUi() { hiddenWatchIndex=null; for (const refresh of hiddenUiRefreshers) { try {refresh();} catch (error) {log('Błąd odświeżania ukrytych',error);} } } function rememberHiddenResultAd(adId,result) { const matches=getHiddenAdMatches(adId,getHiddenResultProfileUrls(result)) .filter(item => item.sourceType !== 'escort-ad' && !getWatchedItemAdUrls(item).some(url => parseAdIdFromUrl(url) === String(adId))); if (!matches.length || !/^\d+$/.test(String(adId))) return; const ids=new Set(matches.map(item => item.id)); saveWatchedItems(getWatchedItems().map(item => ids.has(item.id) ? {...item,resolvedAdUrls:[...item.resolvedAdUrls,`https://pl.escort.club/anons/${adId}.html`]} :item)); } function createHiddenIcon(hidden=true) { const icon=makeElement('span','vm-hidden-icon'); icon.setAttribute('aria-hidden','true'); icon.innerHTML=`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>${hidden ? '<path d="m3 3 18 18"/>':''}</svg>`; Object.assign(icon.style,{display:'inline-flex',flex:'0 0 auto',verticalAlign:'middle'}); return icon; } function restoreHiddenItems(itemIds) { const ids=new Set(itemIds.map(String)); const items=getWatchedItems().map(item => ids.has(item.id) ? {...item,hidden:false,hiddenAt:0,hiddenLastCheckedAt:0,hiddenLastError:null} :item); const removedIds=new Set(items.filter(item => ids.has(item.id) && canRemoveEmptyTrackedItem(item)).map(item => item.id)); saveWatchedItems(items.filter(item => !removedIds.has(item.id))); if (removedIds.size) saveWatchEvents(getWatchEvents().filter(event => !removedIds.has(event.itemId))); initLocalWatchSystem(); document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.(); } async function editHiddenForCurrentPage() { if (!getWatchSettings().enabled) return; const matches=getFavoriteMatchesForCurrentEscortiProfile().filter(item => item.hidden); if (matches.length > 1) return chooseTrackedEntriesDialog(matches,true,true); if (matches.length === 1) return changeTrackedItemStatus(matches[0].id,'hidden'); const context=getTrackedScopeItem(getFavoriteMatchesForCurrentEscortiProfile(),'person') || getCurrentEscortiWatchContextFast(); if (!context) return; const item=saveFavoriteItemFromContext({...context, resolvedAdUrls:[...new Set([...(context.resolvedAdUrls || []),...getEscortiAdUrls()])] },{hidden:true,favorite:context.favorite === true},'person'); initLocalWatchSystem(); showWatchToast('Profil ukryty. Lista jego anonsów zostanie sprawdzona automatycznie.'); runWatchedChecks({force:true,itemIds:[item.id],hiddenOnly:true}) .catch(error => showWatchToast(`Profil pozostaje ukryty. ${error?.message || 'Nie udało się sprawdzić nowych anonsów.'}`,true)); } function getWatchSettings() { try { const main=readHelperSettingsSnapshot(); return { enabled:main.watchEnabled === true, notesEnabled:main.watchEnabled === true, showOnlineCount: main.watchEnabled === true && main.showWatchOnlineCount === true, showAlertBar:main.showWatchAlertBar !== false, nonAlertChangeNewness: normalizeWatchNonAlertChangeNewness( main.nonAlertChangeNewness, DEFAULT_WATCH_SETTINGS.nonAlertChangeNewness ), intervalMinutes:WATCH_INTERVAL_OPTIONS.includes( Number(main.watchIntervalMinutes) ) ? Number(main.watchIntervalMinutes) :DEFAULT_WATCH_SETTINGS.intervalMinutes }; } catch { return {...DEFAULT_WATCH_SETTINGS}; } } function normalizeWatchUrl(value) { try { const url=new URL(value); if (!/^https?:$/.test(url.protocol)) return null; url.search=''; url.hash=''; return url.href; } catch { return null; } } function normalizeWatchedDisplayName(value,fallback='Obserwowane anons') { let name=normalizeEscortAdText(value); if (!name) return fallback; if (/^opinie\s+o\s+/i.test(name)) { name=name.replace(/^opinie\s+o\s+/i,'').trim(); const citySeparator=name.toLocaleLowerCase('pl-PL').lastIndexOf(' z '); if (citySeparator > 0) name=name.slice(0,citySeparator).trim(); } return name || fallback; } function copyWatchedValue(value) { if (!value || typeof value !== 'object') return value; if (Array.isArray(value)) return value.map(copyWatchedValue); const copy={...value}; for (const key of Object.keys(copy)) { if (copy[key] && typeof copy[key] === 'object') copy[key]=copyWatchedValue(copy[key]); } return copy; } function getWatchedItems() { try { if (!watchedItemsReadCache) { const saved=GM_getValue(WATCH_ITEMS_STORAGE_KEY,[]); watchedItemsReadCache={value:(Array.isArray(saved) ? saved:[]).map(normalizeWatchedItem).filter(Boolean)}; } return copyWatchedValue(watchedItemsReadCache.value); } catch {return [];} } function saveWatchedItems(items) { const normalized=(Array.isArray(items) ? items:[]).map(normalizeWatchedItem).filter(Boolean); const removed=new Set(normalized.filter(item => item.sourceType !== 'escort-ad' && !item.keepProfile && canRemoveEmptyTrackedItem(item)).map(item => item.id)); if (removed.size) saveWatchEvents(getWatchEvents().filter(event => !removed.has(event.itemId))); writeHelperStorageValue(WATCH_ITEMS_STORAGE_KEY,normalized.filter(item => !removed.has(item.id))); refreshWatchPageButtons(); } function refreshWatchPageButtons() { if (watchButtonsRefreshScheduled) return; watchButtonsRefreshScheduled=true; // Timer działa też w ukrytej karcie; requestAnimationFrame może tam czekać. setTimeout(() => { watchButtonsRefreshScheduled=false; refreshHiddenItemsUi(); for (const refresh of watchPageButtonRefreshers) { try {refresh();} catch (error) {log('Błąd odświeżania Ulubionych',error);} } },40); } function watchEventTypeToAlertMode(type) { if (type === 'online') return 'online'; if (type === 'location') return 'location'; if (type === 'changes') return 'changes'; if (type === 'new-ad') return 'newAds'; return ''; } function getWatchEvents() { try { const saved=GM_getValue(WATCH_EVENTS_STORAGE_KEY,[]); if (!Array.isArray(saved)) return []; const normalizedEvents=saved .filter(event => event && event.id && event.itemId && event.message ) .map(event => { const normalized={ ...event, id:String(event.id), itemId:String(event.itemId), timestamp: Number(event.timestamp) || Date.now(), acknowledged: !!event.acknowledged, alertTriggered: !!event.alertTriggered, alertMode: String( event.alertMode || watchEventTypeToAlertMode(event.type) ) }; if (!normalized.alertMode) { normalized.alertMode= watchEventTypeToAlertMode( normalized.type ); } return normalized; }) .sort((a,b) => b.timestamp - a.timestamp); const settings=getWatchSettings(); const nonAlertNewness= settings.nonAlertChangeNewness; if (nonAlertNewness !== 'until-open') { const minutes= Number(nonAlertNewness); if (Number.isFinite(minutes) && minutes > 0) { const cutoff= Date.now() - minutes * 60000; let changed=false; for (const event of normalizedEvents) { if ( !event.acknowledged && !event.alertTriggered && event.timestamp <= cutoff ) { event.acknowledged=true; changed=true; } } if (changed) { writeHelperStorageValue( WATCH_EVENTS_STORAGE_KEY, normalizedEvents.slice(0,200) ); } } } return normalizedEvents; } catch { return []; } } function saveWatchEvents(events) { writeHelperStorageValue( WATCH_EVENTS_STORAGE_KEY, (Array.isArray(events) ? events:[]) .sort((a,b) => Number(b.timestamp || 0) - Number(a.timestamp || 0)) .slice(0,200) ); } function acknowledgeNonAlertWatchEvents() { const events=getWatchEvents(); let changed=false; const next=events.map(event => { if ( !event.acknowledged && !event.alertTriggered ) { changed=true; return { ...event, acknowledged:true }; } return event; }); if (changed) { saveWatchEvents(next); } return changed; } function refreshWatchEventViews() { renderWatchNotificationBar(); for (const id of [WATCH_SETTINGS_OVERLAY_ID,'vm-favorite-tiles-test','vm-favorites-changes-timeline-overlay']) { try {document.getElementById(id)?._vmRender?.();} catch (error) {log('Błąd odświeżania alertów',error);} } } function acknowledgeWatchEvent(eventId) { const events=getWatchEvents(),event=events.find(value => value.id === String(eventId)); if (!event || event.acknowledged) return false; event.acknowledged=true; saveWatchEvents(events); refreshWatchEventViews(); return true; } function acknowledgeWatchEventOnArrival() { if (window.top !== window.self) return; const url=new URL(location.href),parts=url.hash.slice(1).split('&'); const marker=parts.find(part => part.startsWith('vm_watch_read=')); if (!marker) return; url.hash=parts.filter(part => !part.startsWith('vm_watch_read=')).join('&'); try {history.replaceState(history.state,'',url.href);} catch {} try { const id=decodeURIComponent(marker.slice('vm_watch_read='.length)); if (getWatchEvents().some(event => event.id === id && event.alertTriggered && !event.acknowledged)) acknowledgeWatchEvent(id); if (url.hash) document.getElementById(decodeURIComponent(url.hash.slice(1)))?.scrollIntoView(); } catch (error) {log('Nie udało się odczytać alertu po otwarciu strony',error);} } function acknowledgeAllAlertWatchEvents() { saveWatchEvents(getWatchEvents().map(event => ( event.alertTriggered ? {...event,acknowledged:true} :event ))); refreshWatchEventViews(); } function extractWatchPageName(doc,fallback='Obserwowane anons') { if (!doc) return fallback; const candidates=[ extractEscortClubTitle(doc), doc.querySelector('meta[property="og:title"]')?.getAttribute('content'), doc.querySelector('h1')?.textContent, doc.title ]; for (const candidate of candidates) { const name=normalizeEscortAdText(candidate) .replace(/\s*[|–\u2014-]\s*(?:Escorti(?:\.pl)?|Escort\.club).*$/i,'') .trim(); if (name && !/^escorti(?:\.pl)?$/i.test(name)) { return normalizeWatchedDisplayName(name,fallback).slice(0,120); } } return fallback; } function extractWatchPageImage(doc,pageUrl=location.href) { if (!doc) return ''; const candidates=[ doc.querySelector('#lightSlider img[src], #lightSlider img[data-src]')?.getAttribute('src'), doc.querySelector('.galleryContainer img[src], .galleryContainer img[data-src]')?.getAttribute('src'), doc.querySelector('.content-gallery-col img[src], .content-gallery-col img[data-src]')?.getAttribute('src'), doc.querySelector('.profile-image img[src], .profile-photo img[src], .escort-image img[src]')?.getAttribute('src'), doc.querySelector('meta[property="og:image"]')?.getAttribute('content'), doc.querySelector('main img[src], main img[data-src]')?.getAttribute('src') ]; for (const candidate of candidates) { if (!candidate || /^data:/i.test(candidate)) continue; try { return new URL(candidate,pageUrl).href; } catch {} } return ''; } const unavailableWatchThumbnailUrls=new Set(); function normalizeWatchThumbnailUrl(value) { const url=String(value || '').trim(); if (!url) return ''; try { const parsed=new URL(url,location.href); if (/(^|\.)escorti\.pl$/i.test(parsed.hostname) && /^\/default_avatar\.jpg$/i.test(parsed.pathname)) return ''; return parsed.href; } catch { return url; } } function markWatchThumbnailUnavailable(url) { const normalized=normalizeWatchThumbnailUrl(url); if (normalized) { unavailableWatchThumbnailUrls.add(normalized); } } function isWatchThumbnailKnownUnavailable(url) { const normalized=normalizeWatchThumbnailUrl(url); return !!normalized && unavailableWatchThumbnailUrls.has(normalized); } function probeWatchThumbnailUrl(url,timeoutMs=7000) { const normalized=normalizeWatchThumbnailUrl(url); if (!normalized) return Promise.resolve(false); if (isWatchThumbnailKnownUnavailable(normalized)) { return Promise.resolve(false); } return new Promise(resolve => { const image=new Image(); let finished=false; const finish=available => { if (finished) return; finished=true; clearTimeout(timer); image.onload=null; image.onerror=null; if (!available) { markWatchThumbnailUnavailable(normalized); } resolve(available); }; const timer=setTimeout( () => finish(false), timeoutMs ); image.referrerPolicy='no-referrer'; image.onload=() => finish( image.naturalWidth > 0 && image.naturalHeight > 0 ); image.onerror=() => finish(false); image.src=normalized; }); } async function findNextAvailableWatchThumbnail( currentUrl, candidates ) { const current=normalizeWatchThumbnailUrl(currentUrl); const unique=[]; const seen=new Set(); for (const candidate of candidates || []) { const normalized= normalizeWatchThumbnailUrl(candidate); if ( !normalized || normalized === current || seen.has(normalized) || isWatchThumbnailKnownUnavailable(normalized) ) { continue; } seen.add(normalized); unique.push(normalized); } for (const candidate of unique) { if (await probeWatchThumbnailUrl(candidate)) { return candidate; } } return ''; } function hasUsefulWatchThumbnail(item) { const imageUrl=normalizeWatchThumbnailUrl(item?.imageUrl); if (!imageUrl) return false; if (item.sourceType !== 'escort-ad') return true; return /static\.escort\.club\/galleries\//i.test(imageUrl); } function formatWatchDateTime(value) { if (!Number(value)) return 'jeszcze nie sprawdzono'; try { return new Intl.DateTimeFormat('pl-PL',{ day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }).format(new Date(Number(value))); } catch { return new Date(Number(value)).toLocaleString(); } } function getEscortiWatchSearchValue(item) { if (!item || ['escort-ad','escort-search'].includes(item.sourceType) || item.escortiProfileOnly || item.phoneProfileOnly) return ''; const mode=normalizeStoredWatchProfileResolutionMode(item.resolutionMode); return mode === 'address-escorti' ? normalizeEscortiAdUrl(item.adUrl) || '' :mode === 'phone-escorti' ? getEscortAdPhoneDigits(item) || '':''; } function getWatchKey(context) { const search=getEscortiWatchSearchValue(context); if (search) return `escorti-search:${search}`; if (context.watchKey) return context.watchKey; const profileIds=(context.profileUrls || []) .map(getEscortiProfileId) .filter(Boolean) .sort((a,b) => Number(a) - Number(b)); if (profileIds.length) return `profiles:${profileIds.join(',')}`; const phone=getEscortAdPhoneDigits(context); if (phone) return `phone:${phone}`; const adId=parseAdIdFromUrl(context.adUrl); if (adId) return `ad:${adId}`; return `other:${Math.random().toString(36).slice(2)}`; } function watchedItemsOverlap(first,second) { if (normalizeStoredWatchProfileResolutionMode(first.resolutionMode) !== normalizeStoredWatchProfileResolutionMode(second.resolutionMode)) return false; const firstSearch=getEscortiWatchSearchValue(first),secondSearch=getEscortiWatchSearchValue(second); if (firstSearch || secondSearch) return !!firstSearch && firstSearch === secondSearch; if (first.watchKey && second.watchKey && first.watchKey === second.watchKey) return true; const firstIsSingleAd=first.sourceType === 'escort-ad'; const secondIsSingleAd=second.sourceType === 'escort-ad'; if (firstIsSingleAd || secondIsSingleAd) { return firstIsSingleAd && secondIsSingleAd && !!first.adUrl && !!second.adUrl && parseAdIdFromUrl(first.adUrl) === parseAdIdFromUrl(second.adUrl); } if ( first.phoneDigits && second.phoneDigits && getEscortAdPhoneDigits(first) === getEscortAdPhoneDigits(second) ) return true; const firstProfiles=new Set((first.profileUrls || []).map(getEscortiProfileId).filter(Boolean)); return (second.profileUrls || []).some(url => firstProfiles.has(getEscortiProfileId(url))); } function upsertWatchedItem(context,modePatch={},locationTarget=null) { return saveFavoriteItemFromContext(context,{ favorite:true, modes:Object.fromEntries(Object.entries(modePatch).filter(([,enabled]) => enabled)), locationTarget }); } function updateWatchedItem(itemId,update) { const items=getWatchedItems(); const index=items.findIndex(item => item.id === itemId); if (index < 0) return null; items[index]=normalizeWatchedItem({ ...items[index], ...(typeof update === 'function' ? update(items[index]):update), keepProfile:false }); saveWatchedItems(items); return items[index]; } function renameWatchedItem(itemId,value) { const newName=normalizeEscortAdText(value).slice(0,120); if (!newName) return null; const items=getWatchedItems(); const index=items.findIndex(item => item.id === itemId); if (index < 0) return null; const previousName=items[index].displayName; items[index]=normalizeWatchedItem({ ...items[index], displayName:newName, customDisplayName:newName }); saveWatchedItems(items); const escapePattern=text => String(text || '') .replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); saveWatchEvents(getWatchEvents().map(event => { if (event.itemId !== itemId) return event; const oldEventName=event.name || previousName; let message=String(event.message || ''); for (const oldName of [oldEventName,previousName]) { if (!oldName) continue; const pattern=new RegExp(`^${escapePattern(oldName)}(?=\\s|$)`,'i'); if (pattern.test(message)) { message=message.replace(pattern,newName); break; } } return {...event,name:newName,message}; })); renderWatchNotificationBar(); return items[index]; } function removeWatchedItem(itemId) { return withTrackedUndo('Usunięto wpis.',()=>{ saveWatchedItems(getWatchedItems().filter(item => item.id !== itemId)); saveWatchEvents(getWatchEvents().filter(event => event.itemId !== itemId)); renderWatchNotificationBar(); }); } const WATCH_CHECK_LOCK_LEASE_MS=5 * 60 * 1000; const WATCH_CHECK_LOCK_HEARTBEAT_MS=45 * 1000; const WATCH_CHECK_LOCK_VERIFY_MS=90; const WATCH_MANUAL_CHECK_PREFIX=WATCH_CHECK_LOCK_KEY + '_manual_'; const watchManualRequests=new Set(); let watchCheckPageClosed=false,watchProgressWrittenAt=0; function publishWatchManualRequests() { const key=WATCH_MANUAL_CHECK_PREFIX + WATCH_INSTANCE_ID; if (!watchManualRequests.size || watchCheckPageClosed) {deleteHelperStorageValue(key); return;} const at=Math.min(...[...watchManualRequests].map(request => request.at)),now=Date.now(),old=GM_getValue(key,null); if (old?.at === at && old.expiresAt > now + WATCH_CHECK_LOCK_LEASE_MS / 2) return; writeHelperStorageValue(key,{at,expiresAt:now + WATCH_CHECK_LOCK_LEASE_MS}); } function getWatchManualTurn() { const pending=[],now=Date.now(); for (const key of GM_listValues()) { if (!key.startsWith(WATCH_MANUAL_CHECK_PREFIX)) continue; const value=GM_getValue(key,null); if (!value || !(value.expiresAt > now)) deleteHelperStorageValue(key); else pending.push({key,at:Number(value.at) || 0}); } return pending.sort((a,b) => a.at - b.at || a.key.localeCompare(b.key))[0]?.key || ''; } async function acquireWatchCheckLock(kind='scheduled') { const now=Date.now(); const current=GM_getValue(WATCH_CHECK_LOCK_KEY,null); if ( current?.owner && current.owner !== WATCH_INSTANCE_ID && Number(current.expiresAt) > now ) return false; writeHelperStorageValue(WATCH_CHECK_LOCK_KEY,{ owner:WATCH_INSTANCE_ID, claimedAt:now, expiresAt:now + WATCH_CHECK_LOCK_LEASE_MS,kind }); // GM_setValue nie blokuje innych kart; po zapisie sprawdź właściciela blokady. await new Promise(resolve => setTimeout(resolve,WATCH_CHECK_LOCK_VERIFY_MS)); const verified=GM_getValue(WATCH_CHECK_LOCK_KEY,null); if (verified?.owner !== WATCH_INSTANCE_ID || Number(verified.expiresAt) <= Date.now()) return false; touchWatchCheckLock(); return true; } function ownsWatchCheckLock() { const current=GM_getValue(WATCH_CHECK_LOCK_KEY,null); return current?.owner === WATCH_INSTANCE_ID && Number(current.expiresAt) > Date.now(); } function touchWatchCheckLock(progress=null) { const current=GM_getValue(WATCH_CHECK_LOCK_KEY,null); if (current?.owner !== WATCH_INSTANCE_ID) return false; const now=Date.now(); if (progress && now - watchProgressWrittenAt < 1000 && progress.phase === current.progress?.phase) return true; watchProgressWrittenAt=now; writeHelperStorageValue(WATCH_CHECK_LOCK_KEY,{ ...current,expiresAt:now + WATCH_CHECK_LOCK_LEASE_MS, ...(progress ? {progress:{phase:progress.phase,checked:progress.checked,total:progress.total,adChecked:progress.adChecked,adTotal:progress.adTotal}}:{}) }); return true; } function releaseWatchCheckLock() { const current=GM_getValue(WATCH_CHECK_LOCK_KEY,null); if (current?.owner === WATCH_INSTANCE_ID) deleteHelperStorageValue(WATCH_CHECK_LOCK_KEY); } async function watchMapLimit(values,limit,worker) { const source=Array.isArray(values) ? values:[]; const results=new Array(source.length); let index=0; async function run() { while (true) { const current=index++; if (current >= source.length) return; results[current]=await worker(source[current],current); } } const workerCount=Math.min( Math.max(1,limit), Math.max(1,source.length) ); const workers=[]; for (let workerIndex=0; workerIndex < workerCount; workerIndex++) { workers.push(run()); } await Promise.all(workers); return results; } function watchCitiesKey(cities) { return [...new Set((cities || []).map(normalizeEscortCityKey).filter(Boolean))] .sort() .join('|'); } function watchCitiesText(cities) { const normalized=[...new Set((cities || []).map(normalizeEscortCity).filter(Boolean))]; return normalized.length ? normalized.join(', '):'b/d'; } function showWatchToast(message,error=false,{id='vm-garso-watch-toast',duration=2800,style=null,actionLabel='',onAction=null}={}) { detachHelperNode(document.getElementById(id)); const toast=prepareHelperStyleApi( makeElement('div') ); toast.id=id; toast.textContent=message;toast.setAttribute('role','status');toast.setAttribute('aria-live','polite'); Object.assign(toast.style,{ position:'fixed', right:'18px', bottom:'18px', zIndex:'2147483647', padding:'10px 14px', borderRadius:'8px', background:error ? 'var(--vm-ui-danger, #c62828)' :'var(--vm-ui-accent, #f54da3)', color:'var(--vm-ui-accent-contrast, #fff)', fontSize:'13px', fontWeight:'700', boxShadow:'var(--vm-ui-shadow, 0 4px 18px rgba(0,0,0,.28))' }); if (style) Object.assign(toast.style,style); document.body.appendChild(toast); const expiresAt=Date.now()+duration; const timer=setTimeout(()=>detachHelperNode(toast),duration); if (onAction) { const button=makeButton('vm-ui-button vm-ui-compact',actionLabel || 'Cofnij'); button.style.marginLeft='12px'; button.addEventListener('click',()=>{ if (button.disabled) return; button.disabled=true;clearTimeout(timer);detachHelperNode(toast); if (Date.now()<=expiresAt) onAction(); },{once:true}); toast.appendChild(button); } } function restoreWatchBarTopElements() { for (const [element,original] of watchBarShiftedTopElements) { try { if (original.value) { element.style.setProperty('top',original.value,original.priority || ''); } else { element.style.removeProperty('top'); } } catch {} } watchBarShiftedTopElements.clear(); document.documentElement.style.setProperty( '--vm-escort-summary-panel-top-offset', '0px' ); } function syncWatchBarPageOffset(bar,spacer) { if (!bar?.isConnected || !spacer?.isConnected) return; const height=Math.ceil(bar.getBoundingClientRect().height); spacer.style.height=`${height}px`; document.documentElement.style.setProperty( '--vm-escort-summary-panel-top-offset', `${height}px` ); const topFixedElements=location.hostname.toLowerCase().endsWith('escorti.pl') ? document.querySelectorAll('#main-app > nav.fixed.top-0') :[]; for (const element of topFixedElements) { if (!watchBarShiftedTopElements.has(element)) { watchBarShiftedTopElements.set(element,{ value:element.style.getPropertyValue('top'), priority:element.style.getPropertyPriority('top') }); } element.style.setProperty('top',`${height}px`,'important'); } } let escortRequestNotificationTimer=null,escortRequestNotificationSignature=''; function getEscortRequestNotification(status=getEscortRequestQueueStatus()) { if (status.reloadRequired) return {until:0,text:'Escort.club: BAN na IP. Wstrzymano pobieranie danych do odświeżenia strony. Skrypt sprawdzi dostęp na podstawie wczytanej strony lub jednego zapytania. Jeśli ponownie otrzyma HTTP 403, pozostanie zatrzymany.'}; if (!(status.blockedUntil > Date.now())) return null; const retryAt=new Date(status.blockedUntil).toLocaleTimeString('pl-PL'); if (status.reason === 'HTTP 403' || status.reason === 'Kontrola antyfloodowa Escort.club') { return {until:status.blockedUntil,text:`Escort.club: Tymczasowy ban na IP. Wstrzymano pobieranie danych. Czas trwania bana nie jest jeszcze ustalony. Kolejna próba skryptu o ${retryAt}. Możesz kontynuować przeglądanie z wykorzystaniem VPN.`}; } const reason=status.reason === 'HTTP 429' ? 'limit zapytań (HTTP 429)':status.reason || 'odmowa dostępu'; return {until:status.blockedUntil,text:`Escort.club: ${reason}. Wstrzymano pobieranie danych do ${retryAt}.`}; } function refreshEscortRequestNotification(status) { if (window.top !== window.self) return; const notice=getEscortRequestNotification(status); if ((notice?.text || '') !== escortRequestNotificationSignature || (notice?.until > Date.now() && !escortRequestNotificationTimer)) renderWatchNotificationBar(); } subscribeEscortRequestQueueStatus(refreshEscortRequestNotification); window.addEventListener('pagehide',() => {clearTimeout(escortRequestNotificationTimer); escortRequestNotificationTimer=null;}); function renderWatchNotificationBar() { if (window.top !== window.self) { detachHelperNode(document.getElementById(WATCH_BAR_ID)); detachHelperNode(document.getElementById(WATCH_BAR_SPACER_ID)); return; } const render=() => { const requestNotice=getEscortRequestNotification(); escortRequestNotificationSignature=requestNotice?.text || ''; clearTimeout(escortRequestNotificationTimer); escortRequestNotificationTimer=requestNotice?.until > Date.now() ? setTimeout(() => refreshEscortRequestNotification(),Math.min(2147483647,Math.max(1,requestNotice.until - Date.now() + 1))):null; const toolbar=document.getElementById(HELPER_MENU_ID); const focused=toolbar?.contains(document.activeElement) ? document.activeElement:null; if (toolbar) {toolbar._vmMoving=true; document.body?.appendChild(toolbar);} const restoreToolbar=() => { if (!toolbar) return; toolbar._vmMoving=false; if (!toolbar.hidden && focused?.isConnected) focused.focus({preventScroll:true}); toolbar._vmPosition?.(); }; const previousBar=document.getElementById(WATCH_BAR_ID); try {previousBar?._vmResizeObserver?.disconnect();} catch {} try {previousBar?._vmResizeCleanup?.();} catch {} detachHelperNode(previousBar); detachHelperNode(document.getElementById(WATCH_BAR_SPACER_ID)); restoreWatchBarTopElements(); const watchSettings=getWatchSettings(); if (!document.body) {restoreToolbar(); return;} const showMenu=toolbar && !toolbar.hidden; const pendingEvents=watchSettings.enabled ? getWatchEvents().filter( event => !event.acknowledged && event.alertTriggered ):[]; toolbar?._vmSetAlertCount?.(pendingEvents.length); const events=watchSettings.showAlertBar ? pendingEvents:[]; const showOnlineCount=watchSettings.enabled && watchSettings.showOnlineCount === true; if (!events.length && !showOnlineCount && !showMenu && !requestNotice) {restoreToolbar(); return;} const watchedItems=showOnlineCount ? getWatchedItems():[]; const onlineItems=watchedItems.filter(item => item.modes?.online && item.state?.online === true ); const onlineCount=onlineItems.length; const bar=prepareHelperStyleApi( makeElement('div') ); bar.id=WATCH_BAR_ID; Object.assign(bar.style,{ position:'fixed', left:'0', right:'0', top:'0', zIndex:'2147483645', display:'flex', alignItems:'center', gap:'8px', padding:'4px 8px', boxSizing:'border-box', background:'var(--vm-ui-accent, #f54da3)', color:'var(--vm-ui-accent-contrast, #fff)', boxShadow:'var(--vm-ui-shadow, 0 1px 5px rgba(0,0,0,.28))', fontSize:'12px', lineHeight:'1.25' }); if (requestNotice) { bar.style.flexWrap='wrap'; const warning=makeElement('div','vm-escort-request-warning',requestNotice.text); warning.setAttribute('role','status'); warning.setAttribute('aria-live','polite'); Object.assign(warning.style,{flex:'1 0 100%',minWidth:'0',fontWeight:'800',whiteSpace:'normal',overflowWrap:'anywhere'}); bar.appendChild(warning); } const createOnlineSummary=compact => { const onlineSummary=makeButton('',`Online: ${onlineCount}`); const onlineNames=onlineItems.map(item => { const type=item.sourceType === 'escort-ad' ? 'Anons':'Profil'; return `${type}: ${item.displayName}`; }); onlineSummary.setAttribute( 'aria-label', onlineNames.length ? `Online: ${onlineCount}. ${onlineNames.join('. ')}. Kliknij, aby otworzyć listę ulubionych.` :'Brak profili lub anonsów online z włączonym alertem online/offline. Kliknij, aby otworzyć listę ulubionych.' ); Object.assign(onlineSummary.style,{ flex:'0 0 auto', whiteSpace:'nowrap', padding:compact ? '3px 8px':'1px 6px', border:compact ? '0' :'1px solid color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 72%, transparent)', borderRadius:compact ? '0 0 7px 7px':'10px', background:compact ? 'transparent':'color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 14%, transparent)', color:'var(--vm-ui-accent-contrast, #fff)', cursor:'pointer', font:'inherit', fontWeight:'800' }); const tooltip=makeElement('div'); tooltip.id=`${WATCH_BAR_ID}-online-tooltip`; tooltip.setAttribute('role','tooltip'); Object.assign(tooltip.style,{ display:'none', position:'fixed', zIndex:'2147483647', width:'min(330px, calc(100vw - 16px))', maxHeight:'min(520px, calc(100vh - 48px))', overflowY:'auto', padding:'8px', border:'1px solid var(--vm-ui-panel-border, rgba(255,255,255,.28))', borderRadius:'8px', background:'var(--vm-ui-panel-bg, #35103d)', color:'var(--vm-ui-panel-text, var(--vm-ui-text, #222))', boxShadow:'var(--vm-ui-shadow, 0 5px 18px rgba(0,0,0,.42))', boxSizing:'border-box', textAlign:'left', whiteSpace:'normal', pointerEvents:'auto' }); const tooltipHeading=makeElement('div','',onlineItems.length ? `Online z włączonym alertem online/offline: ${onlineCount}` :'Brak pozycji online z włączonym alertem online/offline'); Object.assign(tooltipHeading.style,{ margin:'0 0 6px', color:'var(--vm-ui-accent, #f54da3)', fontSize:'12px', fontWeight:'800' }); tooltip.appendChild(tooltipHeading); for (const item of onlineItems) { const activeAdUrl=item.state?.activeUrls?.[0] || item.adUrl || item.profileUrls?.[0] || ''; const row=makeElement(activeAdUrl ? 'a':'div'); if (activeAdUrl) { row.href=activeAdUrl; row.target='_blank'; row.rel='noopener noreferrer'; row.title='Otwórz aktywny anons'; } Object.assign(row.style,{ display:'grid', gridTemplateColumns:'42px minmax(0, 1fr)', gap:'8px', alignItems:'center', padding:'5px 0', borderTop:tooltip.childElementCount > 1 ? '1px solid var(--vm-ui-panel-border, rgba(255,255,255,.12))' :'0', color:'var(--vm-ui-panel-text, var(--vm-ui-text, #222))', textDecoration:'none' }); const imageBox=makeElement('div'); const initial=normalizeEscortAdText(item.displayName) .charAt(0) .toLocaleUpperCase('pl-PL') || '•'; imageBox.textContent=initial; Object.assign(imageBox.style,{ position:'relative', display:'flex', alignItems:'center', justifyContent:'center', width:'42px', height:'42px', overflow:'hidden', border:'1px solid var(--vm-ui-panel-border, rgba(255,255,255,.18))', borderRadius:'6px', background:'var(--vm-ui-panel-bg-alt, rgba(255,255,255,.09))', color:'var(--vm-ui-panel-muted, rgba(255,255,255,.75))', fontSize:'17px', fontWeight:'800', boxSizing:'border-box' }); if (hasUsefulWatchThumbnail(item)) { const image=makeElement('img','',null,{src:item.imageUrl,alt:'',loading:'lazy',referrerPolicy:'no-referrer'}); Object.assign(image.style,{ position:'absolute', inset:'0', width:'100%', height:'100%', objectFit:'cover' }); image.addEventListener('error',() => detachHelperNode(image),{once:true}); imageBox.appendChild(image); } const details=makeElement('div'); details.style.minWidth='0'; const type=makeElement('div','',item.sourceType === 'escort-ad' ? 'Anons Escort.club' :'Profil Escorti'); Object.assign(type.style,{ marginBottom:'2px', color:'var(--vm-ui-panel-muted, rgba(255,255,255,.68))', fontSize:'10px' }); const name=makeElement('div','',item.displayName); Object.assign(name.style,{ color:'var(--vm-ui-panel-text, var(--vm-ui-text, #222))', fontSize:'12px', fontWeight:'700', lineHeight:'1.25', overflowWrap:'anywhere' }); details.append(type,name); row.append(imageBox,details); tooltip.appendChild(row); } const positionTooltip=() => { const rect=onlineSummary.getBoundingClientRect(); const margin=8; const tooltipWidth=tooltip.offsetWidth; const tooltipHeight=tooltip.offsetHeight; const left=Math.min( Math.max(margin,rect.left), Math.max(margin,window.innerWidth - tooltipWidth - margin) ); const below=rect.bottom + 5; const top=below + tooltipHeight <= window.innerHeight - margin ? below :Math.max(margin,rect.top - tooltipHeight - 5); tooltip.style.left=`${Math.round(left)}px`; tooltip.style.top=`${Math.round(top)}px`; }; const showTooltip=() => { tooltip.style.display='block'; requestAnimationFrame(positionTooltip); }; const hideTooltip=() => { tooltip.style.display='none'; }; let tooltipHideTimer=null; const cancelTooltipHide=() => { if (tooltipHideTimer) clearTimeout(tooltipHideTimer); tooltipHideTimer=null; }; const scheduleTooltipHide=() => { cancelTooltipHide(); tooltipHideTimer=setTimeout(hideTooltip,180); }; onlineSummary.addEventListener('mouseenter',() => {cancelTooltipHide(); showTooltip();}); onlineSummary.addEventListener('mouseleave',scheduleTooltipHide); onlineSummary.addEventListener('focus',showTooltip); onlineSummary.addEventListener('blur',scheduleTooltipHide); tooltip.addEventListener('mouseenter',cancelTooltipHide); tooltip.addEventListener('mouseleave',scheduleTooltipHide); onlineSummary.addEventListener('click',() => { hideTooltip(); openWatchedSettingsModal(); }); bar.appendChild(tooltip); return onlineSummary; }; if (showOnlineCount) bar.appendChild(createOnlineSummary(!events.length)); if (showMenu) bar.appendChild(toolbar); if (!events.length) { if (!requestNotice) Object.assign(bar.style,{ left:'8px', right:'auto', width:'auto', maxWidth:'calc(100vw - 16px)', gap:'3px', padding:showMenu ? '2px 4px':'0', borderRadius:'0 0 7px 7px' }); mountBar(); return; } const list=makeElement('div'); Object.assign(list.style,{ display:'flex', alignItems:'center', gap:'6px', flex:'1 1 auto', minWidth:'0', overflow:'hidden', whiteSpace:'nowrap' }); const eventEntries=[]; for (const event of events) { const entry=makeElement('span'); Object.assign(entry.style,{ display:'inline-flex', alignItems:'center', gap:'4px', flex:'0 0 auto', minWidth:'0', padding:'1px 3px 1px 6px', border:'1px solid color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 72%, transparent)', borderRadius:'10px', whiteSpace:'nowrap' }); const link=makeWatchEventLink(event,event.message || '',true); Object.assign(link.style,{ color:'var(--vm-ui-accent-contrast, #fff)', textDecoration:'none' }); const accept=makeButton('','×',{title:'Oznacz alert jako odczytany bez otwierania strony'}); accept.setAttribute('aria-label',accept.title); Object.assign(accept.style,{ width:'18px', height:'18px', padding:'0', border:'0', borderRadius:'8px', background:'color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 18%, transparent)', color:'var(--vm-ui-accent-contrast, #fff)', cursor:'pointer', fontWeight:'800' }); accept.addEventListener('click',click => {stopUiEvent(click); acknowledgeWatchEvent(event.id);}); entry.append(link,accept); list.appendChild(entry); eventEntries.push(entry); } const more=makeButton(); more.title='Zobacz pozostałe powiadomienia'; Object.assign(more.style,{ display:'none', flex:'0 0 auto', padding:'1px 7px', border:'1px solid color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 72%, transparent)', borderRadius:'10px', background:'color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 14%, transparent)', color:'var(--vm-ui-accent-contrast, #fff)', cursor:'pointer', font:'inherit', fontWeight:'800', whiteSpace:'nowrap' }); more.addEventListener('click',openWatchedSettingsModal); list.appendChild(more); bar.appendChild(list); const fitNotificationEntries=() => { if (!list.isConnected) return; for (const entry of eventEntries) entry.style.display='inline-flex'; more.style.display='none'; const available=list.clientWidth; const gap=6; const allWidth=eventEntries.reduce( (sum,entry,index) => sum + entry.offsetWidth + (index ? gap:0), 0 ); if (allWidth <= available) return; more.textContent=`+${eventEntries.length}`; more.style.display='inline-flex'; const reserve=more.offsetWidth + gap; let used=0; let visible=0; for (const entry of eventEntries) { const needed=entry.offsetWidth + (visible ? gap:0); if (used + needed + reserve <= available) { used += needed; visible++; } else { entry.style.display='none'; } } const hidden=eventEntries.length - visible; more.textContent=`+${hidden}`; }; const acceptAll=makeButton('','Zaakceptuj wszystkie',{title:'Zaakceptuj wszystkie'}); Object.assign(acceptAll.style,{ flex:'0 0 auto', height:'24px', padding:'0 9px', border:'1px solid color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 82%, transparent)', borderRadius:'5px', background:'color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 15%, transparent)', color:'var(--vm-ui-accent-contrast, #fff)', cursor:'pointer', fontSize:'11px', fontWeight:'800', lineHeight:'1', whiteSpace:'nowrap' }); acceptAll.addEventListener( 'click', acknowledgeAllAlertWatchEvents ); bar.appendChild(acceptAll); function mountBar(fitEntries) { if (!events.length && !requestNotice) {document.body.appendChild(bar); restoreToolbar(); return;} const spacer=makeElement('div','',null,{id:WATCH_BAR_SPACER_ID}); spacer.setAttribute('aria-hidden','true'); Object.assign(spacer.style,{ display:'block', width:'100%', height:'0px', flex:'0 0 auto', padding:'0', margin:'0', border:'0', pointerEvents:'none', visibility:'hidden' }); document.body.insertBefore(spacer,document.body.firstChild); document.body.appendChild(bar); restoreToolbar(); const syncSpacerHeight=() => { fitEntries?.(); syncWatchBarPageOffset(bar,spacer); }; if (typeof ResizeObserver === 'function') { const observer=new ResizeObserver(syncSpacerHeight); observer.observe(bar); bar._vmResizeObserver=observer; } const onWindowResize=() => requestAnimationFrame(syncSpacerHeight); window.addEventListener('resize',onWindowResize); bar._vmResizeCleanup=() => window.removeEventListener('resize',onWindowResize); requestAnimationFrame(syncSpacerHeight); } mountBar(fitNotificationEntries); }; if (document.body) render(); else document.addEventListener('DOMContentLoaded',render,{once:true}); } function prepareHiddenHelperFrame(iframe) { iframe.setAttribute('aria-hidden','true'); iframe.tabIndex=-1; Object.assign(iframe.style,{ position:'fixed', left:'-10000px', top:'-10000px', width:'1px', height:'1px', opacity:'0', pointerEvents:'none', border:'0', zIndex:'-999999' }); } function createWatchDialogBase(id,titleText) { closeHelperDialog(document.getElementById(id)); const overlay=makeElement('div','',null,{id}); prepareHelperStyleApi(overlay); overlay.classList.add('vm-ui-overlay'); const box=makeElement('div',"vm-ui-dialog"); const title=makeElement('h2',"vm-ui-title",titleText); box.appendChild(title); overlay.appendChild(box); document.body.appendChild(overlay); return {overlay,box}; } function getTopHelperDialog() { return helperDialogStack.findLast(entry => entry.overlay.isConnected && !entry.overlay.hidden && entry.overlay.style.display !== 'none') || null; } function handleHelperDialogKey(event) { if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing) return; const top=getTopHelperDialog(); if (!top) return; event.preventDefault(); event.stopImmediatePropagation(); if (!event.repeat && top.closeOnEscape) top.close('escape'); } function closeHelperDialog(overlay,reason='replace') { if (typeof overlay?._vmClose === 'function') overlay._vmClose(reason); else detachHelperNode(overlay); } function createDialogCloseController(overlay,{ onClose=null,closeOnBackdrop=true,closeOnEscape=true,beforeClose=null }={}) { let closed=false; const onBackdrop=event => { if (event.target === overlay && getTopHelperDialog() === entry) close('backdrop'); }; const close=reason => { if (closed || beforeClose?.(reason) === false) return; closed=true; overlay.removeEventListener('click',onBackdrop); const index=helperDialogStack.indexOf(entry); if (index >= 0) helperDialogStack.splice(index,1); if (!helperDialogStack.length) document.removeEventListener('keydown',handleHelperDialogKey,true); try {onClose?.(reason);} finally {detachHelperNode(overlay);} }; const entry={overlay,close,closeOnEscape}; if (!helperDialogStack.length) document.addEventListener('keydown',handleHelperDialogKey,true); helperDialogStack.push(entry); if (closeOnBackdrop) overlay.addEventListener('click',onBackdrop); overlay._vmClose=close; return {close,bindButton:button => button.addEventListener('click',() => close('button'))}; } function styleDialogActionButton(button,primary=false) { button.classList.add('vm-ui-button'); button.classList.toggle('-primary',primary); return button; } function createDialogCancelButton(controller) { const button=makeButton("vm-ui-button",'Anuluj'); Object.assign(button.style,{marginTop:'10px'}); button.addEventListener('click',() => controller.close('cancel')); return button; } function chooseWatchOptionDialog({id,title,info,choices,primaryValue}) { return new Promise(resolve => { const {overlay,box}=createWatchDialogBase(id,title); let result=null; const controller=createDialogCloseController(overlay,{ onClose:() => resolve(result) }); const infoElement=makeElement('p','',info); infoElement.style.margin='0 0 14px'; const actions=makeElement('div'); Object.assign(actions.style,{display:'grid',gap:'9px'}); for (const [value,label,disabled=false] of choices) { const button=styleDialogActionButton( makeButton('',label), value === primaryValue ); button.disabled=disabled; button.addEventListener('click',() => { if (button.disabled) return; result=value; controller.close('choice'); }); actions.appendChild(button); } const cancel=createDialogCancelButton(controller); box.append(infoElement,actions,cancel); }); } function chooseWatchPhoneCoverage(profileCount) { return chooseWatchOptionDialog({ id:'vm-garso-watch-phone-scope-overlay', title:'Co chcesz dodać?', info:`Ten numer znaleziono w ${profileCount} profilach Escorti.`, choices:[ ['single','Jeden wybrany profil'], ['all',`Wszystkie profile (${profileCount})`] ], primaryValue:'all' }); } function chooseWatchPhoneProfile(profileInfos) { return new Promise(resolve => { const {overlay,box}=createWatchDialogBase( 'vm-garso-watch-phone-profile-overlay', 'Wybierz profil Escorti' ); let result=null; const controller=createDialogCloseController(overlay,{ onClose:() => resolve(result) }); const list=makeElement('div'); Object.assign(list.style,{ display:'grid', gap:'7px', maxHeight:'55vh', overflow:'auto' }); for (const profile of profileInfos) { const button=styleDialogActionButton(makeButton()); button.textContent=profile.profileName || `Profil Escorti ${getEscortiProfileId(profile.profileUrl)}`; button.style.textAlign='left'; button.addEventListener('click',() => { result=profile; controller.close('choice'); }); list.appendChild(button); } const cancel=createDialogCancelButton(controller); box.append(list,cancel); }); } function createWatchLocationTargetControls(current=null) { const root=makeElement('div',"vm-watch-location-target-controls vm-ui-panel"); Object.assign(root.style,{margin:'2px 0 8px 25px'}); const label=makeElement('label',"vm-ui-label",'Powiadom, gdy pojawi się w mieście:'); const input=makeElement('input',"vm-ui-field",null,{type:'text',value:current?.type === 'city' ? current.city || '':'',placeholder:'np. Kraków'}); Object.assign(input.style,{width:'100%'}); const anyLabel=makeElement('label',"vm-ui-choice"); const anyInput=makeElement('input','',null,{type:'checkbox',checked:!current || current.type !== 'city'}); anyInput.style.accentColor=getEscortPagePinkColor(); anyLabel.append( anyInput, document.createTextNode( 'Dowolne - zgłaszaj każdą zmianę lokalizacji' ) ); let enabled=true; const sync=() => { root.style.display=enabled ? 'block':'none'; input.disabled=!enabled || anyInput.checked; input.style.opacity=input.disabled ? '.55':'1'; anyInput.disabled=!enabled; }; anyInput.addEventListener('change',sync); sync(); root.append(label,input,anyLabel); return { root, input, anyInput, setEnabled(value) { enabled=!!value; sync(); }, read() { if (anyInput.checked) return {type:'any',city:''}; const city=normalizeEscortCity(input.value); return city ? {type:'city',city}:null; } }; } function getCurrentEscortiWatchContextFast() { const profileUrl=normalizeWatchUrl(`${location.origin}${location.pathname}`); const source=parseEscortiSearchSource(new URLSearchParams(location.hash.slice(1)).get('vm_escorti_source')); return { sourceType:'escorti-profile', profileUrls:profileUrl ? [profileUrl]:[], adUrl:null, phoneDigits:null, resolutionMode:'address-escorti', ...source, escortiProfileOnly:true, displayName:extractWatchPageName(document,'Profil Escorti'), imageUrl:extractWatchPageImage(document,profileUrl), watchKey:`profiles:${getEscortiProfileId(profileUrl) || profileUrl}` }; } async function addWatchedContext(context,mode,locationTarget=null) { const candidate=normalizeWatchedItem({ id:'watch-candidate', ...context, watchKey:getWatchKey(context), modes:{[mode]:true} }); const existing=getWatchedItems().find(item => watchedItemsOverlap(item,candidate) ); const alreadyWatching=!!existing?.modes?.[mode]; const item=upsertWatchedItem( context, {[mode]:true}, locationTarget ); showWatchToast(alreadyWatching ? 'Ta pozycja jest już zapisana.' :(mode === 'online' ? 'Dodano obserwowanie aktywności.' :'Dodano obserwowanie lokalizacji.')); await runWatchedChecks({force:true,itemIds:[item.id]}); document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.(); return {item,alreadyWatching}; } async function addWatchFromManualInput(rawValue) { const value=String(rawValue || '').trim(); if (!value) throw new Error('Wklej adres profilu, anonsu albo numer telefonu'); let url=null; try { url=new URL(/^https?:\/\//i.test(value) ? value:`https://${value}`); } catch {} if (url) { if ( ['escorti.pl','www.escorti.pl'].includes(url.hostname) && /^\/escort\/\d+\/?$/i.test(url.pathname) ) { const profileUrl=normalizeWatchUrl(url.href); let info=null; try {info=await readEscortiProfileDirect(profileUrl);} catch {} return addWatchedContext({ sourceType:'escorti-profile', profileUrls:[profileUrl], escortiProfileOnly:true, displayName:info?.profileName || `Profil Escorti ${getEscortiProfileId(profileUrl)}`, imageUrl:info?.imageUrl || '', watchKey:`profiles:${getEscortiProfileId(profileUrl)}` },'online'); } if (isEscortClubHost(url.hostname) && !!getEscortAdIdFromPath(url.pathname)) { const scope=await chooseFavoriteScope(); if (!scope) return null; const context=await buildFavoriteContextFromEscortAd(url.href,scope.startsWith('person:') ? 'person':scope,scope.startsWith('person:') ? scope.slice(7):null); if (!context) return null; return addWatchedContext(context,'online'); } } const phoneDigits=normalizeEscortCachedPhone(value); if (!phoneDigits || phoneDigits.length < 9) { throw new Error('Nie rozpoznano adresu ani numeru telefonu'); } let profileUrls=[]; try { const found=await searchEscortiProfilesDirect(phoneDigits); profileUrls=found.profileUrls || []; } catch {} const profileInfos=(await watchMapLimit(profileUrls,2,async profileUrl => { try { const info=await readEscortiProfileDirect(profileUrl); return { profileUrl, profileName:normalizeWatchedDisplayName( info?.profileName, `Profil Escorti ${getEscortiProfileId(profileUrl)}` ), imageUrl:info?.imageUrl || '' }; } catch { return { profileUrl, profileName:`Profil Escorti ${getEscortiProfileId(profileUrl)}`, imageUrl:'' }; } })).filter(Boolean); let selectedProfiles=profileInfos; let coverage=profileInfos.length > 1 ? await chooseWatchPhoneCoverage(profileInfos.length) :'single'; if (!coverage) return null; if (coverage === 'single' && profileInfos.length > 1) { const selected=await chooseWatchPhoneProfile(profileInfos); if (!selected) return null; selectedProfiles=[selected]; } const selectedProfileUrls=selectedProfiles.map(profile => profile.profileUrl); const formattedPhone=phoneDigits.replace(/(\d{3})(?=\d)/g,'$1 '); const selectedName=selectedProfiles.length === 1 ? selectedProfiles[0].profileName :(selectedProfiles.length ? `Wszystkie profile numeru ${formattedPhone}` :`Telefon ${formattedPhone}`); return addWatchedContext({ sourceType:selectedProfileUrls.length ? 'escorti-profile':'phone', profileUrls:selectedProfileUrls, ...(coverage === 'single' && selectedProfileUrls.length ? {escortiProfileOnly:true}:{}), phoneDigits:coverage === 'all' || !selectedProfileUrls.length ? phoneDigits :null, displayName:selectedName || `Telefon ${formattedPhone}`, imageUrl:selectedProfiles[0]?.imageUrl || '', watchKey:selectedProfileUrls.length ? `profiles:${selectedProfileUrls.map(getEscortiProfileId).filter(Boolean).sort().join(',')}` :`phone:${phoneDigits}` },'online'); } function getEscortiAdFavoriteNoteState(adUrl) { const normalizedUrl=normalizeEscortiAdUrl(adUrl); if (!normalizedUrl) return {favorite:false,note:false,noteText:''}; let favorite=false; let noteText=''; for (const item of getWatchedItems()) { if (item?.sourceType !== 'escort-ad') continue; if (normalizeEscortiAdUrl(item.adUrl) !== normalizedUrl) continue; if (item.favorite) favorite=true; if (item.note) noteText=String(item.note).trim(); } return {favorite,note:!!noteText,noteText}; } function renderEscortAdLinkStatusMarker(marker,result) { if (!marker) return; marker.replaceChildren(); if (result?.stale && typeof result.active === 'boolean') { marker.textContent=result.active ? '✓':'×'; marker.style.color='#9ca3af'; marker.title=`Ostatni zapis: anons ${result.active ? 'online':'offline'} • wynik starszy niż 30 minut${result.refreshFailed ? ' • odświeżenie nie powiodło się':''}`; } else if (result?.unknown || !result) { marker.textContent='?'; marker.style.color='#d97706'; marker.title='Nie udało się sprawdzić aktywności'; } else if (result.active) { marker.textContent='✓'; marker.style.color='#16a34a'; marker.title='Anons online'; } else { marker.append(document.createTextNode('×')); marker.style.color='#dc2626'; marker.title='Anons offline'; } const visitedCard=marker.closest('.vm-garso-visited-card'); if ((result?.active === false && !result.unknown) || (visitedCard && (!result || result.unknown))) { const adId=parseAdIdFromUrl(marker.dataset.adUrl); const cacheHost=adId && getEscortCachedAdHost(adId); if (cacheHost) { const cacheBadge=makeElement('a','vm-escorti-ad-cache-badge',visitedCard ? '(cache)':'cache',{href:`https://${cacheHost}${getEscortAdPath(adId,cacheHost)}#${ESCORT_CACHED_AD_HASH_KEY}=${adId}`,target:'_blank',rel:'noopener noreferrer',title:`Otwórz zapisaną wersję (${cacheHost})`}); Object.assign(cacheBadge.style,{ display:'inline-block', padding:'1px 3px', border:'1px solid var(--vm-ui-border,rgba(95,95,105,.42))', borderRadius:'4px', color:'var(--vm-ui-text-muted,#777)', background:'var(--vm-ui-bg-alt,rgba(255,255,255,.55))', fontSize:'8px', fontWeight:'600', lineHeight:'1.15', letterSpacing:'0' }); marker.appendChild(cacheBadge); marker.title+=' • dostępna zapisana wersja z cache'; } else if (visitedCard) marker.title+=' • Brak pełnej kopii w cache. Historia odwiedzin i skrócone dane są zapisywane osobno.'; } marker.setAttribute('aria-label',marker.title); marker.dataset.vmAdStatus=!result || result.unknown ? 'unknown':result.active === false ? 'offline':'online'; marker.dataset.vmStatusReady='1'; } function getAvailableEscortAdUrls(results) { const urls=new Set(); for (const [url,result] of results) { if (!result || result.unknown) continue; if (result.active === true) urls.add(url); else if (result.active === false) { const id=parseAdIdFromUrl(url),host=id && getEscortCachedAdHost(id); if (host) urls.add(addEscortCachedArchiveMarkerToUrl(`https://${host}${getEscortAdPath(id,host)}`)); } } return [...urls]; } function openAvailableEscortAds(results,event) { stopUiEvent(event); for (const url of getAvailableEscortAdUrls(results)) GM_openInTab(url,{active:false,insert:true}); } function updateOpenAvailableEscortAdsButton(button,results,pending=false) { if (!button) return; const count=getAvailableEscortAdUrls(results).length; button.disabled=pending || !count; button.style.opacity=button.disabled ? '.45':'1'; button.style.cursor=button.disabled ? 'default':'pointer'; const label=count ? `Otwórz wszystkie (${count})`:'Otwórz wszystkie'; if (button.textContent !== label) button.textContent=label; button.title=pending ? 'Trwa sprawdzanie dostępności anonsów…' :'Otwórz w nowych kartach anonse online oraz zapisane kopie anonsów offline'; } function initEscortiProfileAdLinkStatuses() { if (!/^\/escort\/\d+\/?$/i.test(location.pathname)) return; if (new URL(location.href).searchParams.has('vm_mode')) return; const statusByUrl=new Map(); const pendingUrls=new Set(); const OPEN_ALL_AVAILABLE_ID='vm-escorti-open-all-available'; const refreshFavoriteNoteMarker=marker => { if (!marker) return; const state=getEscortiAdFavoriteNoteState(marker.dataset.adUrl); const features=getWatchSettings(); const showFavorite=features.enabled && state.favorite; const showNote=features.enabled && state.note; const hiddenMatches=getHiddenAdMatches(parseAdIdFromUrl(marker.dataset.adUrl)) .filter(item => item.sourceType === 'escort-ad'); marker.textContent=showNote ? '✎':''; if (showFavorite) { const star=makeElement('span','','★'); star.style.color='#f4c542'; marker.prepend(star); } if (hiddenMatches.length) marker.appendChild(createHiddenIcon()); marker.style.visibility=showFavorite || showNote || hiddenMatches.length ? 'visible':'hidden'; setControlTitle(marker,[ showFavorite ? 'Anons w ulubionych':'', showNote ? `Notatka: ${state.noteText}`:'', hiddenMatches.length ? `Ukryty w wynikach: ${[...new Set(hiddenMatches.map(item => item.sourceType === 'escort-ad' ? 'anons':'cały profil'))].join(' + ')}`:'' ].filter(Boolean).join(' • ')); }; const refreshAllFavoriteNoteMarkers=() => document.querySelectorAll('.vm-escorti-ad-favorite-note-status').forEach(refreshFavoriteNoteMarker); hiddenUiRefreshers.add(refreshAllFavoriteNoteMarkers); const refreshOpenAllAvailableButton=heading => { if (!heading) return; let button=document.getElementById(OPEN_ALL_AVAILABLE_ID); if (!button) { button=prepareHelperStyleApi(makeButton('','Otwórz wszystkie')); button.id=OPEN_ALL_AVAILABLE_ID; button.type='button'; Object.assign(button.style,{ display:'block',margin:'6px auto 10px',padding:'5px 10px', border:'1px solid var(--vm-ui-border,#bbb)',borderRadius:'5px',background:'var(--vm-ui-bg,#fff)', color:'var(--vm-ui-text,#555)',cursor:'pointer',fontSize:'11px',fontWeight:'700', position:'relative',zIndex:'2147483647',pointerEvents:'auto', touchAction:'manipulation' }); heading.insertAdjacentElement('afterend',button); } if (button.dataset.vmOpenAllHandlerVersion !== SCRIPT_VERSION) { const replacement=button.cloneNode(true); replacement.dataset.vmOpenAllHandlerVersion=SCRIPT_VERSION; replacement.addEventListener('click',event => openAvailableEscortAds(statusByUrl,event)); button.replaceWith(replacement); button=replacement; } Object.assign(button.style,{ position:'relative',zIndex:'2147483647',pointerEvents:'auto', touchAction:'manipulation' }); updateOpenAvailableEscortAdsButton(button,statusByUrl,pendingUrls.size > 0); }; const showResultForUrl=(adUrl,result) => { if (!adUrl) return; statusByUrl.set(adUrl,result || null); for (const marker of document.querySelectorAll( '.vm-escorti-ad-online-status' )) { if (marker.dataset.adUrl === adUrl) { renderEscortAdLinkStatusMarker(marker,result); } } const heading=getEscortiAdLinkSection()?.heading; refreshOpenAllAvailableButton(heading); }; const checkRows=rows => { const urls=[...new Set( rows .map(row => row.adUrl) .filter(adUrl => !statusByUrl.has(adUrl) && !pendingUrls.has(adUrl)) )]; if (!urls.length) return; urls.forEach(adUrl => pendingUrls.add(adUrl)); updateOpenAvailableEscortAdsButton(document.getElementById(OPEN_ALL_AVAILABLE_ID),statusByUrl,true); scanActiveEscortAds(urls,progress => { const resultUrl=normalizeEscortiAdUrl(progress?.result?.url); if (resultUrl) showResultForUrl(resultUrl,progress.result); },false,{activityOnly:true,useActivityCache:true,cacheActivity:true}) .then(summary => { const byUrl=new Map( (summary?.results || []).map(result => [ normalizeEscortiAdUrl(result?.url) || result?.url, result ]) ); for (const adUrl of urls) { if (!statusByUrl.has(adUrl)) { showResultForUrl(adUrl,byUrl.get(adUrl)); } } }) .catch(() => { for (const adUrl of urls) { if (!statusByUrl.has(adUrl)) showResultForUrl(adUrl,null); } }) .finally(() => { urls.forEach(adUrl => pendingUrls.delete(adUrl)); updateOpenAvailableEscortAdsButton(document.getElementById(OPEN_ALL_AVAILABLE_ID),statusByUrl,pendingUrls.size > 0); }); }; const insert=() => { const section=getEscortiAdLinkSection(); if (!section) return false; const {heading}=section; refreshOpenAllAvailableButton(heading); const rows=[]; for (const anchor of section.elements.filter(node => node.matches('a[href]'))) { if (anchor.closest('[id^="vm-"], [class*="vm-"]')) continue; const adUrl=normalizeEscortiAdUrl(anchor.getAttribute('href'),location.href); if (!adUrl) continue; let marker=[...(anchor.parentElement?.children || [])].find(element => element.classList?.contains('vm-escorti-ad-online-status') && element.dataset.adUrl === adUrl ); if (!marker) { const row=makeElement('div','vm-escorti-ad-status-row'); Object.assign(row.style,{ display:'grid', gridTemplateColumns:'44px auto 58px', alignItems:'center', columnGap:'3px', width:'fit-content', minWidth:'0', maxWidth:'100%', margin:'0 auto', verticalAlign:'middle' }); const favoriteNoteMarker=makeElement('span',"vm-escorti-ad-favorite-note-status"); favoriteNoteMarker.dataset.adUrl=adUrl; Object.assign(favoriteNoteMarker.style,{ display:'inline-flex',alignItems:'center',justifyContent:'flex-end', width:'44px',minWidth:'44px',overflow:'visible',gap:'2px', color:getEscortiUiAccentColor(),fontSize:'13px',fontWeight:'800', lineHeight:'1',whiteSpace:'nowrap',letterSpacing:'-1px' }); refreshFavoriteNoteMarker(favoriteNoteMarker); marker=makeElement('span'); marker.className='vm-escorti-ad-online-status'; marker.dataset.adUrl=adUrl; marker.textContent='…'; setControlTitle(marker,'Sprawdzanie aktywności anonsu'); Object.assign(marker.style,{ width:'58px', minWidth:'58px', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:'4px', textAlign:'center', color:'#9ca3af', fontSize:'18px', fontWeight:'900', lineHeight:'1' }); const linkCell=makeElement('span','vm-escorti-ad-link-cell'); Object.assign(linkCell.style,{ display:'inline-flex',alignItems:'center',gap:'3px',minWidth:'0',maxWidth:'100%' }); anchor.parentElement.insertBefore(row,anchor); linkCell.appendChild(anchor); if (SETTINGS.showTopPhoneSearch !== false) linkCell.appendChild(makeGarsoInlineSearchButton(adUrl)); row.append(favoriteNoteMarker,linkCell,marker); Object.assign(anchor.style,{ width:'auto', minWidth:'0' }); } rows.push({adUrl,marker}); } if (!rows.length) return false; for (const row of rows) { if (statusByUrl.has(row.adUrl)) { renderEscortAdLinkStatusMarker(row.marker,statusByUrl.get(row.adUrl)); } } checkRows(rows); return true; }; const scheduleInsert=createAnimationFrameScheduler(insert); const observer=new MutationObserver(mutations => { if (mutations.some(mutation => !mutation.target?.closest?.('[id^="vm-"], [class*="vm-"]'))) { scheduleInsert(); } }); const startObserver=() => { if (!document.body) return false; observer.observe(document.body,{childList:true,subtree:true}); return true; }; if (document.body) { startObserver(); insert(); } else { document.addEventListener('DOMContentLoaded',() => { startObserver(); insert(); },{once:true}); } let attempts=0; const timer=setInterval(() => { if (insert() || ++attempts >= 40) clearInterval(timer); },250); } function normalizeWatchedItem(item) { if (!item || typeof item !== 'object' || !item.id) return null; const customDisplayName=normalizeEscortAdText(item.customDisplayName).slice(0,120); const profileUrls=[...new Set( (Array.isArray(item.profileUrls) ? item.profileUrls:[]) .map(normalizeWatchUrl) .filter(Boolean) )]; const adUrl=item.adUrl ? normalizeEscortiAdUrl(item.adUrl) || normalizeWatchUrl(item.adUrl) :null; const phoneDigits=normalizeEscortCachedPhone(item.phoneDigits); const resolvedAdUrls=[...new Set( (Array.isArray(item.resolvedAdUrls) ? item.resolvedAdUrls:[]) .map(url => normalizeEscortiAdUrl(url)) .filter(Boolean) )]; const targetType=item.locationTarget?.type === 'city' ? 'city':'any'; const targetCity=targetType === 'city' ? normalizeEscortCity(item.locationTarget?.city) :''; const note=String(item.note || '').replace(/\r\n/g,'\n').trim().slice(0,2500); const categories=normalizeFavoriteAssignedCategories(item.categories); const state=item.state && typeof item.state === 'object' ? item.state:null; const sourceType=['escorti-profile','escort-ad','phone','escort-search'].includes(item.sourceType) ? item.sourceType:(adUrl ? 'escort-ad':(phoneDigits ? 'phone':'escorti-profile')); const search=getEscortiWatchSearchValue({...item,sourceType,adUrl,phoneDigits}); return { id:String(item.id), sourceType, watchKey:search ? `escorti-search:${search}`:String(item.watchKey || ''), displayName:customDisplayName || normalizeWatchedDisplayName(item.displayName,'Ulubione'), customDisplayName:customDisplayName || null, imageUrl:normalizeWatchThumbnailUrl(item.imageUrl), profileUrls, adUrl, resolvedAdUrls, phoneDigits:phoneDigits || null, ...(item.escortiProfileOnly === true || item.phoneProfileOnly === true ? {escortiProfileOnly:true}:{}), ...(item.sourceType === 'escort-search' ? { search:normalizeFavoriteSearch(item.search), knownAdIds:[...new Set([ ...(Array.isArray(item.knownAdIds) ? item.knownAdIds:[]), ...resolvedAdUrls.map(parseAdIdFromUrl), ...(Array.isArray(state?.activeUrls) ? state.activeUrls:[]).map(parseAdIdFromUrl) ].filter(id => /^\d+$/.test(String(id))).map(String))] }:{}), favorite:item.favorite !== false, ...(item.preserveProfile === true ? {preserveProfile:true}:{}), ...(item.sourceType !== 'escort-ad' && item.keepProfile === true && !item.favorite && !item.hidden && !watchedItemHasNotesOrCategories(item) ? {keepProfile:true}:{}), hidden:item.hidden === true, hiddenAt:Number(item.hiddenAt) || 0, hiddenLastCheckedAt:Number(item.hiddenLastCheckedAt) || 0, hiddenLastError:item.hiddenLastError ? String(item.hiddenLastError):null, note, categories, resolutionMode:normalizeStoredWatchProfileResolutionMode( item.resolutionMode ), modes:{ online:!!item.modes?.online, location:!!item.modes?.location, changes:!!item.modes?.changes, newAds: item.sourceType !== 'escort-ad' && !!item.modes?.newAds }, locationTarget:{ type:targetType, city:targetCity }, createdAt:Number(item.createdAt) || Date.now(), lastCheckedAt:Number(item.lastCheckedAt) || 0, lastFullCheckedAt: Number(item.lastFullCheckedAt) || 0, lastError: item.lastError ? String(item.lastError) :null, state:state ? { online:typeof state.online === 'boolean' ? state.online:null, activeCount:Number(state.activeCount) || 0, totalAds:Number(state.totalAds) || 0, cities:Array.isArray(state.cities) ? state.cities.map(normalizeEscortCity).filter(Boolean) :[], activeUrls:Array.isArray(state.activeUrls) ? state.activeUrls.map(url => normalizeEscortiAdUrl(url)).filter(url => url && (item.sourceType !== 'escort-search' || resolvedAdUrls.includes(url))) :[], profileDates:Array.isArray(state.profileDates) ? state.profileDates.map(formatEscortiDate).filter(Boolean) :[], unknownCount:Math.max(0,Number(state.unknownCount) || 0), partial:!!state.partial, locationPartial:!!state.locationPartial, checkedAt:Number(state.checkedAt) || 0, lastKnownCities:(Array.isArray(state.lastKnownCities)?state.lastKnownCities:Array.isArray(state.cities)?state.cities:[]).map(normalizeEscortCity).filter(Boolean), escortiData:state.escortiData&&typeof state.escortiData==='object'&&!Array.isArray(state.escortiData)?state.escortiData:null }:null }; } function readWatchedAdDataMirror() { if (watchedAdDataMirrorMemory) return watchedAdDataMirrorMemory; try { const stored=GM_getValue(WATCH_AD_DATA_MIRROR_STORAGE_KEY,{}); watchedAdDataMirrorMemory=stored && typeof stored === 'object' && !Array.isArray(stored) ? stored:{}; } catch { watchedAdDataMirrorMemory={}; } return watchedAdDataMirrorMemory; } function getWatchedAdDataMirror(adId) { const value=readWatchedAdDataMirror()[String(adId || '')]; return value && typeof value === 'object' && !Array.isArray(value) ? value:null; } function makeWatchedAdDataMirrorValue(data) { if (!data || typeof data !== 'object') return null; const keepDescription=normalizeFavoritesDataColumns(readHelperSettingsSnapshot().favoritesDataColumns).includes('ad:description') || getFavoriteTilesOptions().extra.includes('ad:description'); const value={ status:data.status || 'ok', adId:data.adId || null, adUrl:data.adUrl || null, title:data.title || null, imageUrl:data.imageUrl || null, phone:data.phone || null, phoneDigits:data.phoneDigits || null, datePosted:data.datePosted || null, oldestPhotoDate:data.oldestPhotoDate || null, location:data.location || null, availability:data.availability && typeof data.availability === 'object' ? {...data.availability} :null, prices:data.prices && typeof data.prices === 'object' ? {...data.prices} :null, stats:data.stats && typeof data.stats === 'object' ? {...data.stats} :null, tags:Array.isArray(data.tags) ? [...data.tags]:[], isAgencyOrSalonPhone:data.isAgencyOrSalonPhone ?? null, lastVisitedAt:Number(data.lastVisitedAt) || null, checkedAt:Number(data.checkedAt) || null }; if (keepDescription) value.description=data.description || null; return value; } function areJsonCacheValuesEqual(first,second) { if (first === second) return true; try {return JSON.stringify(first) === JSON.stringify(second);} catch {return false;} } function storeWatchedAdDataMirror(adId,data) { const id=String(adId || ''); const clean=makeWatchedAdDataMirrorValue(data); if (!id || !clean) return; const mirror=readWatchedAdDataMirror(); if (areJsonCacheValuesEqual(mirror[id],clean)) return; mirror[id]=clean; try {writeHelperStorageValue(WATCH_AD_DATA_MIRROR_STORAGE_KEY,mirror);} catch {} } function getWatchedAdDataMirrorIds() { const events=getWatchEvents(); const ids=new Set(getWatchedItems() .flatMap(item => getWatchedItemHistoricalAdUrls(item,events)) .map(parseAdIdFromUrl).filter(Boolean).map(String)); for (const key of getEscortAdDataCacheKeys()) { const value=persistentCacheCatalog?.get(key) || readPersistentCacheValue(key,null); const match=String(key).match(/^vm_escort_ad_data_(\d+)$/i); if (match && Number(value?.lastVisitedAt) && getEscortAdPhoneDigits(value)) ids.add(match[1]); } return ids; } async function rebuildWatchedAdDataMirrorFromCentralCache() { if (CURRENT_HOST !== 'pl.escort.club' || (deferredNonTopicCache && !persistentCacheCatalog)) return 0; const wantedIds=getWatchedAdDataMirrorIds(); await ensurePersistentCacheKeys([...wantedIds].map(escortAdDataCacheKey)); const mirror=readWatchedAdDataMirror(); let changed=0; for (const id of wantedIds) { const data=getStoredEscortAdData(id); if (!data || Number(mirror[id]?.checkedAt) > Number(data.checkedAt)) continue; const clean=makeWatchedAdDataMirrorValue(data); if (!clean || areJsonCacheValuesEqual(mirror[id],clean)) continue; mirror[id]=clean; changed++; } const retainedIds=getWatchedAdDataMirrorIds(); for (const id of Object.keys(mirror)) { if (!persistentCacheUsesIndexedDb || retainedIds.has(id)) continue; delete mirror[id]; changed++; } if (changed) { try {writeHelperStorageValue(WATCH_AD_DATA_MIRROR_STORAGE_KEY,mirror);} catch {} } return changed; } function getWatchedItemAdUrls(item,activeOnly=false) { const source=activeOnly ? (item?.state?.activeUrls || []) :[ item?.adUrl, ...(item?.resolvedAdUrls || []), ...(item?.state?.activeUrls || []) ]; return [...new Set( source.map(url => normalizeEscortiAdUrl(url)).filter(Boolean) )]; } function getWatchedItemHistoricalAdUrls(item,events=null) { const urls=new Map(); const addId=id => { const value=String(id || ''); if (!/^\d+$/.test(value)) return; urls.set(value,`https://pl.escort.club/anons/${value}.html`); }; const addUrl=url => { const normalized=normalizeEscortiAdUrl(url); const id=parseAdIdFromUrl(normalized); if (id) urls.set(String(id),normalized); }; if (item?.sourceType === 'escort-search') for (const id of item.knownAdIds || []) addId(id); for (const url of getWatchedItemAdUrls(item,false)) addUrl(url); const itemId=String(item?.id || ''); const history=Array.isArray(events) ? events:getWatchEvents(); for (const event of history) { if (!itemId || String(event?.itemId || '') !== itemId) continue; addUrl(event?.url); for (const source of [event?.message,event?.description]) { for (const line of String(source || '').split(/\r?\n/)) { if (!/anons/i.test(line)) continue; for (const match of line.matchAll(/\b(\d{3,7})\b/g)) addId(match[1]); } } } return [...urls.values()]; } function getWatchedItemVisitedAdUrls(item,mirror=null,events=null) { const dataMirror=mirror && typeof mirror === 'object' ? mirror:readWatchedAdDataMirror(); const urls=new Map(); const phones=new Set(); const addUrl=url => { const normalized=normalizeEscortiAdUrl(url); const id=parseAdIdFromUrl(normalized); if (id) urls.set(String(id),normalized); }; const ownPhone=normalizeEscortCachedPhone(item?.phoneDigits); if (ownPhone) phones.add(ownPhone); for (const url of getWatchedItemHistoricalAdUrls(item,events)) { addUrl(url); const id=parseAdIdFromUrl(url); const phone=getEscortAdPhoneDigits(dataMirror[String(id || '')]); if (phone) phones.add(phone); } if (phones.size) { for (const [id,data] of Object.entries(dataMirror)) { if (!Number(data?.lastVisitedAt)) continue; const phone=getEscortAdPhoneDigits(data); if (!phone || !phones.has(phone)) continue; addUrl(data?.adUrl || `https://pl.escort.club/anons/${id}.html`); } } return [...urls.values()]; } function getWatchedItemCachedAdDetails(item,activeOnly=true) { let urls=getWatchedItemAdUrls(item,activeOnly); if (activeOnly && !urls.length) urls=getWatchedItemAdUrls(item,false); return urls .map(url => { const adId=parseAdIdFromUrl(url); if (!adId) return null; const central=getStoredEscortAdData(adId); const mirror=getWatchedAdDataMirror(adId); if (!central && !mirror) return null; const adData={ ...(mirror || {}), ...(central || {}), imageUrl:central?.imageUrl || mirror?.imageUrl || null }; return {url,adId:String(adId),adData}; }) .filter(Boolean); } function storeWatchedAdDataInCentralCache(adId,adData={},checkedAt=Date.now(),watchSnapshot=undefined) { if (!persistentCacheKeyIsLoaded(escortAdDataCacheKey(adId))) { ensurePersistentCacheKeys([escortAdDataCacheKey(adId)]).then(() => storeWatchedAdDataInCentralCache(adId,adData,checkedAt,watchSnapshot)).catch(error => log('Nie udało się scalić zapisu cache',error)); return null; } const id=String(adId || ''); if (!id) return null; try { const existing=getStoredEscortAdData(id); const value={ status:'ok', ...(existing || {}), ...(adData || {}), imageUrl:adData?.imageUrl || existing?.imageUrl || null, ...(watchSnapshot !== undefined ? { watchSnapshot, watchSnapshotCheckedAt:Number(checkedAt) || Date.now() } :{}), checkedAt:Math.max( Number(existing?.checkedAt) || 0, Number(checkedAt) || 0 ) || Date.now() }; writePersistentCacheValue(escortAdDataCacheKey(id),value); storeWatchedAdDataMirror(id,value); registerEscortDataFieldsFromAdData(value); escortAdDataMemoryCache.set(id,value); return value; } catch (error) { log(`Nie udało się scalić danych obserwowanego anonsu ${id} z cache`,error); return null; } } const WATCH_SYSTEM_NOTIFICATION_CLAIM_PREFIX='vm_garso_watch_notification_claim:'; let watchNotificationPrunedAt=0; const watchSystemNotificationsPending=new Set(); const readWatchNotificationStorage=key=>typeof GM!=='undefined'&&typeof GM.getValue==='function'?GM.getValue(key,null):Promise.resolve(GM_getValue(key,null)); const writeWatchNotificationStorage=(key,value)=>typeof GM!=='undefined'&&typeof GM.setValue==='function'?GM.setValue(key,value):Promise.resolve(writeHelperStorageValue(key,value)); async function pruneWatchSystemNotificationClaims() { const now=Date.now(); if (now-watchNotificationPrunedAt<600000) return; for (const key of GM_listValues()) if (key.startsWith(WATCH_SYSTEM_NOTIFICATION_CLAIM_PREFIX) && !(Number((await readWatchNotificationStorage(key))?.expiresAt)>now)) deleteHelperStorageValue(key); watchNotificationPrunedAt=now; } async function showWatchSystemNotification(event) { const enabled=s => s.watchEnabled && s.watchSystemNotifications; const grouped=Array.isArray(event),events=(grouped ? event:[event]).filter(value => value?.id && value.alertTriggered && !value.acknowledged),claims=[]; const owner=`${WATCH_INSTANCE_ID}:${Math.random().toString(36).slice(2)}`; const release=async key => {if ((await readWatchNotificationStorage(key))?.owner===owner) await writeWatchNotificationStorage(key,null);}; try { if (!events.length || !enabled(getSettings()) || typeof GM_notification!=='function') return false; await pruneWatchSystemNotificationClaims(); for (const value of events) { const adId=value.type==='new-ad' ? getWatchEventAdId(value):''; const key=WATCH_SYSTEM_NOTIFICATION_CLAIM_PREFIX+(adId ? `${value.itemId}:new-ad:${adId}`:value.id); if (watchSystemNotificationsPending.has(key)) continue; watchSystemNotificationsPending.add(key); claims.push({key,id:value.id}); if (!(Number((await readWatchNotificationStorage(key))?.expiresAt)>Date.now())) await writeWatchNotificationStorage(key,{owner,expiresAt:Date.now()+7*86400000}); } if (!claims.length) return false; await new Promise(resolve => setTimeout(resolve,120)); const settings=getSettings(),storedEvents=await readWatchNotificationStorage(WATCH_EVENTS_STORAGE_KEY),current=[]; const stored=new Map((Array.isArray(storedEvents)?storedEvents:[]).map(value => [value.id,value])); for (const {key,id} of claims) { if ((await readWatchNotificationStorage(key))?.owner!==owner) continue; const value=stored.get(id); if (enabled(settings) && value?.alertTriggered && !value.acknowledged) current.push(value); else await release(key); } if (!current.length) return false; await GM_notification({ title:settings.watchNotificationAnonymous ? 'Powiadomienie':'Escort Helper', text:settings.watchNotificationAnonymous ? normalizeWatchNotificationText(settings.watchNotificationText) :grouped ? `Nowe alerty: ${current.length}. Pozycje: ${new Set(current.map(value => value.itemId)).size}.`:current[0].message, tag:`watch:${grouped ? 'summary':claims[0].key.slice(WATCH_SYSTEM_NOTIFICATION_CLAIM_PREFIX.length)}`, onclick:() => {if (grouped) openWatchedSettingsModal(); else if (current[0].url) void openWatchEvent(current[0]); else acknowledgeWatchEvent(current[0].id);} }); return true; } catch (error) { for (const {key} of claims) try {await release(key);} catch {} log('Nie udało się wyświetlić powiadomienia systemowego',error); return false; } finally {for (const {key} of claims) watchSystemNotificationsPending.delete(key);} } function getWatchEventAdId(event) { const fromUrl=parseAdIdFromUrl(event?.url); if (fromUrl) return String(fromUrl); for (const source of [event?.message,event?.description]) { const match=String(source || '').match(/(?:nowy\s+anons(?:\s+profilu)?|anons)\s*[:#-]?\s*(\d{3,7})/i); if (match) return match[1]; } return ''; } function getWatchTransitionEventId(itemId,transitionAt,type,message,description) { let hash=14695981039346656037n; for(const char of JSON.stringify([type,message,description]))hash=BigInt.asUintN(64,(hash^BigInt(char.codePointAt(0)))*1099511628211n); return `change:${itemId}:${transitionAt}:${hash.toString(36)}`; } function createWatchEvent( item, type, message, url, description='', { alertTriggered=false, alertMode='', transitionAt=0, notificationEvents=null }={} ) { const events=getWatchEvents(),adId=type === 'new-ad' ? getWatchEventAdId({message,description,url}):''; const transitionId=Number(transitionAt)>0?getWatchTransitionEventId(item.id,transitionAt,type,message,description):''; const existing=transitionId&&events.find(event=>event.id===transitionId);if(existing)return existing; if (type === 'new-ad') { const duplicate=adId && events.find(event => event?.type === 'new-ad' && String(event?.itemId || '') === String(item.id) && getWatchEventAdId(event) === adId); if (duplicate) return duplicate; } const event={ id:type === 'new-ad' && adId ? `new-ad:${item.id}:${adId}` :transitionId||`${Date.now()}-${Math.random().toString(36).slice(2)}`, itemId:item.id, type, name:item.displayName, message, description:String(description || '').slice(0,5000), url:url || item.state?.activeUrls?.[0] || item.profileUrls?.[0] || item.adUrl || '', timestamp:Date.now(), acknowledged:false, alertTriggered:!!alertTriggered, alertMode: String( alertMode || watchEventTypeToAlertMode(type) ) }; events.unshift(event); saveWatchEvents(events); if (notificationEvents) {if (event.alertTriggered) notificationEvents.push(event);} else showWatchSystemNotification(event); } function hasFavoriteStar(item) { return !!item?.favorite && item.sourceType !== 'escort-search'; } function getWatchItemSourcePhone(item) { const own=getEscortAdPhoneDigits(item) || normalizeEscortCachedPhone(String(item.watchKey || '').match(/^phone:(\d+)$/)?.[1]); if (own) return own; const phones=new Set(getWatchedItemCachedAdDetails(item,false) .map(({adData}) => getEscortAdPhoneDigits(adData)).filter(Boolean)); return phones.size === 1 ? phones.values().next().value:''; } function watchItemSourceText(item) { if (item.sourceType === 'escort-search') return 'Wyszukiwarka'; if (item.sourceType === 'escort-ad') return 'Anons'; if (item.escortiProfileOnly || item.phoneProfileOnly) return 'Escorti.pl'; const mode=normalizeStoredWatchProfileResolutionMode(item.resolutionMode); const phoneDigits=mode.startsWith('phone-') || !item.adUrl ? getWatchItemSourcePhone(item):''; const phone=mode.startsWith('phone-') || !!phoneDigits; const target=mode === 'phone-escort-search' ? 'Escort.club':'Escorti.pl'; const value=phone ? formatEscortPhoneForClipboard(phoneDigits,'local-hyphens') :(normalizeEscortiAdUrl(item.adUrl) || '').split('/').pop()?.split(/[?#]/)[0]; return value ? `[${value}] → ${target}`:phone ? `nr tel. → ${target}` :item.profileUrls?.length ? 'Escorti.pl':'adres anonsu → Escorti.pl'; } function watchItemSourceUrl(item) { if (!item) return ''; if (item.sourceType === 'escort-search') return getFavoriteSearchViewUrl(item); if (item.sourceType === 'escort-ad') return normalizeEscortiAdUrl(item.adUrl) || ''; if (item.escortiProfileOnly || item.phoneProfileOnly) return normalizeWatchUrl(item.profileUrls?.[0]) || ''; const mode=normalizeStoredWatchProfileResolutionMode(item.resolutionMode); const phoneDigits=mode.startsWith('phone-') ? getWatchItemSourcePhone(item):''; if (mode === 'phone-escort-search' && phoneDigits) { return buildEscortClubPhoneSearchUrl(phoneDigits); } if (mode === 'phone-escorti' && phoneDigits) { return buildEscortiSearchUrl(phoneDigits); } if (mode === 'address-escorti' && item.adUrl) { return buildEscortiSearchUrl(normalizeEscortiAdUrl(item.adUrl) || item.adUrl); } return normalizeWatchUrl(item.profileUrls?.[0]) || ''; } function getEscortAdStandardTextColor() { try { const sample=document.querySelector('.content-desc .tab-content, .content-desc, .content-info-col.-desc'); const color=sample && typeof getComputedStyle === 'function' ? getComputedStyle(sample).color :''; if (color && color !== 'rgb(255, 255, 255)' && color !== 'rgba(255, 255, 255, 1)') { return color; } } catch {} return 'rgba(255, 235, 247, .86)'; } function getFavoriteSettings() { const settings=getSettings(); return { profileResolutionMode:normalizeWatchProfileResolutionMode(settings.watchProfileResolutionMode), dataColumns:normalizeFavoritesDataColumns( settings.favoritesDataColumns ), categories:normalizeFavoriteCategories(settings.favoriteCategories), categoryColors:normalizeFavoriteCategoryColors( settings.favoriteCategoryColors, settings.favoriteCategories ) }; } function openFavoriteCategoriesEditor(onSaved=null) { const editorId='vm-favorite-categories-editor'; if (document.getElementById(editorId)) return; const current=getSettings(); const categories=normalizeFavoriteCategories(current.favoriteCategories); const colors=normalizeFavoriteCategoryColors(current.favoriteCategoryColors,categories); const draft=categories.map(label => ({ original:label, label, color:colors[label] || '#c8c8c8' })); const editorOverlay= prepareHelperStyleApi( makeElement('div') ); editorOverlay.id=editorId; const closeController=createDialogCloseController(editorOverlay); editorOverlay.classList.add('vm-ui-overlay'); const editorBox=makeElement('div',"vm-ui-dialog"); Object.assign(editorBox.style,{width:'min(520px, calc(100vw - 32px))'}); const editorTitle=makeElement('div',"vm-ui-title",'Edytuj kategorie'); const rowsHost=makeElement('div',"vm-ui-stack"); const statusLine=makeElement('div',"vm-ui-help vm-ui-error"); Object.assign(statusLine.style,{minHeight:'16px',marginTop:'6px'}); const renderRows=() => { rowsHost.replaceChildren(); draft.forEach((entry,index) => { const row=makeElement('div',"vm-ui-category-row"); const color=makeElement('input'); color.type='color'; color.value=normalizeFavoriteCategoryColor(entry.color) || '#c8c8c8'; color.title='Kolor kategorii'; color.classList.add('vm-ui-field','vm-ui-color'); color.addEventListener('input',() => {entry.color=color.value;}); const label=makeElement('input'); label.type='text'; label.value=entry.label; label.maxLength=60; label.classList.add('vm-ui-field'); Object.assign(label.style,{width:'100%'}); label.addEventListener('input',() => {entry.label=label.value;}); const remove=makeButton("vm-ui-button vm-ui-icon -danger",'×',{title:'Usuń kategorię'}); remove.addEventListener('click',() => { draft.splice(index,1); renderRows(); }); row.append(color,label,remove); rowsHost.appendChild(row); }); const addRow=makeElement('div',"vm-ui-actions"); const addInput=makeElement('input',"vm-ui-field",null,{type:'text',placeholder:'Nowa kategoria',maxLength:60}); Object.assign(addInput.style,{flex:'1 1 auto',minWidth:'0'}); const add=makeButton("vm-ui-button -accent",'Dodaj'); const addCategory=() => { const label=normalizeEscortAdText(addInput.value).slice(0,60); if (!label) return; if (draft.some(entry => normalizeText(entry.label) === normalizeText(label))) { statusLine.textContent='Taka kategoria już istnieje.'; return; } draft.push({original:'',label,color:'#c8c8c8'}); statusLine.textContent=''; renderRows(); }; add.addEventListener('click',addCategory); addInput.addEventListener('keydown',event => { if (event.key === 'Enter') {event.preventDefault(); addCategory();} }); addRow.append(addInput,add); rowsHost.appendChild(addRow); }; renderRows(); const actions=makeElement('div',"vm-ui-actions"); const cancel=makeButton('','Anuluj'); const save=makeButton('','Zapisz'); for (const button of [cancel,save]) button.classList.add('vm-ui-button'); save.classList.add('-primary'); cancel.addEventListener('click',() => closeController.close('button')); save.addEventListener('click',() => { const cleaned=draft.map(entry => ({ ...entry, label:normalizeEscortAdText(entry.label).slice(0,60), color:normalizeFavoriteCategoryColor(entry.color) || '#c8c8c8' })).filter(entry => entry.label); const keys=cleaned.map(entry => entry.label.toLocaleLowerCase('pl-PL')); if (new Set(keys).size !== keys.length) { statusLine.textContent='Nazwy kategorii muszą być unikalne.'; return; } const nextCategories=cleaned.map(entry => entry.label); const nextColors=Object.fromEntries(cleaned.map(entry => [entry.label,entry.color])); const renameMap=new Map(cleaned.filter(entry => entry.original).map(entry => [entry.original,entry.label])); const nextItems=getWatchedItems().map(item => { const assigned=normalizeFavoriteAssignedCategories(item.categories); if (!assigned.length) return item; const nextAssigned=assigned .map(value => renameMap.has(value) ? renameMap.get(value):value) .filter(value => !categories.includes(value) || nextCategories.includes(value)); return normalizeWatchedItem({...item,categories:nextAssigned}); }); const removedNames=categories.filter(name=>!renameMap.has(name) && !nextCategories.includes(name)); withTrackedUndo('Usunięto kategorie.',()=>{ saveWatchedItems(nextItems); updateSettings({favoriteCategories:nextCategories,favoriteCategoryColors:nextColors}); },()=>removedNames.length>0,{names:removedNames,colors}); closeController.close('button'); if (typeof onSaved === 'function') { try { onSaved({ categories:nextCategories, colors:nextColors }); } catch (error) { log( 'Błąd odświeżenia po edycji kategorii', error ); } } }); actions.append(cancel,save); editorBox.append(editorTitle,rowsHost,statusLine,actions); editorOverlay.appendChild(editorBox); document.body.appendChild(editorOverlay); } function getFavoriteMatchesForCurrentEscortAd(adId=getAdIdFromUrl()) { if (!adId) return []; const profileIds=new Set(getHiddenResultProfileUrls(getListCache(adId)).map(getEscortiProfileId).filter(Boolean)); return getWatchedItems().filter(item => getWatchedItemAdUrls(item).some(url => parseAdIdFromUrl(url) === adId) || (item.sourceType !== 'escort-ad' && item.sourceType !== 'escort-search' && item.profileUrls.some(url => profileIds.has(getEscortiProfileId(url)))) ); } function getFavoriteMatchesForCurrentEscortiProfile() { const profileUrl=normalizeWatchUrl(`${location.origin}${location.pathname}`); const profileId=getEscortiProfileId(profileUrl); const watchKey=`profiles:${profileId || profileUrl}`; return getWatchedItems().filter(item => { if (item.sourceType === 'escort-ad') return false; if (item.watchKey && item.watchKey === watchKey) return true; return (item.profileUrls || []).some(url => { if (profileId) return getEscortiProfileId(url) === profileId; return normalizeWatchUrl(url) === profileUrl; }); }); } function saveFavoriteItemFromContext(context,patch={},strictScope=null) { const candidate=normalizeWatchedItem({ id:'favorite-candidate', ...context, watchKey:getWatchKey(context), modes:patch.modes || {}, favorite:patch.favorite !== false, note:patch.note || '', categories:patch.categories || [] }); const items=getWatchedItems(); const index=items.findIndex(item => { if (strictScope === 'single' && item.sourceType !== 'escort-ad') return false; if (strictScope === 'person' && item.sourceType === 'escort-ad') return false; return context.id ? item.id === context.id:watchedItemsOverlap(item,candidate); }); const modePatch=patch.modes || {}; if (index >= 0) { const existing=items[index]; const replacing=existing.resolutionMode !== candidate.resolutionMode; items[index]=normalizeWatchedItem({ ...existing, ...context, id:existing.id, watchKey:replacing ? candidate.watchKey:existing.watchKey || candidate.watchKey, displayName:existing.customDisplayName || context.displayName || existing.displayName, customDisplayName:existing.customDisplayName || null, imageUrl:context.imageUrl || existing.imageUrl, profileUrls:[...new Set([...(replacing || getEscortiWatchSearchValue(context) ? []:existing.profileUrls || []),...(context.profileUrls || [])])], resolvedAdUrls:[...new Set([...(replacing || context.sourceType === 'escort-search' ? []:existing.resolvedAdUrls || []),...(context.resolvedAdUrls || [])])], phoneDigits:context.phoneDigits || existing.phoneDigits, adUrl:context.adUrl || existing.adUrl, favorite:patch.favorite == null ? existing.favorite:!!patch.favorite, hidden:patch.hidden == null ? existing.hidden:!!patch.hidden, hiddenAt:patch.hidden === true && !existing.hidden ? Date.now():existing.hiddenAt, note:patch.note == null ? existing.note:String(patch.note || '').trim(), categories:patch.categories == null ? existing.categories :normalizeFavoriteAssignedCategories(patch.categories), resolutionMode:normalizeStoredWatchProfileResolutionMode( context.resolutionMode || existing.resolutionMode || getFavoriteSettings().profileResolutionMode ), modes:{ online: modePatch.online == null ? existing.modes.online :!!modePatch.online, location: modePatch.location == null ? existing.modes.location :!!modePatch.location, changes: modePatch.changes == null ? existing.modes.changes :!!modePatch.changes, newAds: existing.sourceType !== 'escort-ad' && ( modePatch.newAds == null ? existing.modes.newAds :!!modePatch.newAds ) }, locationTarget:patch.locationTarget || existing.locationTarget, state:replacing ? null:existing.state, lastCheckedAt:replacing ? 0:existing.lastCheckedAt, lastFullCheckedAt:replacing ? 0:existing.lastFullCheckedAt, lastError:replacing ? null:existing.lastError, hiddenLastCheckedAt:replacing ? 0:existing.hiddenLastCheckedAt, hiddenLastError:replacing ? null:existing.hiddenLastError }); if (replacing) saveWatchEvents(getWatchEvents().filter(event => event.itemId !== existing.id)); saveWatchedItems(items); return items[index]; } const created=normalizeWatchedItem({ id:`${Date.now()}-${Math.random().toString(36).slice(2)}`, ...context, watchKey:getWatchKey(context), favorite:patch.favorite == null ? patch.hidden !== true:!!patch.favorite, note:String(patch.note || '').trim(), hidden:patch.hidden === true, hiddenAt:patch.hidden ? Date.now():0, categories:normalizeFavoriteAssignedCategories(patch.categories), resolutionMode:normalizeStoredWatchProfileResolutionMode( context.resolutionMode || getFavoriteSettings().profileResolutionMode ), modes:{ online:!!modePatch.online, location:!!modePatch.location, changes:!!modePatch.changes, newAds: context.sourceType !== 'escort-ad' && !!modePatch.newAds }, locationTarget:patch.locationTarget || {type:'any',city:''}, createdAt:Date.now() }); saveWatchedItems([...items,created]); return created; } function watchedItemHasNotesOrCategories(item) { return !!item?.note || normalizeFavoriteAssignedCategories(item?.categories).length > 0; } function clearAllFavorites() { return withTrackedUndo('Usunięto wszystkie Ulubione.',()=>{ const stored=GM_getValue(WATCH_ITEMS_STORAGE_KEY,[]); if (!Array.isArray(stored)) return 0; const ids=new Set(); const next=stored.map(raw => { const item=normalizeWatchedItem(raw); if (!item?.favorite) return raw; ids.add(item.id); // Keep even empty profiles across reloads and background data refreshes. return {...raw,favorite:false,preserveProfile:true,modes:{online:false,location:false,changes:false,newAds:false}}; }); if (!ids.size) return 0; writeHelperStorageValue(WATCH_ITEMS_STORAGE_KEY,next); const events=getWatchEvents(); if (events.some(event => ids.has(event.itemId) && event.alertTriggered && !event.acknowledged)) { saveWatchEvents(events.map(event => ids.has(event.itemId) && event.alertTriggered && !event.acknowledged ? {...event,acknowledged:true}:event)); } refreshWatchPageButtons(); try {document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();} catch {} renderWatchNotificationBar(); return ids.size; }); } function clearAllFavoriteNotes() { return withTrackedUndo('Usunięto notatki.',() => { const items=getWatchedItems(),changed=items.filter(item => item.note).length; if (!changed) return 0; const next=items.map(item => normalizeWatchedItem({...item,note:''})).filter(item => item && (item.preserveProfile || item.favorite || item.hidden || item.categories.length || watchedItemHasActiveAlerts(item))); writeHelperStorageValue(WATCH_ITEMS_STORAGE_KEY,next); refreshWatchPageButtons(); renderWatchNotificationBar(); document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.(); return changed; }); } function clearAllFavoriteAssignedCategories() { return withTrackedUndo('Usunięto przypisania kategorii.',()=>{ const items=getWatchedItems(); let changed=0; const nextItems=items.map(item => { if ( !normalizeFavoriteAssignedCategories( item?.categories ).length ) { return item; } changed += 1; return normalizeWatchedItem({ ...item, categories:[] }); }); if (changed) { saveWatchedItems(nextItems); document .getElementById( WATCH_SETTINGS_OVERLAY_ID ) ?._vmRender?.(); } return changed; }); } function removeFavoriteOnly(itemId) { return withTrackedUndo('Usunięto z Ulubionych.',()=>{ const items=getWatchedItems(); const index=items.findIndex(item => item.id === itemId); if (index < 0) return; const item=items[index]; const keepForMetadata=item.preserveProfile || item.hidden || watchedItemHasNotesOrCategories(item); if (keepForMetadata) { items[index]=normalizeWatchedItem({ ...item, favorite:false, modes:{online:false,location:false,changes:false,newAds:false} }); } else { items.splice(index,1); saveWatchEvents(getWatchEvents().filter(event => event.itemId !== itemId)); } saveWatchedItems(items); renderWatchNotificationBar(); }); } function getLastFavoriteAlertCity() { try { const stored=GM_getValue(FAVORITE_LAST_ALERT_MODES_STORAGE_KEY,null); return normalizeEscortCity(stored?.locationCity || ''); } catch { return ''; } } function saveLastFavoriteAlertCity(city) { const locationCity=normalizeEscortCity(city); if (!locationCity) return; try { writeHelperStorageValue(FAVORITE_LAST_ALERT_MODES_STORAGE_KEY,{locationCity}); } catch {} } function chooseFavoriteScope() { return chooseTrackedEntriesDialog(getFavoriteMatchesForCurrentEscortAd().filter(item => item.favorite)); } function chooseTrackedEntriesDialog(items,hidden=false,isProfile=false) { if (isProfile && hidden && items.length <= 1) return Promise.resolve(editHiddenForCurrentPage()); return new Promise(resolve => { const name=extractWatchPageName(document,items[0]?.displayName || (isProfile ? 'Profil':'Anons')); const {overlay,box}=createWatchDialogBase('vm-tracked-entries-overlay',hidden ? 'Ukryte':'Ulubione'); const title=box.firstElementChild; const heading=makeElement('div','vm-ui-label',name); heading.style.marginBottom='10px'; box.appendChild(heading); let result=null; const controller=createDialogCloseController(overlay,{onClose:() => resolve(result)}); const action=(value,label,primary=false) => { const button=styleDialogActionButton(makeButton('',label),primary); button.addEventListener('click',() => { if (value.startsWith('remove:') || value.startsWith('restore:')) { const id=value.slice(value.indexOf(':') + 1); if (hidden) restoreHiddenItems([id]); else removeFavoriteOnly(id); items=items.filter(item => item.id !== id); refreshWatchPageButtons(); if (!items.length && isProfile) controller.close('empty'); else render(); return; } result=value; controller.close('choice'); }); return button; }; const render=() => { box.replaceChildren(title,heading); const renderItem=item => { const row=makeElement('div','vm-ui-panel'); Object.assign(row.style,{display:'flex',alignItems:'center',flexWrap:'wrap',gap:'8px',padding:'10px'}); const source=watchItemSourceText(item); const label=makeElement('div','',item.sourceType === 'escort-ad' ? 'Ten anons':source.replace('Profil: ','Profil · ').replace('adres anonsu','adres').replace('wyszukiwarka ','')); Object.assign(label.style,{flex:'1 1 200px',overflowWrap:'anywhere'}); label.title=trackedItemChoiceLabel(item); if (items.filter(other => watchItemSourceText(other) === source).length > 1) { const detail=makeElement('div','vm-ui-help',`${item.displayName} · ${item.phoneDigits || item.profileUrls?.map(getEscortiProfileId).filter(Boolean).join(', ') || item.id}`); label.appendChild(detail); } const actions=makeElement('div','vm-ui-row'); const remove=action(`${hidden ? 'restore':'remove'}:${item.id}`,hidden ? 'Nie ukrywaj':'Usuń'); remove.classList.add('-danger'); remove.setAttribute('aria-label',`${hidden ? 'Nie ukrywaj':'Usuń z ulubionych'}: ${trackedItemChoiceLabel(item)}`); actions.appendChild(remove); row.append(label,actions); return row; }; const sections=makeElement('div'); Object.assign(sections.style,{display:'grid',gap:'12px',maxHeight:'60vh',overflowY:'auto'}); const adActions=isProfile ? []:items.filter(item => item.sourceType === 'escort-ad').map(renderItem); if (!isProfile && !adActions.length) adActions.push(action('single',hidden ? '+ Ukryj ten anons':'+ Dodaj ten anons')); const profileActions=[]; const profiles=items.filter(item => item.sourceType !== 'escort-ad'); if (isProfile) { profileActions.push(...profiles.map(renderItem)); } else { const available=new Set(getSelectableWatchProfileModes().map(([mode]) => mode)); for (const [mode] of WATCH_PROFILE_RESOLUTION_OPTIONS) { const saved=profiles.filter(item => item.resolutionMode === mode); if (saved.length) {profileActions.push(...saved.map(renderItem)); continue;} if (!available.has(mode)) continue; const label=watchItemSourceText({resolutionMode:mode}).replace('Profil: ','').replace('adres anonsu','adres').replace('wyszukiwarka ',''); profileActions.push(action(`person:${mode}`,`${hidden ? '+ Ukryj':'+ Dodaj'} · ${label}`)); } profileActions.push(...profiles.filter(item => item.sourceType === 'escort-search').map(renderItem)); } for (const [label,buttons] of [['Anons',adActions],['Profil',profileActions]]) { if (!buttons.length) continue; const section=makeElement('div','vm-ui-panel'); Object.assign(section.style,{display:'grid',gap:'7px',padding:'10px'}); section.append(makeElement('div','vm-ui-label',label),...buttons); sections.appendChild(section); } const close=createDialogCancelButton(controller); close.textContent='Zamknij'; box.append(sections,close); }; render(); }); } function getSelectableWatchProfileModes(allowedModes=null) { const configured=getFavoriteSettings().profileResolutionMode; const allowed=Array.isArray(allowedModes) && allowedModes.length ? new Set(allowedModes):null; return WATCH_PROFILE_RESOLUTION_OPTIONS.filter(([value]) => value !== 'ask' && (configured === 'ask' || value === configured) && (!allowed || allowed.has(value))); } async function chooseFavoriteProfileResolutionMode(shownModes=[],allowedModes=null) { const choices=getSelectableWatchProfileModes(allowedModes).map(([value,label]) => [value,shownModes.includes(value) ? `${label} - już w panelu`:label,shownModes.includes(value)]); if (choices.length === 1 && !choices[0][2]) return choices[0][0]; return chooseWatchOptionDialog({ id:'vm-garso-favorite-profile-resolution-overlay', title:'Jak ustalić profil osoby?', info:'Wybierz sposób znalezienia wszystkich anonsów tej osoby.', choices, primaryValue:choices.some(([value]) => value === 'address-escorti') ? 'address-escorti':choices[0]?.[0] }); } function trackedItemChoiceLabel(item) { return `${item.displayName} - ${watchItemSourceText(item)}`; } function chooseWatchAlertOptions( existing=null, scopeLabel='', dialogOptions={} ) { return new Promise(resolve => { const locationOnly= dialogOptions.locationOnly === true; const rememberedAlertCity=getLastFavoriteAlertCity(); const title=locationOnly ? 'Alert lokalizacji' :(dialogOptions.scope === 'single' ? 'Alerty anonsu':'Alerty profilu'); const {overlay,box}=createWatchDialogBase( 'vm-garso-favorite-options-overlay', title ); if (!locationOnly) Object.assign(box.style,{maxWidth:'420px',padding:'16px'}); if (locationOnly && scopeLabel) box.appendChild(makeElement('div','vm-ui-label',scopeLabel)); const intro=makeElement('div',"vm-ui-help",dialogOptions.info || (locationOnly ? 'Ustaw sposób obserwowania zmiany lokalizacji' :'Wybierz jakie alerty chcesz otrzymywać')); Object.assign(intro.style,{marginBottom:'12px'}); const makeMode=(label,checked,tooltip='') => { const row=makeElement('label'); if (tooltip) row.title=tooltip; row.classList.add('vm-ui-choice'); const input=makeElement('input','',null,{type:'checkbox',checked:!!checked}); input.style.accentColor='var(--vm-ui-accent, #f54da3)'; const labelText=makeElement('span','',label); labelText.style.fontWeight=input.checked ? '700':'400'; input.addEventListener('change',() => { labelText.style.fontWeight=input.checked ? '700':'400'; }); row.append(input,labelText); return {row,input,labelText}; }; const online=makeMode( 'Obserwuj online / offline', existing?.modes?.online, 'Pokazuje na górnym pasku alert o zmianie stanu online/offline. Historia zmian w kolumnie „Zmiany” jest zapisywana niezależnie od tego checkboxa.' ); const location=makeMode( 'Obserwuj zmianę lokalizacji', existing?.modes?.location, 'Powiadamia o zmianie lokalizacji albo o pojawieniu się w wybranym mieście - zgodnie z ustawieniem poniżej.' ); const changes=makeMode( 'Obserwuj zmiany danych anonsu', existing?.modes?.changes, 'Powiadamia o zmianie nazwy anonsu, adresu, cennika, godzin dostępności, tagów/usług, opisu, numeru telefonu lub danych profilu. Zmiana stanu online/offline nie jest zmianą danych anonsu.' ); const isProfileScope=existing ? existing.sourceType !== 'escort-ad' :dialogOptions.scope === 'person' || /^profil$/i.test(String(scopeLabel || '').trim()); const newAds=makeMode( 'Obserwuj pojawienie się nowego anonsu w profilu', isProfileScope && existing?.modes?.newAds, 'Pokazuje na górnym pasku alert, gdy w obserwowanym profilu pojawi się anons o ID, którego wcześniej w tym profilu nie było. Historia nowych anonsów jest zapisywana niezależnie od tego checkboxa.' ); if (!isProfileScope) { newAds.input.checked=false; newAds.input.disabled=true; newAds.row.style.opacity='.42'; newAds.row.style.cursor='default'; newAds.row.title= 'Powiadamia o pojawieniu się nowego anonsu. Ten alert jest dostępny tylko dla całego profilu, nie dla pojedynczego anonsu.'; } const locationControls=createWatchLocationTargetControls( existing?.modes?.location || existing?.locationTarget?.type === 'city' ? existing.locationTarget :{type:rememberedAlertCity ? 'city':'any',city:rememberedAlertCity} ); const syncLocationControls=() => { locationControls.setEnabled(location.input.checked); }; location.input.addEventListener('change',syncLocationControls); syncLocationControls(); const actions=makeElement('div',"vm-ui-actions"); const right=makeElement('div',"vm-ui-row"); const cancel=makeButton('','Anuluj'); const save=makeButton('','Zapisz'); for (const btn of [cancel,save]) { btn.classList.add('vm-ui-button'); } save.classList.add('-primary'); right.append(cancel,save); actions.appendChild(right); let dialogResult=null; const closeController=createDialogCloseController(overlay,{ onClose:() => resolve(dialogResult) }); const finish=value => { dialogResult=value; closeController.close('button'); }; cancel.addEventListener('click',() => finish(null)); save.addEventListener('click',() => { if (save.disabled) return; let locationTarget= existing?.locationTarget || {type:'any',city:''}; if (location.input.checked) { const selectedTarget=locationControls.read(); if (!selectedTarget) { locationControls.input.focus(); return; } locationTarget=selectedTarget; } const selectedModes={ online:locationOnly ? !!existing?.modes?.online :online.input.checked, location:location.input.checked, changes:locationOnly ? !!existing?.modes?.changes :changes.input.checked, newAds: isProfileScope && ( locationOnly ? !!existing?.modes?.newAds :newAds.input.checked ) }; if (selectedModes.location && locationTarget.type === 'city') saveLastFavoriteAlertCity(locationTarget.city); finish({ modes:selectedModes, locationTarget }); }); if (locationOnly) { box.append( intro, location.row, locationControls.root, actions ); } else { box.append( intro, online.row, location.row, locationControls.root, changes.row, ...(isProfileScope ? [newAds.row]:[]), actions ); } setTimeout(() => { if ( location.input.checked && !locationControls.anyInput.checked ) { locationControls.input.focus(); } },0); }); } function getCurrentEscortAdBaseData(adUrl,needPhone=false) { const adId=parseAdIdFromUrl(adUrl); if (!adId) return Promise.resolve(null); if (CURRENT_HOST === 'pl.escort.club' && getAdIdFromUrl() === adId) { const data=extractEscortClubAdData(document,adId,adUrl); if (!needPhone) return Promise.resolve(data); return resolveEscortPhoneForCurrentPage(data.phoneId).then(phone => { if (phone) Object.assign(data,phone); return data; }); } return getEscortAdData(adId,adUrl,false,null,null,{needPhone}).then(result => result?.status === 'ok' ? result:null); } async function buildFavoriteContextFromEscortAd(adUrl,scope='single',resolutionMode=null) { const normalizedAdUrl=normalizeEscortiAdUrl(adUrl); const adId=parseAdIdFromUrl(normalizedAdUrl); if (!normalizedAdUrl || !adId) throw new Error('Nieprawidłowy adres anonsu Escort.club'); if (scope === 'single') { const data=await getCurrentEscortAdBaseData(normalizedAdUrl); return { sourceType:'escort-ad', adUrl:normalizedAdUrl, profileUrls:[], phoneDigits:data?.phoneDigits || null, displayName:data?.title || `Anons ${adId}`, imageUrl:getAdIdFromUrl() === adId ? extractWatchPageImage(document,normalizedAdUrl):'', watchKey:`ad:${adId}`, resolutionMode:'single' }; } const configured=getFavoriteSettings().profileResolutionMode; let mode=normalizeWatchProfileResolutionMode(configured === 'ask' ? resolutionMode || configured:configured); if (mode === 'ask') { mode=await chooseFavoriteProfileResolutionMode(); if (!mode) return null; } if (mode === 'address-escorti') { const data=await getCurrentEscortAdBaseData(normalizedAdUrl); const context={sourceType:'escorti-profile',resolutionMode:mode,adUrl:normalizedAdUrl,profileUrls:[],phoneDigits:null, displayName:data?.title || `Anons ${adId}`,imageUrl:getAdIdFromUrl() === adId ? extractWatchPageImage(document,normalizedAdUrl):''}; const result=await resolveWatchedProfileData(context); return {...context,profileUrls:result.profileUrls,resolvedAdUrls:result.adUrls, displayName:result.displayName,imageUrl:result.imageUrl,watchKey:getWatchKey(context)}; } const data=await getCurrentEscortAdBaseData(normalizedAdUrl,true); const phoneDigits=getEscortAdPhoneDigits(data); if (!phoneDigits) throw new Error('Nie udało się odczytać numeru telefonu dla tego anonsu'); if (mode === 'phone-escort-search') { return { sourceType:'phone', adUrl:normalizedAdUrl, profileUrls:[], resolvedAdUrls:[normalizedAdUrl], phoneDigits, displayName:data?.title || `Profil numeru ${phoneDigits}`, imageUrl:getAdIdFromUrl() === adId ? extractWatchPageImage(document,normalizedAdUrl):'', watchKey:`phone:${phoneDigits}`, resolutionMode:mode }; } const found=await searchEscortiProfilesDirect(phoneDigits); const profileUrls=found.status === 'ok' ? found.profileUrls || []:[]; const profileInfos=(await watchMapLimit(profileUrls,2,async profileUrl => { try {return await readEscortiProfileDirect(profileUrl);} catch {return null;} })).filter(Boolean); return { sourceType:profileUrls.length ? 'escorti-profile':'phone', adUrl:normalizedAdUrl, profileUrls, resolvedAdUrls:[...new Set(profileInfos.flatMap(info => info.adUrls || []))], phoneDigits, displayName:profileInfos[0]?.profileName || data?.title || `Profil numeru ${phoneDigits}`, imageUrl:profileInfos[0]?.imageUrl || (getAdIdFromUrl() === adId ? extractWatchPageImage(document,normalizedAdUrl):''), watchKey:profileUrls.length ? `profiles:${profileUrls.map(getEscortiProfileId).filter(Boolean).sort().join(',')}` :`phone:${phoneDigits}`, resolutionMode:mode }; } async function resolveWatchedProfileData(item,requestContext=null) { const operationContext=createRequestContext('watch.profile-resolution',requestContext); if (item.sourceType === 'escort-search') return resolveFavoriteSearch(item,null,null,operationContext); const mode=normalizeStoredWatchProfileResolutionMode(item.resolutionMode); if (mode === 'phone-escort-search' && item.phoneDigits) { const found=await fetchEscortClubPhoneSearchAds(item.phoneDigits,null,operationContext); return { profileUrls:item.profileUrls || [], adUrls:[...new Set([...(found.adUrls || []),...(item.resolvedAdUrls || [])])], displayName:item.displayName, imageUrl:item.imageUrl, profileInfos:[] }; } let profileUrls=[...new Set(item.profileUrls || [])]; const search=getEscortiWatchSearchValue(item); if (search) { const found=await searchEscortiProfilesDirect(search,null,operationContext); if (found.status !== 'ok') throw new Error('Nie udało się odczytać wyszukiwania Escorti - zachowuję poprzednie powiązania.'); profileUrls=found.profileUrls || []; } else if (!profileUrls.length && item.phoneDigits && !item.escortiProfileOnly && !item.phoneProfileOnly) { const found=await searchEscortiProfilesDirect(item.phoneDigits,null,operationContext); profileUrls=found.status === 'ok' ? found.profileUrls || []:[]; } const profileInfos=(await watchMapLimit(profileUrls,2,async profileUrl => { try {return await readEscortiProfileDirect(profileUrl,null,operationContext);} catch {return null;} })).filter(Boolean); if (profileInfos.length !== profileUrls.length) throw new Error('Nie udało się odczytać wszystkich profili Escorti - zachowuję poprzednie powiązania.'); const adUrls=[...new Set([ ...profileInfos.flatMap(info => info.adUrls || []) ].map(url => normalizeEscortiAdUrl(url)).filter(Boolean))]; return { profileUrls, adUrls, displayName:profileInfos.map(info => info.profileName).find(Boolean) || item.displayName, imageUrl:profileInfos.map(info => info.imageUrl).find(Boolean) || item.imageUrl, profileInfos }; } const WATCH_FULL_CHECK_INTERVAL_MS=60 * 60 * 1000; const WATCH_SCHEDULE_POLL_INTERVAL_MS=60000; function watchedItemHasActiveAlerts(item) { return !!( item?.modes?.online || item?.modes?.location || item?.modes?.changes || item?.modes?.newAds ); } function getWatchedItemCheckTiming(item,settings) { const hasAlerts=watchedItemHasActiveAlerts(item); const eligible=!!(settings?.enabled && (item?.favorite || hasAlerts || (item?.note && item.sourceType !== 'escort-ad'))); const intervalMs=Math.max(1,Number(settings?.intervalMinutes) || 15) * 60000; const hourly=intervalMs < WATCH_FULL_CHECK_INTERVAL_MS && !hasAlerts; const expectedMs=hourly ? WATCH_FULL_CHECK_INTERVAL_MS:intervalMs; const lastCheckedAt=Number(item?.lastCheckedAt) || 0; const referenceAt=Number(hourly && item?.favorite ? item.lastFullCheckedAt:lastCheckedAt) || 0; return {eligible,expectedMs,referenceAt,dueAt:Math.max(referenceAt ? referenceAt + expectedMs:0, item?.lastError || item?.state?.partial ? lastCheckedAt + intervalMs:0)}; } function shouldRunScheduledFullWatchCheck( item, settings, now=Date.now() ) { if (Number(settings?.intervalMinutes) >= 60) return true; const lastFullCheckedAt=Number(item?.lastFullCheckedAt) || 0; return !lastFullCheckedAt || now - lastFullCheckedAt >= WATCH_FULL_CHECK_INTERVAL_MS; } async function inspectWatchedItem( item, onAdProgress=null, {fullCheck=true,requestContext=null}={} ) { const operationContext=createRequestContext('watch.item',requestContext); let adUrls=[]; let profileUrls=item.profileUrls || []; let displayName=item.displayName; let imageUrl=item.imageUrl; let profileImageUrl=''; let profileInfos=[],verifiedAdResults=[]; if (item.sourceType === 'escort-ad') { if (!item.adUrl) throw new Error('Brak adresu anonsu Escort.club'); adUrls=[item.adUrl]; } else { const profileData=await resolveWatchedProfileData(item,operationContext); profileUrls=profileData.profileUrls; adUrls=profileData.adUrls; displayName=profileData.displayName; profileImageUrl=profileData.imageUrl || ''; profileInfos=profileData.profileInfos || []; verifiedAdResults=profileData.verifiedAdResults || []; } const profileNoteDiscoveryOnly=item.sourceType !== 'escort-ad' && !!item.note && !item.favorite && !item.modes.online && !item.modes.location && !item.modes.changes && !item.modes.newAds; const newAdsOnlyCheck= !fullCheck && item.sourceType !== 'escort-ad' && item.modes.newAds && !item.modes.online && !item.modes.location && !item.modes.changes; if (profileNoteDiscoveryOnly || newAdsOnlyCheck) { const previousState=item.state && typeof item.state === 'object' ? item.state:{}; const state={ ...previousState, online:typeof previousState.online === 'boolean' ? previousState.online:null, activeCount:Number(previousState.activeCount) || 0, totalAds:adUrls.length, cities:Array.isArray(previousState.cities) ? previousState.cities:[], activeUrls:Array.isArray(previousState.activeUrls) ? previousState.activeUrls:[], profileDates:[...new Set(profileInfos.map(info => formatEscortiDate(info.creationDate)).filter(Boolean))], checkedAt:Date.now() }; return { displayName, imageUrl:profileImageUrl || imageUrl, profileUrls, adUrls, activeResults:verifiedAdResults, profileInfos, state, historyFullCheckCompleted:false, fullCheckCompleted:false }; } onAdProgress?.({ checked:0, total:adUrls.length, activeCount:0, url:null, result:null }); const needsDetails=fullCheck || item.modes.location; const activitySummary=await scanActiveEscortAds(adUrls,onAdProgress,true,{activityOnly:!needsDetails,headFirst:needsDetails, forceDetails:false,detailMinCheckedAt:needsDetails && Number(item.lastFullCheckedAt) > 0 ? Number(item.lastFullCheckedAt) + 1:0, verifiedResults:new Map(verifiedAdResults.map(result=>[result.url,result])), requestContext:createRequestContext(needsDetails ? 'watch.details':'watch.activity',operationContext)}); const results=Array.isArray(activitySummary?.results) ? activitySummary.results :[]; const activeResults=results.filter(result => result?.active && !result.stale && !result.unknown && !result.error); let inspectedImageUrl= activeResults .map(result => result.imageUrl) .find(Boolean) || ''; if ( fullCheck && item.sourceType !== 'escort-ad' && isWatchThumbnailKnownUnavailable(item.imageUrl) ) { const cachedAdImages=adUrls .map(url => parseAdIdFromUrl(url)) .filter(Boolean) .map(adId => getStoredEscortAdData(adId)?.imageUrl || '' ) .filter(Boolean); const replacementImageUrl= await findNextAvailableWatchThumbnail( item.imageUrl, [ ...activeResults .map(result => result.imageUrl) .filter(Boolean), ...cachedAdImages, profileImageUrl ] ); imageUrl=replacementImageUrl || ''; if (replacementImageUrl) { inspectedImageUrl=replacementImageUrl; } } const unknownUrls=new Set(results.filter(result => result?.unknown).map(result => normalizeEscortiAdUrl(result.url)).filter(Boolean)); const unknownCount=unknownUrls.size; const activeUrls=[...new Set([...activeResults.map(result => normalizeEscortiAdUrl(result.url)), ...(item.state?.activeUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(url => unknownUrls.has(url))].filter(Boolean))]; const observedCities=[...new Set(activeResults.map(result => normalizeEscortCity(result.city)).filter(Boolean))]; const activeWithoutCity=activeResults.some(result => !normalizeEscortCity(result.city)); const locationPartial=unknownCount > 0 || activeWithoutCity; const previousCities=Array.isArray(item.state?.cities) ? item.state.cities.map(normalizeEscortCity).filter(Boolean):[]; const cities=locationPartial ? previousCities:observedCities; const activeCount=activeUrls.length; const previousOnline=typeof item.state?.online === 'boolean' ? item.state.online:null; const online=adUrls.length === 0 ? null:(activeCount > 0 ? true:(unknownCount > 0 ? previousOnline:false)); if ( !hasUsefulWatchThumbnail(item) && online === true && previousOnline !== true ) { imageUrl= inspectedImageUrl || ( isWatchThumbnailKnownUnavailable( profileImageUrl ) ? '' :profileImageUrl ) || imageUrl; } const state={ online, activeCount, totalAds:adUrls.length, cities, activeUrls, adDetails:activeResults.filter(result => result?.adData).map(result => ({ url:normalizeEscortiAdUrl(result.url) || result.url, adData:result.adData })), profileDates:[...new Set(profileInfos.map(info => formatEscortiDate(info.creationDate)).filter(Boolean))], unknownCount, partial:unknownCount > 0 || !!activitySummary?.detailsUnknownCount, locationPartial, checkedAt:Date.now() }; registerEscortDataFieldsFromProfileState(state); return { displayName, imageUrl, profileUrls, adUrls, activeResults, profileInfos, state, historyFullCheckCompleted:!!fullCheck, fullCheckCompleted:!!fullCheck && !unknownCount && !activitySummary?.detailsUnknownCount }; } function describeFavoriteVisitChanges( changes, {includeAddress=true}={} ) { const lines=[]; if (changes.title) { lines.push( `Nazwa: ${ formatEscortChangedValue( changes.title.previous ) } → ${ formatEscortChangedValue( changes.title.current ) }` ); } if (changes.image) { lines.push( 'Główne zdjęcie: zmieniono' ); } if (includeAddress && changes.address) { lines.push( `Adres: ${ formatEscortChangedValue( changes.address.previous ) } → ${ formatEscortChangedValue( changes.address.current ) }` ); } for (const change of changes.prices || []) { lines.push(`Cena ${getEscortPriceDurationLabel(change.durationKey)}: ${formatEscortChangedPrice(change.previous)} → ${formatEscortChangedPrice(change.current)}`); } for (const change of changes.availability || []) lines.push(`${change.day}: ${change.previous} → ${change.current}`); if (changes.tags) { if (changes.tags.added?.length) lines.push(`Dodane tagi: ${changes.tags.added.join(', ')}`); if (changes.tags.removed?.length) lines.push(`Usunięte tagi: ${changes.tags.removed.join(', ')}`); } if (changes.description) lines.push('Opis: zmieniono treść'); if (changes.phone) lines.push(`Telefon: ${changes.phone.previous} → ${changes.phone.current}`); for (const change of changes.stats || []) lines.push(`${change.label}: ${formatEscortChangedValue(change.previous)} → ${formatEscortChangedValue(change.current)}`); return lines; } function getWatchHistoricalAdIdsBefore(cutoff,itemId='') { const before=Number(cutoff) || 0; const ownItemId=String(itemId || ''); const ids=new Set(); const addUrl=url => { const id=parseAdIdFromUrl(url); if (id) ids.add(String(id)); }; for (const event of getWatchEvents()) { if (ownItemId && String(event?.itemId || '') !== ownItemId) continue; if (before && Number(event?.timestamp) >= before) continue; addUrl(event?.url); for (const source of [event?.message,event?.description]) { for (const line of String(source || '').split(/\r?\n/)) { for (const match of line.matchAll(/\banons(?:u|\s+profilu)?\s*[:#-]?\s*(\d{3,7})\b/gi)) ids.add(match[1]); const list=line.match(/(?:włączone|wyłączone)\s+anonse\s*:\s*([\d,\s]+)/i); if (list) for (const match of list[1].matchAll(/\b(\d{3,7})\b/g)) ids.add(match[1]); } } } return ids; } function isActuallyNewFavoriteSearchAd(item,entry,historicalIds) { if (item?.sourceType !== 'escort-search') return true; const id=String(entry?.id || ''); if (!id || historicalIds?.has(id)) return false; // Równe watchSnapshotCheckedAt i lastFullCheckedAt potwierdzają wcześniejszy wynik // tego wyszukiwania, także po utracie knownAdIds/resolvedAdUrls. const lastFullCheckedAt=Number(item.lastFullCheckedAt) || 0; if (lastFullCheckedAt) { const cached=getStoredEscortAdData(id); if (cached?.watchSnapshot && Number(cached.watchSnapshotCheckedAt) === lastFullCheckedAt) return false; } // Dane innych profili i wyszukiwań nie tłumią alertu tego wyszukiwania. return true; } function applyWatchedResult(item,result,notificationEvents=null) { const previous=item.state; const current={...result.state}; current.lastKnownCities=current.cities?.length&&!current.locationPartial?current.cities:(previous?.lastKnownCities?.length?previous.lastKnownCities:previous?.cities||[]); current.escortiData=result.profileInfos?.length?compactEscortListResult(mergeEscortiProfileResults(result.profileInfos,result.profileInfos.length)):previous?.escortiData||null; if(current.online===false&&previous?.escortiData?.garsoTopicTitles?.length&&!current.escortiData?.garsoTopicTitles?.length&& JSON.stringify([...(previous.escortiData.profileIds||[])].sort())===JSON.stringify([...(current.escortiData?.profileIds||[])].sort())) current.escortiData={...current.escortiData,garsoTopicTitles:previous.escortiData.garsoTopicTitles,garsoTopics:previous.escortiData.garsoTopics}; if(current.escortiData)registerEscortDataFieldsFromEscortiResult(expandEscortListCacheEntry(current.escortiData,item.resolutionMode)); const emitEvent=(entry,type,message,url,description='',options={})=>createWatchEvent(entry,type,message,url,description,{...options,transitionAt:previous?.checkedAt,notificationEvents}); const defaultEventUrl= result.activeResults?.[0]?.url || result.profileUrls?.[0] || item.adUrl; const contentChangeLabels=new Set(); const contentChangeDescriptions=[]; const addressChangeDescriptions=[]; const previousKnownAdIds=new Set( [...(item.sourceType === 'escort-search' ? item.knownAdIds || []:[]), ...(item.sourceType === 'escort-search' ? getWatchedItemHistoricalAdUrls(item).map(parseAdIdFromUrl):[]), ...(item.resolvedAdUrls || []).map(parseAdIdFromUrl)].filter(Boolean).map(String) ); const historicalSearchAdIds=item.sourceType === 'escort-search' ? getWatchHistoricalAdIdsBefore(previous?.checkedAt || item.lastCheckedAt,item.id) :null; const newlyDiscoveredAds= item.sourceType === 'escort-ad' ? [] :(result.adUrls || []) .map(url => ({ url:normalizeEscortiAdUrl(url) || url, id:parseAdIdFromUrl(url) })) .filter(entry => entry.id && !previousKnownAdIds.has(String(entry.id)) ) .filter(entry => isActuallyNewFavoriteSearchAd(item,entry,historicalSearchAdIds)); for (const detail of current.adDetails || []) { const adId=parseAdIdFromUrl(detail.url); if (!adId || !detail.adData) continue; const central=getStoredEscortAdData(adId); const previousSnapshot=central?.watchSnapshot || null; const currentSnapshot=createEscortVisitSnapshot( detail.adData, previousSnapshot ); if ( previous?.checkedAt && previousSnapshot ) { const changes= compareEscortAdVisitData( previousSnapshot, currentSnapshot ); const labels= changes.labels || []; for (const label of labels) { if (label === 'adres') continue; contentChangeLabels.add(label); } const contentLines= describeFavoriteVisitChanges( changes, {includeAddress:false} ); if (contentLines.length) { contentChangeDescriptions.push( `Anons ${adId}:\n${ contentLines.join('\n') }` ); } if (changes.address) { addressChangeDescriptions.push( `Anons ${adId}:\nAdres: ${ formatEscortChangedValue( changes.address.previous ) } → ${ formatEscortChangedValue( changes.address.current ) }` ); } } storeWatchedAdDataInCentralCache( adId, detail.adData, current.checkedAt, result.historyFullCheckCompleted ? currentSnapshot :previousSnapshot ); } const previousActiveIds=new Set( (previous?.activeUrls || []) .map(parseAdIdFromUrl) .filter(Boolean) .map(String) ); const currentActiveIds=new Set( (current?.activeUrls || []) .map(parseAdIdFromUrl) .filter(Boolean) .map(String) ); const enabledAds=[...currentActiveIds] .filter(id => !previousActiveIds.has(id) ); const disabledAds=[...previousActiveIds] .filter(id => !currentActiveIds.has(id) ); const updated=normalizeWatchedItem({ ...item, displayName:item.customDisplayName || result.displayName || item.displayName, customDisplayName:item.customDisplayName || null, imageUrl:result.imageUrl || item.imageUrl, profileUrls:result.profileUrls || item.profileUrls, ...(item.sourceType === 'escort-search' ? {knownAdIds:[...previousKnownAdIds]}:{}), resolvedAdUrls: item.sourceType === 'escort-search' ? (result.adUrls || item.resolvedAdUrls) :[...new Set([ ...getWatchedItemHistoricalAdUrls(item), ...(result.adUrls || []) ])], state:current, lastCheckedAt:current.checkedAt, lastError:null }); if (previous?.checkedAt) { if ( item.sourceType !== 'escort-ad' && newlyDiscoveredAds.length ) { for (const entry of newlyDiscoveredAds) { const activeResult=result.activeResults?.find( active => String(parseAdIdFromUrl(active?.url)) === String(entry.id) ); const city= normalizeEscortCity(activeResult?.city) || normalizeEscortCity( result.profileInfos ?.map(info => info.currentCity) .find(Boolean) ); emitEvent( updated, 'new-ad', `${updated.displayName} - nowy anons ${entry.id}${city ? ` • ${city}`:''}.`, entry.url || defaultEventUrl, [ `Nowy anons profilu: ${entry.id}`, city ? `Lokalizacja: ${city}`:'' ].filter(Boolean).join('\n'), { alertTriggered:!!item.modes.newAds, alertMode:'newAds' } ); } } if (enabledAds.length) { emitEvent( updated, 'online', `${updated.displayName} - Włączone anonse: ${ enabledAds.join(', ') }.`, current.activeUrls?.[0] || defaultEventUrl, `Włączone anonse: ${enabledAds.join(', ')}`, { alertTriggered:!!item.modes.online, alertMode:'online' } ); } if (disabledAds.length) { emitEvent( updated, 'online', `${updated.displayName} - Wyłączone anonse: ${ disabledAds.join(', ') }.`, result.profileUrls?.[0] || item.profileUrls?.[0] || defaultEventUrl, `Wyłączone anonse: ${disabledAds.join(', ')}`, { alertTriggered:false, alertMode:'online' } ); } if ( previous.online === false && current.online === true && !enabledAds.length ) { emitEvent( updated, 'online', `${updated.displayName} pojawiła się online (${ current.activeCount } aktywne).`, current.activeUrls?.[0] || defaultEventUrl, '', { alertTriggered:!!item.modes.online, alertMode:'online' } ); } else if ( previous.online === true && current.online === false && !disabledAds.length ) { emitEvent( updated, 'online', `${updated.displayName} jest offline.`, result.profileUrls?.[0] || item.profileUrls?.[0] || defaultEventUrl, '', { alertTriggered:false, alertMode:'online' } ); } if ( !current.locationPartial && current.cities.length && watchCitiesKey(previous.cities) !== watchCitiesKey(current.cities) ) { const target= item.locationTarget || {type:'any',city:''}; let locationAlertTriggered=false; let locationMessage= `${watchCitiesText(previous.cities)} → ${watchCitiesText(current.cities)}`; let locationUrl= current.activeUrls?.[0] || defaultEventUrl; if (item.modes.location) { if (target.type === 'city') { const targetKey= normalizeEscortCityKey(target.city); const previousMatch= (previous.cities || []).some( city => normalizeEscortCityKey(city) === targetKey ); const currentMatch= current.cities.some( city => normalizeEscortCityKey(city) === targetKey ); locationAlertTriggered= !previousMatch && currentMatch; if (locationAlertTriggered) { locationMessage= `${updated.displayName} pojawiła się w mieście ${target.city}.`; locationUrl= result.activeResults?.find( active => normalizeEscortCityKey(active.city) === targetKey )?.url || locationUrl; } } else { locationAlertTriggered=true; } } emitEvent( updated, 'location', locationMessage, locationUrl, '', { alertTriggered: locationAlertTriggered, alertMode:'location' } ); } if (addressChangeDescriptions.length) { emitEvent( updated, 'location', `${updated.displayName} - zmiana lokalizacji: adres.`, current.activeUrls?.[0] || defaultEventUrl, addressChangeDescriptions.join('\n\n'), { alertTriggered: !!item.modes.location && item.locationTarget?.type !== 'city', alertMode:'location' } ); } if (contentChangeLabels.size) { const labels=[...contentChangeLabels]; emitEvent( updated, 'changes', `${updated.displayName} - zmiany: ${labels.join(', ')}.`, current.activeUrls?.[0] || defaultEventUrl, contentChangeDescriptions.join('\n\n'), { alertTriggered:!!item.modes.changes, alertMode:'changes' } ); } } return updated; } async function runWatchedChecks(options={}) { if (!getWatchSettings().enabled) return {status:'disabled',message:'Sprawdzanie wyłączone w ustawieniach.'}; const manual=options.force === true && !options.scheduled; if (!manual && watchedCheckPromise) return watchedCheckPromise; let request=null; if (manual) { const items=new Map(getWatchedItems().map(item => [item.id,item])); request={at:Date.now(),items:options.itemIds ? new Map(options.itemIds.map(id => [String(id),items.get(String(id))])):items}; watchManualRequests.add(request); } let lastMessage=''; try { while (!watchCheckPageClosed && !request?.cancelled && getWatchSettings().enabled) { if (request) publishWatchManualRequests(); const turn=getWatchManualTurn(); if (!watchedCheckPromise && (!turn || manual && turn === WATCH_MANUAL_CHECK_PREFIX + WATCH_INSTANCE_ID)) { const reused=request ? getWatchedItems().filter(item => { const before=request.items.get(item.id); const field=options.hiddenOnly ? 'hiddenLastCheckedAt':item.sourceType !== 'escort-ad' && !item.favorite && !watchedItemHasActiveAlerts(item) ? 'lastCheckedAt':'lastFullCheckedAt'; return before && sameWatchedDefinition(item,before) && !item[options.hiddenOnly ? 'hiddenLastError':'lastError'] && Number(item[field]) >= request.at && Number(item[field]) > (Number(before[field]) || 0); }).map(item => item.id):[]; const remaining=request ? [...request.items.keys()].filter(id => !reused.includes(id)):options.itemIds; let result; if (request && !remaining.length) result={status:reused.length ? 'completed':'empty',checked:0,failed:0,skipped:0}; else { const task=performWatchedChecks({...options,itemIds:remaining}); watchedCheckPromise=task; try {result=await task;} finally {if (watchedCheckPromise === task) watchedCheckPromise=null;} } if (reused.length && result.status === 'empty') Object.assign(result,{status:'completed',message:''}); if (result.status !== 'busy' || !manual) return request ? {...result,checked:(result.checked || 0) + reused.length,total:request.items.size, ...(reused.length && !remaining.length ? {message:`Odświeżono: ${reused.length}/${request.items.size}. Wykorzystano wyniki sprawdzania zakończonego podczas oczekiwania.`}:{})}:result; } else if (!manual) return watchedCheckPromise || {status:'busy',message:'Oczekuje ręczne odświeżenie zapisanych pozycji.'}; const lock=GM_getValue(WATCH_CHECK_LOCK_KEY,null),progress=lock?.progress,queue=getEscortRequestQueueStatus(); const message=queue.reloadRequired ? 'Odświeżenie oczekuje. Escort.club: BAN na IP — po odzyskaniu dostępu odśwież stronę.' :queue.blockedUntil > Date.now() ? `Odświeżenie oczekuje. Pobieranie wstrzymane do ${new Date(queue.blockedUntil).toLocaleTimeString('pl-PL')}.` :lock?.expiresAt > Date.now() ? `Odświeżenie oczekuje. Trwa ${lock.kind === 'scheduled' ? 'automatyczne':'ręczne'} sprawdzanie zapisanych pozycji${progress?.total ? `: ${progress.checked || 0}/${progress.total}`:''}${progress?.adTotal ? ` • dane ${progress.adChecked || 0}/${progress.adTotal}`:''}.` :'Odświeżenie zapisane w kolejce — rozpocznie się automatycznie.'; if (message !== lastMessage) {lastMessage=message; options.onProgress?.({phase:'waiting',message});} await new Promise(resolve => setTimeout(resolve,1000)); } return {status:'interrupted',message:watchCheckPageClosed || request?.cancelled ? 'Anulowano oczekujące odświeżenie po opuszczeniu strony.':'Sprawdzanie wyłączone w ustawieniach.'}; } finally {if (request) {watchManualRequests.delete(request); publishWatchManualRequests();}} } async function performWatchedChecks({force=false,itemIds=null,onProgress=null,scheduled=false,hiddenOnly=false}={}) { const operationContext=createRequestContext(hiddenOnly ? 'watch.hidden':scheduled ? 'watch.scheduled':'watch.manual'); const settings=getWatchSettings(); if (!settings.enabled) { return {status:'disabled',message:'Sprawdzanie wyłączone w ustawieniach.'}; } if (!(await acquireWatchCheckLock(scheduled ? 'scheduled':'manual'))) { return {status:'busy',message:'Trwa sprawdzanie zapisanych pozycji w innej karcie.'}; } const notificationEvents=[]; const watchLockHeartbeat=setInterval(() => { try {touchWatchCheckLock(); if (watchManualRequests.size) publishWatchManualRequests();} catch {} },WATCH_CHECK_LOCK_HEARTBEAT_MS); return (async () => { let items=getWatchedItems(); const allowedIds=itemIds ? new Set(itemIds.map(String)):null; const now=Date.now(); const dueBefore= now - settings.intervalMinutes * 60000; let scheduledFullCheckCompleted=false; const isWatchDue=item => { const timing=getWatchedItemCheckTiming(item,settings); if (hiddenOnly || !timing.eligible) return false; if (scheduled) return now >= timing.dueAt; if (force) return true; return !item.lastCheckedAt || item.lastCheckedAt <= dueBefore; }; const isHiddenDue=item => item.hidden && item.sourceType !== 'escort-ad' && ((force && !scheduled) || now - item.hiddenLastCheckedAt >= HIDDEN_PROFILE_CHECK_INTERVAL_MS); const due=items.filter(item => (!allowedIds || allowedIds.has(item.id)) && (isWatchDue(item) || isHiddenDue(item))); const outcome={status:'completed',checked:0,failed:0, skipped:allowedIds ? allowedIds.size - due.length:0,total:allowedIds?.size ?? due.length}; if (!due.length) Object.assign(outcome,{status:'empty',message:'Nie rozpoczęto sprawdzania - brak pozycji do sprawdzenia. Wybrane wpisy mogły zostać usunięte lub nie wymagają sprawdzania.'}); if (due.length) {await persistentCacheInitialization; await ensureFullPersistentCache();} for (let index=0; index < due.length; index++) { if (scheduled && getWatchManualTurn()) {outcome.status='yielded'; break;} if (!getWatchSettings().enabled || !ownsWatchCheckLock()) {outcome.status='interrupted'; break;} touchWatchCheckLock(); const candidate=getWatchedItems().find(item => item.id === due[index].id); if (!candidate || (!isWatchDue(candidate) && !isHiddenDue(candidate))) {outcome.skipped++; continue;} const discoveryOnly=!isWatchDue(candidate); const itemContext=createRequestContext(discoveryOnly ? 'watch.hidden-item':'watch.checked-item',operationContext); const reportProgress=adProgress => { const progress={ checked:index, total:due.length, itemIndex:index + 1, itemName:candidate.displayName, adChecked:Number(adProgress?.checked) || 0, adTotal:Number(adProgress?.total) || 0, activeCount:Number(adProgress?.activeCount) || 0, phase:'checking' }; touchWatchCheckLock(progress); onProgress?.(progress); }; reportProgress({checked:0,total:0,activeCount:0}); try { const result=discoveryOnly ? await resolveWatchedProfileData(candidate,itemContext):await inspectWatchedItem(candidate,reportProgress,{ fullCheck:!scheduled || shouldRunScheduledFullWatchCheck(candidate,settings),requestContext:itemContext }); if (!ownsWatchCheckLock()) {outcome.status='interrupted'; outcome.skipped++; break;} if (discoveryOnly) { updateWatchedItem(candidate.id,current => current.hidden && sameWatchedDefinition(current,candidate) ? { displayName:current.customDisplayName || result.displayName || current.displayName, imageUrl:result.imageUrl || current.imageUrl, profileUrls:result.profileUrls, resolvedAdUrls:result.adUrls, state:{...current.state,activeUrls:(current.state?.activeUrls || []).filter(url => result.adUrls.includes(url))}, hiddenLastCheckedAt:Date.now(),hiddenLastError:null }:{}); } else { if (result.fullCheckCompleted) { scheduledFullCheckCompleted=true; } const latest=getWatchedItems().find(entry => entry.id === candidate.id); if (!sameWatchedDefinition(latest,candidate)) {outcome.skipped++; continue;} touchWatchCheckLock(); const updated=applyWatchedResult(latest,result,notificationEvents); updateWatchedItem( candidate.id, current => watchedResultUpdatePatch( current, updated, result.fullCheckCompleted ) ); } } catch (error) { if (!ownsWatchCheckLock()) {outcome.status='interrupted'; outcome.skipped++; break;} outcome.failed++; updateWatchedItem(candidate.id,current => sameWatchedDefinition(current,candidate) ? ({ ...(!discoveryOnly ? {lastCheckedAt:Date.now(),lastError:formatHelperDiagnosticError(error)}:{}), ...(current.hidden ? {hiddenLastCheckedAt:Date.now(),hiddenLastError:formatHelperDiagnosticError(error)}:{}) }):{}); } outcome.checked++; items=getWatchedItems(); renderWatchNotificationBar(); document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.(); const currentItem=items.find(item => item.id === candidate.id); const progress={ checked:index + 1, total:due.length, itemIndex:index + 1, itemName:candidate.displayName, adChecked:Number(currentItem?.state?.totalAds) || 0, adTotal:Number(currentItem?.state?.totalAds) || 0, activeCount:Number(currentItem?.state?.activeCount) || 0, phase:'done' }; touchWatchCheckLock(progress); onProgress?.(progress); } if (scheduled) { recordDiagnosticWatchScheduleRun( scheduledFullCheckCompleted ); } return outcome; })().finally(async () => { clearInterval(watchLockHeartbeat); releaseWatchCheckLock(); await showWatchSystemNotification(notificationEvents); }); } function watchedResultUpdatePatch(current,updated,fullCheckCompleted=true) { return { displayName: current.customDisplayName || updated.displayName || current.displayName, imageUrl:updated.imageUrl || current.imageUrl, profileUrls:updated.profileUrls || current.profileUrls, resolvedAdUrls:updated.resolvedAdUrls || current.resolvedAdUrls, ...(current.sourceType === 'escort-search' ? {knownAdIds:updated.knownAdIds}:{}), state:updated.state, lastCheckedAt:updated.lastCheckedAt, lastFullCheckedAt:fullCheckCompleted ? updated.lastCheckedAt :current.lastFullCheckedAt, lastError:updated.lastError, ...(current.hidden && current.sourceType !== 'escort-ad' ? {hiddenLastCheckedAt:updated.lastCheckedAt,hiddenLastError:null}:{}) }; } function sameWatchedDefinition(current,previous) { return !!current && current.resolutionMode === previous.resolutionMode && !!current.escortiProfileOnly === !!previous.escortiProfileOnly && getEscortiWatchSearchValue(current) === getEscortiWatchSearchValue(previous) && (previous.sourceType !== 'escort-search' || current.watchKey === previous.watchKey); } async function refreshFavoriteItemData(item,reportError=false) { const operationContext=createRequestContext('watch.manual-refresh'); try { const result=await inspectWatchedItem(item,null,{requestContext:operationContext}); const latest= getWatchedItems().find(entry => entry.id === item.id); if (!sameWatchedDefinition(latest,item)) return latest; const updated=applyWatchedResult(latest,result); return updateWatchedItem( item.id, current => watchedResultUpdatePatch(current,updated,result.fullCheckCompleted) ) || latest; } catch (error) { if (reportError && sameWatchedDefinition(getWatchedItems().find(entry => entry.id === item.id),item)) { const saved=updateWatchedItem(item.id,{lastError:error?.message || 'Nie udało się sprawdzić profilu.'}); if (saved) showWatchToast(`Profil zapisany. Sprawdzenie nie powiodło się: ${saved.lastError}`,true); } return getWatchedItems().find(entry => entry.id === item.id) || item; } } function getTrackedScopeItem(matches,scope) { const items=Array.isArray(matches) ? matches:[]; return scope === 'single' ? items.find(item => item.sourceType === 'escort-ad') || null :items.find(item => item.sourceType !== 'escort-ad') || null; } function trackedRemovalHint(kind) { const hints={ profile:'Usunę cały wpis: notatkę, przypisane kategorie, Ulubione, ukrywanie, alerty, obserwowanie i historię powiadomień. Inne profile i osobne wpisy anonsów pozostaną.', favorite:'Usunę z Ulubionych i wyłączę wszystkie alerty oraz obserwowanie. Notatka, kategorie i ukrywanie pozostaną.', hidden:'Wyłączę ukrywanie tego wpisu. Ulubione, notatka i kategorie pozostaną. Anons nadal może być ukryty przez inny wpis.', notes:'Usunę notatkę i wszystkie przypisane kategorie. Ulubione i ukrywanie pozostaną. Nie usunę kategorii ze słownika ani danych innych wpisów.', category:'Usunę tylko przypisanie tej kategorii. Notatka, inne kategorie, Ulubione i ukrywanie pozostaną. Kategoria pozostanie w słowniku.' }; return hints[kind] + (kind === 'profile' ? '':' Profil bez notatki, kategorii, Ulubionych i ukrywania zostanie usunięty wraz z obserwowaniem i historią powiadomień.'); } let trackedUndoDepth=0; function restoreTrackedUndoValue(before,after,current) { const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); if (same(before,after)) return current; if (same(current,after)) return copyWatchedValue(before); if ([before,after,current].every(value=>value && typeof value==='object' && !Array.isArray(value))) { const next={...current}; for (const key of new Set([...Object.keys(before),...Object.keys(after)])) { const value=restoreTrackedUndoValue(before[key],after[key],current[key]); if (value===undefined) delete next[key]; else next[key]=value; } return next; } return current; } function withTrackedUndo(message,action,enabled=()=>true,categoryDictionary=null) { if (trackedUndoDepth || !enabled()) return action(); const before=getWatchedItems(),eventsBefore=getWatchEvents(); let result; trackedUndoDepth++; try {result=action();} finally {trackedUndoDepth--;} const after=new Map(getWatchedItems().map(item=>[item.id,item])); const changed=before.filter(item=>JSON.stringify(item)!==JSON.stringify(after.get(item.id))); if (!changed.length && !categoryDictionary?.names.length) return result; const eventIds=new Set(getWatchEvents().map(event=>event.id)); const removedEvents=eventsBefore.filter(event=>!eventIds.has(event.id)); showWatchToast(message,false,{id:'vm-tracked-undo',duration:10000,style:{bottom:'62px'},actionLabel:'Cofnij',onAction:()=>{ const current=getWatchedItems(),restored=new Set(); const indices=new Map(current.map((item,index)=>[item.id,index])); let restoredDefinitions=0; if (categoryDictionary) { const settings=getFavoriteSettings(),missing=categoryDictionary.names.filter(name=>!settings.categories.includes(name)); if (missing.length) { updateSettings({favoriteCategories:[...settings.categories,...missing],favoriteCategoryColors:{...settings.categoryColors, ...Object.fromEntries(missing.map(name=>[name,categoryDictionary.colors[name] || '#c8c8c8']))}}); restoredDefinitions=missing.length; } } for (const original of changed) { const expected=after.get(original.id),index=indices.get(original.id) ?? -1; if (!expected) { if (index<0 && !current.some(item=>watchedItemsOverlap(item,original))) {current.push(original);restored.add(original.id);} continue; } if (index<0) continue; const latest=current[index],next=restoreTrackedUndoValue(original,expected,latest); // Przywróć usunięte kategorie, zachowując później dodane. const removed=original.categories.filter(category=>!expected.categories.includes(category) && (!categoryDictionary || categoryDictionary.names.includes(category))); next.categories=normalizeFavoriteAssignedCategories([...latest.categories,...removed]); if (JSON.stringify(next)!==JSON.stringify(latest)) {current[index]=next;restored.add(original.id);} } if (!restored.size && !restoredDefinitions) return showWatchToast('Wpis został później zmieniony. Nie nadpisano tych zmian.',false); saveWatchedItems(current); const events=getWatchEvents(),ids=new Set(events.map(event=>event.id)); saveWatchEvents([...events,...removedEvents.filter(event=>restored.has(event.itemId)&&!ids.has(event.id))]); initLocalWatchSystem();renderWatchNotificationBar();refreshWatchPageButtons(); showWatchToast('Przywrócono usunięte dane.'); }}); return result; } function changeTrackedItemStatus(itemId,action) { const item=getWatchedItems().find(entry => entry.id === itemId); if (!item) return; if (action === 'remove') { if (window.confirm(`Usunąć cały wpis „${item.displayName}”?\n\n${trackedRemovalHint('profile')}\n\nPrzez 10 sekund będzie dostępne „Cofnij”.`)) removeWatchedItem(itemId); } else if (action === 'favorite' && item.favorite) removeFavoriteOnly(itemId); else if (action === 'hidden' && item.hidden) restoreHiddenItems([itemId]); else if (action === 'favorite' || action === 'hidden') { const saved=updateWatchedItem(itemId,{[action]:true, ...(action === 'hidden' ? {hiddenAt:Date.now(),hiddenLastCheckedAt:0,hiddenLastError:null}:{})}); initLocalWatchSystem(); if (saved) refreshFavoriteItemData(saved,true); } } function canRemoveEmptyTrackedItem(item) { return !!item && !item.preserveProfile && !item.hidden && !item.favorite && !watchedItemHasNotesOrCategories(item) && (item.sourceType !== 'escort-ad' || !watchedItemHasActiveAlerts(item)); } function editTrackedNoteDialog(title,value='') { return new Promise(resolve => { const previous=document.activeElement; const {overlay,box}=createWatchDialogBase('vm-tracked-note-editor',title); let result=null; box.setAttribute('role','dialog'); box.setAttribute('aria-modal','true'); box.setAttribute('aria-label',title); const field=makeElement('textarea','vm-ui-field vm-ui-textarea',null,{value,rows:7,maxLength:2500}); field.setAttribute('aria-label','Notatka'); const controller=createDialogCloseController(overlay,{onClose:() => { if (previous?.isConnected) previous.focus({preventScroll:true}); resolve(result); }}); const actions=makeElement('div','vm-ui-actions'); const cancel=makeButton('vm-ui-button','Anuluj'),save=makeButton('vm-ui-button -primary','Zapisz'); cancel.addEventListener('click',() => controller.close('cancel')); const commit=() => {result=field.value.trim(); controller.close('save');}; save.addEventListener('click',commit); field.addEventListener('keydown',event => { if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {stopUiEvent(event); commit();} }); box.addEventListener('keydown',event => { if (event.key !== 'Tab') return; if (event.shiftKey && document.activeElement === field) {event.preventDefault(); save.focus();} else if (!event.shiftKey && document.activeElement === save) {event.preventDefault(); field.focus();} }); actions.append(cancel,save); box.append(field,actions); field.focus(); }); } function ensureTrackedNoteCategoryStyles() { if (document.getElementById('vm-tracked-note-category-style')) return; appendHelperStyle('vm-tracked-note-category-style',` .vm-escort-favorite-note-scope { min-width:0; max-width:100%; padding:4px 7px; box-sizing:border-box; border:1px solid color-mix(in srgb,var(--vm-ui-accent,#f54da3) 40%,transparent); border-left:2px solid var(--vm-ui-accent,#f54da3); border-radius:5px; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 7%,transparent); color:var(--vm-ui-panel-text,var(--vm-ui-text,var(--vm-tracked-text-fallback,#222))); } .vm-escort-favorite-note-line { min-width:0; max-width:100%; line-height:1.22; white-space:normal; overflow-wrap:anywhere; } .vm-escort-favorite-note-label { margin-right:5px; color:var(--vm-ui-panel-muted,var(--vm-ui-text-muted,var(--vm-tracked-text-fallback,#666))); font-size:8px; font-weight:400; white-space:nowrap; opacity:.72; } .vm-escort-favorite-note-text { line-height:1.22; white-space:pre-wrap; overflow-wrap:anywhere; text-decoration:none; } .vm-escort-inline-category-row { display:flex; flex-wrap:wrap; align-items:center; gap:4px; min-width:0; margin-top:3px; } .vm-escort-inline-category-chip { display:inline-flex; align-items:center; gap:3px; min-width:0; min-height:22px; max-width:100%; padding:1px 4px 1px 7px; border-radius:999px; font-size:10px; font-weight:700; line-height:16px; box-sizing:border-box; white-space:normal; overflow-wrap:anywhere; color:var(--vm-ui-panel-text,var(--vm-ui-text,var(--vm-tracked-text-fallback,#222))); } .vm-escort-inline-category-remove { flex:0 0 16px; width:16px; height:16px; padding:0; margin:0; border:0; border-radius:50%; background:transparent; color:var(--vm-ui-text-muted,#777); font-size:11px; font-weight:700; line-height:12px; text-align:center; cursor:pointer; opacity:.65; } .vm-escort-inline-category-add { display:inline-flex; align-items:center; justify-content:center; width:18px; min-width:18px; height:18px; padding:0; margin:0; border:1px solid var(--vm-ui-border,#d4d4d8); border-radius:999px; background:var(--vm-ui-bg-alt,rgba(250,250,250,.82)); color:var(--vm-ui-text-muted,#777); font-size:13px; font-weight:700; line-height:16px; text-align:center; box-sizing:border-box; cursor:pointer; opacity:.30; } .vm-escort-inline-alerts { flex:0 0 26px; margin-left:auto; width:26px; min-width:26px; height:26px; opacity:.8; } .vm-escort-inline-alerts[data-vm-active="1"] { color:#382b08; background:#f4c542; border-color:#cda42e; opacity:1; } .vm-escort-inline-alerts:hover, .vm-escort-inline-alerts:focus-visible { opacity:1; outline:1px solid currentColor; outline-offset:1px; } .vm-escort-inline-alerts:disabled { cursor:wait; opacity:.45; } .vm-escort-inline-alerts[data-vm-favorite-required="1"]:disabled { cursor:not-allowed; opacity:.30; } .vm-tracked-box { display:grid; align-content:start; gap:6px; width:100%; max-width:520px; min-width:0; text-align:left; } .vm-tracked-box[hidden], .vm-tracked-box [hidden] { display:none !important; } .vm-tracked-list { display:grid; gap:8px; min-width:0; } #vm-escort-favorite-note-row, #vm-escorti-profile-note-display { margin:8px 0 10px; } #vm-escorti-profile-note-display { margin-inline:auto; } /* Nagłówek i notatki obok pływającej galerii. */ .content-name:has(> #vm-escort-favorite-note-row) { display:flow-root; min-width:0; } #vm-escort-favorite-note-row { clear:none !important; float:none !important; width:auto; max-width:100%; overflow-wrap:anywhere; } .vm-tracked-entry, .vm-tracked-summary { --vm-ui-panel-text:var(--vm-ui-text,var(--vm-tracked-text-fallback,#222)); --vm-ui-panel-muted:var(--vm-ui-text-muted,var(--vm-tracked-text-fallback,#666)); display:flex; flex-direction:column; gap:5px; padding:8px 10px; font-size:11px; text-align:left; } .vm-tracked-entry-heading { display:flex; align-items:baseline; gap:6px; min-width:0; } .vm-tracked-entry-name { flex:0 1 auto; min-width:0; font-size:12px; font-weight:600; overflow-wrap:anywhere; } .vm-tracked-entry-delete, .vm-tracked-profile-picker { display:inline-flex; align-items:center; gap:4px; flex:0 0 auto; padding:2px 0; border:0; background:transparent; color:var(--vm-ui-panel-muted,var(--vm-ui-text-muted,#888)); font-size:10px; font-weight:400; cursor:pointer; } .vm-tracked-profile-picker { color:var(--vm-ui-accent,#f54da3); font-size:11px; } .vm-tracked-profile-picker:hover { text-decoration:underline; } .vm-tracked-profile-picker:disabled { opacity:.5; cursor:wait; } .vm-tracked-entry-delete { margin-left:auto; align-self:flex-start; } .vm-tracked-entry-delete svg { margin:0 !important; } .vm-tracked-entry-delete:hover, .vm-tracked-entry-delete:focus-visible { color:var(--vm-ui-danger,#c62828); } .vm-tracked-entry-heading [data-kind="rename"] { opacity:0; transition:opacity .12s ease; } .vm-tracked-entry:hover [data-kind="rename"], .vm-tracked-entry:focus-within [data-kind="rename"] { opacity:1; } @media (hover:none) { .vm-tracked-entry-heading [data-kind="rename"] { opacity:1; } } .vm-tracked-entry-source { font-size:10px; line-height:1.3; white-space:normal; overflow-wrap:anywhere; color:var(--vm-ui-panel-muted,var(--vm-ui-text-muted,#888)); } .vm-tracked-entry .vm-escort-favorite-note-label { display:none; } .vm-tracked-entry .vm-escort-favorite-note-line { padding:3px 0; } .vm-tracked-entry-action { flex:0 0 26px; width:26px; min-width:26px; height:26px; border-radius:5px; opacity:1; font-size:19px; font-weight:400; color:var(--vm-ui-accent,#f54da3); } .vm-tracked-entry-action[data-kind="favorite"][data-vm-active="1"] { background:#f4c542; color:#000; border-color:#cda42e; } .vm-tracked-entry-action[data-kind="hidden"][data-vm-active="1"] { background:#ff9b9b; color:#000; border-color:#db7777; } .vm-tracked-entry-action:focus-visible, .vm-tracked-entry:focus { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:2px; } .vm-tracked-entry-action:disabled { opacity:.5; cursor:wait; } .vm-tracked-summary.vm-escort-favorite-note-scope { position:relative; padding:2px 0; gap:5px; border:0; border-radius:0; background:transparent; box-shadow:none; color:var(--vm-ui-panel-text); } .vm-tracked-entry.vm-escort-favorite-note-scope { width:100%; border:1px solid color-mix(in srgb,var(--vm-ui-accent,#f54da3) 55%,transparent); border-radius:7px; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 5%,var(--vm-ui-bg,transparent)); } .vm-tracked-summary-caption { display:flex; align-items:center; gap:8px; color:var(--vm-ui-panel-muted); font-size:11px; line-height:1.5; } .vm-tracked-summary-caption::before { content:""; flex:0 0 6px; width:6px; height:6px; margin:0 2px 3px; border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor; transform:rotate(45deg); pointer-events:none; } .vm-tracked-summary[aria-expanded="true"] > .vm-tracked-summary-caption::before { transform:rotate(225deg); margin-top:6px; } .vm-tracked-category-add { width:auto; min-width:24px; height:24px; padding:0 7px; opacity:1; font-size:11px; } .vm-tracked-summary:focus-visible, .vm-tracked-profile-picker:focus-visible { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:2px; } .vm-tracked-summary .vm-ui-help { color:var(--vm-ui-panel-muted); } .vm-tracked-summary .vm-escort-favorite-note-text { display:block; white-space:pre-wrap; font-size:12px !important; line-height:1.45; } .vm-tracked-summary .vm-escort-inline-alerts { margin-left:0; } .vm-tracked-summary, .vm-tracked-summary .vm-tracked-entry-action { cursor:pointer; } .vm-escort-inline-category-row:hover .vm-escort-inline-category-remove, .vm-escort-inline-category-row:hover .vm-escort-inline-category-add, .vm-escort-inline-category-row:focus-within .vm-escort-inline-category-remove, .vm-escort-inline-category-row:focus-within .vm-escort-inline-category-add { opacity:1; } .vm-native-escort-action-hidden { display:none !important; } `); } function createTrackedAlertsControl({alertItems=[],onEditAlerts=null,readOnly=false,alertsDisabled=false,labelText='Profil'}={}) { ensureTrackedNoteCategoryStyles(); let bell=null; const activeAlerts=alertItems.filter(watchedItemHasActiveAlerts); if (onEditAlerts || (readOnly && activeAlerts.length)) { bell=readOnly ? makeElement('span'):makeButton(); bell.className='vm-escort-inline-category-add vm-escort-inline-alerts'; const active=activeAlerts.length > 0; bell.dataset.vmActive=active ? '1':'0'; setControlTitle(bell,readOnly ? `Włączone alerty: ${activeAlerts.map(trackedItemChoiceLabel).join('; ')}` :`Alerty ${labelText === 'Anons' ? 'anonsu':'profilu'} - ${active ? 'włączone':'wyłączone'}. Kliknij, aby ustawić.`); if (readOnly) bell.setAttribute('role','img'); else bell.setAttribute('aria-haspopup','dialog'); bell.innerHTML='<svg aria-hidden="true" viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></svg>'; if (!readOnly && alertsDisabled) { bell.disabled=true; bell.dataset.vmFavoriteRequired='1'; setControlTitle(bell,`Najpierw dodaj ${labelText === 'Anons' ? 'anons':'profil'} do Ulubionych, aby ustawić alerty.`); } if (!readOnly) bell.addEventListener('click',async event => { stopUiEvent(event); if (bell.disabled) return; bell.disabled=true; try {await onEditAlerts();} catch (error) {showWatchToast(`Nie udało się otworzyć lub zapisać alertów: ${error?.message || error}`,true);} finally {bell.disabled=false;} }); } return bell; } function renderTrackedNoteCategoryBlock({ host, labelText, readOnly=false, noteText='', categories=[], enabled=true, favoriteSettings, accent='#f54da3', fallbackText='#222', ownerKey='', alertItems=[], onEditAlerts=null, alertsDisabled=false, actions=[], onEditNote, onRemoveCategory, onAddCategory }) { ensureTrackedNoteCategoryStyles(); prepareHelperStyleApi(host); host.classList.add('vm-escort-favorite-note-scope'); host.style.setProperty('--vm-tracked-text-fallback',fallbackText); host.replaceChildren(); const bell=createTrackedAlertsControl({alertItems,onEditAlerts,readOnly,alertsDisabled,labelText}); const noteRow=makeElement('div','vm-escort-favorite-note-line'); if (labelText) noteRow.appendChild(makeElement('span','vm-escort-favorite-note-label',`${labelText}:`)); if (enabled && (!readOnly || noteText)) { if (!readOnly) { noteRow.tabIndex=0; noteRow.setAttribute('role','button'); noteRow.title=noteText ? 'Kliknij tekst notatki, aby ją zmienić':'Dodaj notatkę'; noteRow.style.cursor='pointer'; } const value=makeElement('span','vm-escort-favorite-note-text',noteText || 'Dodaj notatkę'); Object.assign(value.style,{ color:noteText ? 'var(--vm-ui-panel-text,var(--vm-ui-text,var(--vm-tracked-text-fallback,#222)))' :'var(--vm-ui-panel-muted,var(--vm-ui-text-muted,var(--vm-tracked-text-fallback,#666)))', fontSize:noteText ? '11px':'9px', fontWeight:noteText ? '500':'400', paddingBottom:noteText ? '0':'1px', opacity:noteText ? '1':'.70' }); if (!noteText) value.style.setProperty('border-bottom','1px dotted currentColor','important'); if (!readOnly) { const edit=async event => { stopUiEvent(event); await onEditNote?.(); }; noteRow.addEventListener('click',edit); noteRow.addEventListener('keydown',event => { if (event.key === 'Enter' || event.key === ' ') edit(event); }); } noteRow.appendChild(value); } if (noteRow.childElementCount) host.appendChild(noteRow); if (!enabled && !bell && !actions.length) return host; const normalizedCategories=enabled ? normalizeFavoriteAssignedCategories(categories):[]; const categoryRow=makeElement('div','vm-escort-inline-category-row',null,{title:`Kategorie ${String(labelText).toLowerCase()}`}); for (const category of normalizedCategories) { const color=favoriteSettings.categoryColors[category] || accent; const chip=makeElement('span','vm-escort-inline-category-chip',null,{title:readOnly ? category:`${category} - kliknij ×, aby usunąć`}); chip.style.border=`1px solid ${color}`; chip.style.background=favoriteCategoryTint(color,.10); chip.appendChild(makeElement('span','',category)); if (!readOnly) { const remove=makeButton('vm-escort-inline-category-remove','×',{title:`Kategoria „${category}”. ${trackedRemovalHint('category')}`}); remove.setAttribute('aria-label',`Usuń kategorię ${category}`); remove.addEventListener('click',event => { stopUiEvent(event); Promise.resolve(onRemoveCategory?.(category)).catch(error => log('Błąd usuwania kategorii',error)); }); chip.appendChild(remove); } categoryRow.appendChild(chip); } const available=(enabled ? favoriteSettings.categories:[]).filter( value => !normalizedCategories.includes(value) ); if (!readOnly && available.length) { const add=makeButton('vm-escort-inline-category-add vm-tracked-category-add',normalizedCategories.length ? '+':'+ Kategoria',{title:'Dodaj kategorię'}); add.setAttribute('aria-label','Dodaj kategorię'); add.setAttribute('aria-haspopup','menu'); add.addEventListener('click',event => { stopUiEvent(event); openEscortInlineCategoryPicker( add, available, favoriteSettings, value => onAddCategory?.(value), ownerKey ); }); categoryRow.appendChild(add); } if (bell) categoryRow.appendChild(bell); categoryRow.append(...actions); if (categoryRow.childElementCount) host.appendChild(categoryRow); return host; } function openEscortInlineCategoryPicker(anchor,availableCategories,settings,onSelect,ownerKey='') { const doc=anchor.ownerDocument,view=doc.defaultView || window; closeHelperDialog(doc.querySelector('.vm-favorites-category-picker')); const values=normalizeFavoriteCategories(availableCategories); if (!values.length) return; const picker=prepareHelperStyleApi(makeElement('div','vm-favorites-category-picker')); picker.dataset.vmFavoriteCategoryOwner=String(ownerKey || ''); picker._vmAnchor=anchor; picker.setAttribute('role','menu'); picker.setAttribute('aria-label','Dodaj kategorię'); anchor.setAttribute('aria-expanded','true'); Object.assign(picker.style,{ position:'fixed',zIndex:'2147483647',minWidth:'160px',maxWidth:'260px',maxHeight:'min(340px,75vh)',overflowY:'auto', padding:'4px',border:'1px solid var(--vm-ui-border,#cfcfd3)',borderRadius:'7px',background:'var(--vm-ui-bg,#fff)', boxShadow:'var(--vm-ui-shadow,0 5px 18px rgba(0,0,0,.22))',boxSizing:'border-box',pointerEvents:'auto' }); const options=[]; const controller=createDialogCloseController(picker,{closeOnBackdrop:false,onClose:reason => { anchor.setAttribute('aria-expanded','false'); doc.removeEventListener('pointerdown',outside,true); doc.removeEventListener('keydown',keyboard,true); view.removeEventListener('resize',viewport,true); view.removeEventListener('scroll',viewport,true); if (['escape','choice','tab','viewport'].includes(reason) && anchor.isConnected) anchor.focus({preventScroll:true}); }}); const outside=event => {if (!picker.contains(event.target) && !anchor.contains(event.target)) controller.close('outside');}; const viewport=event => {if (!picker.contains(event.target)) controller.close('viewport');}; const keyboard=event => { if (!picker.contains(doc.activeElement)) return; if (event.key === 'Escape') {stopUiEvent(event); controller.close('escape'); return;} const index=options.indexOf(doc.activeElement); if (event.key === 'Tab') {controller.close('tab'); return;} const next=event.key === 'ArrowDown' ? (index + 1) % options.length:event.key === 'ArrowUp' ? (index + options.length - 1) % options.length:event.key === 'Home' ? 0:event.key === 'End' ? options.length - 1:null; if (next != null) {stopUiEvent(event); options[next].focus();} }; for (const value of values) { const option=makeButton('vm-favorites-category-picker-option',value); option.setAttribute('role','menuitem'); Object.assign(option.style,{display:'block',width:'100%',minHeight:'28px',padding:'4px 8px',border:'0', borderLeft:`4px solid ${settings.categoryColors[value] || '#b8b8bd'}`,borderRadius:'4px',background:'transparent', color:'var(--vm-ui-text,#333)',fontSize:'11px',textAlign:'left',cursor:'pointer',overflowWrap:'anywhere'}); option.addEventListener('mouseenter',() => option.style.background='var(--vm-ui-bg-hover,#f4f4f6)'); option.addEventListener('mouseleave',() => option.style.background='transparent'); option.addEventListener('click',event => { stopUiEvent(event); controller.close('choice'); Promise.resolve(onSelect?.(value)).catch(error => log('Błąd dodawania kategorii',error)); }); options.push(option); picker.appendChild(option); } doc.body.appendChild(picker); const a=anchor.getBoundingClientRect(),p=picker.getBoundingClientRect(); picker.style.left=`${Math.round(Math.max(6,Math.min(a.right - p.width,view.innerWidth - p.width - 6)))}px`; picker.style.top=`${Math.round(Math.max(6,a.bottom + p.height + 3 <= view.innerHeight - 6 ? a.bottom + 3:a.top - p.height - 3))}px`; doc.addEventListener('pointerdown',outside,true); doc.addEventListener('keydown',keyboard,true); view.addEventListener('resize',viewport,true); view.addEventListener('scroll',viewport,true); options[0]?.focus({preventScroll:true}); } function hideNativeEscortFavoriteAndNoteActions(enabled) { ensureTrackedNoteCategoryStyles(); const scope=document.querySelector('.content-actions-col') || document; for (const element of scope.querySelectorAll('a,button')) { if (element.id?.startsWith('vm-')) continue; const label=normalizeText(element.textContent); const isFavoriteAction= /dodaj do ulubion|usuń z ulubion|ukryj w wynikach|ukryj anons/.test(label); const isNoteAction=/dodaj notat|notatka/.test(label); element.classList.toggle('vm-native-escort-action-hidden', enabled && (isFavoriteAction || isNoteAction) ); } } function updateTrackedAlertModes(itemId,modes,locationTarget=null) { if (!getWatchSettings().enabled) return null; const current=getWatchedItems().find(item=>item.id===itemId); if (!current?.favorite) return null; const saved=updateWatchedItem(itemId,item=>({modes:{...item.modes,...modes}, ...(locationTarget ? {locationTarget}:{})})); initLocalWatchSystem(); if (saved && watchedItemHasActiveAlerts(saved)) refreshFavoriteItemData(saved,true); return saved; } async function changeTrackedEntry(itemId,action,value=null,{getMatches=getWatchedItems,getContext=null,scope='single'}={}) { const read=() => { const matches=getMatches(); const item=itemId ? matches.find(entry => entry.id === itemId):getTrackedScopeItem(matches,scope); if ((itemId || !getContext) && !item) throw new Error('Ten wpis nie jest już dostępny. Odśwież stronę.'); return item; }; try { const settings=getWatchSettings(); if (!settings.enabled) return; let item=read(); const label=item ? trackedItemChoiceLabel(item):scope === 'single' ? 'Ten anons':'Ten profil'; if (action === 'rename') { const name=window.prompt('Nowa nazwa:',item.displayName); if (name?.trim()) {read(); renameWatchedItem(item.id,name);} return; } if (item && ['remove','favorite','hidden'].includes(action)) return changeTrackedItemStatus(item.id,action); let patch; if (action === 'note') { const note=await editTrackedNoteDialog(`Notatka - ${label}`,item?.note || ''); if (note == null || (!item && !note.trim())) return; patch={note:note.trim()}; } else if (action === 'clearNotes') { patch={note:'',categories:[]}; } else if (action === 'alerts') { if (!item?.favorite) return; const options=await chooseWatchAlertOptions(item,label,{ scope:item ? (item.sourceType === 'escort-ad' ? 'single':'person'):scope, info:`${label}. Alerty dotyczą wyłącznie tego wpisu.` }); if (!options || !getWatchSettings().enabled || (!item && !watchedItemHasActiveAlerts(options))) return; patch={modes:options.modes,locationTarget:options.locationTarget}; } item=read(); if (action==='alerts') return updateTrackedAlertModes(item.id,patch.modes,patch.locationTarget); const update=current => action === 'favorite' ? {favorite:!current?.favorite} :action === 'hidden' ? {hidden:!current?.hidden,hiddenAt:current?.hidden ? 0:Date.now(),hiddenLastCheckedAt:0,hiddenLastError:null} :action === 'categories' ? {categories:normalizeFavoriteAssignedCategories(value(normalizeFavoriteAssignedCategories(current?.categories)))} :patch; return withTrackedUndo('Zmieniono notatkę lub kategorie.',()=>{ const saved=item ? updateWatchedItem(item.id,update):saveFavoriteItemFromContext(getContext(),{favorite:false,...update(null)},scope); if (!saved) return; if (canRemoveEmptyTrackedItem(saved)) {removeWatchedItem(saved.id); return;} if (['favorite','hidden'].includes(action)) initLocalWatchSystem(); if ((action === 'favorite' && saved.favorite) || (action === 'hidden' && saved.hidden)) refreshFavoriteItemData(saved,true); },()=>action==='clearNotes' || (action==='note' && !!item?.note && !patch.note) || (action==='categories' && item?.categories?.some(category=>!update(item).categories.includes(category)))); } catch (error) {showWatchToast(error?.message || 'Nie udało się zmienić wpisu.',true);} } function trackedEntryRenderKey(item) { return item ? [item.id,item.displayName,item.sourceType,item.resolutionMode,item.phoneDigits,item.adUrl, item.profileUrls,item.favorite,item.hidden,item.note,item.categories,item.modes,item.locationTarget,item.keepProfile,watchItemSourceUrl(item)]:null; } function renderTrackedEntriesSummary(summary,entries,expanded,{accent='#f54da3',fallbackText='#222'}={}) { const settings=getWatchSettings(),favoriteSettings=getFavoriteSettings(),actions=[]; const signature=JSON.stringify([entries.map(trackedEntryRenderKey),settings.enabled,favoriteSettings.categoryColors,expanded,accent,fallbackText]); if (summary.dataset.vmSignature === signature) return; summary.dataset.vmSignature=signature; const hasData=settings.enabled && entries.some(item => item.note?.trim() || item.categories?.length || hasFavoriteStar(item) || item.hidden || watchedItemHasActiveAlerts(item)); if (expanded) summary.replaceChildren(); else { if (settings.enabled) for (const kind of ['favorite','hidden']) { const active=entries.filter(item => kind === 'favorite' ? hasFavoriteStar(item):item.hidden); if (!active.length) continue; const icon=makeElement('span','vm-escort-inline-category-add vm-tracked-entry-action',kind === 'favorite' ? '★':''); icon.dataset.kind=kind; icon.dataset.vmActive='1'; icon.title=`${kind === 'favorite' ? 'W Ulubionych':'Ukryty w wynikach Escort.club'}: ${active.map(trackedItemChoiceLabel).join('; ')}`; icon.setAttribute('role','img'); icon.setAttribute('aria-label',icon.title); if (kind === 'hidden') icon.appendChild(createHiddenIcon()); actions.push(icon); } const notes=entries.filter(item => item.note?.trim()); renderTrackedNoteCategoryBlock({ host:summary,labelText:'',readOnly:true, noteText:notes.map(item => item.note.trim()).join('\n'), categories:entries.flatMap(item => item.categories || []),enabled:settings.enabled, favoriteSettings,accent,fallbackText,alertItems:settings.enabled ? entries:[],actions }); summary.querySelector('.vm-escort-inline-category-row')?.prepend(...actions); for (const chip of summary.querySelectorAll('.vm-escort-inline-category-chip')) { chip.title=`${chip.textContent}: ${entries.filter(item => item.categories?.includes(chip.textContent)).map(trackedItemChoiceLabel).join('; ')}`; } } const fields=settings.enabled ? 'kategorię, notatkę':''; const count=entries.length > 1 ? ` · Wpisy: ${entries.length}`:''; const text=expanded ? `Zwiń szczegóły${count}`:hasData ? `Rozwiń szczegóły${count}`:`Rozwiń, aby dodać ${fields}…${count}`; summary.prepend(makeElement('span','vm-tracked-summary-caption',text)); summary.setAttribute('aria-expanded',String(expanded)); summary.title=expanded ? 'Kliknij, aby zwinąć szczegóły':'Kliknij, aby rozwinąć szczegóły'; } function makeTrackedEntryAction(kind,text,title,onClick,active=false) { const button=makeButton('vm-escort-inline-category-add vm-tracked-entry-action',text); button.dataset.kind=kind; button.dataset.vmActive=active ? '1':'0'; setControlTitle(button,title); if (['favorite','hidden'].includes(kind)) button.setAttribute('aria-pressed',String(active)); if (kind === 'hidden') button.appendChild(createHiddenIcon(active)); if (kind === 'remove') setTrashIcon(button); button.addEventListener('click',async event => { stopUiEvent(event); if (button.disabled) return; button.disabled=true; try {await onClick();} catch (error) {showWatchToast(error?.message || 'Nie udało się zmienić wpisu.',true);} finally {button.disabled=false;} }); return button; } function renderTrackedEntriesEditor(list,items,changeEntry,{accent='#f54da3',fallbackText='#222',emptyAdOwnerKey='single'}={}) { const doc=list.ownerDocument,settings=getWatchSettings(),favoriteSettings=getFavoriteSettings(); const previous=new Map([...list.children].map(row => [row.dataset.vmEntryId,row])),used=new Set(); for (let index=0; index < items.length; index++) { const item=items[index]; const single=!item || item.sourceType === 'escort-ad'; const id=item?.id || null; const name=item?.displayName || 'Ten anons'; const source=single ? 'Anons':watchItemSourceText(item); const key=id || 'single',old=previous.get(key); const signature=JSON.stringify([trackedEntryRenderKey(item),settings.enabled,favoriteSettings.categories,favoriteSettings.categoryColors,accent,fallbackText]); used.add(key); if (old?.dataset.vmSignature === signature) { if (list.children[index] !== old) list.insertBefore(old,list.children[index] || null); continue; } const picker=doc.querySelector('.vm-favorites-category-picker'); const focused=old?.contains(doc.activeElement) ? doc.activeElement :picker?.contains(doc.activeElement) && old?.contains(picker._vmAnchor) ? picker._vmAnchor:null; const focusKind=focused?.dataset.kind,focusNote=focused?.classList.contains('vm-escort-favorite-note-line'); if (old && picker?._vmAnchor && old.contains(picker._vmAnchor)) closeHelperDialog(picker); const block=makeElement('div','vm-tracked-entry'); block.dataset.vmEntryId=key; block.dataset.vmSignature=signature; block.tabIndex=-1; const action=(kind,text,title,active=false) => makeTrackedEntryAction(kind,text, `${title}: ${single ? name:`${name} - ${source}`}`,() => changeEntry(id,kind),active); const actions=[]; if (settings.enabled) { if (item?.sourceType !== 'escort-search') actions.push(action('favorite',item?.favorite ? '★':'☆',item?.favorite ? 'Usuń z Ulubionych':'Dodaj do Ulubionych',!!item?.favorite)); else actions.push(makeFavoriteSearchEditButton(item)); actions.push(action('hidden','',item?.hidden ? 'Nie ukrywaj':'Ukryj w wynikach',!!item?.hidden)); } renderTrackedNoteCategoryBlock({ host:block,labelText:single ? 'Anons':'Profil',noteText:item?.note || '',categories:item?.categories || [], enabled:settings.enabled,favoriteSettings, accent,fallbackText,ownerKey:id || emptyAdOwnerKey, alertItems:item ? [item]:[], alertsDisabled:!item?.favorite, actions, onEditAlerts:settings.enabled ? () => changeEntry(id,'alerts'):null, onEditNote:() => changeEntry(id,'note'), onRemoveCategory:category => changeEntry(id,'categories',values => values.filter(value => value !== category)), onAddCategory:category => changeEntry(id,'categories',values => [...values,category]) }); const heading=makeElement('div','vm-tracked-entry-heading'); heading.appendChild(makeElement('div','vm-tracked-entry-name',name)); if (id) { const rename=action('rename','✎',single ? 'Zmień nazwę anonsu':'Zmień nazwę profilu'); rename.className='vm-ui-button vm-ui-icon vm-ui-compact vm-ui-table-icon'; heading.appendChild(rename); } if (!single && id) { const remove=action('remove','',trackedRemovalHint('profile')); remove.className='vm-tracked-entry-delete'; remove.appendChild(makeElement('span','','Usuń profil')); heading.appendChild(remove); } const sourceUrl=!single ? watchItemSourceUrl(item):''; const sourceLabel=makeElement(sourceUrl ? 'a':'div','vm-tracked-entry-source',source); if (sourceUrl) Object.assign(sourceLabel,{href:sourceUrl,target:'_blank',rel:'noopener noreferrer',title:'Otwórz źródło danych profilu'}); block.prepend(sourceLabel); block.prepend(heading); if (old) old.replaceWith(block); else list.insertBefore(block,list.children[index] || null); if (list.children[index] !== block) list.insertBefore(block,list.children[index] || null); if (focused) { const focusTarget=focusKind ? block.querySelector(`[data-kind="${focusKind}"]`):focusNote ? block.querySelector('.vm-escort-favorite-note-line'):block.querySelector('.vm-tracked-category-add'); (focusTarget || block).focus({preventScroll:true}); } } for (const [key,row] of previous) if (!used.has(key)) { const picker=doc.querySelector('.vm-favorites-category-picker'); if (picker?._vmAnchor && row.contains(picker._vmAnchor)) closeHelperDialog(picker); detachHelperNode(row); } } function createTrackedEntriesBox(changeEntry,options={}) { const root=prepareHelperStyleApi(makeElement('div','vm-tracked-box')); const summary=makeElement('div','vm-tracked-summary',null,{tabIndex:0}); summary.setAttribute('role','button'); summary.setAttribute('aria-label','Rozwiń lub zwiń zapisane dane profili i anonsów'); const panel=makeElement('div','vm-tracked-details vm-ui-stack'); panel.hidden=true; panel.setAttribute('role','region'); panel.setAttribute('aria-label','Notatki i kategorie'); const list=makeElement('div','vm-tracked-list'); panel.appendChild(list); root.append(summary,panel); let items=[]; const render=entries => { if (entries) items=entries; renderTrackedEntriesSummary(summary,items.filter(Boolean),!panel.hidden,options); if (!panel.hidden) renderTrackedEntriesEditor(list,items,changeEntry,options); }; const toggle=event => { if (event.type === 'keydown' && !['Enter',' '].includes(event.key)) return; stopUiEvent(event); panel.hidden=!panel.hidden; render(); }; summary.addEventListener('click',toggle); summary.addEventListener('keydown',toggle); return {root,panel,list,render}; } function initEscortClubWatchButtons() { if (!getEscortAdIdFromPath(location.pathname)) return; const insert=() => { const settings=getWatchSettings(); if (!settings.enabled) return true; hideNativeEscortFavoriteAndNoteActions(settings.enabled); const title=queryFirstEscortElement(['.content-info-col.-desc .content-name h1','.content-name.-desc-name h1','.content-name h1']); if (!title) return false; const existingRoot=document.getElementById('vm-escort-favorite-note-row'); if (existingRoot) { if (title.nextElementSibling !== existingRoot) title.after(existingRoot); existingRoot._vmRender(); return true; } const change=(id,action,value) => changeTrackedEntry(id,action,value,{ getMatches:getFavoriteMatchesForCurrentEscortAd, getContext:() => ({sourceType:'escort-ad',resolutionMode:'single',adUrl:location.href, displayName:extractWatchPageName(document,`Anons ${getAdIdFromUrl()}`),imageUrl:extractWatchPageImage(document,location.href)}) }); const {root,panel,list,render}=createTrackedEntriesBox(change,{ accent:getEscortPagePinkColor(),fallbackText:getEscortAdStandardTextColor(),emptyAdOwnerKey:`single:${getAdIdFromUrl()}` }); root.id='vm-escort-favorite-note-row'; root.classList.add('vm-ad-summary-box'); root.dataset.vmWatchWrapper='1'; panel.id='vm-escort-tracked-panel'; list.id='vm-escort-favorite-note-list'; const footer=makeElement('div','vm-ui-row'); const add=makeButton('vm-tracked-profile-picker','+ Dodaj profil'); add.id='vm-escort-add-profile'; add.title='Wybierz sposób wyszukania powiązanych anonsów, aby edytować ustawienia ich profilu.'; const status=makeElement('div','vm-ui-help'); status.setAttribute('role','status'); footer.append(add,status); panel.appendChild(footer); title.parentElement.insertBefore(root,title.nextSibling); const refresh=() => { if (!root.isConnected) return; const matches=getFavoriteMatchesForCurrentEscortAd(); render([getTrackedScopeItem(matches,'single'),...matches.filter(item => item.sourceType !== 'escort-ad')]); }; root._vmRender=refresh; watchPageButtonRefreshers.add(refresh); add.addEventListener('click',async event => { stopUiEvent(event); if (add.disabled) return; add.disabled=true; try { const mode=await chooseFavoriteProfileResolutionMode(getFavoriteMatchesForCurrentEscortAd().filter(item => item.sourceType !== 'escort-ad').map(item => item.resolutionMode)); if (!mode) return; status.textContent='Wyszukiwanie profilu…'; const context=await buildFavoriteContextFromEscortAd(location.href,'person',mode); if (!context || !root.isConnected || !getWatchSettings().enabled) return; const existing=getWatchedItems().find(item => item.sourceType !== 'escort-ad' && watchedItemsOverlap(item,context)); const saved=saveFavoriteItemFromContext({...context,keepProfile:true, resolvedAdUrls:[...new Set([...(context.resolvedAdUrls || []),normalizeEscortiAdUrl(location.href)])] },{favorite:existing?.favorite === true},'person'); status.textContent=''; const target=[...list.children].find(row => row.dataset.vmEntryId === saved.id); if (!panel.hidden) { target?.focus({preventScroll:true}); target?.scrollIntoView?.({block:'nearest',behavior:'smooth'}); } } catch (error) {status.textContent=error?.message || 'Nie udało się dodać profilu.';} finally {add.disabled=false;} }); refresh(); return true; }; if (insert()) return; let attempts=0; const timer=setInterval(() => {if (insert() || ++attempts >= 40) clearInterval(timer);},250); } function getEscortiUiAccentColor() { const sample=document.querySelector('[class*="text-primary-600"], [class*="text-primary-500"]'); const color=sample ? getComputedStyle(sample).color:''; return color && color !== 'rgb(0, 0, 0)' ? color:'#c026d3'; } function getEscortiProfileWatchHeading() { return ( document.querySelector('h3.text-2xl') || document.querySelector('main h1, main h2, main h3') || document.querySelector('h1,h2,h3') ); } function initEscortiProfileWatchButtons() { if (!/^\/escort\/\d+\/?$/i.test(location.pathname)) return; const insert=() => { const settings=getWatchSettings(); if (!settings.enabled) return; const heading=getEscortiProfileWatchHeading(); if (!heading) return; for (const element of document.querySelectorAll('a,button')) { if (element.closest('[data-vm-watch-wrapper="1"], .vm-helper-ui')) continue; const label=normalizeText(element.textContent); const hidden=/dodaj do ulubion|usuń z ulubion|obserwuj|dodaj notat|notatka/.test(label); element.classList.toggle('vm-native-escort-action-hidden',!!hidden); } let block=document.getElementById('vm-escorti-profile-note-display'); if (!block) { const getMatches=() => getFavoriteMatchesForCurrentEscortiProfile().filter(item => item.sourceType !== 'escort-search'); const getContext=() => ({...getCurrentEscortiWatchContextFast(),resolvedAdUrls:getEscortiAdUrls()}); const change=(id,action,value) => changeTrackedEntry(id,action,value,{getMatches,getContext,scope:'person'}); const {root,render}=createTrackedEntriesBox(change,{accent:getEscortiUiAccentColor(),fallbackText:'#4b5563',emptyAdOwnerKey:`escorti:${location.pathname}`}); block=root; block.id='vm-escorti-profile-note-display'; block.classList.add('vm-ad-summary-box'); block.dataset.vmWatchWrapper='1'; block._vmRender=() => { if (!block.isConnected) return; const settings=getWatchSettings(),matches=getMatches(); block.hidden=!settings.enabled; render(matches.length ? matches:[getContext()]); }; watchPageButtonRefreshers.add(block._vmRender); } if (heading.nextElementSibling !== block) heading.after(block); block._vmRender(); }; insert(); const root=document.documentElement; if (!root.__vmEscortiProfileWatchObserver) { const schedule=createAnimationFrameScheduler(insert); const observer=new MutationObserver(() => { const heading=getEscortiProfileWatchHeading(),block=document.getElementById('vm-escorti-profile-note-display'); if (heading && (!block?.isConnected || heading.nextElementSibling !== block)) schedule(); }); observer.observe(root,{childList:true,subtree:true}); root.__vmEscortiProfileWatchObserver=observer; } } function getCachedEscortiResultsForFavoriteItem(item) { const urls=[ item?.adUrl, ...(item?.resolvedAdUrls || []), ...(item?.state?.activeUrls || []) ].filter(Boolean); const adIds=[...new Set(urls.map(parseAdIdFromUrl).filter(Boolean))]; const results=[]; const seen=new Set(),profileIds=new Set((item?.profileUrls||[]).map(getEscortiProfileId).filter(Boolean)); const add=result=>{ if(!result)return; if(profileIds.size&&(!result.profileIds?.length||result.profileIds.some(id=>!profileIds.has(id))))return; const signature=JSON.stringify({profiles:result.profileIds,ads:result.adIds,date:result.creationDate,city:result.currentCity,garso:result.garsoTopicTitles||result.garsoTopics}); if(!seen.has(signature)){seen.add(signature);results.push(result);} }; add(expandEscortListCacheEntry(item?.state?.escortiData,item?.resolutionMode)); for (const adId of adIds) { try { const store=readPersistentCacheValue(listCacheKey(adId),null); for (const [mode,entry] of Object.entries(store?.modes || {})) { const expanded=expandEscortListCacheEntry(entry,mode); add(expanded); } } catch {} } return results; } function getFavoriteLocationCities(item,view=null) { const clean=values=>[...new Set((values||[]).map(normalizeEscortCity).filter(Boolean))]; const current=clean(item?.state?.cities);if(current.length)return current; const last=clean(item?.state?.lastKnownCities);if(item?.state?.online!==true&&last.length)return last; const cached=clean((view?.details || getWatchedItemCachedAdDetails(item,true)).map(({adData})=>adData.location?.city)); if(cached.length)return cached;if(last.length)return last; return clean((view?.escorti || getCachedEscortiResultsForFavoriteItem(item)).flatMap(result=>result.currentCity?[result.currentCity]:(result.profileSummaries||[]).map(profile=>profile.currentCity))); } function getFavoriteLastVisitedAt(item) { const urls=getWatchedItemAdUrls(item); let latest=0; for (const url of urls) { const adId=parseAdIdFromUrl(url); if (!adId) continue; const visitedAt=Number( (getStoredEscortAdData(adId) || getWatchedAdDataMirror(adId))?.lastVisitedAt ) || 0; if (visitedAt > latest) latest=visitedAt; } return latest || null; } function formatFavoriteColumnValue(item,field,view=null) { if (field === 'profile:cities') return watchCitiesText(view?.cities || getFavoriteLocationCities(item)); if (field === 'profile:hidden') return isWatchedItemHidden(item) ? 'Tak':'Nie'; if (field === 'profile:totalAds' && item.sourceType === 'escort-search') return String(getWatchedItemHistoricalAdUrls(item).length); const descriptor=getEscortDataFieldDescriptor(field); if (!descriptor) return ''; if ( descriptor.source === 'ad' && descriptor.path?.[0] === 'lastVisitedAt' ) { const visitedAt=view ? view.lastVisited:getFavoriteLastVisitedAt(item); return visitedAt ? formatEscortUniversalDataValue( descriptor, visitedAt, false ) :'b/d'; } if ( descriptor.source === 'escorti' && descriptor.path?.[0] === 'historicalCityCount' ) { const cities=getEscortiHistoricalCitiesFromResults(view?.escorti || getCachedEscortiResultsForFavoriteItem(item)); return cities.length ? String(cities.length) :'b/d'; } if (descriptor.source === 'garso') { const stats=getCachedGarsoStatsForAds(view?.all || getWatchedItemAdUrls(item,false),item?.phoneDigits); return formatEscortUniversalDataValue(descriptor,getGarsoDataValue(stats,descriptor)) || 'b/d'; } let values; if (descriptor.source === 'profile') { values=[getEscortDataValueByPath(item?.state || {},descriptor.path)]; } else if (descriptor.source === 'escorti') { values=(view?.escorti || getCachedEscortiResultsForFavoriteItem(item)) .map(result => getEscortDataValueByPath(result,descriptor.path)); } else { values=(view?.details || getWatchedItemCachedAdDetails(item,true)) .map(detail => getEscortDataFieldRawValue(detail.adData,descriptor)); } const unique=[...new Set(values.map(value => formatEscortUniversalDataValue(descriptor,value,false)).filter(Boolean))]; return unique.length ? unique.join(' • '):'b/d'; } function favoriteDataSortValue(item,field,text=formatFavoriteColumnValue(item,field),view=null) { const descriptor=getEscortDataFieldDescriptor(field),kind=descriptor?.kind; text=String(Array.isArray(text)?text[0]??'':text??'').split(' • ')[0].trim(); if(!text||/^(?:b\/d|brak danych|[?—-])$/i.test(text))return null; if(kind==='relative-time')return (view ? view.lastVisited:getFavoriteLastVisitedAt(item))||null; if(kind==='date'||kind==='datetime'){ const date=text.match(/^(\d{2}\.\d{2}\.\d{4})(?:,?\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?/); const time=date?profileDateToTime(date[1])+((+date[2]||0)*3600+(+date[3]||0)*60+(+date[4]||0))*1000:Date.parse(text); return Number.isFinite(time)?time:null; } if(kind==='price'||kind==='number'||descriptor?.path?.[0]==='stats'){ const match=text.replace(/\s+/g,'').replace(',','.').match(/-?\d+(?:\.\d+)?/);if(match)return Number(match[0]); } return normalizeText(text); } function compareFavoriteValues(a,b,direction=1) { if(a==null||b==null)return Number(a==null)-Number(b==null); return (typeof a==='number'&&typeof b==='number'?a-b:String(a).localeCompare(String(b),'pl-PL',{numeric:true,sensitivity:'base'}))*direction; } function getFavoriteColumnLabel(field) { return getEscortDataFieldDescriptor(field)?.label || 'Dane'; } let watchImagePreview=null; let watchImagePreviewImg=null; function getQuickAdPreviewData(anchor,cachedData=null) { const url=normalizeEscortiAdUrl(anchor.href),id=parseAdIdFromUrl(url); if (!id) return null; const data=[getWatchedAdDataMirror(id),cachedData,getStoredEscortAdData(id)] .filter(Boolean).sort((a,b) => (Number(a.checkedAt) || 0) - (Number(b.checkedAt) || 0)) .reduce((value,source) => mergeEscortDefinedObject(value,source),{}); const tile=makeGarsoAdTileData(id,{...data,adUrl:url}); const card=anchor.closest('.item-col.col, .vm-garso-visited-card'); const text=selector => normalizeEscortAdText(card?.querySelector(selector)?.textContent); const img=card?.querySelector('img'); tile.title=text('.item-name') || tile.title || anchor.textContent.trim() || `Anons ${id}`; tile.locationText=text('.item-stats').replace(/,\s*\d{1,3}\s*(?:y|lat|lata|rok|years?)\b.*$/i,'') || text('.vm-garso-visited-location') || tile.locationText; tile.priceText=tile.priceText || text('.vm-escort-tile-price, .vm-garso-visited-price'); tile.profileStatsText=tile.profileStatsText || text('.vm-garso-visited-stats'); tile.imageUrl=img?.currentSrc || img?.getAttribute('data-src') || img?.getAttribute('src') || tile.imageUrl; const profiles=new Set(getHiddenResultProfileUrls(getListCache(id)).map(getEscortiProfileId)); const phone=normalizePhoneReputationPhone(getEscortAdPhoneDigits(data))?.local; const items=getWatchedItems().filter(item => getWatchedItemAdUrls(item).some(value => parseAdIdFromUrl(value) === id) || (item.sourceType !== 'escort-ad' && item.sourceType !== 'escort-search' && ((item.profileUrls || []).some(value => profiles.has(getEscortiProfileId(value))) || (phone && !item.escortiProfileOnly && !item.phoneProfileOnly && item.resolutionMode?.startsWith('phone-') && normalizePhoneReputationPhone(item.phoneDigits)?.local === phone)))); const offlineOnly=!Object.keys(data).length && !getEscortCachedAdHost(id) && !tile.imageUrl && !tile.locationText && !tile.priceText && !tile.visitedAt && !items.some(item => item.note?.trim() || item.categories?.length || hasFavoriteStar(item) || item.hidden) && [...document.querySelectorAll('[data-vm-ad-status="offline"]')].some(marker => parseAdIdFromUrl(marker.dataset.adUrl) === id); return {...tile,offlineOnly,notes:[...new Set(items.map(item => item.note?.trim()).filter(Boolean))], categories:[...new Set(items.flatMap(item => item.categories || []))], favorite:items.some(hasFavoriteStar),hidden:items.some(item => item.hidden)}; } function positionQuickAdPreview(popup,anchor) { popup.style.maxHeight=''; const rect=(anchor.closest('.vm-garso-visited-card') || anchor).getBoundingClientRect(); const box=popup.getBoundingClientRect(),margin=8,width=window.innerWidth,height=window.innerHeight; let left,top=Math.max(margin,Math.min(rect.top,height - box.height - margin)); if (rect.right + margin + box.width <= width - margin) left=rect.right + margin; else if (rect.left - margin - box.width >= margin) left=rect.left - margin - box.width; else { const below=height - rect.bottom - margin * 2,above=rect.top - margin * 2; const useBelow=below >= box.height || (above < box.height && below >= above); const available=Math.min(box.height,useBelow ? below:above); if (available < 44) return false; popup.style.maxHeight=`${available}px`; top=useBelow ? rect.bottom + margin:rect.top - margin - available; left=Math.max(margin,Math.min(rect.left,width - box.width - margin)); } popup.style.left=`${left}px`; popup.style.top=`${top}px`; return true; } function initQuickAdPreview() { const supported=/^(?:www\.)?(?:garsoniera\.com\.pl|escorti\.pl)$/i.test(location.hostname) || isEscortClubHost(location.hostname); if (!supported || window.top !== window.self || document.__vmQuickAdPreview) return; document.__vmQuickAdPreview=true; let anchor=null,popup=null,timer=null,closeTimer=null,revision=0; const cacheReads=new Map(); const hide=() => { revision++; clearTimeout(timer); clearTimeout(closeTimer); if (anchor && popup) { const ids=(anchor.getAttribute('aria-describedby') || '').split(/\s+/).filter(id => id && id !== popup.id); if (ids.length) anchor.setAttribute('aria-describedby',ids.join(' ')); else anchor.removeAttribute('aria-describedby'); } anchor=null; if (popup) popup.hidden=true; }; const find=target => { if (SETTINGS.quickAdPreview === false || !target?.closest || target.closest('#vm-ad-quick-preview, [role="dialog"], .vm-ui-overlay')) return null; if (target.closest('button, input, select, textarea')) return null; const link=target.closest('a[href]'); if (!link || !normalizeEscortiAdUrl(link.href)) return null; if (IS_ESCORT_CLUB) { try { if (isEscortClubHost(new URL(link.href,location.href).hostname)) return null; } catch {} } link.setAttribute('title',''); for (const child of link.querySelectorAll('[title]')) child.removeAttribute('title'); return link; }; const render=value => { if (!anchor?.isConnected || SETTINGS.quickAdPreview === false) {hide(); return;} if (!value) {hide(); return;} if (!popup) { const style=makeElement('style'); style.textContent=`#vm-ad-quick-preview{position:fixed;z-index:2147483647;box-sizing:border-box;width:420px;max-width:calc(100vw - 16px);max-height:min(80vh,560px);overflow:auto;padding:12px;border:1px solid var(--vm-ui-border,#ddd);border-radius:10px;background:var(--vm-ui-bg,#fff);color:var(--vm-ui-text,#222);box-shadow:var(--vm-ui-shadow,0 8px 28px #0003);font:13px/1.45 var(--vm-ui-font-family,Arial,sans-serif);overflow-wrap:anywhere;text-align:left}#vm-ad-quick-preview[hidden]{display:none!important}#vm-ad-quick-preview .vm-preview-head{display:flex;gap:12px}#vm-ad-quick-preview .vm-preview-photo{width:100px;height:135px;flex:none;object-fit:cover;border-radius:6px;background:var(--vm-ui-bg-hover,#eee);display:grid;place-items:center;color:var(--vm-ui-text-muted,#777)}#vm-ad-quick-preview .vm-preview-info{min-width:0;flex:1}#vm-ad-quick-preview strong{display:block;margin-bottom:6px;font-size:14px}#vm-ad-quick-preview .vm-preview-note{white-space:pre-wrap;margin-top:9px}#vm-ad-quick-preview .vm-preview-chips{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}#vm-ad-quick-preview .vm-preview-chip{padding:2px 7px;border:1px solid var(--vm-ui-border,#ccc);border-radius:12px}#vm-ad-quick-preview .vm-preview-status{margin-top:9px;color:var(--vm-ui-text-muted,#666)}`; document.head.appendChild(style); popup=prepareHelperStyleApi(makeElement('div')); popup.id='vm-ad-quick-preview'; popup.setAttribute('role','tooltip'); popup.addEventListener('pointerenter',() => clearTimeout(closeTimer)); popup.addEventListener('pointerleave',hide); document.body.appendChild(popup); } popup.replaceChildren(); popup.style.width=value.offlineOnly ? 'auto':''; if (value.offlineOnly) popup.textContent='offline'; else { const head=makeElement('div','vm-preview-head'),photo=makeElement('span','vm-preview-photo','Brak zdjęcia'); if (value.imageUrl) { try { const url=new URL(value.imageUrl,location.href); if (/^https?:$/.test(url.protocol) || url.protocol === 'blob:' || /^data:image\//i.test(url.href)) { const img=makeElement('img','vm-preview-photo'); img.alt=''; img.referrerPolicy='no-referrer'; img.addEventListener('error',() => img.replaceWith(photo)); img.src=url.href; head.appendChild(img); } } catch {} } if (!head.firstElementChild) head.appendChild(photo); const info=makeElement('div','vm-preview-info'); info.append(makeElement('strong','',value.title),makeElement('div','',value.locationText || 'Brak danych o mieście')); const previewStats=createGarsoProfileStatsElement(value.profileStats,'vm-preview-stats'); if (previewStats) info.appendChild(previewStats); else if (value.profileStatsText) info.appendChild(makeElement('div','vm-preview-stats',value.profileStatsText)); info.appendChild(makeElement('div','',value.priceText || 'Brak zapisanych cen')); head.appendChild(info); popup.appendChild(head); popup.appendChild(makeElement('div','vm-preview-status',value.visitedAt ? `Ostatnio obejrzany: ${formatWatchDateTime(value.visitedAt)} (${formatEscortLastVisit(value.visitedAt)})`:'Brak zapisanego obejrzenia')); const chips=makeElement('div','vm-preview-chips'),colors=getFavoriteSettings().categoryColors; for (const category of value.categories) {const chip=makeElement('span','vm-preview-chip',category); chip.style.borderColor=colors[category] || '#ccc'; chips.appendChild(chip);} if (value.categories.length) popup.appendChild(chips); if (value.notes.length) popup.appendChild(makeElement('div','vm-preview-note',value.notes.join('\n'))); const status=[value.favorite ? '★ W Ulubionych':'',value.hidden ? 'Ukryty w wyszukiwaniu':''].filter(Boolean).join(' · '); if (status) popup.appendChild(makeElement('div','vm-preview-status',status)); } hideWatchImagePreview(); popup.hidden=false; const ids=new Set((anchor.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean)); ids.add(popup.id); anchor.setAttribute('aria-describedby',[...ids].join(' ')); if (!positionQuickAdPreview(popup,anchor)) hide(); }; const show=async () => { if (!anchor?.isConnected || SETTINGS.quickAdPreview === false) return; const link=anchor,currentRevision=revision,id=parseAdIdFromUrl(link.href); let cached=cacheReads.get(id); const value=getQuickAdPreviewData(link,cached?.data); render(value); if (anchor !== link || !value || cached?.data || (value.imageUrl && value.locationText && value.priceText && (value.profileStats?.text || value.profileStatsText))) return; const host=getEscortCachedAdHost(id); if (!host) return; const message=makeElement('div','vm-preview-status','Odczytywanie danych z cache…'); popup.appendChild(message); if (!positionQuickAdPreview(popup,link)) hide(); try { if (!cached) { cached={}; cached.promise=requestPersistentCacheBridgeAction('read-ad-data',{host,adIds:[id]}).then(response => { cached.data=response.ads?.find(entry => String(entry.adData?.adId) === id)?.adData; if (!cached.data) throw new Error('Brak danych anonsu w cache.'); return cached.data; }); cacheReads.set(id,cached); } const data=await cached.promise; if (anchor === link && revision === currentRevision) render(getQuickAdPreviewData(link,data)); } catch { cacheReads.delete(id); if (anchor === link && revision === currentRevision && !popup.hidden) { message.textContent=`Aby odczytać pełny cache, otwórz kartę ${host} i najedź ponownie.`; if (!positionQuickAdPreview(popup,link)) hide(); } } }; const enter=event => { if (event.pointerType === 'touch' || popup?.contains(event.target)) return; const next=find(event.target); if (next === anchor) {clearTimeout(closeTimer); return;} hide(); if (next) {anchor=next; timer=setTimeout(show,300);} }; document.addEventListener('pointerover',enter); document.addEventListener('focusin',enter); for (const type of ['pointerout','focusout']) document.addEventListener(type,event => { if (!anchor || find(event.relatedTarget) === anchor || popup?.contains(event.relatedTarget)) return; clearTimeout(timer); clearTimeout(closeTimer); closeTimer=setTimeout(hide,120); }); document.addEventListener('keydown',event => {if (event.key === 'Escape') hide();}); document.addEventListener('pointerdown',event => {if (!popup?.contains(event.target)) hide();}); document.addEventListener('scroll',event => {if (!popup?.contains(event.target)) hide();},true); window.addEventListener('resize',hide); window.addEventListener('blur',hide); window.addEventListener('pagehide',hide); } function ensureWatchImagePreview(container=document.body) { if (watchImagePreview?.isConnected) { if (container && watchImagePreview.parentElement !== container) container.appendChild(watchImagePreview); return watchImagePreview; } if (!document.getElementById('vm-watch-image-preview-style')) { appendHelperStyle('vm-watch-image-preview-style',`.vm-watch-image-preview { position:fixed; z-index:2147483647; display:none; width:220px; height:300px; padding:3px; border:1px solid var(--vm-ui-border-strong,rgba(0,0,0,.32)); border-radius:6px; background:var(--vm-ui-bg,#fff); box-shadow:var(--vm-ui-shadow,0 8px 24px rgba(0,0,0,.34)); box-sizing:border-box; pointer-events:none; } .vm-watch-image-preview > img { display:block; width:100%; height:100%; object-fit:cover; border-radius:3px; }`); } watchImagePreview=prepareHelperStyleApi(makeElement('div','vm-watch-image-preview')); watchImagePreviewImg=makeElement('img'); watchImagePreviewImg.alt=''; watchImagePreviewImg.referrerPolicy='no-referrer'; watchImagePreviewImg.addEventListener('error',() => { markWatchThumbnailUnavailable(watchImagePreviewImg.currentSrc || watchImagePreviewImg.src); hideWatchImagePreview(); }); watchImagePreview.appendChild(watchImagePreviewImg); (container || document.body).appendChild(watchImagePreview); return watchImagePreview; } function hideWatchImagePreview() { if (watchImagePreview) watchImagePreview.style.display='none'; } function showWatchImagePreview(anchor,imageUrl,container=document.body) { if (!anchor || !imageUrl) return; const preview=ensureWatchImagePreview(container); watchImagePreviewImg.src=imageUrl; const rect=anchor.getBoundingClientRect(),margin=8,width=220,height=300; let left=rect.right + margin; if (left + width > window.innerWidth - margin) left=rect.left - width - margin; left=Math.max(margin,Math.min(left,window.innerWidth - width - margin)); let top=rect.top + rect.height / 2 - height / 2; top=Math.max(margin,Math.min(top,window.innerHeight - height - margin)); preview.style.left=`${Math.round(left)}px`; preview.style.top=`${Math.round(top)}px`; preview.style.display='block'; } function prepareFavoriteTableStyleApi(overlay,box) { prepareHelperStyleApi(overlay); overlay.classList.add('vm-favorites-ui'); overlay.dataset.vmFavoritesStyleApi='1'; box.classList.add('vm-favorites-modal'); if (document.getElementById('vm-favorites-style-api')) return; appendHelperStyle('vm-favorites-style-api',` .vm-favorites-ui { font-family:var(--vm-fav-font-family,var(--vm-ui-font-family,Arial,sans-serif)); } .vm-favorites-ui .vm-favorites-checkbox { accent-color: var(--vm-fav-checkbox-accent, var(--vm-fav-accent, var(--vm-ui-accent, #f54998))); } .vm-favorites-ui .vm-favorites-filter:focus { border-color: var(--vm-fav-accent, var(--vm-ui-accent, #f54998)) !important; box-shadow: var(--vm-fav-filter-focus-shadow, 0 0 0 2px rgba(245,73,152,.10)) !important; background: var(--vm-fav-filter-focus-bg, var(--vm-ui-input-bg, #fff)) !important; } .vm-favorites-ui .vm-favorites-toolbar-button, .vm-favorites-ui .vm-favorites-icon-button, .vm-favorites-ui .vm-favorites-header-action { font-family: inherit; } .vm-favorites-ui.vm-ui-overlay { padding:8px; } .vm-favorites-ui .vm-ui-table-dialog { width:calc(100vw - 16px); height:calc(100vh - 16px); max-height:calc(100vh - 16px); display:flex; flex-direction:column; padding:0; overflow:hidden; } .vm-favorites-ui .vm-favorites-toolbar { display: flex; align-items: center; gap: 7px; padding: 2px 10px; border-bottom: 1px solid var(--vm-fav-toolbar-border, var(--vm-ui-border, #e6e6e6)); flex: 0 0 auto; flex-wrap: wrap; } .vm-favorites-ui .vm-favorites-toolbar-actions { display:flex; align-items:center; justify-content:flex-end; flex-wrap:wrap; gap:7px; min-width:0; max-width:100%; margin-left:auto; } .vm-favorites-ui .vm-favorites-title { flex: 0 0 auto; margin: 0 8px 0 0; } .vm-favorites-ui .vm-favorites-count { color:var(--vm-ui-text-muted,#666); font-size:.8em; font-weight:600; white-space:nowrap; font-variant-numeric:tabular-nums; } .vm-favorites-ui .vm-favorites-manual-input { flex: 0 1 390px; min-width: 220px; border:1px solid var(--vm-ui-input-border,#bbb); border-radius:var(--vm-ui-radius,6px); background:var(--vm-ui-input-bg,#fff); color:var(--vm-ui-input-text,var(--vm-ui-text,#222)); box-sizing:border-box; } .vm-favorites-ui .vm-favorites-add-button { white-space: nowrap; } .vm-favorites-ui .vm-favorites-status { display: none; padding: 3px 10px; background: var(--vm-fav-status-bg, var(--vm-ui-bg-alt, #fff5f9)); color: var(--vm-fav-accent, var(--vm-ui-accent, #f54998)); font-size: 11px; font-weight: 700; flex: 0 0 auto; } .vm-favorites-ui .vm-favorites-table-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; padding: 0 10px 10px; box-sizing: border-box; } .vm-favorites-ui .vm-favorites-table { position: relative; min-height: 100%; } .vm-favorites-ui .vm-favorites-add-column-select { display: none; max-width: 160px; } .vm-favorites-ui .vm-favorites-add-column, .vm-favorites-ui .vm-favorites-remove-all { white-space: nowrap; } .vm-favorites-ui .vm-favorites-table-header { position: sticky; top: 0; z-index: 5; display: grid; grid-template-columns: var(--vm-fav-current-grid); gap: var(--vm-fav-grid-gap, 3px); align-items: stretch; padding: var(--vm-fav-header-padding, 2px 4px 3px); border: 1px solid var(--vm-fav-header-border, #d7d9df); border-radius: var(--vm-fav-header-radius, 8px 8px 0 0); background: var(--vm-fav-header-bg, linear-gradient(180deg, #f0f1f4 0%, #f7f7f9 100%)); box-shadow: var(--vm-fav-header-shadow, 0 3px 12px rgba(0,0,0,.055)); color: var(--vm-fav-header-text, #555); font-size: var(--vm-fav-header-font-size, 9px); font-weight: 700; backdrop-filter: blur(5px); } .vm-favorites-ui .vm-favorites-sort-button { max-width: 100%; min-width: 0; padding: 0; border: 0; background: transparent; color: var(--vm-fav-header-text, #555); cursor: pointer; font: inherit; font-size: var(--vm-fav-header-font-size, 9px); font-weight: 800; text-align: left; white-space: normal; line-height: 1.08; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } .vm-favorites-ui .vm-favorites-sort-button.-active { color: var(--vm-fav-accent, var(--vm-ui-accent, #f54998)); font-weight: 900; } .vm-favorites-ui .vm-favorites-filter { width: 100%; min-width: 0; height: var(--vm-fav-filter-height, 20px); min-height: var(--vm-fav-filter-height, 20px); padding: 0 5px; border: 1px solid var(--vm-fav-filter-border,var(--vm-ui-input-border,#bbb)); border-radius: var(--vm-fav-filter-radius, 6px); outline: none; background: var(--vm-fav-filter-bg,var(--vm-ui-input-bg,#fff)); color: var(--vm-fav-filter-text,var(--vm-ui-input-text,#555)); font-size: var(--vm-fav-filter-font-size, 8.8px); box-sizing: border-box; transition: border-color .12s ease, box-shadow .12s ease, background-color .12s ease; } .vm-favorites-ui select.vm-favorites-filter { padding: 0 15px 0 5px; } .vm-favorites-ui .vm-favorites-filter-active { border-color: var(--vm-fav-accent, var(--vm-ui-accent, #f54998)) !important; background: var(--vm-fav-filter-active-bg, #fff5f9) !important; color: var(--vm-fav-filter-active-text,var(--vm-fav-accent,var(--vm-ui-accent,#f54998))) !important; font-weight:700; } .vm-favorites-ui .vm-favorites-header-cell { min-width: 0; display: grid; grid-template-rows: var(--vm-fav-header-rows, var(--vm-fav-header-title-height, 24px)); gap: var(--vm-fav-header-gap, 0); align-content: center; padding: 0; } .vm-favorites-ui .vm-favorites-header-cell > :first-child { margin-top: 2px; margin-bottom: 2px; } .vm-favorites-ui .vm-favorites-header-group { display: flex; align-items: center; gap: 3px; min-width: 0; } .vm-favorites-ui .vm-favorites-row { display: grid; grid-template-columns: var(--vm-fav-current-grid); gap: var(--vm-fav-grid-gap, 3px); align-items: center; min-height: var(--vm-fav-row-min-height, 64px); padding: var(--vm-fav-row-padding, 2px 4px); border-left: 1px solid var(--vm-fav-row-edge-border, #e3e4e7); border-right: 1px solid var(--vm-fav-row-edge-border, #e3e4e7); border-bottom: 1px solid var(--vm-fav-row-border, #e7e8ea); background: var(--vm-fav-row-current-bg, var(--vm-fav-row-bg, #fff)); box-shadow: var(--vm-fav-row-current-shadow, none); transition: background-color .11s ease, box-shadow .11s ease; } .vm-favorites-ui .vm-favorites-row.-unavailable { background: var(--vm-fav-row-unavailable-bg, color-mix(in srgb, #888 18%, var(--vm-fav-row-bg, var(--vm-ui-bg, #fff)))) !important; } .vm-favorites-ui .vm-favorites-row.-unavailable + .vm-favorites-row.-unavailable { border-top: 1px solid var(--vm-fav-section-separator, var(--vm-fav-row-edge-border, #c7c9ce)); } .vm-favorites-ui .vm-favorites-row.-last { border-radius: var(--vm-fav-last-row-radius, 0 0 7px 7px); } .vm-favorites-ui .vm-favorites-image-cell { width: 100%; height: 100%; min-height: var(--vm-fav-row-min-height, 64px); align-self: stretch; border-radius: 5px; background: var(--vm-ui-bg-alt, #f2f2f2); overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; color: var(--vm-ui-text-faint, #bbb); font-size: 9px; } .vm-favorites-ui .vm-favorites-image-cell > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } .vm-favorites-ui .vm-favorites-profile-cell { min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 1px; padding: 1px 0; } .vm-favorites-ui .vm-favorites-profile-name-row { display: flex; align-items: center; gap: 3px; min-width: 0; margin-bottom: 4px; } .vm-favorites-ui .vm-favorites-profile-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vm-fav-accent, var(--vm-ui-accent, #f54998)); font-weight: 700; font-size: 13px; line-height: 1.15; text-decoration: none; } .vm-favorites-ui .vm-favorites-note-cell { min-width: 0; display: flex; align-items: stretch; align-self: stretch; height: 100%; min-height: 0; overflow: hidden; } .vm-favorites-ui .vm-favorites-category-cell { position: relative; z-index: 2; display: flex; align-items: flex-start; justify-content: flex-start; flex-wrap: nowrap; gap: 3px; min-width: 0; overflow: visible; } .vm-favorites-ui .vm-favorites-category-chips { display: flex; flex: 1 1 auto; align-items: center; flex-wrap: wrap; gap: 2px; min-width: 0; } .vm-favorites-ui .vm-favorites-actions-cell { display: flex; gap: 3px; align-items: center; justify-content: center; } .vm-favorites-ui .vm-favorites-location-cell, .vm-favorites-ui .vm-favorites-data-cell { min-width: 0; color: var(--vm-ui-text, #444); font-size: 8.5px; line-height: 1.08; } .vm-favorites-ui .vm-favorites-cell-text { display: block; min-width: 0; max-height: 4.4em; overflow: hidden; overflow-wrap: anywhere; } .vm-favorites-ui .vm-ui-button { background:var(--vm-fav-button-bg,var(--vm-ui-bg,#fff)); } .vm-favorites-ui .vm-ui-button.-accent { border-color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); } .vm-favorites-ui .vm-ui-button.-primary { border-color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); background:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); color:var(--vm-fav-accent-contrast,var(--vm-ui-accent-contrast,#fff)); } .vm-favorites-ui .vm-favorites-icon-button { border-color:var(--vm-fav-icon-border,var(--vm-ui-input-border,#bbb)); background:var(--vm-fav-icon-bg,var(--vm-ui-bg,#fff)); color:var(--vm-fav-icon-text,var(--vm-ui-text,#222)); } .vm-favorites-ui .vm-favorites-change-history-button { opacity:.4; border-color:transparent; background:transparent; color:var(--vm-ui-text-muted,#888); } .vm-favorites-ui .vm-favorites-changes-cell:hover .vm-favorites-change-history-button, .vm-favorites-ui .vm-favorites-change-history-button:focus-visible { opacity:1; border-color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); background:var(--vm-ui-bg-hover,#f5f5f5); color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); } @media (hover:none) { .vm-favorites-ui .vm-favorites-change-history-button { opacity:1; } } .vm-favorites-ui .vm-favorites-icon-button:hover:enabled { border-color:var(--vm-fav-icon-hover-border,var(--vm-ui-input-border,#bbb)); background:var(--vm-fav-icon-hover-bg,var(--vm-ui-bg-hover,#f5f5f5)); color:var(--vm-fav-accent,var(--vm-ui-accent,#f54da3)); } .vm-favorites-ui .vm-favorites-close { background:var(--vm-fav-close-bg,var(--vm-ui-bg,#fff)); color:var(--vm-fav-close-text,var(--vm-ui-text,#222)); } `); } function getWatchEventUrl(event,markRead=true) { if (!event?.url) return ''; try { const url=new URL(event.url,location.href); if (!/^https?:$/.test(url.protocol)) return ''; if (markRead && event.alertTriggered && event.id) { const parts=url.hash.slice(1).split('&').filter(part => part && !part.startsWith('vm_watch_read=')); parts.push(`vm_watch_read=${encodeURIComponent(event.id)}`); url.hash=parts.join('&'); } return url.href; } catch {return '';} } async function openWatchEvent(event,active=true) { const url=getWatchEventUrl(event); if (!url) return false; try { await GM_openInTab(url,{active,insert:true,setParent:true}); if (event.alertTriggered) acknowledgeWatchEvent(event.id); return true; } catch (error) {log('Nie udało się otworzyć alertu',error); showWatchToast('Nie udało się otworzyć strony alertu.',true); return false;} } function makeWatchEventLink(event,text=event.message || '',markReadOnArrival=true) { const url=getWatchEventUrl(event,markReadOnArrival); const link=makeElement('a','',text,{href:url || '#',target:'_blank',rel:'noopener noreferrer',title:event.description || event.message || ''}); if (url && markReadOnArrival && event.alertTriggered) { const read=click => { if (click.button > 1) return; setTimeout(() => {if (!click.defaultPrevented) acknowledgeWatchEvent(event.id);},0); }; link.addEventListener('click',read); link.addEventListener('auxclick',read); } return link; } function getWatchEventTimelineLabel(event) { if (event?.type === 'online') return 'Stan online'; if (event?.type === 'location') return 'Lokalizacja'; if (event?.type === 'changes') return 'Zmiana treści'; if (event?.type === 'new-ad') return 'Nowy anons'; return 'Zmiana'; } function clearWatchHistoryForItem(itemId) { const id=String(itemId || ''); if (!id) return 0; const events=getWatchEvents(),remaining=events.filter(event=>event.itemId!==id); const removed=events.length-remaining.length; if (removed) { saveWatchEvents(remaining); refreshWatchPageButtons(); try {document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();} catch {} renderWatchNotificationBar(); } return removed; } function openWatchChangesTimeline( item, itemEvents ) { const timelineAlertStyle={ padding:'0', border:'0', borderRadius:'0', background:'transparent', color: 'var(--vm-ui-text-muted, #777)', fontSize:'8px', fontWeight:'400' }; const events=( Array.isArray(itemEvents) ? itemEvents :[] ) .filter(event=>String(event.itemId)===String(item?.id)).sort( (a,b) => Number(b.timestamp || 0) - Number(a.timestamp || 0) ); const {overlay,box}= createWatchDialogBase( 'vm-favorites-changes-timeline-overlay', `Historia zmian - ${ item?.displayName || 'profil/anons' }` ); prepareFavoriteTableStyleApi( overlay, box ); box.classList.add('vm-ui-timeline-dialog'); Object.assign(box.style,{width: 'min(760px, calc(100vw - 32px))'}); const title=box.querySelector('h2'); if (title) { title.style.marginBottom='6px'; } const summary=makeElement('div',"vm-ui-help",`${events.length} ${ polishCountForm( events.length, 'zmiana', 'zmiany', 'zmian' ) }`); Object.assign(summary.style,{marginBottom:'12px'}); const timeline=makeElement( 'div', 'vm-favorites-changes-timeline' ),updates=new Map(); Object.assign(timeline.style,{ display:'grid', gap:'0' }); if (!events.length) { const empty=makeElement( 'div', '', 'Brak zapisanych zmian.' ); empty.style.color= 'var(--vm-ui-text-muted, #777)'; timeline.appendChild(empty); } events.forEach((event,index) => { const row=makeElement( 'div', 'vm-favorites-changes-timeline-row' ); Object.assign(row.style,{ display:'grid', gridTemplateColumns: '108px 22px minmax(0,1fr)', gap:'8px', minWidth:'0' }); const date=new Date( Number(event.timestamp || 0) ); const time=makeElement( 'div', '', Number.isFinite(date.getTime()) ? date.toLocaleString( 'pl-PL', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' } ) :'' ); Object.assign(time.style,{ paddingTop:'5px', color: 'var(--vm-ui-text-muted, #777)', fontSize:'9.5px', textAlign:'right', whiteSpace:'nowrap' }); const axis=makeElement('div'); Object.assign(axis.style,{ position:'relative', minHeight:'52px' }); if (index < events.length - 1) { const vertical=makeElement('span'); Object.assign(vertical.style,{ position:'absolute', left:'10px', top:'16px', bottom:'-2px', width:'1px', background: 'var(--vm-ui-border, #ddd)' }); axis.appendChild(vertical); } const dot=makeElement('span'); Object.assign(dot.style,{ position:'absolute',left:'5px',top:'7px',width:'11px',height:'11px',borderRadius:'50%', boxSizing:'border-box',border:'2px solid var(--vm-ui-bg, #fff)',boxShadow:'0 0 0 1px var(--vm-ui-border-strong, #bbb)' }); axis.appendChild(dot); const content=makeElement('div',"vm-ui-panel"); content.style.marginBottom='8px'; const top=makeElement('div'); Object.assign(top.style,{ display:'flex', alignItems:'center', gap:'6px', flexWrap:'wrap', marginBottom:'3px' }); const type=makeElement( 'span', '', getWatchEventTimelineLabel( event ) ); Object.assign(type.style,{fontSize:'9px',fontWeight:'800'}); top.appendChild(type); const timelineAlertLabel=event.alertTriggered ? makeElement('span'):null; const timelineAlertAccept=event.alertTriggered ? makeButton('vm-ui-button vm-ui-icon vm-ui-compact vm-ui-table-icon','✓'):null; if (timelineAlertAccept) { setControlTitle(timelineAlertAccept,'Zaakceptuj alert / oznacz jako odczytany'); timelineAlertAccept.addEventListener('click',click => {stopUiEvent(click); acknowledgeWatchEvent(event.id);}); top.append(timelineAlertLabel,timelineAlertAccept); } const message=makeWatchEventLink(event); Object.assign(message.style,{display:'block',textDecoration:'underline',textUnderlineOffset:'2px',fontSize:'10.5px',lineHeight:'1.3',overflowWrap:'anywhere'}); const sync=current => { if (!current) {detachHelperNode(row); return;} event.acknowledged=current.acknowledged; const pending=!event.acknowledged && event.alertTriggered; dot.style.background=event.acknowledged ? 'var(--vm-ui-text-faint, #aaa)':pending ? 'var(--vm-ui-danger, #c62828)':'var(--vm-ui-accent, #f54da3)'; message.style.color=event.acknowledged ? 'var(--vm-ui-text, #444)':pending ? 'var(--vm-ui-danger, #c62828)':'var(--vm-ui-accent, #f54da3)'; type.style.color=pending ? 'var(--vm-ui-danger, #c62828)':'var(--vm-ui-text, #333)'; content.style.border=pending ? '1px solid var(--vm-ui-danger, #c62828)':'1px solid var(--vm-ui-border, #ddd)'; content.style.background=pending ? 'color-mix(in srgb, var(--vm-ui-danger, #c62828) 8%, var(--vm-ui-bg-alt, #fafafa))':'var(--vm-ui-bg-alt, #fafafa)'; if (timelineAlertLabel) { timelineAlertLabel.textContent=pending ? 'alert':'alert odczytany'; Object.assign(timelineAlertLabel.style,pending ? {padding:'1px 5px',border:'1px solid var(--vm-ui-danger, #c62828)',borderRadius:'999px',background:'color-mix(in srgb, var(--vm-ui-danger, #c62828) 8%, transparent)',color:'var(--vm-ui-danger, #c62828)',fontSize:'8px',fontWeight:'800'}:timelineAlertStyle); timelineAlertAccept.style.display=pending ? 'inline-flex':'none'; } }; updates.set(event.id,sync); sync(event); content.append(top,message); const description= String( event.description || '' ).trim(); if (description) { const detail=makeElement( 'div', '', description ); Object.assign(detail.style,{ marginTop:'4px', paddingTop:'4px', borderTop: '1px solid var(--vm-ui-border, #e5e5e5)', color: 'var(--vm-ui-text-muted, #666)', fontSize:'9.5px', lineHeight:'1.3', whiteSpace:'pre-wrap', overflowWrap:'anywhere' }); content.appendChild(detail); } row.append( time, axis, content ); timeline.appendChild(row); }); overlay._vmRender=() => { const current=new Map(getWatchEvents().map(event => [event.id,event])); let count=0; for (const [id,sync] of updates) {sync(current.get(id)); if (current.has(id)) count++;} summary.textContent=`${count} ${polishCountForm(count,'zmiana','zmiany','zmian')}`; }; const actions=makeElement('div',"vm-ui-actions"); const clear=makeButton('vm-favorites-clear-history vm-ui-button -danger','Wyczyść historię'); clear.disabled=!item?.id || !events.length; clear.style.marginRight='auto'; setControlTitle(clear,'Usuń historię zmian tylko tego profilu/anonsu. Pozostałe dane i historia innych pozycji pozostaną.'); const feedback=makeElement('div','vm-ui-help');feedback.setAttribute('role','status'); clear.addEventListener('click',event=>{ stopUiEvent(event); if(clear.disabled)return; if(!window.confirm(`Wyczyścić historię zmian „${item.displayName || 'profil/anons'}”?\n\n`+ 'Zostanie usunięta tylko historia tej pozycji, w tym jej oczekujące powiadomienia. Profil, notatki, kategorie i ustawienia alertów pozostaną bez zmian.\n\nTej operacji nie można cofnąć.'))return; try{ const removed=clearWatchHistoryForItem(item.id); events.length=0;updates.clear();summary.textContent='0 zmian'; timeline.replaceChildren(makeElement('div','vm-ui-help','Brak zapisanych zmian.')); clear.disabled=true;feedback.textContent=`Historia tej pozycji została wyczyszczona. Usunięte wpisy: ${removed}.`; close.focus({preventScroll:true}); }catch(error){feedback.textContent='Nie udało się wyczyścić historii.';log('Błąd czyszczenia historii pozycji',error);} }); const close=makeButton("vm-favorites-toolbar-button vm-ui-button",'Zamknij'); const closeController=createDialogCloseController(overlay); closeController.bindButton(close); actions.append(clear,close); box.append( summary, timeline, feedback, actions ); } function ensureHiddenItemsStyles() { if (document.getElementById('vm-hidden-items-style')) return; appendHelperStyle('vm-hidden-items-style',` .vm-hidden-result { display:none !important; } `); } function openWatchedSettingsModal(initialMode=false,forceTable=false) { if (deferUntilFullPersistentCacheReady(() => openWatchedSettingsModal(initialMode,forceTable))) return; const collectionMode=initialMode === true || initialMode === 'hidden'; if (!collectionMode && !forceTable && getSettings().favoritesDefaultView === 'tiles') { const options=getFavoriteTilesOptions();openFavoriteTiles(options.fields,options.extra);return; } favoriteTilesOpenGeneration++;closeHelperDialog(document.getElementById('vm-favorite-tiles-test')); const titleText=collectionMode ? TRACKED_ITEMS_LABEL:'Ulubione'; const tableStateKey=FAVORITE_TABLE_STATE_STORAGE_KEY + (collectionMode ? ':notes':''); const actionFilters=[['all','Wszystkie'],['favorite','Ulubione'],['not-favorite','Nie ulubione'],['hidden','Ukryte'],['not-hidden','Nie ukryte']]; const isListItem=item => collectionMode ? item.hidden || watchedItemHasNotesOrCategories(item):item.favorite; closeHelperDialog(document.getElementById(WATCH_SETTINGS_OVERLAY_ID)); const acknowledgeNonAlertUntilOpen= !collectionMode && getWatchSettings() .nonAlertChangeNewness === 'until-open'; const nonAlertEventIdsNewAtOpen=new Set(); if (acknowledgeNonAlertUntilOpen) { for (const event of getWatchEvents()) { if (!event.acknowledged && !event.alertTriggered) { nonAlertEventIdsNewAtOpen.add(event.id); } } if ( nonAlertEventIdsNewAtOpen.size && acknowledgeNonAlertWatchEvents() ) { renderWatchNotificationBar(); } } const {overlay,box}=createWatchDialogBase(WATCH_SETTINGS_OVERLAY_ID,titleText); const closeController=createDialogCloseController(overlay,{ onClose:() => { stopCheckProgress(); clearTimeout(checkStatusTimer); watchPageButtonRefreshers.delete(scheduleTableRender);tableRows.clear(); for(const id of tableStorageListeners)try{GM_removeValueChangeListener(id);}catch {} if (acknowledgeNonAlertUntilOpen) { acknowledgeNonAlertWatchEvents(); renderWatchNotificationBar(); } } }); prepareFavoriteTableStyleApi(overlay,box); box.classList.add('vm-ui-table-dialog'); detachHelperNode(box.firstElementChild); const header=makeElement('div','vm-favorites-toolbar'); const title=makeElement('h2',"vm-favorites-title vm-ui-title",titleText); const visibleCount=makeElement('span','vm-favorites-count'); visibleCount.setAttribute('aria-live','polite'); title.appendChild(visibleCount); const manualInput=makeElement('input',"vm-favorites-manual-input vm-ui-field vm-ui-compact",null,{type:'text',placeholder:'Adres profilu Escorti, anonsu Escort.club albo numer telefonu'}); const addButton=makeButton("vm-favorites-toolbar-button vm-favorites-add-button vm-ui-button vm-ui-compact -accent",'Dodaj do ulubionych'); const headerActions=makeElement('div','vm-favorites-toolbar-actions'); const settingsGear=makeSettingsTabGearButton( 'watch', 'Ustawienia - Ulubione i zapisane anonse' ); settingsGear.classList.add('vm-favorites-settings-gear'); const close=makeButton("vm-favorites-close vm-ui-button vm-ui-icon vm-ui-compact",'×'); setControlTitle(close,'Zamknij'); closeController.bindButton(close); headerActions.append(settingsGear,close); header.append(title,manualInput,addButton,headerActions); const status=makeElement('div','vm-favorites-status'); status.setAttribute('role','status'); const body=makeElement('div','vm-favorites-table-scroll'); let favoriteTableSavedScrollTop=0; let favoriteTableSavedScrollLeft=0; let favoriteTableScrollRenderGeneration=0; let suppressFavoriteTableScrollCapture=false; body.addEventListener('scroll',() => { if (suppressFavoriteTableScrollCapture) return; favoriteTableSavedScrollTop=body.scrollTop; favoriteTableSavedScrollLeft=body.scrollLeft; }); const itemsHost=makeElement('div','vm-favorites-table'); const tableRows=createFavoriteListRenderer(itemsHost); let nextTableNodes=[]; body.append(itemsHost); box.append(header,status,body); const refresh=makeButton("vm-favorites-toolbar-button vm-favorites-refresh-all vm-ui-button vm-ui-compact -primary",'Sprawdź widoczne', {title:'Sprawdź tylko pozycje widoczne w tej tabeli po zastosowaniu filtrów.'}); let visibleItemIds=[]; const tileViewButton=makeFavoriteViewButton('tiles',()=>switchFavoriteView('tiles'));tileViewButton.classList.add('vm-favorites-toolbar-button'); const headerAddColumnButton=makeButton("vm-favorites-toolbar-button vm-favorites-add-column vm-ui-button vm-ui-compact -accent",'Dodaj kolumnę'); const headerAddColumnSelect=makeElement('select',"vm-favorites-add-column-select vm-ui-field vm-ui-compact"); headerAddColumnSelect.style.display='none'; const quickFilters=collectionMode ? [['hidden','Tylko ukryte'],['not-favorite','Tylko nieulubione']].map(([value,label]) => { const button=makeButton('vm-favorites-toolbar-button vm-favorites-quick-filter vm-ui-button vm-ui-compact',label); button.dataset.vmFavoritePreset=value; button.title='Filtruj tę listę; ponowne kliknięcie wyłącza ten filtr. Pozostałe filtry pozostają aktywne.'; button.addEventListener('click',() => rerenderFavoriteFilter('actions',favoriteTableState.filters.actions === value ? 'all':value)); headerActions.insertBefore(button,settingsGear); return button; }):[]; const restoreHiddenButton=collectionMode ? makeButton('vm-favorites-toolbar-button vm-favorites-restore-hidden vm-ui-button vm-ui-compact','Odkryj widoczne'):null; headerActions.insertBefore(headerAddColumnButton,settingsGear); headerActions.insertBefore(headerAddColumnSelect,settingsGear); headerActions.insertBefore(refresh,settingsGear); if (!collectionMode) headerActions.insertBefore(tileViewButton,settingsGear); if (restoreHiddenButton) headerActions.insertBefore(restoreHiddenButton,settingsGear); const defaultFavoriteTableState={ sortKey:'lp', sortDirection:'asc', filters:{ profile:'', favorite:'all', note:'', category:'all', watch:'all', state:'all', location:'', data:{}, changes:'all' } }; const savedFavoriteTableState=readFavoriteViewPreferences(tableStateKey); const favoriteTableState={ ...defaultFavoriteTableState, ...(savedFavoriteTableState && typeof savedFavoriteTableState === 'object' ? savedFavoriteTableState:{}), filters:{ ...defaultFavoriteTableState.filters, ...(savedFavoriteTableState?.filters && typeof savedFavoriteTableState.filters === 'object' ? savedFavoriteTableState.filters :{}), data:savedFavoriteTableState?.filters?.data && typeof savedFavoriteTableState.filters.data === 'object' ? {...savedFavoriteTableState.filters.data} :{} } }; if (typeof favoriteTableState.sortKey !== 'string') favoriteTableState.sortKey=defaultFavoriteTableState.sortKey; if (collectionMode) { if (['favorite','watch','changes'].includes(favoriteTableState.sortKey)) favoriteTableState.sortKey='lp'; for (const key of ['favorite','watch','changes']) favoriteTableState.filters[key]='all'; if (!actionFilters.some(([key]) => key === favoriteTableState.filters.actions)) favoriteTableState.filters.actions='all'; if (initialMode === 'hidden') favoriteTableState.filters.actions='hidden'; } if (!['asc','desc'].includes(favoriteTableState.sortDirection)) favoriteTableState.sortDirection='asc'; delete favoriteTableState.filters.duplicates; const persistFavoriteTableState=() => saveFavoriteViewPreferences(tableStateKey,{ sortKey:favoriteTableState.sortKey, sortDirection:favoriteTableState.sortDirection, filters:{ ...favoriteTableState.filters, data:{...favoriteTableState.filters.data} } }); const normalizeFavoriteFilterText=value => normalizeText(String(value || '')); let viewData; const favoriteProfileFilterText=item => normalizeFavoriteFilterText([ item.displayName, watchItemSourceText(item), ...item.profileUrls, ...viewData(item).all ].filter(Boolean).join(' ')); const favoriteNoteFilterText=item => normalizeFavoriteFilterText(item.note); const favoriteWatchSortValue=item => { const online=item.modes?.online ? 1:0; const locationMode=item.modes?.location ? 1:0; const changes=item.modes?.changes ? 1:0; const newAds=item.modes?.newAds ? 1:0; return ( (online + locationMode + changes + newAds) * 10 + online * 8 + locationMode * 4 + changes * 2 + newAds ); }; const getSortValue=(item,sortKey) => { if (sortKey === 'lp') return Number(item.createdAt) || 0; if (sortKey === 'favorite') return hasFavoriteStar(item) ? 1:0; if (sortKey === 'note') return favoriteNoteFilterText(item); if (sortKey === 'watch') return favoriteWatchSortValue(item); if (sortKey === 'state') return viewData(item).stateRank; if (sortKey === 'category') return normalizeFavoriteFilterText((item.categories || []).join(' ')); if (sortKey === 'location') return normalizeFavoriteFilterText(watchCitiesText(viewData(item).cities)); if (sortKey === 'changes') return viewData(item).latestChange; if (sortKey.startsWith('data:')) return viewData(item).sortValue(sortKey.slice(5)); return normalizeFavoriteFilterText(item.displayName); }; const filterAndSortFavoriteItems=(source,dataColumns) => { const filters=favoriteTableState.filters; const profileQuery=normalizeFavoriteFilterText(filters.profile); const noteQuery=normalizeFavoriteFilterText(filters.note); const locationQuery=normalizeFavoriteFilterText(filters.location); const filtered=source.filter(item => { if (collectionMode && ((filters.actions === 'favorite' && !item.favorite) || (filters.actions === 'not-favorite' && item.favorite) || (filters.actions === 'hidden' && !item.hidden) || (filters.actions === 'not-hidden' && item.hidden))) return false; const itemEvents=viewData(item).events; if (profileQuery && !favoriteProfileFilterText(item).includes(profileQuery)) return false; if (noteQuery && !favoriteNoteFilterText(item).includes(noteQuery)) return false; const itemCategories=normalizeFavoriteAssignedCategories(item.categories); if (filters.category === '__none__' && itemCategories.length) return false; if (filters.category !== 'all' && filters.category !== '__none__' && !itemCategories.includes(filters.category)) return false; if (filters.watch === 'online' && !item.modes?.online) return false; if (filters.watch === 'location' && !item.modes?.location) return false; if (filters.watch === 'changes' && !item.modes?.changes) return false; if (filters.watch === 'newAds' && !item.modes?.newAds) return false; if ( filters.watch === 'none' && ( item.modes?.online || item.modes?.location || item.modes?.changes || item.modes?.newAds ) ) return false; if (filters.state === 'online' && item.state?.online !== true) return false; if (filters.state === 'online-open' && (item.state?.online !== true || viewData(item).availability !== true)) return false; if (filters.state === 'offline' && item.state?.online !== false) return false; if (filters.state === 'unknown' && (item.state?.online === true || item.state?.online === false)) return false; if (filters.state === 'open' && viewData(item).availability !== true) return false; if (filters.state === 'closed' && viewData(item).availability !== false) return false; if (locationQuery && !normalizeFavoriteFilterText(watchCitiesText(viewData(item).cities)).includes(locationQuery)) return false; for (const field of dataColumns) { const query=normalizeFavoriteFilterText(filters.data[field]); if (query && !normalizeFavoriteFilterText(viewData(item).column(field)).includes(query)) return false; } if (filters.changes === 'new' && !itemEvents.some(event => !event.acknowledged)) return false; if (filters.changes === 'any' && !itemEvents.length) return false; if (filters.changes === 'alert' && !itemEvents.some(event => !event.acknowledged && event.alertTriggered)) return false; if (filters.changes === 'none' && itemEvents.length) return false; return true; }); const direction=favoriteTableState.sortDirection === 'desc' ? -1:1; return filtered.sort((first,second) => { const primary=compareFavoriteValues( getSortValue(first,favoriteTableState.sortKey), getSortValue(second,favoriteTableState.sortKey),direction ); if (primary) return primary; return String(first.displayName || '').localeCompare(String(second.displayName || ''),'pl-PL',{ numeric:true, sensitivity:'base' }); }); }; const rerenderFavoriteFilter=(key,value,sourceControl=null,dataField=null) => { if (key === 'data' && dataField) favoriteTableState.filters.data[dataField]=value; else favoriteTableState.filters[key]=value; persistFavoriteTableState(); const selectionStart=sourceControl?.selectionStart; const selectionEnd=sourceControl?.selectionEnd; render(); if (!sourceControl || sourceControl.tagName !== 'INPUT') return; requestAnimationFrame(() => { const selector=dataField ? `[data-vm-favorite-filter="data:${dataField}"]` :`[data-vm-favorite-filter="${key}"]`; const next=itemsHost.querySelector(selector); if (!(next instanceof HTMLInputElement)) return; next.focus(); if (Number.isInteger(selectionStart) && Number.isInteger(selectionEnd)) { try {next.setSelectionRange(selectionStart,selectionEnd);} catch {} } }); }; const saveDataColumns=columns => { const normalized= normalizeFavoritesDataColumns(columns); updateSettings({ favoritesDataColumns:normalized }); return normalized; }; const addFavoriteColumnSeparators=row => { const cells=[...row.children]; cells.forEach((cell,index) => { cell.classList.add('vm-favorites-cell'); cell.style.boxSizing='border-box'; if (!index) return; if (index !== 1) { const isSectionEdge=index >= cells.length - 2; cell.style.alignSelf='stretch'; cell.style.borderLeft=isSectionEdge ? '1px solid var(--vm-fav-section-separator, rgba(70,70,70,.22))' :'1px solid var(--vm-fav-column-separator, rgba(70,70,70,.105))'; cell.style.paddingLeft='var(--vm-fav-cell-padding-left, 4px)'; } if (!cell.style.display && !cell.classList.contains('vm-favorites-header-cell')) { cell.style.display='flex'; cell.style.flexDirection='column'; cell.style.justifyContent='center'; } }); }; const favoriteRowsToMeasure=[]; const appendFavoriteTableRow=(row,dataColumns,categoryCell=null) => { const keys=['lp','profile','note','category','alert','state','location', ...dataColumns.map(field => `data:${field}`),'changes','actions']; [...row.children].forEach((cell,index) => { cell.dataset.vmFavoriteColumn=keys[index]; if ((collectionMode && ['alert','changes'].includes(keys[index])) || cell.style.display === 'none') detachHelperNode(cell); }); addFavoriteColumnSeparators(row); nextTableNodes.push(row); if (!categoryCell) return; for (const cell of row.children) { if (cell === categoryCell) continue; Object.assign(cell.style,{minHeight:'0',maxHeight:'100%',overflow:'hidden'}); } favoriteRowsToMeasure.push([row,categoryCell]); }; const pendingChecks=new Set(); const stopCheckProgress=attachWatchCheckProgress(status,() => pendingChecks.size > 0); let checkStatusTimer=null; const runListCheck=async ids => { if (pendingChecks.size || !ids.length) return; clearTimeout(checkStatusTimer); ids.forEach(id => pendingChecks.add(id)); let progressText='Sprawdzanie…'; let completedWithoutIssues=false; status.style.color=''; render(); showStatusText(status,progressText); try { const result=await runWatchedChecks({force:true,itemIds:ids, onProgress:progress => { progressText=progress.message || `Sprawdzanie ${progress.itemIndex || progress.checked || 0}/${progress.total || 0}: ${progress.itemName || ''}`; showStatusText(status,progressText); } }); completedWithoutIssues=result.status === 'completed' && result.checked > 0 && !result.failed && !result.skipped; progressText=result.message || [ result.status === 'interrupted' ? 'Sprawdzanie przerwane.' :result.checked ? 'Sprawdzanie zakończone.':'Nie sprawdzono żadnej pozycji.', `Sprawdzono: ${result.checked}/${result.total}.`, result.failed ? `Błędy: ${result.failed}. Najedź na przycisk „Sprawdź”, aby odczytać szczegóły.`:'', result.skipped ? `Pominięto: ${result.skipped} - wpisy zmienione, usunięte lub niewymagające sprawdzania.`:'' ].filter(Boolean).join(' '); if (result.failed || result.status === 'interrupted') status.style.color='var(--vm-ui-danger,#c62828)'; } catch (error) { progressText=error?.message || 'Nie udało się sprawdzić pozycji.'; status.style.color='var(--vm-ui-danger,#c62828)'; } finally { pendingChecks.clear(); showStatusText(status,progressText); render(); if (completedWithoutIssues) checkStatusTimer=setTimeout(() => {status.style.display='none';},4000); } }; const makeItemActions=item => { const actions=makeElement('div','vm-favorites-actions-cell'); const check=makeButton('vm-favorites-icon-button vm-favorites-check-one vm-ui-button vm-ui-icon vm-ui-compact',pendingChecks.has(item.id) ? '…':'↻',{title:item.lastError ? `Sprawdź ponownie: ${item.lastError}`:'Sprawdź'}); check.setAttribute('aria-label',`${check.title}: ${item.displayName}`); check.disabled=pendingChecks.size > 0; check.addEventListener('click',() => runListCheck([item.id])); actions.appendChild(check); Object.assign(actions.style,{display:'grid',gridTemplateColumns:'repeat(2,24px)',alignContent:'center'}); for (const kind of ['hidden','favorite']) { if (kind === 'favorite' && item.sourceType === 'escort-search') continue; const button=makeButton('vm-favorites-icon-button vm-ui-button vm-ui-icon vm-ui-compact',kind === 'favorite' ? (hasFavoriteStar(item) ? '★':'☆'):''); if (kind === 'hidden') button.appendChild(createHiddenIcon(!!item.hidden)); setControlTitle(button,kind === 'favorite' ? (hasFavoriteStar(item) ? trackedRemovalHint('favorite'):'Dodaj do Ulubionych'):item.hidden ? trackedRemovalHint('hidden'):'Ukryj w wynikach wyszukiwania. Ulubione, notatka, kategorie i alerty pozostaną.'); button.setAttribute('aria-pressed',String(kind === 'favorite' ? hasFavoriteStar(item):!!item.hidden)); if (kind === 'favorite' ? hasFavoriteStar(item):item.hidden) Object.assign(button.style,{background:kind === 'favorite' ? '#f4c542':'#ff9b9b',color:'#000'}); button.addEventListener('click',() => { changeTrackedItemStatus(item.id,kind); render(); }); actions.appendChild(button); } const remove=makeButton('vm-favorites-icon-button vm-favorites-remove-one vm-ui-button vm-ui-icon vm-ui-compact'); setTrashIcon(remove); remove.title=trackedRemovalHint(collectionMode ? 'notes':'profile'); remove.disabled=collectionMode && !watchedItemHasNotesOrCategories(item); remove.setAttribute('aria-label',`${remove.title}: ${item.displayName}`); remove.addEventListener('click',async () => { if (!collectionMode) changeTrackedItemStatus(item.id,'remove'); else { if (!window.confirm(`Usunąć notatkę i wszystkie przypisane kategorie dla „${item.displayName}”?`)) return; await changeTrackedEntry(item.id,'clearNotes'); } render(); }); actions.insertBefore(remove,check.nextSibling); if (item.sourceType === 'escort-search') actions.appendChild(makeFavoriteSearchEditButton(item,true)); return actions; }; headerAddColumnButton.addEventListener('click',() => { if (headerAddColumnButton.disabled) return; headerAddColumnSelect.style.display=headerAddColumnSelect.style.display === 'none' ? 'inline-block':'none'; if (headerAddColumnSelect.style.display !== 'none') { headerAddColumnSelect.focus(); try {headerAddColumnSelect.showPicker?.();} catch {} } }); headerAddColumnSelect.addEventListener('change',() => { const field=normalizeEscortDataFieldId(headerAddColumnSelect.value); if (!field || field === 'none') return; const currentColumns=getFavoriteSettings().dataColumns; saveDataColumns([...currentColumns,field]); headerAddColumnSelect.value=''; headerAddColumnSelect.style.display='none'; render(); }); restoreHiddenButton?.addEventListener('click',() => { const ids=getWatchedItems().filter(item => item.hidden && visibleItemIds.includes(item.id)).map(item => item.id); if (!ids.length || !window.confirm(`Odkryć widoczne pozycje (${ids.length})? Notatki, kategorie i Ulubione pozostaną bez zmian.`)) return; restoreHiddenItems(ids); render(); }); const render=() => { if(!overlay.isConnected)return; favoriteRowsToMeasure.length=0; const renderGeneration= ++favoriteTableScrollRenderGeneration; if (!suppressFavoriteTableScrollCapture) { favoriteTableSavedScrollTop=body.scrollTop; favoriteTableSavedScrollLeft=body.scrollLeft; } suppressFavoriteTableScrollCapture=true; for (const control of [manualInput,addButton]) control.style.display=collectionMode ? 'none':''; for (const button of quickFilters) { const active=favoriteTableState.filters.actions === button.dataset.vmFavoritePreset; button.setAttribute('aria-pressed',String(active));button.classList.toggle('-accent',active); } const allItems=getWatchedItems().filter(isListItem); const events=(collectionMode ? []:getWatchEvents()).map(event => nonAlertEventIdsNewAtOpen.has(event.id) ? {...event,acknowledged:false} :event ); viewData=createFavoriteViewData(events); const settings=getFavoriteSettings(); const notesEnabled=getWatchSettings().enabled; const dataColumns=settings.dataColumns; if (!notesEnabled) { favoriteTableState.filters.note=''; if (favoriteTableState.sortKey === 'note') { favoriteTableState.sortKey='lp'; favoriteTableState.sortDirection='asc'; } } if (favoriteTableState.sortKey.startsWith('data:') && !dataColumns.includes(favoriteTableState.sortKey.slice(5))) { favoriteTableState.sortKey='lp'; favoriteTableState.sortDirection='asc'; } for (const key of Object.keys(favoriteTableState.filters.data)) { if (!dataColumns.includes(key)) delete favoriteTableState.filters.data[key]; } const items=filterAndSortFavoriteItems(allItems,dataColumns); visibleItemIds=items.map(item => item.id); const countText=` (${items.length}${items.length !== allItems.length ? '/' + allItems.length:''})`; if (visibleCount.textContent !== countText) visibleCount.textContent=countText; const countTitle=`Wyświetlane pozycje: ${items.length} z ${allItems.length}. Licznik uwzględnia filtry.`; if (visibleCount.title !== countTitle) setControlTitle(visibleCount,countTitle); refresh.disabled=pendingChecks.size > 0 || !items.length; nextTableNodes=[]; itemsHost.dataset.vmStyleApi='1'; const hasExtraDataColumns=dataColumns.length > 0; itemsHost.style.minWidth=`var(--vm-fav-table-min-width, ${(hasExtraDataColumns ? 1036:1078) + dataColumns.length * 92}px)`; const availableColumns=getEscortDataFieldOptions('none',true) .filter(([field]) => field !== 'none' && !dataColumns.includes(field)); headerAddColumnSelect.replaceChildren(); appendSelectOptions(headerAddColumnSelect,[['','wybierz…'],...availableColumns],''); headerAddColumnButton.disabled=!availableColumns.length || dataColumns.length >= FAVORITES_MAX_DATA_COLUMNS; headerAddColumnButton.style.opacity=headerAddColumnButton.disabled ? '.45':'1'; headerAddColumnButton.style.cursor=headerAddColumnButton.disabled ? 'default':'pointer'; if (headerAddColumnButton.disabled) headerAddColumnSelect.style.display='none'; if (restoreHiddenButton) { restoreHiddenButton.style.display=favoriteTableState.filters.actions === 'hidden' ? '':'none'; restoreHiddenButton.disabled=!items.some(item => item.hidden); } if (!allItems.length) { const empty=makeElement('div',"vm-ui-panel vm-ui-empty",collectionMode ? 'Brak pozycji z notatką, kategorią lub oznaczeniem ukrycia.' :'Nie dodano jeszcze żadnego profilu ani anonsu do ulubionych.'); tableRows.render([empty]);suppressFavoriteTableScrollCapture=false; return; } const dynamicColumns=dataColumns .map(() => 'var(--vm-fav-col-data, minmax(72px,92px))') .join(' '); const listGridColumns=[ 'var(--vm-fav-col-image, 48px)', 'var(--vm-fav-col-profile, minmax(100px,0.75fr))', notesEnabled ? ( collectionMode ? 'var(--vm-fav-col-notes-note, minmax(260px,2fr))':hasExtraDataColumns ? 'var(--vm-fav-col-note, 108px)' :'var(--vm-fav-col-note, 125px)' ) :'', collectionMode ? 'var(--vm-fav-col-notes-category, 155px)':'var(--vm-fav-col-category, 135px)', collectionMode ? '':'var(--vm-fav-col-alert, 84px)', 'var(--vm-fav-col-state, 54px)', 'var(--vm-fav-col-location, 81px)', dynamicColumns, collectionMode ? '':hasExtraDataColumns ? 'var(--vm-fav-col-changes, minmax(225px,1.3fr))' :'var(--vm-fav-col-changes, minmax(250px,1.5fr))', collectionMode ? 'var(--vm-fav-col-notes-actions, 96px)':'var(--vm-fav-col-actions, 58px)' ].filter(Boolean).join(' '); const listHeader=makeElement('div','vm-favorites-table-header'); listHeader.style.setProperty('--vm-fav-current-grid',listGridColumns); const makeSortableHeaderButton=(label,sortKey) => { const button=makeButton('vm-favorites-sort-button',label); button.dataset.vmFavoriteSort=sortKey; const active=favoriteTableState.sortKey === sortKey; if (active) button.textContent += favoriteTableState.sortDirection === 'asc' ? ' ↑':' ↓'; button.title=active ? `${label} - odwróć sortowanie` :`${label} - sortuj według tej kolumny`; button.classList.toggle('-active',active); button.addEventListener('click',() => { if (favoriteTableState.sortKey === sortKey) { favoriteTableState.sortDirection=favoriteTableState.sortDirection === 'asc' ? 'desc':'asc'; } else { favoriteTableState.sortKey=sortKey; favoriteTableState.sortDirection=sortKey === 'changes' ? 'desc':'asc'; } persistFavoriteTableState(); render(); }); return button; }; const styleModernFilter=(control,active) => { control.classList.add('vm-favorites-filter'); control.classList.toggle('vm-favorites-filter-active',active); return control; }; const makeTextFilter=(key,placeholder,dataField=null) => { const input=makeElement('input','',null,{type:'search',value:dataField ? (favoriteTableState.filters.data[dataField] || ''):(favoriteTableState.filters[key] || ''),placeholder}); input.dataset.vmFavoriteFilter=dataField ? `data:${dataField}`:key; styleModernFilter(input,!!input.value.trim()); input.addEventListener('input',() => rerenderFavoriteFilter(key,input.value,input,dataField)); return input; }; const makeSelectFilter=(key,options) => { const select=makeElement('select'); appendSelectOptions(select,options,favoriteTableState.filters[key]); select.dataset.vmFavoriteFilter=key; styleModernFilter(select,select.value !== 'all'); select.addEventListener('change',() => rerenderFavoriteFilter(key,select.value)); return select; }; const makeHeaderCell=(top,filter=null) => { const cell=makeElement('div','vm-favorites-header-cell'); if (filter) { cell.style.setProperty('--vm-fav-header-rows','var(--vm-fav-header-title-height, 24px) var(--vm-fav-filter-height, 20px)'); cell.style.setProperty('--vm-fav-header-gap','2px'); } if (top) cell.appendChild(top); if (filter) cell.appendChild(filter); return cell; }; const ordinalSort=makeSortableHeaderButton('#','lp'); ordinalSort.title=favoriteTableState.sortKey === 'lp' ? 'L.p. = liczba porządkowa. Sortowanie zgodnie z datą dodania do ulubionych. Kliknij, aby odwrócić kolejność.' :'L.p. = liczba porządkowa. Sortuj zgodnie z datą dodania do ulubionych.'; if (collectionMode) ordinalSort.title='Sortuj zgodnie z datą zapisania pozycji'; Object.assign(ordinalSort.style,{ width:'100%', textAlign:'center', whiteSpace:'nowrap', fontSize:'11px' }); listHeader.appendChild(makeHeaderCell(ordinalSort)); listHeader.appendChild(makeHeaderCell( makeSortableHeaderButton('Profil','profile'), makeTextFilter('profile','Szukaj…') )); const noteHeader=makeHeaderCell( makeSortableHeaderButton('Notatka','note'), makeTextFilter('note','Szukaj…') ); if (!notesEnabled) noteHeader.style.display='none'; listHeader.appendChild(noteHeader); const categoryTop=makeElement('div','vm-favorites-header-group'); const categorySort=makeSortableHeaderButton('Kategoria','category'); categorySort.style.flex='0 1 auto'; const editCategories=makeButton("vm-favorites-header-action vm-favorites-edit-categories vm-ui-button vm-ui-icon vm-ui-compact -accent vm-ui-table-icon",'✎',{title:'Edytuj kategorie'}); editCategories.addEventListener('mouseenter',() => {editCategories.style.background='var(--vm-fav-header-action-hover-bg, #fff0f6)';}); editCategories.addEventListener('mouseleave',() => {editCategories.style.background='transparent';}); editCategories.addEventListener('click',event => { event.stopPropagation(); openFavoriteCategoriesEditor(() => { render(); }); }); categoryTop.append(categorySort,editCategories); listHeader.appendChild(makeHeaderCell(categoryTop,makeSelectFilter('category',[ ['all','Wszystkie'], ['__none__','Bez kategorii'], ...settings.categories.map(value => [value,value]) ]))); listHeader.appendChild(makeHeaderCell( makeSortableHeaderButton('Alert','watch'), makeSelectFilter('watch',[ ['all','Wszystkie'], ['online','Online'], ['location','Lokalizacja'], ['changes','Zmiany treści'], ['newAds','Nowy anons'], ['none','Brak'] ]) )); listHeader.appendChild(makeHeaderCell( makeSortableHeaderButton('Stan','state'), makeSelectFilter('state',[ ['all','Wszystkie'], ['online','✓ online'], ['online-open','online i otwarte'], ['offline','× offline'], ['unknown','? brak danych'], ['open','otwarte'], ['closed','zamknięte'] ]) )); listHeader.appendChild(makeHeaderCell( makeSortableHeaderButton('Lokalizacja','location'), makeTextFilter('location','Miasto…') )); for (const field of dataColumns) { const top=makeElement('div','vm-favorites-header-group'); Object.assign(top.style,{display:'flex',alignItems:'flex-start',gap:'2px',minWidth:'0'}); const sortButton=makeSortableHeaderButton(getFavoriteColumnLabel(field),`data:${field}`); sortButton.style.flex='1 1 auto'; const removeColumn=makeButton("vm-favorites-header-action vm-favorites-remove-column vm-ui-button vm-ui-icon vm-ui-compact vm-ui-table-icon",'×',{title:'Usuń tę kolumnę'}); removeColumn.addEventListener('mouseenter',() => { removeColumn.style.background='var(--vm-ui-bg-hover, #f1f2f4)'; removeColumn.style.color='var(--vm-ui-text-muted, #666)'; }); removeColumn.addEventListener('mouseleave',() => { removeColumn.style.background='transparent'; removeColumn.style.color='var(--vm-ui-text-faint, #aaa)'; }); removeColumn.addEventListener('click',event => { event.stopPropagation(); saveDataColumns(dataColumns.filter(value => value !== field)); delete favoriteTableState.filters.data[field]; persistFavoriteTableState(); render(); }); top.append(sortButton,removeColumn); listHeader.appendChild(makeHeaderCell(top,makeTextFilter('data','Filtr…',field))); } const changesTop=makeElement('div','vm-favorites-header-group vm-favorites-changes-header'); Object.assign(changesTop.style,{ display:'flex', alignItems:'center', gap:'3px', minWidth:'0', overflow:'hidden' }); const changesSort=makeSortableHeaderButton( 'Zmiany', 'changes' ); changesSort.style.flex='0 1 auto'; const markAllNonAlertRead=makeButton("vm-favorites-header-action vm-favorites-mark-non-alert-read vm-ui-button vm-ui-compact -accent",'Odczytaj zmiany',{title:'Oznacz wszystkie nie-alertowe zmiany jako nie-nowe',disabled:!events.some( event => !event.acknowledged && !event.alertTriggered )}); Object.assign(markAllNonAlertRead.style,{ minHeight:'18px', padding:'0 4px', fontSize:'7.5px', lineHeight:'1', whiteSpace:'nowrap' }); markAllNonAlertRead.addEventListener( 'click', event => { event.stopPropagation(); if (markAllNonAlertRead.disabled) { return; } nonAlertEventIdsNewAtOpen.clear(); acknowledgeNonAlertWatchEvents(); renderWatchNotificationBar(); render(); } ); const acceptAllAlertsAndChanges=makeButton("vm-favorites-header-action vm-favorites-accept-all vm-ui-button vm-ui-compact -danger",'Zaakceptuj wszystkie alerty',{title:'Zaakceptuj wszystkie alerty',disabled:!events.some(event => !event.acknowledged && event.alertTriggered )}); Object.assign(acceptAllAlertsAndChanges.style,{ minHeight:'18px', padding:'0 4px', fontSize:'7.5px', lineHeight:'1', whiteSpace:'nowrap' }); acceptAllAlertsAndChanges.addEventListener( 'click', event => { event.stopPropagation(); if (!acceptAllAlertsAndChanges.disabled) { acknowledgeAllAlertWatchEvents(); } } ); changesTop.append( changesSort, markAllNonAlertRead, acceptAllAlertsAndChanges ); listHeader.appendChild(makeHeaderCell(changesTop,makeSelectFilter('changes',[ ['all','Wszystkie'], ['new','Tylko nowe'], ['any','Z historią'], ['alert','Z alertem'], ['none','Bez zmian'] ]))); const actionsTitle=makeElement('span','vm-favorites-actions-title','Akcje'); Object.assign(actionsTitle.style,{alignSelf:'center',color:'var(--vm-fav-header-text, #666)',fontSize:'var(--vm-fav-header-font-size, 9px)',fontWeight:'700'}); const actionsFilter=collectionMode ? makeSelectFilter('actions',actionFilters):null; if (actionsFilter) actionsFilter.setAttribute('aria-label','Filtr Ulubionych i ukrytych'); listHeader.appendChild(makeHeaderCell(actionsTitle,actionsFilter)); appendFavoriteTableRow(listHeader,dataColumns); if (!items.length) { const emptyFiltered=makeElement('div',"vm-ui-panel vm-ui-empty",'Brak pozycji spełniających wybrane filtry.'); nextTableNodes.push(emptyFiltered);tableRows.render(nextTableNodes);suppressFavoriteTableScrollCapture=false; return; } let availableStripeIndex=0; items.forEach((item,itemIndex) => { const availabilityNow=viewData(item).availability; const rowUnavailable=item.state?.online === false || availabilityNow === false; const stripeIndex=rowUnavailable ? 0:availableStripeIndex++; const itemEvents=viewData(item).events; const signature=JSON.stringify([item,itemEvents,itemIndex,stripeIndex,items.length,listGridColumns,settings,notesEnabled,pendingChecks.size>0,availabilityNow,viewData(item).allDetails,dataColumns.map(field=>viewData(item).column(field)),Math.floor(Date.now()/60000)]); const previous=tableRows.get(item.id,signature); if(previous){nextTableNodes.push(previous);favoriteRowsToMeasure.push([previous,previous.querySelector('.vm-favorites-category-cell')]);return;} const hasUnack=itemEvents.some(event => !event.acknowledged); const pendingAlertModes=new Set( itemEvents .filter(event => !event.acknowledged && event.alertTriggered ) .map(event => event.alertMode || watchEventTypeToAlertMode(event.type) ) .filter(Boolean) ); const card=makeElement('div','vm-favorites-row'); card.dataset.vmFavoriteItemId=item.id; card.dataset.vmFavoriteHasNewChanges=hasUnack ? '1':'0'; const cardBaseBackground=stripeIndex % 2 ? 'var(--vm-fav-row-alt-bg, #fbfbfc)' :'var(--vm-fav-row-bg, #fff)'; card.style.setProperty('--vm-fav-current-grid',listGridColumns); card.style.setProperty( '--vm-fav-row-current-bg', pendingAlertModes.size ? `color-mix(in srgb, var(--vm-ui-danger, #c62828) 6%, ${cardBaseBackground})` :cardBaseBackground ); card.style.setProperty( '--vm-fav-row-current-shadow', pendingAlertModes.size ? 'inset 3px 0 0 var(--vm-ui-danger, #c62828)':'none' ); card.classList.toggle('-last',itemIndex === items.length - 1); const imageBox=makeElement('div','vm-favorites-image-cell','-'); const imageUrls=[...new Set([item.imageUrl,...viewData(item).allDetails.map(entry => entry.adData.imageUrl)] .map(normalizeWatchThumbnailUrl).filter(url => url && !isWatchThumbnailKnownUnavailable(url)))]; if (imageUrls.length) { const img=makeElement('img','',null,{src:imageUrls.shift(),alt:'',loading:'lazy',referrerPolicy:'no-referrer'}); img.addEventListener('error',() => { markWatchThumbnailUnavailable(img.src); hideWatchImagePreview(); if (imageUrls.length) {img.src=imageUrls.shift(); return;} detachHelperNode(img); imageBox.title='Brak dostępnego zapisanego zdjęcia.'; }); imageBox.appendChild(img); imageBox.title='Najedź, aby powiększyć zdjęcie'; imageBox.addEventListener('mouseenter',() => { if (img.parentElement === imageBox) showWatchImagePreview(imageBox,img.src,overlay); }); imageBox.addEventListener('mouseleave',hideWatchImagePreview); } const details=makeElement('div','vm-favorites-profile-cell'); const nameRow=makeElement('div','vm-favorites-profile-name-row'); const name=makeElement('a','vm-favorites-profile-name',item.displayName,{href:(item.sourceType === 'escort-search' ? watchItemSourceUrl(item):item.state?.activeUrls?.[0] || item.profileUrls?.[0] || item.adUrl) || '#',target:'_blank',rel:'noopener noreferrer'}); const rename=makeButton("vm-favorites-icon-button vm-favorites-rename vm-ui-button vm-ui-icon vm-ui-compact vm-ui-table-icon",'✎',{title:'Zmień nazwę'}); Object.assign(rename.style,{opacity:'0',transition:'opacity .12s ease'}); rename.addEventListener('click',async()=>{await changeTrackedEntry(item.id,'rename');render();}); nameRow.append(name,rename); const sourceUrl=watchItemSourceUrl(item); const source=sourceUrl ? makeElement('a','vm-favorites-profile-source',watchItemSourceText(item)) :makeElement('div','vm-favorites-profile-source',watchItemSourceText(item)); if (sourceUrl) { source.href=sourceUrl; source.target='_blank'; source.rel='noopener noreferrer'; source.title='Otwórz źródło danych profilu'; } Object.assign(source.style,{ display:'block', maxWidth:'100%', padding:'2px 0', color:sourceUrl ? 'var(--vm-fav-source-text, var(--vm-ui-text-muted, #6f6f74))':'var(--vm-fav-source-empty-text, var(--vm-ui-text-faint, #9a9a9a))', fontSize:'9px', fontWeight:sourceUrl ? '600':'400', lineHeight:'1.15', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', textDecoration:sourceUrl ? 'underline':'none', textUnderlineOffset:'2px' }); details.append(nameRow,source); const noteCell=makeElement('div','vm-favorites-note-cell'); if (!notesEnabled) noteCell.style.display='none'; const note=makeButton('vm-favorites-note',item.note || 'dodaj notatkę'); note.dataset.vmFavoriteHasNote=item.note ? '1':'0'; note.title=item.note ? `${item.note} Kliknij, aby zmienić notatkę`:'Dodaj notatkę'; Object.assign(note.style,{ display:'-webkit-box', width:'100%', maxWidth:'100%', height:'100%', minHeight:'0', maxHeight:'100%', boxSizing:'border-box', margin:'0', padding:item.note ? '2px 4px':'0', border:'0', borderRadius:item.note ? '4px':'0', background:item.note ? 'var(--vm-fav-note-bg, rgba(244,67,143,.026))':'transparent', color:item.note ? 'var(--vm-fav-note-text, #666)':'var(--vm-fav-note-empty-text, #aaa)', cursor:'pointer', fontSize:'9px', lineHeight:'1.14', fontStyle:item.note ? 'normal':'italic', textDecoration:'none', textAlign:'left', whiteSpace:'normal', overflow:'hidden', overflowWrap:'anywhere', WebkitBoxOrient:'vertical', WebkitLineClamp:'4' }); note.addEventListener('click',async()=>{await changeTrackedEntry(item.id,'note');render();}); noteCell.appendChild(note); card.addEventListener('mouseenter',() => {rename.style.opacity='1';}); card.addEventListener('mouseleave',() => {rename.style.opacity='0';}); const categoryCell=makeElement('div','vm-favorites-category-cell'); const chips=makeElement('div','vm-favorites-category-chips'); const assignedCategories=fillFavoriteCategoryControls(chips,item,settings,async change => { await changeTrackedEntry(item.id,'categories',change);render(); },{pointerDown:true}); categoryCell.dataset.vmFavoriteCategory=assignedCategories.join('|'); categoryCell.appendChild(chips); categoryCell.title=assignedCategories.length ? assignedCategories.join(', ') :'Dodaj kategorię'; const modes=makeElement('div','vm-favorites-alert-cell'); Object.assign(modes.style,{ display:'flex', flexDirection:'column', alignItems:'flex-start', gap:'2px', justifyContent:'center', minHeight:'58px', padding:'2px 0', boxSizing:'border-box', fontSize:'8px', lineHeight:'11px' }); const makeModeCheck=( mode, label, { disabled=false, disabledTitle='', tooltip='' }={} ) => { const row=makeElement( 'label', 'vm-favorites-alert-option' ); row.dataset.vmFavoriteAlert=mode; row.title=tooltip || label; Object.assign(row.style,{ display:'flex', alignItems:'center', gap:'1px', minWidth:'0', height:'11px', margin:'0', padding:'0', whiteSpace:'nowrap', overflow:'visible' }); const input=makeElement('input','vm-favorites-checkbox',null,{type:'checkbox',checked:!!item.modes[mode],disabled}); if (disabled) { row.style.opacity='.35'; row.style.cursor='default'; row.title= disabledTitle || 'Ten alert nie jest dostępny dla tej pozycji.'; } row.classList.toggle( 'vm-favorites-alert-option-active', input.checked ); Object.assign(input.style,{ flex:'0 0 11px', width:'11px', height:'11px', margin:'0', padding:'0', accentColor:'var(--vm-fav-checkbox-accent, var(--vm-fav-accent, var(--vm-ui-accent, #f54998)))' }); const labelText=makeElement('span','',label); const hasUnreadAlert= pendingAlertModes.has(mode); Object.assign(labelText.style,{ minWidth:'0', overflow:'visible', textOverflow:'clip', whiteSpace:'nowrap', lineHeight:'11px', color:hasUnreadAlert ? 'var(--vm-fav-accent, var(--vm-ui-accent, #f54998))' :'var(--vm-ui-text-muted, #666)', fontWeight: input.checked || hasUnreadAlert ? '800' :'400' }); if (hasUnreadAlert) { row.classList.add( 'vm-favorites-alert-option-unread' ); Object.assign(row.style,{ height:'13px', padding:'0 3px', border:'1px solid var(--vm-ui-danger, #c62828)', borderRadius:'4px', background: 'color-mix(in srgb, var(--vm-ui-danger, #c62828) 10%, transparent)', boxSizing:'border-box' }); labelText.style.color= 'var(--vm-ui-danger, #c62828)'; labelText.style.fontWeight='900'; input.style.outline= '1px solid var(--vm-ui-danger, #c62828)'; input.style.outlineOffset='0'; row.title=`${row.title} • Niezaakceptowany alert`; } row.append(input,labelText); input.addEventListener('change',async () => { if (disabled) { input.checked=false; return; } if (mode === 'location') { input.checked=!!item.modes.location; row.classList.toggle( 'vm-favorites-alert-option-active', input.checked ); labelText.style.fontWeight= input.checked ? '700':'400'; const scopeLabel= item.sourceType === 'escort-ad' ? 'Anons' :'Profil'; const options=await chooseWatchAlertOptions( item, scopeLabel, {locationOnly:true} ); if (!options) { render(); return; } updateTrackedAlertModes(item.id,{location:options.modes.location},options.locationTarget); } else { updateTrackedAlertModes(item.id,{[mode]:input.checked}); } render(); }); return row; }; modes.append( makeModeCheck( 'online', 'online', { tooltip: 'Śledzi stan online/offline. Powiadomienie pojawia się, gdy profil/anons wróci online po wcześniejszym stanie offline.' } ), makeModeCheck( 'location', item.locationTarget?.type === 'city' ? `lok.→${item.locationTarget.city}` :'lokalizacja', { tooltip: item.locationTarget?.type === 'city' ? `Pokazuje na górnym pasku alert, gdy profil/anons pojawi się w mieście ${item.locationTarget.city}. Historia lokalizacji jest zapisywana niezależnie od tego checkboxa. Kliknięcie checkboxa otwiera ustawienia lokalizacji.` :'Pokazuje na górnym pasku alert o każdej zmianie lokalizacji, w tym o zmianie adresu anonsu. Historia lokalizacji jest zapisywana niezależnie od tego checkboxa. Kliknięcie checkboxa otwiera ustawienia lokalizacji.' } ), makeModeCheck( 'changes', 'zmiany treści', { tooltip: 'Pokazuje na górnym pasku alert o zmianie nazwy anonsu, głównego zdjęcia, cennika, godzin dostępności, tagów/usług, opisu, numeru telefonu lub danych profilu. Historia zmian jest zapisywana niezależnie od tego checkboxa. Zmiana adresu jest traktowana jako lokalizacja, nie zmiana treści.' } ), makeModeCheck( 'newAds', 'nowy anons', { disabled:item.sourceType === 'escort-ad', disabledTitle: 'Alert „nowy anons” jest dostępny tylko dla profilu. Powiadamia, gdy w profilu pojawi się anons o ID, którego wcześniej w nim nie było.', tooltip: 'Powiadamia, gdy w obserwowanym profilu pojawi się anons o ID, którego wcześniej w tym profilu nie było.' } ) ); const stateCell=makeElement('div','vm-favorites-state-cell'); const stateKind=item.state?.online === true ? 'online':(item.state?.online === false ? 'offline':'unknown'); stateCell.dataset.vmFavoriteState=stateKind; Object.assign(stateCell.style,{textAlign:'center',lineHeight:'1.05',minWidth:'0'}); const stateIcon=makeElement('div',`vm-favorites-state-icon vm-favorites-state-${stateKind}`,item.state?.online === true ? '✓':(item.state?.online === false ? '×':'?')); Object.assign(stateIcon.style,{ color:item.state?.online === true ? 'var(--vm-ui-success, #16823a)':(item.state?.online === false ? 'var(--vm-ui-danger, #c62828)':'var(--vm-ui-text-muted, #777)'), fontSize:'13px', fontWeight:'900' }); const stateTop=makeElement('div','vm-favorites-state-top'); Object.assign(stateTop.style,{display:'flex',alignItems:'center',justifyContent:'center',gap:'4px',minHeight:'16px'}); stateTop.appendChild(stateIcon); const totalAds=item.sourceType==='escort-search'?getWatchedItemHistoricalAdUrls(item,itemEvents).length:item.state?.totalAds || 0; const activeLine=makeElement('div','vm-favorites-state-detail',`aktywne ${item.state?.activeCount || 0}/${totalAds}`); if(item.sourceType==='escort-search')activeLine.title=`Łącznie ${totalAds} unikalnych anonsów znalezionych dotąd w tym wyszukiwaniu; aktywne według ostatniego sprawdzenia.`; Object.assign(activeLine.style,{marginTop:'1px',color:'var(--vm-ui-text-muted, #777)',fontSize:'8px',whiteSpace:'nowrap'}); card.classList.toggle('-unavailable',rowUnavailable); const availabilityLine=makeElement('div','vm-favorites-state-availability'); Object.assign(availabilityLine.style,{marginTop:'1px',color:'var(--vm-ui-text-muted, #777)',fontSize:'8px',lineHeight:'1',whiteSpace:'nowrap',minHeight:'8px'}); if (availabilityNow != null) { const availabilityDot=makeElement('span','vm-favorites-availability-now','●'); Object.assign(availabilityDot.style,{color:availabilityNow ? 'var(--vm-ui-success, #1aa34a)':'var(--vm-ui-danger, #d32f2f)',fontSize:'8px',fontWeight:'900',lineHeight:'1'}); availabilityLine.append(availabilityDot,document.createTextNode(` ${availabilityNow ? 'otwarte':'zamknięte'}`)); availabilityLine.title=viewData(item).availabilityTooltip || (availabilityNow ? 'Aktualnie czynne':'Aktualnie zamknięte'); } stateCell.title=item.state?.online === true ? 'online':(item.state?.online === false ? 'offline':'brak danych'); stateCell.append(stateTop,activeLine,availabilityLine); const locationValue=watchCitiesText(viewData(item).cities); const locationCell=makeElement('div','vm-favorites-location-cell'); const locationTitle=locationValue!=='b/d'&&!item.state?.cities?.length?`Ostatnia znana lokalizacja: ${locationValue}`:locationValue; const locationText=makeElement('span',"vm-favorites-cell-text",locationValue,{title:locationTitle}); locationCell.appendChild(locationText); const dataCells=dataColumns.map(field => { const value=viewData(item).column(field); const cell=makeElement('div','vm-favorites-data-cell'); cell.dataset.vmFavoriteDataField=field; cell.title=value === 'b/d' ? '':value; cell.classList.add('vm-favorites-data-cell'); const valueText=makeElement('span',"vm-favorites-cell-text",value); cell.appendChild(valueText); return cell; }); const changesCell=makeElement( 'div', 'vm-favorites-changes-cell' ); Object.assign(changesCell.style,{ position:'relative', display:'grid', gridAutoRows:'minmax(0, 1fr)', gap:'1px', minWidth:'0', minHeight:'0', maxHeight:'100%', overflow:'hidden', color:'var(--vm-ui-text, #444)', fontSize:'8.5px', lineHeight:'1.1' }); const visibleItemEvents= itemEvents.slice(0,4); if (!itemEvents.length) { const dash=makeElement('span','','-'); dash.style.color='var(--vm-ui-text-faint, #aaa)'; changesCell.appendChild(dash); } for (const event of visibleItemEvents) { const line=makeElement('div',event.acknowledged ? 'vm-favorites-change vm-favorites-change-acknowledged':'vm-favorites-change vm-favorites-change-new'); line.dataset.vmFavoriteChangeState=event.acknowledged ? 'acknowledged':'new'; Object.assign(line.style,{ display:'flex', alignItems:'center', gap:'3px', minWidth:'0', minHeight:'0', overflow:'hidden', opacity: event.acknowledged ? '.55' :'1' }); if ( !event.acknowledged && event.alertTriggered ) { Object.assign(line.style,{ padding:'0 3px', border: '1px solid var(--vm-ui-danger, #c62828)', borderRadius:'4px', background: 'color-mix(in srgb, var(--vm-ui-danger, #c62828) 10%, transparent)', boxSizing:'border-box' }); } const content=makeElement('div'); Object.assign(content.style,{ display:'flex', alignItems:'baseline', gap:'3px', minWidth:'0', flex:'1 1 auto', minHeight:'0', overflow:'hidden', whiteSpace:'nowrap' }); const timestamp=makeElement( 'span', 'vm-favorites-change-time', formatWatchDateTime(event.timestamp) ); Object.assign(timestamp.style,{ flex:'0 0 auto', color:'var(--vm-ui-text-faint, #999)', fontSize:'7.5px', whiteSpace:'nowrap' }); const separator=makeElement('span','','-'); separator.style.color='var(--vm-ui-text-faint, #aaa)'; const compact=String(event.message || '').replace( new RegExp( `^${String(item.displayName).replace( /[.*+?^${}()|[\]\\]/g, '\\$&' )}\\s*`, 'i' ), '' ); const link=makeWatchEventLink(event,compact); link.className='vm-favorites-change-link'; Object.assign(link.style,{ minWidth:'0', flex:'1 1 auto', color:event.acknowledged ? 'var(--vm-fav-change-ack-text, var(--vm-ui-text-muted, #707075))' :( event.alertTriggered ? 'var(--vm-ui-danger, #c62828)' :'var(--vm-ui-accent, #f54da3)' ), textDecoration:event.description ? 'underline dotted' :'none', textUnderlineOffset:'2px', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }); content.append(timestamp,separator,link); if ( !event.acknowledged && event.alertTriggered ) { const ok=makeButton("vm-ui-button vm-ui-icon vm-ui-compact -accent vm-ui-table-icon",'✓'); Object.assign(ok.style,{opacity:'.18',transition:'opacity .1s ease'}); line.addEventListener( 'mouseenter', () => { ok.style.opacity='1'; } ); line.addEventListener( 'mouseleave', () => { ok.style.opacity='.18'; } ); ok.title='Zaakceptuj alert'; ok.addEventListener( 'click', () => acknowledgeWatchEvent( event.id ) ); line.append( ok, content ); } else { line.appendChild(content); } changesCell.appendChild(line); } const openTimeline=makeButton('vm-favorites-change-history-button',null,{title:`Pokaż wszystkie zapisane zmiany (${itemEvents.length})`}); openTimeline.setAttribute( 'aria-label', openTimeline.title ); openTimeline.innerHTML= '<svg viewBox="0 0 18 18" width="13" height="13" aria-hidden="true" focusable="false">' + '<path d="M3.5 3.5h5v1.5h-3.5v8h8v-3.5h1.5v5h-11z" fill="currentColor"/>' + '<path d="M9.5 2.5h6v6h-1.5v-3.45l-5.8 5.8-1.05-1.05 5.8-5.8h-3.45z" fill="currentColor"/>' + '</svg>'; openTimeline.classList.add('vm-ui-button','vm-ui-icon','vm-ui-compact','-accent','vm-ui-table-icon'); Object.assign(openTimeline.style,{position:'absolute',right:'1px',bottom:'1px',zIndex:'3'}); openTimeline.addEventListener( 'click', event => { stopUiEvent(event); openWatchChangesTimeline( item, itemEvents ); } ); changesCell.appendChild( openTimeline ); changesCell.style.paddingRight= '18px'; const actions=makeItemActions(item); card.append(imageBox,details,noteCell,categoryCell,modes,stateCell,locationCell,...dataCells,changesCell,actions); tableRows.set(item.id,signature,card); appendFavoriteTableRow(card,dataColumns,categoryCell); }); tableRows.render(nextTableNodes); favoriteRowsToMeasure.forEach(([row]) => {row.style.height='48px';}); favoriteRowsToMeasure.map(([row,cell]) => [row,Math.max(48,Math.ceil(Math.max(cell.scrollHeight || 0,cell.offsetHeight || 0)) + 4)]) .forEach(([row,height]) => {row.style.height=`${height}px`;}); const restoreFavoriteTableScroll=() => { if ( renderGeneration !== favoriteTableScrollRenderGeneration ) { return; } if (!body.isConnected) { suppressFavoriteTableScrollCapture=false; return; } body.scrollTop= favoriteTableSavedScrollTop; body.scrollLeft= favoriteTableSavedScrollLeft; }; queueMicrotask(restoreFavoriteTableScroll); requestAnimationFrame(() => { restoreFavoriteTableScroll(); requestAnimationFrame(() => { restoreFavoriteTableScroll(); if ( renderGeneration === favoriteTableScrollRenderGeneration ) { suppressFavoriteTableScrollCapture= false; } }); }); }; const scheduleTableRender=createAnimationFrameScheduler(()=>{if(overlay.isConnected)render();}); const tableStorageListeners=[]; watchPageButtonRefreshers.add(scheduleTableRender); try{for(const key of [WATCH_EVENTS_STORAGE_KEY,WATCH_ITEMS_STORAGE_KEY,WATCH_AD_DATA_MIRROR_STORAGE_KEY,SETTINGS_STORAGE_KEY])tableStorageListeners.push(GM_addValueChangeListener(key,scheduleTableRender));}catch {} overlay._vmRender=scheduleTableRender; refresh.addEventListener('click',() => runListCheck(visibleItemIds.slice())); const addManual=async () => { addButton.disabled=true; status.style.display='block'; status.style.color='var(--vm-ui-accent, #f54da3)'; status.textContent='Dodawanie do ulubionych…'; try { const added=await addWatchFromManualInput(manualInput.value); if (added?.item) { updateWatchedItem(added.item.id,{favorite:true}); manualInput.value=''; status.textContent='Dodano do ulubionych.'; } } catch (error) { status.textContent=error?.message || 'Nie udało się dodać'; status.style.color='var(--vm-ui-danger, #c62828)'; } finally { addButton.disabled=false; render(); } }; addButton.addEventListener('click',addManual); manualInput.addEventListener('keydown',event => {if (event.key === 'Enter') addManual();}); render(); } function removeWatchPageUi() { document.querySelectorAll('[data-vm-watch-wrapper="1"]').forEach(element => detachHelperNode(element)); for (const id of ['vm-escorti-watch-online','vm-escorti-watch-location','vm-escort-watch-online','vm-escort-watch-location','vm-escort-favorite-star','vm-escort-favorite-note-row','vm-escorti-profile-note-display']) detachHelperNode(document.getElementById(id)); watchPageButtonRefreshers.clear(); renderWatchNotificationBar(); } function initLocalWatchSystem() { const host=location.hostname.toLowerCase(); if (!IS_ESCORT_CLUB && ![ 'escorti.pl', 'www.escorti.pl', 'garsoniera.com.pl', 'www.garsoniera.com.pl' ].includes(host)) return; const pageUrl=new URL(location.href); if (pageUrl.searchParams.has('vm_mode')) return; if (!watchStorageListenersInitialized) { watchStorageListenersInitialized=true; window.addEventListener('pagehide',() => { watchCheckPageClosed=true; for (const request of watchManualRequests) request.cancelled=true; watchManualRequests.clear(); publishWatchManualRequests(); releaseWatchCheckLock(); }); window.addEventListener('pageshow',() => {watchCheckPageClosed=false;}); const items=getWatchedItems(); if (items.some(item => item.sourceType !== 'escort-ad' && canRemoveEmptyTrackedItem(item))) saveWatchedItems(items.map(item => ({...item,keepProfile:false}))); try { pruneWatchSystemNotificationClaims().catch(error=>log('Nie udało się posprzątać blokad powiadomień',error)); GM_addValueChangeListener(WATCH_EVENTS_STORAGE_KEY,refreshWatchEventViews); GM_addValueChangeListener(WATCH_ITEMS_STORAGE_KEY,() => { refreshWatchPageButtons(); initLocalWatchSystem(); document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.(); }); GM_addValueChangeListener(SETTINGS_STORAGE_KEY,() => { setTimeout(() => { SETTINGS=getSettings(); applyFavoriteSearchPageSettings(); applyWatchFeatureState(); },0); }); } catch {} } const watchSettings=getWatchSettings(); if (!watchSettings.enabled) removeWatchPageUi(); if (!watchSettings.enabled) { if (watchedPollTimer) { clearInterval(watchedPollTimer); watchedPollTimer=null; } watchedPollIntervalMs=0; renderWatchNotificationBar(); return; } renderWatchNotificationBar(); if (!watchedInitialCheckScheduled) { watchedInitialCheckScheduled=true; setTimeout(() => runWatchedChecks({ scheduled:true }).catch(error => log('Błąd ulubionych',error) ),3500); } const pollIntervalMs=WATCH_SCHEDULE_POLL_INTERVAL_MS; if (watchedPollTimer && watchedPollIntervalMs !== pollIntervalMs) { clearInterval(watchedPollTimer); watchedPollTimer=null; } if (!watchedPollTimer) { watchedPollIntervalMs=pollIntervalMs; watchedPollTimer=setInterval(() => { runWatchedChecks({ scheduled:true }).catch(error => log('Błąd ulubionych',error)); },pollIntervalMs); } } function applyWatchFeatureState() { if (window.top !== window.self) return; const host=location.hostname.toLowerCase(); if (!IS_ESCORT_CLUB && ![ 'escorti.pl', 'www.escorti.pl', 'garsoniera.com.pl', 'www.garsoniera.com.pl' ].includes(host)) return; initLocalWatchSystem(); const watchSettings=getWatchSettings(); if (isEscortClubHost(host) && !!getEscortAdIdFromPath(location.pathname)) { hideNativeEscortFavoriteAndNoteActions(watchSettings.enabled); } refreshHiddenItemsUi(); if (!watchSettings.enabled) return; if (host === 'escorti.pl' || host === 'www.escorti.pl') { initEscortiProfileWatchButtons(); } else if (!!getEscortAdIdFromPath(location.pathname)) { initEscortClubWatchButtons(); } } function getEscortSavedSearchFilters() { try { const saved=GM_getValue(ESCORT_SAVED_FILTERS_STORAGE_KEY,[]); if (!Array.isArray(saved)) return []; return saved .filter(item => item && typeof item === 'object' && item.id && item.name && item.state) .map(item => ({ id:String(item.id), name:String(item.name).trim().slice(0,80), savedAt:Number(item.savedAt) || 0, state:item.state })) .filter(item => item.name); } catch (error) { log('Błąd odczytu zapisanych filtrów Escort.club',error); return []; } } function setEscortSavedSearchFilters(filters) { try { writeHelperStorageValue(ESCORT_SAVED_FILTERS_STORAGE_KEY,Array.isArray(filters) ? filters:[]); return true; } catch (error) { log('Błąd zapisu filtrów Escort.club',error); return false; } } function captureEscortAdvancedFilterState() { const advanced=document.getElementById('advansed-search'); if (!advanced) return null; const state={ named:{}, ranges:{}, breasts:null }; const groups=new Map(); for (const control of advanced.querySelectorAll('input[name], select[name], textarea[name]')) { const name=String(control.name || '').trim(); if (!name) continue; if (!groups.has(name)) groups.set(name,[]); groups.get(name).push(control); } for (const [name,controls] of groups) { const first=controls[0]; const types=controls.map(control => String(control.type || '').toLowerCase()); if (types.every(type => type === 'radio')) { const checked=controls.find(control => control.checked); state.named[name]={ kind:'radio', value:checked ? String(checked.value ?? ''):null }; continue; } if (types.every(type => type === 'checkbox')) { state.named[name]={ kind:'checkbox', values:controls .filter(control => control.checked) .map(control => String(control.value ?? '')) }; continue; } if (first instanceof HTMLSelectElement && first.multiple) { state.named[name]={ kind:'multiple', values:[...first.selectedOptions].map(option => String(option.value)) }; continue; } state.named[name]={ kind:'values', values:controls.map(control => String(control.value ?? '')) }; } for (const key of ['price','age','weight','height']) { const from=advanced.querySelector(`input[data-${key}-from]`); const to=advanced.querySelector(`input[data-${key}-to]`); if (!from && !to) continue; state.ranges[key]={ from:from ? String(from.value ?? ''):null, to:to ? String(to.value ?? ''):null }; } const breastsInput=advanced.querySelector('#range_breasts_filter'); if (breastsInput) { let from=breastsInput.getAttribute('data-from'); let to=breastsInput.getAttribute('data-to'); try { const jq=window.jQuery; const slider=jq ? jq(breastsInput).data('ionRangeSlider'):null; if (slider?.result) { from=slider.result.from; to=slider.result.to; } } catch {} state.breasts={ value:String(breastsInput.value ?? ''), from:from == null ? null:String(from), to:to == null ? null:String(to) }; } return state; } function dispatchEscortFilterControlEvents(control) { try { control.dispatchEvent(new Event('input',{bubbles:true})); control.dispatchEvent(new Event('change',{bubbles:true})); } catch {} } function refreshEscortFilterSelect(control) { if (!(control instanceof HTMLSelectElement)) return; try { const jq=window.jQuery; if (!jq) return; if (control.multiple && jq.fn?.select2 && jq(control).hasClass('select2-hidden-accessible')) { jq(control).trigger('change'); } if (jq.fn?.selectpicker && jq(control).parent().hasClass('bootstrap-select')) { jq(control).selectpicker('refresh'); } } catch (error) { log('Nie udało się odświeżyć kontrolki filtra Escort.club',error); } } function applyEscortAdvancedFilterState(state) { const advanced=document.getElementById('advansed-search'); if (!advanced || !state || typeof state !== 'object') return false; const named=state.named && typeof state.named === 'object' ? state.named:{}; for (const [name,stored] of Object.entries(named)) { const controls=[...advanced.querySelectorAll('[name]')] .filter(control => String(control.name || '') === name); if (!controls.length || !stored || typeof stored !== 'object') continue; if (stored.kind === 'radio') { for (const control of controls) { control.checked=stored.value != null && String(control.value) === String(stored.value); dispatchEscortFilterControlEvents(control); } continue; } if (stored.kind === 'checkbox') { const selected=new Set((stored.values || []).map(value => String(value))); for (const control of controls) { control.checked=selected.has(String(control.value ?? '')); dispatchEscortFilterControlEvents(control); } continue; } if (stored.kind === 'multiple') { const values=(stored.values || []).map(value => String(value)); const selected=new Set(values); for (const control of controls) { if (!(control instanceof HTMLSelectElement)) continue; for (const option of control.options) { option.selected=selected.has(String(option.value)); } dispatchEscortFilterControlEvents(control); refreshEscortFilterSelect(control); } continue; } const values=Array.isArray(stored.values) ? stored.values:[]; controls.forEach((control,index) => { const value=values[index] ?? values[0] ?? ''; control.value=String(value); dispatchEscortFilterControlEvents(control); refreshEscortFilterSelect(control); }); } const ranges=state.ranges && typeof state.ranges === 'object' ? state.ranges:{}; for (const key of ['price','age','weight','height']) { const stored=ranges[key]; if (!stored) continue; const from=advanced.querySelector(`input[data-${key}-from]`); const to=advanced.querySelector(`input[data-${key}-to]`); if (from && stored.from != null) { from.value=String(stored.from); dispatchEscortFilterControlEvents(from); } if (to && stored.to != null) { to.value=String(stored.to); dispatchEscortFilterControlEvents(to); } } if (state.breasts) { const breastsInput=advanced.querySelector('#range_breasts_filter'); if (breastsInput) { const from=Number(state.breasts.from); const to=Number(state.breasts.to); let sliderUpdated=false; try { const jq=window.jQuery; const slider=jq ? jq(breastsInput).data('ionRangeSlider'):null; if (slider && Number.isFinite(from) && Number.isFinite(to)) { slider.update({from,to}); sliderUpdated=true; } } catch {} if (!sliderUpdated) { if (state.breasts.from != null) breastsInput.setAttribute('data-from',String(state.breasts.from)); if (state.breasts.to != null) breastsInput.setAttribute('data-to',String(state.breasts.to)); } if (state.breasts.value != null) { breastsInput.value=String(state.breasts.value); } dispatchEscortFilterControlEvents(breastsInput); } } for (const select of advanced.querySelectorAll('select')) { refreshEscortFilterSelect(select); } return true; } function submitEscortAdvancedFilters() { const advanced=document.getElementById('advansed-search'); if (!advanced) return false; const form=advanced.closest('form'); if (!form) return false; const submitter=[ ...advanced.querySelectorAll( 'button[type="submit"], input[type="submit"]' ) ].find(control => { const label=String( control.textContent || control.value || '' ) .replace(/\s+/g,' ') .trim(); return !label || /^Szukaj$/i.test(label); }) || null; try { if (typeof form.requestSubmit === 'function') { if (submitter) form.requestSubmit(submitter); else form.requestSubmit(); } else if (submitter) { submitter.click(); } else { HTMLFormElement.prototype.submit.call(form); } return true; } catch (error) { log( 'Nie udało się uruchomić zapisanych filtrów Escort.club', error ); return false; } } const ESCORT_NATIVE_FILTER_DEFAULTS=Object.freeze({ filter_price_type:'', filter_price:'0;25000', filter_age:'18;100', filter_weight:'30;200', filter_height:'100;220', filter_breasts:'0;8', breasts_type:'', hair_colors:'', sexual_orientation:'', searchlang:'', zodiac_sign:'', q:'' }); function getEscortNativeFiltersFromUrl() { const defaults=ESCORT_NATIVE_FILTER_DEFAULTS; const labels={ filter_price_type:'Typ ceny', filter_price:'Cena', filter_age:'Wiek', filter_weight:'Waga', filter_height:'Wzrost', filter_breasts:'Biust', breasts_type:'Typ biustu', hair_colors:'Kolor włosów', sexual_orientation:'Orientacja', searchlang:'Język', zodiac_sign:'Znak zodiaku', q:'Fraza' }; const ignored=new Set(['province','district','page','sort','order']); const params=new URLSearchParams(location.search); const active=[]; const normalizeParamBase=name => String(name || '') .replace(/\[\d+\]$|\[\]$/g,'') .toLowerCase(); const controls=[ ...document.querySelectorAll( '#advansed-search select[name]' ) ]; const findOptionLabel=(name,value) => { const base=normalizeParamBase(name); const select=controls.find(control => normalizeParamBase(control.name) === base ); if (!select) return ''; const option=[...select.options].find( candidate => String(candidate.value) === String(value) ); const label=normalizeEscortAdText( option?.textContent ); return label && !/^(dowoln|wszystk|wybierz)/i.test(label) ? label :''; }; const formatValue=value => { const shown=String(value ?? '').trim(); if ( /^-?\d+(?:[.,]\d+)?;-?\d+(?:[.,]\d+)?$/.test( shown ) ) { return shown.replace(';','-'); } return shown.replace(/;/g,', '); }; const add=(key,label,value='') => { const shownValue=formatValue(value); active.push({ key, label, value:shownValue, text:shownValue ? `${label}: ${shownValue}` :label }); }; const pathParts= location.pathname.split('/').filter(Boolean); const adsIndex=pathParts.indexOf('anonse'); const adType= adsIndex >= 0 ? decodeURIComponent( pathParts[adsIndex + 1] || '' ) :''; if ( adType && adType.toLowerCase() !== 'towarzyskie' ) { add( 'ad_type', adType .replace(/-/g,' ') .replace( /^./, char => char.toUpperCase() ) ); } for ( const [name,defaultValue] of Object.entries(defaults) ) { const value=params.get(name); if ( value == null || value === defaultValue ) { continue; } const label=labels[name]; const optionLabel= findOptionLabel(name,value); if (optionLabel) { add(name,label,optionLabel); } else if ( /^\d+$/.test(String(value).trim()) && [ 'breasts_type', 'hair_colors', 'sexual_orientation', 'searchlang', 'zodiac_sign' ].includes(name) ) { add(name,label); } else { add(name,label,value); } } const groupedDefs={ sexual_interest:{ label:'Tagi' }, sexual_interests:{ label:'Tagi' }, nationality:{ label:'Narodowość' }, nationalities:{ label:'Narodowość' }, ethnicity:{ label:'Etniczność' }, ethnicities:{ label:'Etniczność' } }; const grouped=new Map(); for (const name of new Set([...params.keys()])) { const base=normalizeParamBase(name); const def=groupedDefs[base]; if (!def) continue; const bucket= grouped.get(def.label) || { def, count:0, labels:[], unresolved:false }; for (const value of params.getAll(name)) { if (!String(value).trim()) continue; bucket.count += 1; const optionLabel= findOptionLabel(name,value); if (optionLabel) { bucket.labels.push(optionLabel); } else { bucket.unresolved=true; } } grouped.set(def.label,bucket); } for (const [label,group] of grouped) { if (!group.count) continue; add( label.toLowerCase(), label, String(group.count) ); } for (const name of new Set([...params.keys()])) { if ( name in defaults || ignored.has(name) ) { continue; } const base=normalizeParamBase(name); if (groupedDefs[base]) continue; const values= params.getAll(name) .map(value => String(value).trim() ) .filter(Boolean); if (!values.length) continue; const label=name .replace(/\[\d+\]$|\[\]$/g,'') .replace(/^filter_/,'') .replace(/_/g,' ') .replace( /^./, char => char.toUpperCase() ); const optionLabels= values.map(value => findOptionLabel(name,value) ); if ( optionLabels.every(Boolean) ) { add( name, label, [...new Set(optionLabels)].join(', ') ); } else if ( values.every(value => /^\d+$/.test(value) ) ) { add(name,label); } else { add( name, label, values.join(', ') ); } } return active.filter( (item,index,list) => list.findIndex( other => other.key === item.key && other.text === item.text ) === index ); } function buildEscortNativeFilterResetUrl() { const url=new URL(location.href); const province=url.searchParams.get('province'); const district=url.searchParams.get('district'); const parts=url.pathname.split('/').filter(Boolean); const adsIndex=parts.indexOf('anonse'); if (adsIndex >= 0 && parts[adsIndex + 1]) { parts[adsIndex + 1]='towarzyskie'; url.pathname=`/${parts.join('/')}/`; } url.search=''; if (province != null) url.searchParams.set('province',province); if (district != null) url.searchParams.set('district',district); for (const [name,value] of Object.entries(ESCORT_NATIVE_FILTER_DEFAULTS)) { url.searchParams.set(name,value); } return url.href; } function initEscortNativeFilterResetFix() { if (!IS_ESCORT_CLUB) return; if (document.documentElement.dataset.vmNativeFilterResetFix === '1') return; document.documentElement.dataset.vmNativeFilterResetFix='1'; document.addEventListener('click',event => { const control=event.target instanceof Element ? event.target.closest('#advansed-search .search-fiters.-reset a, #advansed-search .search-fiters.-reset button, #advansed-search .search-fiters.-reset input') :null; if (!control) return; const label=String(control.textContent || control.value || '') .replace(/\s+/g,' ') .trim(); if (!/^Resetuj$/i.test(label)) return; event.preventDefault(); event.stopImmediatePropagation(); location.assign(buildEscortNativeFilterResetUrl()); },true); } function ensureEscortNativeFilterTheme() { appendHelperStyle('vm-native-filter-theme-style',` #vm-native-filter-controls-group.vm-helper-ui { color:var(--vm-ui-text,#654426); } #vm-native-filter-controls-group.vm-helper-ui.-active { border-color:var(--vm-ui-border,#d3b796) !important; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 20%,var(--vm-ui-bg,#fffaf4)) !important; } #vm-native-filter-controls-group .vm-native-filter-chip { border-color:var(--vm-ui-border-strong,var(--vm-ui-border,#c7a984)) !important; background:var(--vm-ui-bg,#fffaf4) !important; color:var(--vm-ui-text,#654426) !important; box-shadow:none; } .vm-helper-ui.vm-native-filter-toggle, #vm-native-filter-reset-button { border-color:var(--vm-ui-accent,#f54da3) !important; background:var(--vm-ui-accent,#f54da3) !important; color:var(--vm-ui-accent-contrast,#211d22) !important; text-decoration:none; } #vm-native-filter-reset-button { box-shadow:inset 2px 0 0 var(--vm-ui-bg,#411034) !important; } .vm-helper-ui.vm-native-filter-toggle:is(:hover,:focus-visible), #vm-native-filter-reset-button:is(:hover,:focus-visible):not([aria-disabled="true"]) { background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 85%,var(--vm-ui-accent-contrast,#211d22)) !important; } .vm-helper-ui.vm-native-filter-toggle:focus-visible, #vm-native-filter-reset-button:focus-visible { outline:2px solid var(--vm-ui-text,#fff); outline-offset:2px; } #vm-native-filter-reset-button[aria-disabled="true"] { opacity:.45; cursor:default; } `); } function getEscortNativeAdvancedFilterLinks() { return [...document.querySelectorAll('.search-links a, .mobile-trigger a')] .filter(link => /^Pokaż filtry(?: zaawansowane)?(?:\s*\(\d+\))?$/i.test(normalizeEscortAdText(link.textContent))); } function getEscortNativeDesktopFilterLink(links) { return links.find(link => !link.closest('.mobile-trigger') && link.offsetParent !== null) || links.find(link => !link.closest('.mobile-trigger')); } function initEscortNativeAdvancedFilterCounter() { if (!IS_ESCORT_CLUB) return; if (document.documentElement.dataset.vmNativeFilterCounterInit === '1') return; document.documentElement.dataset.vmNativeFilterCounterInit='1'; ensureEscortNativeFilterTheme(); const update=() => { const active=getEscortNativeFiltersFromUrl(); const summary=active.map(item => item.text); const links=getEscortNativeAdvancedFilterLinks(); for (const link of links) { prepareHelperStyleApi(link).classList.add('vm-native-filter-toggle'); link.textContent=`Pokaż filtry${active.length ? ` (${active.length})`:''}`; link.title=active.length ? `Aktywne filtry: ${summary.join(', ')}`:'Brak aktywnych filtrów'; } const link=getEscortNativeDesktopFilterLink(links); if (!link) return; let resetButton=document.getElementById( 'vm-native-filter-reset-button' ); if (!resetButton) { resetButton=link.cloneNode(false); resetButton.id='vm-native-filter-reset-button'; resetButton.classList.remove('vm-native-filter-toggle'); resetButton.classList.add('vm-native-filter-reset-button'); resetButton.removeAttribute('onclick'); resetButton.textContent='×'; resetButton.setAttribute('role','button'); resetButton.setAttribute('aria-label','Resetuj filtry'); resetButton.addEventListener('click',event => { event.preventDefault(); event.stopPropagation(); if (!getEscortNativeFiltersFromUrl().length) return; location.assign(buildEscortNativeFilterResetUrl()); }); } resetButton.title='Resetuj filtry'; resetButton.style.fontSize='22px'; resetButton.classList.toggle('disabled',!active.length); resetButton.setAttribute('aria-disabled',String(!active.length)); if ( resetButton.parentElement !== link.parentElement || resetButton.previousElementSibling !== link ) { link.insertAdjacentElement('afterend',resetButton); } link.style.borderTopRightRadius='0'; link.style.borderBottomRightRadius='0'; resetButton.style.borderTopLeftRadius='0'; resetButton.style.borderBottomLeftRadius='0'; link.style.borderRight='0'; resetButton.style.borderLeft='0'; let controlsGroup=document.getElementById( 'vm-native-filter-controls-group' ); if (!controlsGroup) { controlsGroup=prepareHelperStyleApi(makeElement('span')); controlsGroup.id='vm-native-filter-controls-group'; } const linkParent=link.parentElement; if (!linkParent) return; if (!controlsGroup.isConnected) { linkParent.insertBefore(controlsGroup,link); } let buttons=controlsGroup.querySelector('.vm-native-filter-buttons'); if (!buttons) {buttons=makeElement('span','vm-native-filter-buttons'); controlsGroup.appendChild(buttons);} if (link.parentElement !== buttons || resetButton.parentElement !== buttons) buttons.append(link,resetButton); Object.assign(buttons.style,{display:'inline-flex',alignItems:'stretch',minWidth:'0',maxWidth:'100%'}); Object.assign(link.style,{flex:'1 1 auto',minWidth:'0'}); resetButton.style.flex='0 0 auto'; Object.assign(controlsGroup.style,{ display:'inline-flex', alignItems:'center', flex:'0 1 auto', flexWrap:'nowrap', gap:'0', minWidth:'0', maxWidth:'100%', boxSizing:'border-box', verticalAlign:'middle' }); let info=document.getElementById('vm-native-filter-info'); if (!info) { info=makeElement('span'); info.id='vm-native-filter-info'; } controlsGroup.classList.toggle('-active',!!active.length); info.replaceChildren(); if (!active.length) { detachHelperNode(info); Object.assign(controlsGroup.style,{padding:'0',border:'0',borderRadius:'0',background:'transparent',boxShadow:'none'}); return; } Object.assign(controlsGroup.style,{ padding:'0 0 0 6px', border:'1px solid var(--vm-ui-border,#d3b796)', borderRadius:'9px', background:'var(--vm-ui-bg-alt,#f0e2d2)', boxShadow:'none' }); Object.assign(info.style,{ display:'inline-grid', gridAutoFlow:'column', gridTemplateRows:'repeat(2, 16px)', gridAutoColumns:'max-content', alignItems:'center', alignContent:'center', gap:'2px 4px', padding:'0 7px 0 0', margin:'0', verticalAlign:'middle', flex:'0 1 auto', minWidth:'0', maxWidth:'100%', overflowX:'auto', overflowY:'hidden', boxSizing:'border-box' }); for (const item of active) { const chip=makeElement('span','vm-native-filter-chip',item.text); Object.assign(chip.style,{ display:'inline-flex', alignItems:'center', height:'16px', padding:'0 6px', boxSizing:'border-box', border:'1px solid', borderRadius:'4px', font:'700 9px var(--vm-ui-font-family,Lato,Arial,sans-serif)', lineHeight:'1', whiteSpace:'nowrap' }); chip.title=`Aktywny filtr: ${item.text}`; info.appendChild(chip); } info.title=`Aktywne filtry: ${summary.join(', ')}`; if ( info.parentElement !== controlsGroup || info.nextElementSibling !== buttons ) { controlsGroup.insertBefore(info,buttons); } }; update(); setTimeout(update,300); } function initEscortSavedSearchFilters() { if (!IS_ESCORT_CLUB) return; if (document.getElementById('vm-escort-saved-filters')) return; const advanced=document.getElementById('advansed-search'); const footer=advanced?.querySelector('.search-fiters.-reset'); if (!advanced || !footer) { if (!document.documentElement.dataset.vmSavedFiltersRetry) { document.documentElement.dataset.vmSavedFiltersRetry='1'; let attempts=0; const timer=setInterval(() => { attempts++; if (document.getElementById('advansed-search')?.querySelector('.search-fiters.-reset')) { clearInterval(timer); delete document.documentElement.dataset.vmSavedFiltersRetry; initEscortSavedSearchFilters(); } else if (attempts >= 40) { clearInterval(timer); delete document.documentElement.dataset.vmSavedFiltersRetry; } },250); } return; } const pagePinkColor=getEscortPagePinkColor(); const styleId='vm-escort-saved-filters-style'; if (!document.getElementById(styleId)) { appendHelperStyle(styleId,` #vm-escort-saved-filters { display: inline-flex; align-items: center; flex: 0 1 auto; flex-wrap: nowrap; gap: 7px; max-width: 100%; margin-right: auto; padding-right: 14px; padding-bottom: 2px; overflow-x: auto; font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-escort-saved-filters .vm-saved-filters-label { color: var(--vm-ui-text,#211d22); font-size: 12px; font-weight: 700; line-height: 32px; white-space: nowrap; } #vm-escort-saved-filters .vm-saved-filter-select-wrap { --vm-saved-filter-select-width: ${ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH}px; position: relative; width: var(--vm-saved-filter-select-width) !important; min-width: ${ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH}px; max-width: ${ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH}px; flex: 0 0 var(--vm-saved-filter-select-width) !important; height: 32px; margin: 0; transition: width .15s ease, flex-basis .15s ease; } #vm-escort-saved-filters .vm-saved-filter-select-wrap::after { content: ''; position: absolute; top: 13px; right: 10px; width: 0; height: 0; border-left: 7px solid transparent; border-right: 7px solid transparent; border-top: 7px solid var(--vm-ui-accent,#ff4c99); pointer-events: none; } #vm-escort-saved-filters-select { appearance: none !important; -webkit-appearance: none !important; -moz-appearance: none !important; box-sizing: border-box; width: 100% !important; min-width: 100% !important; max-width: 100% !important; height: 32px; min-height: 32px; margin: 0; padding: 0 30px 0 12px !important; border: 0 !important; border-bottom: 1px solid var(--vm-ui-accent,#ff4c99) !important; border-radius: 0 !important; outline: 0 !important; background: transparent !important; color: var(--vm-ui-text,#211d22); font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); font-size: 14px; line-height: 31px; text-overflow: ellipsis; cursor: pointer; box-shadow: none !important; } #vm-escort-saved-filters-select:focus { border-bottom-color: var(--vm-ui-accent,#d90070) !important; } #vm-escort-saved-filters .vm-saved-filter-action { box-sizing: border-box; display: inline-flex !important; align-items: center; justify-content: center; flex: 0 0 auto !important; width: auto !important; min-width: max-content !important; max-width: none !important; height: 32px; margin: 0; padding: 7px 12px !important; border-radius: 7px !important; font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); font-size: 12px; font-weight: 700; line-height: 16px; white-space: nowrap !important; text-decoration: none; cursor: pointer; transition: background-color .15s ease, border-color .15s ease, color .15s ease; } #vm-escort-saved-filters .vm-saved-filter-delete { border: 1px solid var(--vm-ui-accent,${pagePinkColor}) !important; background: var(--vm-ui-bg,#fff) !important; color: var(--vm-ui-accent,${pagePinkColor}) !important; } #vm-escort-saved-filters .vm-saved-filter-delete:hover:not(:disabled), #vm-escort-saved-filters .vm-saved-filter-delete:focus:not(:disabled) { background: var(--vm-ui-bg-hover,#fff1f8) !important; border-color: var(--vm-ui-accent,${pagePinkColor}) !important; color: var(--vm-ui-accent,${pagePinkColor}) !important; filter: brightness(.9); } #vm-escort-saved-filters .vm-saved-filter-load:disabled, #vm-escort-saved-filters .vm-saved-filter-delete:disabled { border-color: var(--vm-ui-border,#d8d2d5) !important; background: var(--vm-ui-bg-alt,#f4f1f3) !important; color: var(--vm-ui-text-faint,#aaa3a7) !important; cursor: not-allowed; opacity: .72; } #vm-escort-saved-filters .vm-saved-filters-status { color: var(--vm-ui-text-muted,#8d858a); font-size: 10px; line-height: 16px; white-space: nowrap; } #vm-escort-saved-filters-quick { display: inline-flex; align-items: stretch; flex: 0 0 auto; margin-left: 10px; vertical-align: middle; } #vm-escort-saved-filters-quick-select { appearance: auto; -webkit-appearance: auto; box-sizing: border-box; min-width: 180px; max-width: 280px; margin: 0; padding: 0 34px 0 15px; border: 2px solid var(--vm-ui-accent, ${pagePinkColor}); border-radius: 7px; background: var(--vm-ui-bg, #fff); color: var(--vm-ui-accent, ${pagePinkColor}); font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); font-size: 13px; font-weight: 800; line-height: 1; cursor: pointer; box-shadow: none; } #vm-escort-saved-filters-quick-select:hover, #vm-escort-saved-filters-quick-select:focus { background: var( --vm-ui-bg-hover, color-mix( in srgb, var(--vm-ui-accent, ${pagePinkColor}) 7%, var(--vm-ui-bg, #fff) ) ); color: var(--vm-ui-accent, ${pagePinkColor}); outline: none; } #vm-escort-saved-filters-quick-select:disabled { opacity: .58; cursor: default; } @media (max-width: 767px) { #vm-escort-saved-filters-quick { display: none !important; } #vm-escort-saved-filters { flex: 0 1 100%; width: 100%; margin-bottom: 10px; padding-right: 0; } #vm-escort-saved-filters .vm-saved-filters-status { display: none; } } `); } const root=makeElement('div'); root.id='vm-escort-saved-filters'; const label=makeElement('span','sub-label vm-saved-filters-label','Zapisane filtry:'); const select=makeElement('select'); select.id='vm-escort-saved-filters-select'; select.setAttribute('aria-label','Zapisane filtry wyszukiwania'); const selectWrap=makeElement('div','vm-saved-filter-select-wrap'); selectWrap.appendChild(select); const quickRoot=prepareHelperStyleApi( makeElement('span') ); quickRoot.id='vm-escort-saved-filters-quick'; const controlsShell=makeElement('div','vm-escort-results-controls-shell'); const quickSelect=makeElement('select','',null,{id:'vm-escort-saved-filters-quick-select'}); quickSelect.setAttribute( 'aria-label', 'Szybki wybór zapisanego zestawu filtrów' ); quickSelect.title='Wczytaj zapisany zestaw filtrów'; quickRoot.appendChild(quickSelect); function updateSavedFilterSelectWidth() { const selectedOption=select.options[select.selectedIndex]; const text=String(selectedOption?.textContent || '').trim(); const canvas=makeElement('canvas'); const context=canvas.getContext('2d'); if (context) { context.font='14px Lato, Arial, sans-serif'; } const textWidth=context ? context.measureText(text).width:text.length * 8; const naturalWidth=Math.ceil(textWidth) + 48; const width=Math.max( ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH, Math.min(ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH,naturalWidth) ); selectWrap.style.setProperty('--vm-saved-filter-select-width',`${width}px`); select.title=naturalWidth > ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH ? text:''; } const loadBtn=makeButton('btn btn-pink vm-saved-filter-action vm-saved-filter-load','Wczytaj',{disabled:true}); loadBtn.setAttribute('aria-disabled','true'); loadBtn.title='Najpierw wybierz zapisany zestaw'; const saveBtn=makeButton('btn btn-pink vm-saved-filter-action vm-saved-filter-save','Zapisz'); const deleteBtn=makeButton("btn vm-saved-filter-action vm-saved-filter-delete",'Usuń',{disabled:true}); deleteBtn.setAttribute('aria-disabled','true'); deleteBtn.title='Najpierw wybierz zapisany zestaw'; const status=makeElement('span','sub-label vm-saved-filters-status'); status.setAttribute('aria-live','polite'); function getSelectedId() { return String(select.value || ''); } const getPresetById=id => getEscortSavedSearchFilters().find(item => item.id === String(id || '')) || null; const syncPresetSelection=id => { const value=String(id || ''); select.value=value; quickSelect.value=value; updateSavedFilterSelectWidth(); updatePresetActionState(); }; const fillPresetSelect=(target,filters,placeholderText,selectedId) => { target.replaceChildren(); const placeholder=makeElement('option','',placeholderText,{value:''}); target.appendChild(placeholder); for (const filter of filters) { const option=makeElement('option','',filter.name,{value:filter.id,selected:filter.id === selectedId}); target.appendChild(option); } }; function updatePresetActionState() { const enabled=!!getSelectedId(); for (const [button,enabledTitle] of [ [loadBtn,'Wczytaj wybrany zapisany zestaw'], [deleteBtn,'Usuń wybrany zapisany zestaw'] ]) { button.disabled=!enabled; button.setAttribute('aria-disabled',String(!enabled)); button.title=enabled ? enabledTitle:'Najpierw wybierz zapisany zestaw'; } } function refreshList(selectedId='') { const filters=getEscortSavedSearchFilters(); fillPresetSelect( select, filters, filters.length ? '- wybierz -':'- brak zapisanych -', selectedId ); fillPresetSelect( quickSelect, filters, filters.length ? 'Zapisane filtry':'Brak zapisanych filtrów', selectedId ); if ( selectedId && !filters.some(filter => filter.id === selectedId) ) { select.value=''; quickSelect.value=''; } quickSelect.disabled=!filters.length; quickSelect.title=filters.length ? 'Wczytaj zapisany zestaw filtrów' :'Brak zapisanych zestawów filtrów'; updateSavedFilterSelectWidth(); updatePresetActionState(); } select.addEventListener('change',() => { const id=getSelectedId(); quickSelect.value=id; updateSavedFilterSelectWidth(); updatePresetActionState(); if (!id) { status.textContent=''; return; } const preset=getPresetById(id); if (!preset) { status.textContent='nie znaleziono zestawu'; return; } status.textContent=`wybrano „${preset.name}”`; }); quickSelect.addEventListener('change',() => { const id=String(quickSelect.value || ''); if (!id) return; const preset=getPresetById(id); if (!preset) { quickSelect.value=''; return; } syncPresetSelection(id); if (applyEscortAdvancedFilterState(preset.state)) { status.textContent=`uruchamiam „${preset.name}”`; requestAnimationFrame(() => { setTimeout(() => { if (!submitEscortAdvancedFilters()) { status.textContent= 'nie udało się uruchomić filtrowania'; quickSelect.value=''; select.value=''; updatePresetActionState(); } },0); }); } else { status.textContent='nie udało się wczytać'; quickSelect.value=''; select.value=''; updatePresetActionState(); } }); loadBtn.addEventListener('click',event => { event.preventDefault(); const id=getSelectedId(); if (!id) return; const preset=getPresetById(id); if (!preset) { status.textContent='nie znaleziono zestawu'; refreshList(''); return; } if (applyEscortAdvancedFilterState(preset.state)) { quickSelect.value=id; status.textContent=`wczytano „${preset.name}”`; } else { status.textContent='nie udało się wczytać'; } }); saveBtn.addEventListener('click',event => { event.preventDefault(); const currentId=getSelectedId(); const currentPreset=getEscortSavedSearchFilters().find(item => item.id === currentId); const proposed=window.prompt( 'Podaj nazwę zestawu filtrów:', currentPreset?.name || '' ); if (proposed == null) return; const name=proposed.replace(/\s+/g,' ').trim().slice(0,80); if (!name) { window.alert('Nazwa zestawu filtrów nie może być pusta.'); return; } const state=captureEscortAdvancedFilterState(); if (!state) { status.textContent='nie udało się odczytać filtrów'; return; } const filters=getEscortSavedSearchFilters(); const existing=filters.find(item => item.name.toLocaleLowerCase('pl') === name.toLocaleLowerCase('pl')); let id; if (existing) { if (!window.confirm(`Zestaw „${existing.name}” już istnieje. Nadpisać go?`)) return; existing.name=name; existing.state=state; existing.savedAt=Date.now(); id=existing.id; } else { id=`preset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,8)}`; filters.unshift({ id, name, state, savedAt:Date.now() }); } if (!setEscortSavedSearchFilters(filters)) { status.textContent='błąd zapisu'; return; } refreshList(id); status.textContent=`zapisano „${name}”`; }); deleteBtn.addEventListener('click',event => { event.preventDefault(); const id=getSelectedId(); if (!id) return; const filters=getEscortSavedSearchFilters(); const preset=filters.find(item => item.id === id); if (!preset) return; if (!window.confirm(`Usunąć zapisany zestaw „${preset.name}”?`)) return; const next=filters.filter(item => item.id !== id); if (!setEscortSavedSearchFilters(next)) { status.textContent='błąd usuwania'; return; } refreshList(''); status.textContent=`usunięto „${preset.name}”`; }); root.appendChild(label); root.appendChild(selectWrap); root.appendChild(loadBtn); root.appendChild(saveBtn); root.appendChild(deleteBtn); root.appendChild(status); footer.insertBefore(root,footer.firstChild); const placeQuickSelect=() => { const desktopLink=getEscortNativeDesktopFilterLink(getEscortNativeAdvancedFilterLinks()); if (!desktopLink?.parentElement) return false; const searchLinks=desktopLink.closest('.search-links'); const resultsLabel= searchLinks?.querySelector(':scope > .sub-label') || document.querySelector('.vm-escort-results-count'); const topSearch=document.getElementById('top-search'); if (resultsLabel && topSearch?.parentElement) { markEscortResultCounter(resultsLabel); topSearch.style.paddingBottom='0'; } const locationSearchBox= findEscortSearchLocationControls()?.searchBox; if (locationSearchBox) { const referenceStyle=getComputedStyle(locationSearchBox); Object.assign(controlsShell.style,{ boxSizing:referenceStyle.boxSizing, width:'100%', maxWidth: locationSearchBox.style.maxWidth || referenceStyle.maxWidth, margin:referenceStyle.margin, paddingTop:referenceStyle.paddingTop, paddingBottom:referenceStyle.paddingBottom, paddingRight:referenceStyle.paddingRight, paddingLeft:referenceStyle.paddingLeft, display:'flex', alignItems:'center', justifyContent:'space-between', flexWrap:'wrap', minWidth:'0' }); } if (searchLinks?.parentElement) { if (searchLinks.parentElement !== controlsShell) { searchLinks.parentElement.insertBefore(controlsShell,searchLinks); controlsShell.appendChild(searchLinks); } Object.assign(searchLinks.style,{ width:'100%', maxWidth:'none', margin:'0', padding:'0', flex:'1 1 auto', minWidth:'0' }); } if (resultsLabel && searchLinks && controlsShell.contains(searchLinks)) { Object.assign(resultsLabel.style,{ display:'block', flex:'0 1 auto', minWidth:'0', maxWidth:'100%', margin:'0', whiteSpace:'nowrap' }); if ( resultsLabel.parentElement !== controlsShell || resultsLabel.nextElementSibling !== searchLinks ) { controlsShell.insertBefore(resultsLabel,searchLinks); } } const radiusLink=document.getElementById('vm-escort-radius-open')?.parentElement; if (radiusLink && controlsShell.parentElement) controlsShell.before(radiusLink); const controlsParent=desktopLink.parentElement; if (controlsParent && controlsParent !== searchLinks) { controlsParent.style.marginLeft='auto'; } else { desktopLink.style.marginLeft='auto'; } const resetButton=document.getElementById( 'vm-native-filter-reset-button' ); const filterControlsGroup=desktopLink.closest('#vm-native-filter-controls-group'); const quickAnchor= filterControlsGroup || ( resetButton?.parentElement === desktopLink.parentElement ? resetButton :desktopLink ); if ( quickRoot.parentElement !== quickAnchor.parentElement || quickRoot.previousElementSibling !== quickAnchor ) { quickAnchor.insertAdjacentElement('afterend',quickRoot); } const rect=desktopLink.getBoundingClientRect(); if (rect.height > 0) { quickSelect.style.height= `${Math.round(rect.height)}px`; quickSelect.style.minHeight= `${Math.round(rect.height)}px`; } const computed=getComputedStyle(desktopLink); if (computed.borderRadius) { quickSelect.style.borderRadius= computed.borderRadius; } return true; }; if (!placeQuickSelect()) { let quickAttempts=0; const quickTimer=setInterval(() => { quickAttempts++; if ( placeQuickSelect() || quickAttempts >= 40 ) { clearInterval(quickTimer); } },250); } refreshList(''); document.fonts?.ready .then(() => { if (document.contains(select)) updateSavedFilterSelectWidth(); }) .catch(() => {}); } function initReverseImageUrlPrefill() { if (!/^(?:www\.)?(?:picdetective\.com|hivedetect\.ai)$/.test(location.hostname)) return false; if (window.top !== window.self) return true; const pageUrl=new URL(location.href); const imageUrl=new URLSearchParams(pageUrl.hash.slice(1)).get('vm_image_url') || pageUrl.searchParams.get('vm_image_url'); try { const url=new URL(imageUrl); if (!/^https?:$/.test(url.protocol) || url.username || url.password || imageUrl.length > 8192) return true; } catch {return true;} let attempts=0,timer=null; const fill=() => { const input=[...document.querySelectorAll('input,textarea')].find(el => !el.disabled && !el.readOnly && /^(?:text|url|search|textarea)$/.test(el.type) && el.getClientRects().length && getComputedStyle(el).visibility === 'visible' && (el.type === 'url' || /url/i.test([el.placeholder,el.name,el.id,el.getAttribute('aria-label')].join(' ')))); if (!input || (input.value.trim() && input.value !== imageUrl)) return false; const proto=input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype:HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto,'value').set.call(input,imageUrl); for (const name of ['input','change']) input.dispatchEvent(new Event(name,{bubbles:true})); input.focus(); const clean=new URL(location.href),fragment=new URLSearchParams(clean.hash.slice(1)); if (fragment.has('vm_image_url')) {fragment.delete('vm_image_url'); clean.hash=fragment.toString();} clean.searchParams.delete('vm_image_url'); try {history.replaceState(history.state,document.title,clean.href);} catch {} return true; }; if (!fill()) timer=setInterval(() => { if (fill() || ++attempts >= 240) clearInterval(timer); },250); return true; } function initEscortiSearchSettingsGear() { const url=new URL(location.href); if (url.pathname !== '/search' || url.searchParams.has('vm_mode')) { return; } const ensure=() => { if (document.getElementById('vm-escorti-search-settings-gear')) { return true; } const profilesTab=document.querySelector( 'button[data-tab="profiles"]' ); const host= profilesTab?.parentElement || document.querySelector('.tabs, .tab-nav, main'); if (!host) return false; const gear=makeSettingsTabGearButton( 'results', 'Ustawienia - Lista wyników' ); gear.id='vm-escorti-search-settings-gear'; Object.assign(gear.style,{ width:'25px', height:'25px', marginLeft:'6px', verticalAlign:'middle' }); if (profilesTab) { profilesTab.insertAdjacentElement('afterend',gear); } else { host.prepend(gear); } return true; }; if (ensure()) return; let attempts=0; const timer=setInterval(() => { attempts++; if (ensure() || attempts >= 40) clearInterval(timer); },250); } function parseEscortiSearchSource(value) { const term=String(value || '').trim(); const adUrl=normalizeEscortiAdUrl(term); if (adUrl) return {resolutionMode:'address-escorti',adUrl,phoneDigits:null}; const phoneDigits=/^\+?[\d\s().-]+$/.test(term) && normalizeEscortCachedPhone(term); return phoneDigits ? {resolutionMode:'phone-escorti',phoneDigits,adUrl:null}:null; } function withEscortiSearchSource(href) { try { const page=new URL(location.href),url=new URL(href,page); if (page.pathname !== '/search' || page.searchParams.get('vm_mode') === 'check' || window.top !== window.self || !/^https?:$/.test(url.protocol) || !/^(?:www\.)?escorti\.pl$/i.test(url.hostname) || !/^\/escort\/\d+\/?$/.test(url.pathname)) return href; const source=parseEscortiSearchSource(page.searchParams.get('search')); if (!source) return href; const hash=new URLSearchParams(url.hash.slice(1)); hash.set('vm_escorti_source',source.phoneDigits || source.adUrl); url.hash=hash.toString(); return url.href; } catch {return href;} } function initEscortiSearchSourceLinks() { if (window.top !== window.self || location.pathname !== '/search' || new URL(location.href).searchParams.get('vm_mode') === 'check' || document.__vmEscortiSourceLinks) return; document.__vmEscortiSourceLinks=true; const update=() => { for (const link of document.querySelectorAll('a[href*="/escort/"]')) { const href=withEscortiSearchSource(link.href); if (href !== link.href) link.href=href; } syncEscortiWatchSearchResults(); }; update(); new MutationObserver(createAnimationFrameScheduler(update)).observe(document.documentElement,{childList:true,subtree:true}); } function syncEscortiWatchSearchResults() { const page=new URL(location.href),settings=getWatchSettings(); if (page.pathname !== '/search' || page.searchParams.has('vm_mode') || !settings.enabled) return; const source=parseEscortiSearchSource(page.searchParams.get('search')); const search=getEscortiWatchSearchValue(source); if (!search) return; const profileUrls=getEscortiProfileUrls(); if (!profileUrls.length || getDeclaredProfileCount() !== profileUrls.length) return; const ids=profileUrls.map(getEscortiProfileId).sort().join(','),items=getWatchedItems(); let changed=false; for (const item of items) { if (getEscortiWatchSearchValue(item) !== search || item.profileUrls.map(getEscortiProfileId).sort().join(',') === ids) continue; item.profileUrls=profileUrls; changed=true; } if (changed) saveWatchedItems(items); } function handleEscortiPage() { const url=new URL(location.href); const mode=url.searchParams.get('vm_mode'); const resultKey=url.searchParams.get('vm_key'); const bootKey=url.searchParams.get('vm_boot'); const mergeProfiles=url.searchParams.get('vm_merge') === '1'; const stateKey=url.searchParams.get('vm_state'); initEscortiSearchSourceLinks(); if (mode === 'check' && bootKey) { writeHelperStorageValue(bootKey,{started:true,timestamp:Date.now()}); } if (!mode) return; if (url.pathname === '/search') return processEscortiSearchPage(mode,resultKey,mergeProfiles); if (/^\/escort\/\d+\/?$/.test(url.pathname)) processEscortiProfilePage(mode,resultKey,mergeProfiles,stateKey); } function processEscortiSearchPage(mode,resultKey,mergeProfiles=false) { let attempts=0; const timer=setInterval(() => { attempts++; const profileUrls=getEscortiProfileUrls(); const declaredProfileCount=getDeclaredProfileCount(); if (declaredProfileCount != null && declaredProfileCount !== profileUrls.length) { if (attempts < 40) return; clearInterval(timer); if (mode === 'check') finishEscortiCheck(resultKey,{status:'error',message:'Nie udało się odczytać pełnej listy profili Escorti'}); else removeVmParamsFromCurrentUrl(); return; } if (profileUrls.length === 1 && (declaredProfileCount === 1 || (declaredProfileCount == null && attempts >= 4))) { clearInterval(timer); const profileUrl=new URL(profileUrls[0]); if (mode === 'check') { profileUrl.searchParams.set('vm_mode',mode); if (resultKey) profileUrl.searchParams.set('vm_key',resultKey); if (mergeProfiles) profileUrl.searchParams.set('vm_merge','1'); location.replace(profileUrl.href); } else { removeVmParamsFromCurrentUrl(); location.assign(withEscortiSearchSource(profileUrl.href)); } return; } if (profileUrls.length > 1) { clearInterval(timer); if (mode === 'check' && mergeProfiles) { startMultiProfileFlow(resultKey,profileUrls); return; } if (mode === 'check') finishEscortiCheck(resultKey,{status:'ok',profiles:profileUrls.length}); else removeVmParamsFromCurrentUrl(); return; } if (declaredProfileCount > 0 && attempts < 40) return; if (declaredProfileCount === 0) { clearInterval(timer); if (mode === 'check') finishEscortiCheck(resultKey,{status:'ok',profiles:0}); else removeVmParamsFromCurrentUrl(); return; } if (attempts >= 40) { clearInterval(timer); if (mode === 'check') finishEscortiCheck(resultKey,{status:'timeout'}); else removeVmParamsFromCurrentUrl(); } },150); } function getEscortiProfileUrls(sourceDocument=document,baseUrl=location.href) { const unique=new Set(); for (const link of sourceDocument.querySelectorAll('#tab-profiles a[href], a[href*="/escort/"]')) { try { const url=new URL(link.getAttribute('href'),baseUrl); if ((url.hostname === 'escorti.pl' || url.hostname === 'www.escorti.pl') && /^\/escort\/\d+\/?$/.test(url.pathname)) { url.search=''; url.hash=''; unique.add(url.href); } } catch {} } return [...unique]; } function getDeclaredProfileCount(sourceDocument=document) { const tab=sourceDocument.querySelector('button[data-tab="profiles"]'); if (!tab) return null; for (const span of tab.querySelectorAll('span')) { const text=(span.textContent || '').trim(); if (/^\d+$/.test(text)) return Number(text); } return null; } function extractEscortiCurrentCity(sourceDocument=document) { const breadcrumbItems=[...sourceDocument.querySelectorAll( 'nav[aria-label="breadcrumb"] li' )]; const last=breadcrumbItems.at(-1); return normalizeEscortCity(last?.textContent) || null; } function normalizeEscortiHistoryDate(value) { const text=normalizeEscortAdText(value); const match=text.match( /(\d{4}-\d{2}-\d{2}|\d{2}[.\-/]\d{2}[.\-/]\d{4})/ ); return match ? formatEscortiDate(match[1]):null; } function extractEscortiCityHistory(sourceDocument=document) { const entries=new Map(); const add=(dateValue,cityValue) => { const date=normalizeEscortiHistoryDate(dateValue); const city=normalizeEscortCity(cityValue) .replace(/^(?:miasto|lokalizacja)\s*:?\s*/i,'') .trim(); if (!date || !city || /^brak$/i.test(city)) return; const key=`${date}|${normalizeEscortCityKey(city)}`; if (!entries.has(key)) entries.set(key,{date,city}); }; const locationHeadings=[...sourceDocument.querySelectorAll( 'h1, h2, h3, h4, h5, h6' )].filter(heading => normalizeText(heading.textContent).startsWith('odwiedzane lokalizacje') ); for (const heading of locationHeadings) { const section=heading.parentElement; if (!section) continue; for (const entry of section.querySelectorAll('div > span')) { const dateNode=[...entry.querySelectorAll('span')].find(node => normalizeEscortiHistoryDate(node.textContent) ); if (!dateNode) continue; const cityNode=entry.cloneNode(true); cityNode.querySelectorAll('span').forEach(node => detachHelperNode(node)); add(dateNode.textContent,cityNode.textContent); } } for (const table of sourceDocument.querySelectorAll('table')) { const headers=[...table.querySelectorAll('thead th')] .map(cell => normalizeText(cell.textContent)); if (!headers.length) continue; const dateIndex=headers.findIndex(header => /data|dzień|opublikowano|dodano|okres|pierwsz|ostatn|widzian|(^|\s)od(\s|$)/.test(header) ); const cityIndex=headers.findIndex(header => /miasto|lokalizacj|miejscowość/.test(header) ); if (dateIndex < 0 || cityIndex < 0) continue; for (const row of table.querySelectorAll('tbody tr')) { const cells=[...row.querySelectorAll(':scope > td')]; if (!cells[dateIndex] || !cells[cityIndex]) continue; add(cells[dateIndex].textContent,cells[cityIndex].textContent); } } for (const element of sourceDocument.querySelectorAll( '[data-city][data-date], [data-miasto][data-data], ' + '[data-location][data-date], [data-city][data-created-at]' )) { add( element.getAttribute('data-date') || element.getAttribute('data-data') || element.getAttribute('data-created-at'), element.getAttribute('data-city') || element.getAttribute('data-miasto') || element.getAttribute('data-location') ); } for (const row of sourceDocument.querySelectorAll('tr, li, article')) { const date=normalizeEscortiHistoryDate(row.textContent); if (!date) continue; const cityLink=[...row.querySelectorAll('a[href]')].find(anchor => { try { const url=new URL(anchor.getAttribute('href'),ESCORTI_BASE_URL); return /^\/escorts\/[^/]+\/?$/i.test(url.pathname) && !/\/(?:wszystkie|polska|poland)\/?$/i.test(url.pathname); } catch { return false; } }); if (cityLink) add(date,cityLink.textContent); } return [...entries.values()].sort((a,b) => (profileDateToTime(a.date) || 0) - (profileDateToTime(b.date) || 0) || a.city.localeCompare(b.city,'pl') ); } function makeEscortiProfileResult(info) { return { status:'ok', profiles:1, profileUrl:info.profileUrl, profileUrls:[info.profileUrl], adLinks:info.adLinks, adLinksVersion:info.adLinksVersion, adUrls:info.adUrls, creationDate:info.creationDate, currentCity:info.currentCity, cityHistory:info.cityHistory, profileSummaries:[{ profileUrl:info.profileUrl, profileName:info.profileName, creationDate:info.creationDate, currentCity:info.currentCity, adLinks:info.adLinks, cityHistory:info.cityHistory }], garsoTopics:info.garsoTopics, garsoTopicTitles:info.garsoTopicTitles }; } function processEscortiProfilePage(mode,resultKey,mergeProfiles=false,stateKey=null) { let attempts=0; const timer=setInterval(() => { attempts++; const info=readEscortiProfileInfo(); if (info && (info.adUrls.length > 0 || attempts >= 40)) { clearInterval(timer); const currentProfileUrl=`${location.origin}${location.pathname}`; info.profileUrl=currentProfileUrl; if (mode === 'check' && mergeProfiles && stateKey) { continueMultiProfileFlow(resultKey,stateKey,info,null); return; } if (mode === 'check') { finishEscortiCheck(resultKey,{ ...makeEscortiProfileResult(info), profileName:info.profileName, imageUrl:info.imageUrl }); } else { removeVmParamsFromCurrentUrl(); } return; } if (attempts >= 40) { clearInterval(timer); if (mode === 'check' && mergeProfiles && stateKey) { continueMultiProfileFlow(resultKey,stateKey,null,'Nie udało się odczytać danych profilu'); return; } if (mode === 'check') finishEscortiCheck(resultKey,{status:'error',message:'Nie udało się odczytać danych profilu'}); } },150); } function readEscortiProfileInfo(sourceDocument=document,pageUrl=location.href) { const bodyText=sourceDocument.body ? (sourceDocument.body.innerText || sourceDocument.body.textContent || '') :''; const dateMatch=bodyText.match(/Data utworzenia profilu\s*:?\s*(\d{4}-\d{2}-\d{2}|\d{2}[.\-/]\d{2}[.\-/]\d{4})/i); const section=getEscortiAdLinkSection(sourceDocument); if (!dateMatch || !section) return null; const adUrls=getEscortiAdUrls(sourceDocument,pageUrl,section); const garsoTopicTitles=getEscortiGarsoTopicTitles(sourceDocument); return { creationDate:formatEscortiDate(dateMatch[1]), adUrls, adLinks:adUrls.length, adLinksVersion:1, profileName:extractWatchPageName(sourceDocument,'Profil Escorti'), imageUrl:extractWatchPageImage(sourceDocument,pageUrl), currentCity:extractEscortiCurrentCity(sourceDocument), cityHistory:extractEscortiCityHistory(sourceDocument), garsoTopicTitles, garsoTopics:garsoTopicTitles.length }; } function normalizeEscortiAdUrl(href,baseUrl=location.href) { try { let raw=String(href || '').trim(); if (!raw || /^[?#]/.test(raw)) return null; if (/^(?:(?:[a-z]{2}|www)\.)?escort\.club\//i.test(raw)) raw='https://' + raw; const relativeAd=/^\/?(?:anons|personal-ads)\/\d+\.html(?:[?#]|\/?$)/i.test(raw); const u=new URL(relativeAd ? '/' + raw.replace(/^\//,''):raw,relativeAd ? 'https://pl.escort.club/':baseUrl); if (!/^https?:$/.test(u.protocol) || !isEscortClubHost(u.hostname) || u.username || u.password) return null; const adId=getEscortAdIdFromPath(u.pathname); return adId ? `https://pl.escort.club/anons/${adId}.html`:null; } catch { return null; } } function getEscortiAdLinkSection(sourceDocument=document) { const ignored='.vm-helper-ui,.vm-favorites-ui,[data-vm-watch-wrapper],[id^="vm-"],.vm-escorti-ad-cache-badge,nav,aside,footer,script,template'; const headings=[...sourceDocument.querySelectorAll('h1,h2,h3,h4,h5,h6')].filter(node => !node.closest(ignored)); const index=headings.findIndex(node => /^linki do (?:anonsów|anonsow|ogłoszeń|ogloszen)\s*:?\s*(?:\(\d+\))?$/.test(normalizeText(node.textContent))); const heading=headings[index]; if (!heading) return null; const end=headings[index+1],parent=heading.closest('section,article,.card,.panel') || heading.parentElement,range=sourceDocument.createRange(); if (!end && (!parent || /^(?:BODY|HTML)$/.test(parent.tagName))) return null; range.setStartAfter(heading); if (end && parent.contains(end)) range.setEndBefore(end); else range.setEnd(parent,parent.childNodes.length); const elements=[...range.commonAncestorContainer.querySelectorAll('a[href],[data-href],[data-url],[data-link],[onclick]')] .filter(node => !node.closest(ignored) && !node.contains(heading) && !(end && node.contains(end)) && range.intersectsNode(node)); return {heading,elements}; } function getEscortiAdUrls(sourceDocument=document,baseUrl=location.href,section=getEscortiAdLinkSection(sourceDocument)) { const unique=new Set(); const add=value => {const url=normalizeEscortiAdUrl(value,baseUrl); if (url) unique.add(url);}; for (const node of section?.elements || []) { for (const name of ['href','data-href','data-url','data-link']) add(node.getAttribute(name)); // Nie szukaj adresu anonsu wewnątrz obcego URL; onclick może zawierać bezpośredni cel. for (const match of (node.getAttribute('onclick') || '').replace(/\\\//g,'/').matchAll(/(['"])(.*?)\1/g)) add(match[2]); } return [...unique]; } function findEscortiGarsoTable(sourceDocument=document) { const heading=[...sourceDocument.querySelectorAll('h1,h2,h3,h4,h5,h6')] .find(el => normalizeText(el.textContent).includes('znalezione dyskusje na forum garsoniera')); if (!heading) return null; let box=heading.parentElement; for (let level=0; level < 7 && box; level++,box=box.parentElement) { const table=box.querySelector('table'); if (table) return table; } return null; } function getEscortiGarsoTopicTitles(sourceDocument=document) { const table=findEscortiGarsoTable(sourceDocument); if (!table) return []; const unique=new Map(); for (const row of table.querySelectorAll('tbody > tr')) { const firstCell=row.querySelector('td'); const title=(firstCell?.querySelector('a')?.textContent || firstCell?.textContent || '') .replace(/\s+/g,' ') .trim(); if (!title) continue; const key=normalizeTopicTitleKey(title); if (key && !unique.has(key)) unique.set(key,title); } return [...unique.values()]; } function startMultiProfileFlow(resultKey,profileUrls) { if (!resultKey || !profileUrls.length) { finishEscortiCheck(resultKey,{status:'error',message:'Brak profili do agregacji'}); return; } const stateKey=`${resultKey}_multi`; writeHelperStorageValue(stateKey,{ profileUrls:[...new Set(profileUrls)], index:0, results:[], errors:[] }); openMultiProfileAtIndex(resultKey,stateKey,0); } function openMultiProfileAtIndex(resultKey,stateKey,index) { const state=GM_getValue(stateKey,null); if (!state || !Array.isArray(state.profileUrls) || !state.profileUrls[index]) { finishEscortiCheck(resultKey,{status:'error',message:'Błąd stanu agregacji profili'}); return; } const u=new URL(state.profileUrls[index]); u.searchParams.set('vm_mode','check'); u.searchParams.set('vm_key',resultKey); u.searchParams.set('vm_merge','1'); u.searchParams.set('vm_state',stateKey); location.replace(u.href); } function continueMultiProfileFlow(resultKey,stateKey,info,errorMessage) { const state=GM_getValue(stateKey,null); if (!state || !Array.isArray(state.profileUrls)) { finishEscortiCheck(resultKey,{status:'error',message:'Utracono stan agregacji profili'}); return; } if (info) state.results.push(info); if (errorMessage) state.errors.push({index:state.index,message:errorMessage}); state.index += 1; writeHelperStorageValue(stateKey,state); if (state.index < state.profileUrls.length) { openMultiProfileAtIndex(resultKey,stateKey,state.index); return; } const merged=mergeEscortiProfileResults(state.results,state.profileUrls.length,state.errors.length,state.profileUrls); deleteHelperStorageValue(stateKey); finishEscortiCheck(resultKey,merged); } function normalizeEscortiCityHistory(entries) { return (Array.isArray(entries) ? entries:[]).map(entry => ({ date:formatEscortiDate(entry?.date) || null, city:normalizeEscortCity(entry?.city) || null })).filter(entry => entry.date && entry.city); } function mergeEscortiProfileResults(results,totalProfiles,errorCount=0,allProfileUrls=[]) { if (!results.length) { return { status:'error', profiles:totalProfiles, message:'Nie udało się odczytać żadnego ze znalezionych profili' }; } const adMap=new Map(); const topicMap=new Map(); let earliestDate=null; let earliestTime=null; const profileSummaries=[]; for (const result of results) { for (const url of (result.adUrls || [])) { const normalized=normalizeEscortiAdUrl(url); if (!normalized) continue; const id=normalized.match(/\/anons\/(\d+)\.html$/i)?.[1] || normalized; adMap.set(id,normalized); } for (const title of (result.garsoTopicTitles || [])) { const key=normalizeTopicTitleKey(title); if (key && !topicMap.has(key)) topicMap.set(key,title); } const time=profileDateToTime(result.creationDate); if (time != null && (earliestTime == null || time < earliestTime)) { earliestTime=time; earliestDate=formatEscortiDate(result.creationDate); } profileSummaries.push({ profileUrl:result.profileUrl || null, profileName:result.profileName || null, creationDate:result.creationDate || null, currentCity:result.currentCity || null, adLinks:Number.isFinite(Number(result.adLinks)) ? Number(result.adLinks) :null, cityHistory:normalizeEscortiCityHistory(result.cityHistory) }); } const profileUrls=[...new Set( (allProfileUrls.length ? allProfileUrls:results.map(r => r.profileUrl)) .filter(Boolean) .map(value => { try { const u=new URL(value,ESCORTI_BASE_URL); u.search=''; u.hash=''; return u.href; } catch { return null; } }) .filter(Boolean) )]; return { status:'ok', profiles:totalProfiles, profileUrls, merged:totalProfiles > 1, partial:errorCount > 0, profileResultsRead:results.length, profileErrors:errorCount, adUrls:[...adMap.values()], adLinks:adMap.size, adLinksVersion:results.every(info => info.adLinksVersion === 1) ? 1:0, profileSummaries, cityHistory:profileSummaries.flatMap(profile => profile.cityHistory.map(entry => ({ ...entry, profileUrl:profile.profileUrl })) ), garsoTopicTitles:[...topicMap.values()], garsoTopics:topicMap.size, creationDate:earliestDate }; } function finishEscortiCheck(resultKey,result) { if (resultKey) writeHelperStorageValue(resultKey,{...result,timestamp:Date.now()}); if (window.top === window.self) setTimeout(() => {try {window.close();} catch {}},300); } function removeVmParamsFromCurrentUrl() { const url=new URL(location.href); ['vm_mode','vm_key','vm_boot','vm_merge','vm_state'].forEach(k => url.searchParams.delete(k)); history.replaceState(history.state,document.title,url.href); } async function fetchWatchDocumentDirect(url,cancelToken=null,options={}) { const response=await gmRequest({ method:'GET', url, cancelToken, headers:{ Accept:'text/html,application/xhtml+xml' }, ...options }); const status=Number(response.status) || 0; const html=String(response.responseText || ''); if ((status && (status < 200 || status >= 400)) || !html) { throw new Error(status ? `HTTP ${status}`:'Pusta odpowiedź serwera'); } return { doc:new DOMParser().parseFromString(html,'text/html'), finalUrl:response.finalUrl || url }; } async function searchEscortiProfilesDirect(searchValue,cancelToken=null,requestContext=null) { requestContext ||= createRequestContext('escorti.search'); const searchUrl= `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(searchValue)}`; const {doc,finalUrl}=await fetchWatchDocumentDirect( searchUrl, cancelToken, {requestContext} ); const profileUrls=getEscortiProfileUrls(doc,finalUrl); const declaredProfileCount=getDeclaredProfileCount(doc); if (declaredProfileCount != null && declaredProfileCount !== profileUrls.length) throw new Error('Nie udało się odczytać pełnej listy profili Escorti'); if (profileUrls.length > 0) { return { status:'ok', profiles:profileUrls.length, profileUrls }; } if (declaredProfileCount === 0) { return { status:'ok', profiles:0, profileUrls:[] }; } recordParserIssue( 'Escorti.pl - wyniki wyszukiwania', 'Strona nie zawiera rozpoznanej listy profili ani jednoznacznego licznika zero.' ); throw new Error('Nie udało się odczytać wyników wyszukiwania Escorti'); } async function readEscortiProfileDirectUnshared(profileUrl,cancelToken=null,requestContext=null) { const {doc,finalUrl}=await fetchWatchDocumentDirect( profileUrl, cancelToken, {requestContext} ); const info=readEscortiProfileInfo(doc,finalUrl); if (!info) { recordParserIssue( 'Escorti.pl - profil', 'Nie rozpoznano danych profilu w pobranym dokumencie.' ); throw new Error('Nie udało się odczytać danych profilu Escorti'); } const normalizedProfileUrl=new URL(finalUrl,ESCORTI_BASE_URL); normalizedProfileUrl.search=''; normalizedProfileUrl.hash=''; info.profileUrl=normalizedProfileUrl.href; return info; } const escortiRecentProfileData=new Map(); let escortiRecentProfileEpoch=0; function clearEscortiRecentProfileData() { escortiRecentProfileData.clear(); escortiRecentProfileEpoch++; } async function readEscortiProfileDirect(profileUrl,cancelToken=null,requestContext=null,reuseRecentProfileData=false) { cancelToken?.throwIfCancelled(); requestContext ||= createRequestContext('escorti.profile'); const key=normalizeProfileUrls({profileUrls:[profileUrl]})[0] || profileUrl; const ttlMs=Math.min(5 * 60 * 1000,getListCacheTtlMs()); const cached=escortiRecentProfileData.get(key); if (reuseRecentProfileData && cached && Date.now() - cached.checkedAt < ttlMs) { return copyWatchedValue(cached.info); } escortiRecentProfileData.delete(key); const epoch=escortiRecentProfileEpoch; return runSharedHelperRequest('profile:' + key,async token => { const info=await readEscortiProfileDirectUnshared(key,token,requestContext); token?.throwIfCancelled(); if (reuseRecentProfileData && epoch === escortiRecentProfileEpoch && ttlMs > 0) { const checkedAt=Date.now(); for (const [id,entry] of escortiRecentProfileData) { if (checkedAt - entry.checkedAt >= ttlMs) escortiRecentProfileData.delete(id); } escortiRecentProfileData.set(key,{info:copyWatchedValue(info),checkedAt}); if (escortiRecentProfileData.size > 512) escortiRecentProfileData.delete(escortiRecentProfileData.keys().next().value); } return info; },cancelToken); } async function checkEscortiDirect( searchValue, {mergeProfiles=false,cancelToken=null,requestContext=null,reuseRecentProfileData=false}={} ) { requestContext ||= createRequestContext('escorti.check'); cancelToken?.throwIfCancelled(); const searchResult=await searchEscortiProfilesDirect( searchValue, cancelToken, requestContext ); if (searchResult.profiles === 0) return searchResult; if (mergeProfiles) { const results=[]; const errors=[]; for (const profileUrl of searchResult.profileUrls) { cancelToken?.throwIfCancelled(); try { results.push(await readEscortiProfileDirect( profileUrl, cancelToken, requestContext, reuseRecentProfileData )); } catch (error) { if (isOperationCancelledError(error)) throw error; errors.push(error); } } return mergeEscortiProfileResults( results, searchResult.profiles, errors.length, searchResult.profileUrls ); } if (searchResult.profiles === 1) { const info=await readEscortiProfileDirect( searchResult.profileUrls[0], cancelToken, requestContext, reuseRecentProfileData ); return makeEscortiProfileResult(info); } return searchResult; } function checkEscortiBackground( searchValue, { timeoutMs=60000, mergeProfiles=false, preferDirect=false, cancelToken=null, requestContext=null, reuseRecentProfileData=false }={} ) { cancelToken?.throwIfCancelled(); requestContext ||= createRequestContext('escorti.check'); if (preferDirect) { return checkEscortiDirect(searchValue,{mergeProfiles,cancelToken,requestContext,reuseRecentProfileData}) .then(result => { if (result?.status === 'ok') return result; return checkEscortiBackground(searchValue,{ timeoutMs, mergeProfiles, preferDirect:false, cancelToken, requestContext, reuseRecentProfileData }); }) .catch(error => { if (isOperationCancelledError(error)) throw error; return checkEscortiBackground(searchValue,{ timeoutMs, mergeProfiles, preferDirect:false, cancelToken, requestContext, reuseRecentProfileData }); }); } return new Promise(resolve => { const randomId=`${Date.now()}_${Math.random().toString(36).slice(2)}`; const resultKey=`vm_escorti_result_list_${randomId}`; const bootKey=`vm_escorti_boot_list_${randomId}`; const multiStateKey=`${resultKey}_multi`; deleteHelperStorageValue(resultKey); deleteHelperStorageValue(bootKey); deleteHelperStorageValue(multiStateKey); let finished=false; let iframeStarted=false,trace=null; let fallbackStarted=false; let iframe=null; let fallbackTimer=null; let totalTimer=null; let unsubscribeCancel=() => {}; function cleanup() { if (fallbackTimer) clearTimeout(fallbackTimer); if (totalTimer) clearTimeout(totalTimer); unsubscribeCancel(); try {detachHelperNode(iframe);} catch {} try {GM_removeValueChangeListener(resultListenerId);} catch {} try {GM_removeValueChangeListener(bootListenerId);} catch {} deleteHelperStorageValue(resultKey); deleteHelperStorageValue(bootKey); deleteHelperStorageValue(multiStateKey); } function finish(result) { if (finished) return; finished=true; trace?.(0,result?.status === 'ok' ? 'response':result?.status || 'error'); cleanup(); resolve(result); } const resultListenerId=GM_addValueChangeListener(resultKey,(_n,_o,newValue) => { if (!newValue || finished) return; finish(newValue); }); const bootListenerId=GM_addValueChangeListener(bootKey,(_n,_o,newValue) => { if (!newValue || finished) return; iframeStarted=true; if (fallbackTimer) { clearTimeout(fallbackTimer); fallbackTimer=null; } }); const mergeParam=mergeProfiles ? '&vm_merge=1':''; const iframeUrl=`${ESCORTI_BASE_URL}search?search=${encodeURIComponent(searchValue)}&vm_mode=check&vm_key=${encodeURIComponent(resultKey)}&vm_boot=${encodeURIComponent(bootKey)}${mergeParam}`; iframe=makeElement('iframe'); iframe.src=iframeUrl; prepareHiddenHelperFrame(iframe); trace=recordDiagnosticRequest(iframeUrl,'IFRAME',requestContext,'iframe'); document.body.appendChild(iframe); unsubscribeCancel=cancelToken?.onCancel(() => { finish({status:'cancelled'}); }) || (() => {}); fallbackTimer=setTimeout(() => { if (finished || iframeStarted || fallbackStarted) return; fallbackStarted=true; trace?.(0,'cancelled'); try {detachHelperNode(iframe); iframe=null;} catch {} checkEscortiDirect(searchValue,{mergeProfiles,cancelToken,requestContext,reuseRecentProfileData}) .then(finish) .catch(error => finish(isOperationCancelledError(error) ? {status:'cancelled'} :{ status:'error', message:error?.message || 'Nie udało się odczytać Escorti' })); },4000); totalTimer=setTimeout(() => finish({status:'timeout'}),timeoutMs); }); } function listCacheKey(adId) { return `vm_escorti_list_${adId}`; } function getEscortiProfileId(value) { try { const url=new URL(value,ESCORTI_BASE_URL); return url.pathname.match(/^\/escort\/(\d+)\/?$/i)?.[1] || null; } catch { return null; } } function buildEscortiProfileUrl(profileId) { return `${ESCORTI_BASE_URL}escort/${profileId}/`; } function normalizeProfileUrls(result) { const values=Array.isArray(result?.profileUrls) ? result.profileUrls :(result?.profileUrl ? [result.profileUrl]:[]); const unique=new Set(); for (const value of values) { try { const url=new URL(value,ESCORTI_BASE_URL); if ( !['escorti.pl','www.escorti.pl'].includes(url.hostname.toLowerCase()) || !/^\/escort\/\d+\/?$/i.test(url.pathname) ) { continue; } url.protocol='https:'; url.hostname='escorti.pl'; url.pathname=`${url.pathname.replace(/\/+$/,'')}/`; url.search=''; url.hash=''; unique.add(url.href); } catch {} } return [...unique]; } function getCachedEscortiAdUrlsForProfiles(profileUrls) { const wantedProfileIds=[...new Set( (Array.isArray(profileUrls) ? profileUrls:[]) .map(getEscortiProfileId) .filter(Boolean) )]; if (!wantedProfileIds.length) return []; const index=getEscortiProfileAdIndexMemory(); const adIds=new Set(); for (const profileId of wantedProfileIds) { for (const adId of index.get(profileId) || []) adIds.add(adId); } return [...adIds].map(id => `https://pl.escort.club/anons/${id}.html`); } function normalizeEscortListProfileSummary(profile,compact=false) { const profileId=compact ? getEscortiProfileId(profile?.profileUrl) :String(profile?.profileId || '').trim(); if (!/^\d+$/.test(String(profileId || ''))) return null; const cityHistory=normalizeEscortiCityHistory(profile?.cityHistory); const common={ profileName:normalizeEscortAdText(profile?.profileName) || null, creationDate:formatEscortiDate(profile?.creationDate) || null, currentCity:normalizeEscortCity(profile?.currentCity) || null, adLinks:Number.isFinite(Number(profile?.adLinks)) ? Number(profile.adLinks) :null, cityHistory }; return compact ? {profileId,...common} :{profileUrl:buildEscortiProfileUrl(profileId),...common}; } function compactEscortListResult(result) { const profileUrls=Array.isArray(result?.profileUrls) ? result.profileUrls :(result?.profileUrl ? [result.profileUrl]:[]); const profileIds=[...new Set(profileUrls.map(getEscortiProfileId).filter(Boolean))]; const adIds=[...new Set( (Array.isArray(result?.adUrls) ? result.adUrls:[]) .map(parseAdIdFromUrl) .filter(Boolean) )]; const rawProfileSummaries=Array.isArray(result?.profileSummaries) ? result.profileSummaries :(profileIds.length === 1 ? [{ profileUrl:profileUrls[0], profileName:result?.profileName || null, creationDate:result?.creationDate || null, currentCity:result?.currentCity || null, adLinks:result?.adLinks, cityHistory:result?.cityHistory || [] }]:[]); const profileSummaries=rawProfileSummaries .map(profile => normalizeEscortListProfileSummary(profile,true)) .filter(Boolean); return { profileIds, adIds, adLinksVersion:result?.adLinksVersion || 0, creationDate:result?.creationDate || null, profileSummaries, garsoTopicTitles:Array.isArray(result?.garsoTopicTitles) ? result.garsoTopicTitles:null, garsoTopics:Number.isFinite(result?.garsoTopics) ? result.garsoTopics :null, partial:!!result?.partial, checkedAt:Date.now() }; } function expandEscortListCacheEntry(entry,searchMode) { if (!entry || typeof entry !== 'object' || !entry.checkedAt) return null; if (searchMode !== 'phone-escort-search' && entry.adIds?.length && entry.adLinksVersion !== 1) return null; const profileIds=[...new Set( (Array.isArray(entry.profileIds) ? entry.profileIds:[]) .map(value => String(value || '').trim()) .filter(value => /^\d+$/.test(value)) )]; const adIds=[...new Set( (Array.isArray(entry.adIds) ? entry.adIds:[]) .map(value => String(value || '').trim()) .filter(value => /^\d+$/.test(value)) )]; const profileUrls=profileIds.map(buildEscortiProfileUrl); const adUrls=adIds.map(id => `https://pl.escort.club/anons/${id}.html`); const profileSummaries=(Array.isArray(entry.profileSummaries) ? entry.profileSummaries :[]) .map(profile => normalizeEscortListProfileSummary(profile)) .filter(Boolean); return { status:'ok', searchMode, adLinksVersion:entry.adLinksVersion, profileIds, profileUrls, profiles:profileIds.length, merged:profileIds.length > 1, partial:!!entry.partial, adIds, adUrls, adLinks:adIds.length, creationDate:entry.creationDate || null, profileSummaries, currentCity:profileSummaries.length === 1 ? profileSummaries[0].currentCity :null, cityHistory:profileSummaries.flatMap(profile => profile.cityHistory.map(history => ({ ...history, profileUrl:profile.profileUrl })) ), garsoTopicTitles:Array.isArray(entry.garsoTopicTitles) ? entry.garsoTopicTitles:null, garsoTopics:Number.isFinite(entry.garsoTopics) ? entry.garsoTopics :null, checkedAt:Number(entry.checkedAt) }; } function getListCache(adId,searchMode=getEscortListSearchMode()) { try { const store=readPersistentCacheValue(listCacheKey(adId),null); return expandEscortListCacheEntry(store?.modes?.[searchMode],searchMode); } catch { return null; } } function setListCache(adId,result,searchMode=getEscortListSearchMode()) { if (!persistentCacheKeyIsLoaded(listCacheKey(adId))) { ensurePersistentCacheKeys([listCacheKey(adId)]).then(() => setListCache(adId,result,searchMode)).catch(error => log('Nie udało się scalić zapisu cache',error)); return null; } if (result?.status !== 'ok') return; try { rememberHiddenResultAd(adId,result); const existing=readPersistentCacheValue(listCacheKey(adId),null); const modes=existing?.modes && typeof existing.modes === 'object' ? {...existing.modes} :{}; modes[searchMode]=compactEscortListResult(result); writePersistentCacheValue(listCacheKey(adId),{modes}); registerEscortDataFieldsFromEscortiResult(result); invalidateEscortiProfileAdIndexMemory(); if (String(getAdIdFromUrl()) === String(adId)) refreshHiddenItemsUi(); } catch (e) { log('Nie udało się zapisać cache listy',e); } } function isListCacheFresh(cache) { return !!(cache && cache.checkedAt && Date.now() - cache.checkedAt < getListCacheTtlMs()); } function parseAdIdFromUrl(url) { const canonical=normalizeEscortiAdUrl(url); return canonical ? getEscortAdIdFromPath(new URL(canonical).pathname):null; } function getRelatedAdsTileModeLabel(mode) { return { 'address-escorti':'Escorti.pl wg adresu anonsu', 'phone-escorti':'Escorti.pl wg numeru telefonu', 'phone-escort-search':'Escort.club wg numeru telefonu' }[normalizeEscortRelatedAdsTileLinkMode(mode)] || 'wybrana metoda'; } function showRelatedAdsTileReport(report,error=false) { showWatchToast(report,error,{ id:'vm-related-ads-tile-report',duration:error ? 6500:8000, style:{maxWidth:'420px',fontSize:'12px',lineHeight:'1.45',whiteSpace:'pre-line'} }); } async function getTileRelatedAdUrls(adId,adUrl,mode) { const normalizedMode=normalizeEscortRelatedAdsTileLinkMode(mode); const requestContext=createRequestContext('tile.related'); await ensureEscortPageCache([adId]); const cached=getListCache(adId,normalizedMode); if (isListCacheFresh(cached) && Array.isArray(cached.adUrls)) { return { urls:[...new Set(cached.adUrls.map(url => normalizeEscortiAdUrl(url) || url ).filter(Boolean))], source:'cache' }; } let phone=''; if (normalizedMode !== 'address-escorti') { let adData=getStoredEscortAdData(adId); phone=isEscortAdPhoneFresh(adData) ? getEscortAdPhoneDigits(adData):null; if (!phone) { adData=await getEscortAdData(adId,adUrl,false,null,requestContext); phone=isEscortAdPhoneFresh(adData) ? getEscortAdPhoneDigits(adData):null; } if (!phone) { throw new Error('Nie udało się odczytać numeru telefonu z tego anonsu.'); } } if (normalizedMode === 'phone-escort-search') { const found=await fetchEscortClubPhoneSearchAds(phone,null,requestContext); const result={ status:'ok', profileUrls:[], adUrls:found.adUrls || [] }; setListCache(adId,result,normalizedMode); return { urls:[...new Set(result.adUrls.map(url => normalizeEscortiAdUrl(url) || url ).filter(Boolean))], source:'wyszukiwanie' }; } const searchValue=normalizedMode === 'phone-escorti' ? phone:adUrl; const result=await getOrStartListCheck( adId, searchValue, normalizedMode, null, requestContext ); if (result?.status !== 'ok') { throw new Error(result?.message || 'Nie udało się znaleźć profilu tej osoby.'); } return { urls:[...new Set((result.adUrls || []).map(url => normalizeEscortiAdUrl(url) || url ).filter(Boolean))], source:'wyszukiwanie' }; } async function openAllActiveRelatedAdsFromTile(adId,adUrl,mode,onProgress=null) { const listResult=await getTileRelatedAdUrls(adId,adUrl,mode); const adUrls=listResult.urls; if (!adUrls.length) { return { found:0, active:0, opened:0, inactive:0, unknown:0, skippedByLimit:0, source:listResult.source }; } if (onProgress) onProgress({phase:'activity',checked:0,total:adUrls.length}); const summary=await scanActiveEscortAds( adUrls, progress => onProgress?.({phase:'activity',...progress}), false, {activityOnly:true,useActivityCache:true,cacheActivity:true} ); const activeUrls=[...new Set( (summary?.results || []) .filter(result => result?.active === true) .map(result => normalizeEscortiAdUrl(result.url) || result.url) .filter(Boolean) )]; const maxTabs=Math.max( 1, Math.floor(Number(SETTINGS.maxRelatedAdsTabs) || DEFAULT_SETTINGS.maxRelatedAdsTabs) ); const toOpen=activeUrls.slice(0,maxTabs); for (const url of toOpen) { GM_openInTab(url,{active:false,insert:true}); } return { found:adUrls.length, active:activeUrls.length, opened:toOpen.length, inactive:Math.max( 0, (summary?.totalChecked || 0) - activeUrls.length - (summary?.unknownCount || 0) ), unknown:summary?.unknownCount || 0, skippedByLimit:Math.max(0,activeUrls.length - toOpen.length), source:listResult.source }; } function addRelatedAdsTileLinkButton(card,adId,adUrl) { let btn=card.querySelector('.vm-related-ads-open-button'); if (btn) return btn; btn=makeElement('button'); btn.type='button'; btn.className='vm-related-ads-open-button'; btn.textContent=SETTINGS.openAllActiveRelatedAdsFromTile ? 'Anonse ↗':'Profil ↗'; const mode=normalizeEscortRelatedAdsTileLinkMode( SETTINGS.relatedAdsTileLinkMode ); btn.title=SETTINGS.openAllActiveRelatedAdsFromTile ? 'Otwórz wszystkie aktywne ogłoszenia tej osoby w nowych kartach' :(mode === 'address-escorti' ? 'Otwórz profil tej osoby w Escorti.pl według adresu anonsu' :(mode === 'phone-escorti' ? 'Otwórz profil tej osoby w Escorti.pl według numeru telefonu' :'Otwórz listę ogłoszeń tej osoby w wyszukiwarce Escort.club według numeru telefonu')); if (getComputedStyle(card).position === 'static') { card.style.position='relative'; } Object.assign(btn.style,{ position:'absolute', top:'4px', right:'2px', zIndex:'20', padding:'2px 5px', border:'1px solid rgba(255,255,255,.65)', borderRadius:'4px', background:'rgba(20,20,20,.72)', color:'#ffffff', fontSize:'9px', lineHeight:'1.2', fontWeight:'700', cursor:'pointer', boxShadow:'0 1px 3px rgba(0,0,0,.35)' }); btn.addEventListener('click',async e => { e.preventDefault(); e.stopPropagation(); const selectedMode=normalizeEscortRelatedAdsTileLinkMode( SETTINGS.relatedAdsTileLinkMode ); if (SETTINGS.openAllActiveRelatedAdsFromTile) { const previousText=btn.textContent; btn.disabled=true; btn.textContent='Szukam…'; try { const report=await openAllActiveRelatedAdsFromTile( adId, adUrl, selectedMode, progress => { if (progress.phase === 'activity') { btn.textContent=progress.total ? `${progress.checked}/${progress.total}` :'Sprawdzam…'; } } ); const lines=[ `Znaleziono: ${formatPolishCount(report.found,'anons','anonse','anonsów')} • aktywne: ${report.active}`, `Metoda: ${getRelatedAdsTileModeLabel(selectedMode)} • lista: ${report.source === 'cache' ? 'z cache':'wyszukana teraz'}`, report.opened ? `Otwieram: ${formatPolishCount(report.opened,'kartę','karty','kart')}` :'Nie otwarto żadnej karty' ]; const omitted=[]; if (report.skippedByLimit) { omitted.push( `${formatPolishCount(report.skippedByLimit,'aktywny anons','aktywne anonse','aktywnych anonsów')} pominięto przez limit` ); } if (report.inactive) { omitted.push( `${formatPolishCount(report.inactive,'nieaktywny','nieaktywne','nieaktywnych')}` ); } if (report.unknown) { omitted.push( `${formatPolishCount(report.unknown,'nierozpoznany','nierozpoznane','nierozpoznanych')}` ); } if (omitted.length) lines.push(`Nie otwieram: ${omitted.join(' • ')}`); showRelatedAdsTileReport(lines.join('\n'),report.opened === 0); } catch (error) { log('Nie udało się otworzyć aktywnych ogłoszeń z kafelka',error); showRelatedAdsTileReport( error?.message || 'Nie udało się znaleźć aktywnych ogłoszeń tej osoby.', true ); } finally { btn.disabled=false; btn.textContent=previousText; } return; } if (selectedMode === 'address-escorti') { GM_openInTab( buildEscortiSearchUrl(adUrl), {active:true,insert:true} ); return; } const previousText=btn.textContent; btn.disabled=true; btn.textContent='…'; try { let adData=getStoredEscortAdData(adId); let phone=isEscortAdPhoneFresh(adData) ? getEscortAdPhoneDigits(adData):null; if (!phone) { adData=await getEscortAdData(adId,adUrl,false); phone=isEscortAdPhoneFresh(adData) ? getEscortAdPhoneDigits(adData):null; } if (!phone) { window.alert('Nie udało się odczytać numeru telefonu z tego anonsu.'); return; } const url=selectedMode === 'phone-escorti' ? buildEscortiPhoneSearchUrl(phone) :buildEscortClubPhoneSearchUrl(phone); if (url) GM_openInTab(url,{active:true,insert:true}); } catch (error) { log('Nie udało się otworzyć profilu z kafelka',error); window.alert('Nie udało się otworzyć profilu tej osoby.'); } finally { btn.disabled=false; btn.textContent=previousText; } }); card.appendChild(btn); return btn; } function getEscortProfileKeys(result) { if (!result || result.status !== 'ok' || result.profiles === 0) return []; const urls=Array.isArray(result.profileUrls) ? result.profileUrls :(result.profileUrl ? [result.profileUrl]:[]); const keys=new Set(); for (const value of urls) { try { const u=new URL(value,ESCORTI_BASE_URL); const m=u.pathname.match(/^\/escort\/(\d+)\/?$/i); if (m) keys.add(`escort:${m[1]}`); else keys.add(`${u.hostname.toLowerCase()}${u.pathname.replace(/\/$/,'')}`); } catch {} } return [...keys]; } function buildEscortListCacheIndex(keys=null,index=null) { index ||= { byAdId:new Map(), freshProfileToAdIds:new Map(), adIdToFreshProfileKeys:new Map() }; const listKeyPattern=/^vm_escorti_list_(\d+)$/i; const requiredSearchMode=getEscortListSearchMode(); for (const key of keys || getEscortListCacheKeys()) { const match=key.match(listKeyPattern); if (!match) continue; const adId=match[1]; try { const store=readPersistentCacheValue(key,null); const cache=expandEscortListCacheEntry( store?.modes?.[requiredSearchMode], requiredSearchMode ); if (!cache) continue; const relatedAdIds=[...new Set([ adId, ...(Array.isArray(cache.adIds) ? cache.adIds:[]) ].map(String).filter(value => /^\d+$/.test(value)))]; for (const relatedAdId of relatedAdIds) { const existing=index.byAdId.get(relatedAdId); if (!existing || Number(cache.checkedAt) > Number(existing.checkedAt || 0)) { index.byAdId.set(relatedAdId,cache); } } if (!isListCacheFresh(cache)) continue; const profileKeys=getEscortProfileKeys(cache); if (!profileKeys.length) continue; for (const relatedAdId of relatedAdIds) { const relatedKeys=index.adIdToFreshProfileKeys.get(relatedAdId) || new Set(); for (const profileKey of profileKeys) { relatedKeys.add(profileKey); if (!index.freshProfileToAdIds.has(profileKey)) { index.freshProfileToAdIds.set(profileKey,new Set()); } index.freshProfileToAdIds.get(profileKey).add(relatedAdId); } index.adIdToFreshProfileKeys.set(relatedAdId,relatedKeys); } } catch {} } return index; } function removeAdFromFreshCacheProfileIndex(index,adId) { const oldKeys=index.adIdToFreshProfileKeys.get(adId); if (!oldKeys) return; for (const profileKey of oldKeys) { const ids=index.freshProfileToAdIds.get(profileKey); if (!ids) continue; ids.delete(adId); if (!ids.size) { index.freshProfileToAdIds.delete(profileKey); } } index.adIdToFreshProfileKeys.delete(adId); } function updateEscortListCacheIndex(index,adId,result) { const relatedAdIds=[...new Set([ String(adId || ''), ...(Array.isArray(result?.adIds) ? result.adIds:[]) ].map(String).filter(value => /^\d+$/.test(value)))]; for (const relatedAdId of relatedAdIds) { removeAdFromFreshCacheProfileIndex(index,relatedAdId); } if (!result || result.status !== 'ok') { return; } for (const relatedAdId of relatedAdIds) { index.byAdId.set(relatedAdId,result); } const profileKeys=getEscortProfileKeys(result); if (!profileKeys.length) return; for (const relatedAdId of relatedAdIds) { index.adIdToFreshProfileKeys.set(relatedAdId,new Set(profileKeys)); for (const profileKey of profileKeys) { if (!index.freshProfileToAdIds.has(profileKey)) { index.freshProfileToAdIds.set(profileKey,new Set()); } index.freshProfileToAdIds.get(profileKey).add(relatedAdId); } } } function normalizeEscortCachedPhone(value) { const digits=digitsOnly(value); if (digits.length < 8 || digits.length > 15) return null; if (digits.length > 7 && digits.length % 2 === 0) { const half=digits.length / 2; if (digits.slice(0,half) === digits.slice(half)) return null; } return digits; } function getEscortAdPhoneDigits(data) { for (const value of [data?.phoneDigits,data?.phone]) { const digits=normalizeEscortCachedPhone(value); if (digits) return digits; } return null; } function sanitizeEscortAdDataPhone(value) { if (!value || typeof value !== 'object') return value; const phone=getEscortAdPhoneDigits(value); const rawValues=[value.phoneDigits,value.phone] .map(candidate => String(candidate ?? '').trim()) .filter(Boolean); const malformedNumericPhone=rawValues.some(candidate => /^[+\d\s()./-]+$/.test(candidate) && digitsOnly(candidate) && !normalizeEscortCachedPhone(candidate) ); if (phone) { return normalizeEscortCachedPhone(value.phoneDigits) === phone ? value :{...value,phoneDigits:phone}; } return malformedNumericPhone ? { ...value, phone:null, phoneDigits:null, phoneFetchStatus:'invalid-cache' } :value; } function addEscortPhoneIndexEntry(index,adId,phone) { index.adIdToPhone.set(adId,phone); const ids=index.phoneToAdIds.get(phone) || new Set(); ids.add(adId); index.phoneToAdIds.set(phone,ids); } function buildEscortAdPhoneCacheIndex(keys=null,index=null) { index ||= { phoneToAdIds:new Map(), adIdToPhone:new Map() }; const adDataKeyPattern=/^vm_escort_ad_data_(\d+)$/i; for (const key of keys || getEscortAdDataCacheKeys()) { const match=key.match(adDataKeyPattern); if (!match) continue; const adId=match[1]; try { const cache=readPersistentCacheValue(key,null); if ( !cache || cache.status !== 'ok' ) { continue; } const phone=isEscortAdPhoneFresh(cache) ? getEscortAdPhoneDigits(cache):null; if (!phone) continue; addEscortPhoneIndexEntry(index,adId,phone); } catch {} } return index; } function updateEscortAdPhoneCacheIndex(index,adId,adData) { const oldPhone=index.adIdToPhone.get(adId); if (oldPhone) { const oldIds=index.phoneToAdIds.get(oldPhone); oldIds?.delete(adId); if (oldIds && !oldIds.size) index.phoneToAdIds.delete(oldPhone); index.adIdToPhone.delete(adId); } const phone=adData?.status === 'ok' && isEscortAdPhoneFresh(adData) ? getEscortAdPhoneDigits(adData) :null; if (!phone) return; addEscortPhoneIndexEntry(index,adId,phone); } function polishCountForm(value,one,few,many) { const n=Math.abs(Math.trunc(Number(value) || 0)); if (n === 1) return one; const last=n % 10; const lastTwo=n % 100; return last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14) ? few :many; } function formatPolishCount(value,one,few,many) { const n=Math.max(0,Math.trunc(Number(value) || 0)); return `${n} ${polishCountForm(n,one,few,many)}`; } function polishAdWord(n) { return polishCountForm(n,'anons','anonse','anonsów'); } function getEscortListCardAgeAndCity(card) { const text=normalizeEscortAdText(card?.querySelector('.item-stats')?.textContent); const match=text.match(/^(.*?),\s*(\d{1,3})\s*y(?:\b|$)/i); return { city:match?.[1] ? normalizeText(match[1]):null, age:match?.[2] ? Number(match[2]):null }; } function getEscortStatDifferenceLabels(stats) { return Array.isArray(stats) ? (stats.length > 2 ? ['wymiary']:stats):[]; } function getEscortListGroupDataDifferences(members,cachedOnlyAdData=[]) { const cities=new Set(); const ages=new Set(); const statValues=new Map(); const selectedPriceValues=new Set(); const selectedPriceDuration=normalizeEscortPriceDuration( SETTINGS.searchResultPriceDuration ); function collectAdData(adData,preferredCity=null,preferredAge=null) { const stats=adData?.stats && typeof adData.stats === 'object' ? adData.stats :null; const city=normalizeText(preferredCity || adData?.location?.city); if (city) cities.add(city); let age=preferredAge; if (!Number.isFinite(age) && stats) { const ageEntry=Object.entries(stats).find(([label]) => normalizeEscortChangeComparisonValue(label).replace(/:\s*$/,'') === 'wiek' ); const ageMatch=normalizeEscortAdText(ageEntry?.[1]).match(/\d{1,3}/); age=ageMatch ? Number(ageMatch[0]):null; } if (Number.isFinite(age)) ages.add(age); if (!adData || adData.status !== 'ok') return; for (const [rawLabel,rawValue] of Object.entries(stats || {})) { const label=normalizeEscortChangeComparisonValue(rawLabel) .replace(/:\s*$/,''); if ( !label || label === 'wiek' || normalizeEscortClubPriceDurationKey(label) ) { continue; } const value=normalizeEscortChangeComparisonValue(rawValue); if (!value) continue; if (!statValues.has(label)) statValues.set(label,new Set()); statValues.get(label).add(value); } const price=adData.prices?.[selectedPriceDuration]; const hasAmount=price?.amount != null && Number.isFinite(Number(price.amount)); const currency=normalizeEscortAdText(price?.currency).toUpperCase(); selectedPriceValues.add( hasAmount ? `${Number(price.amount)}|${currency}` :'__brak__' ); } for (const member of members || []) { const cardData=getEscortListCardAgeAndCity(member?.card); const adData=member?.adData?.status === 'ok' ? member.adData:null; collectAdData(adData,cardData.city,cardData.age); } for (const adData of cachedOnlyAdData || []) { if (adData?.status === 'ok') collectAdData(adData); } const stats=[...statValues.entries()] .filter(([,values]) => values.size > 1) .map(([label]) => label); return { city:cities.size > 1, age:ages.size > 1, stats, price:selectedPriceValues.size > 1 }; } function getListAggregateLine(line) { if (!line?.parentElement) return null; let aggregateLine=line.parentElement.querySelector('.item-escorti-aggregate'); if (aggregateLine) return aggregateLine; aggregateLine=makeElement('span'); aggregateLine.className='item-escorti-aggregate'; Object.assign(aggregateLine.style,{ display:'none', width:'fit-content', maxWidth:'100%', marginTop:'2px', padding:'2px 4px', border:'1px solid rgba(255,255,255,.68)', borderRadius:'4px', background:'rgba(0,0,0,.18)', boxSizing:'border-box', fontSize:'9px', lineHeight:'1.15', fontWeight:'600', color:'#ffffff', whiteSpace:'normal', textShadow:'0 1px 2px rgba(0,0,0,.75)' }); aggregateLine.addEventListener('click',e => { e.preventDefault(); e.stopPropagation(); if (typeof aggregateLine._vmToggleHandler === 'function') { aggregateLine._vmToggleHandler(); } }); line.insertAdjacentElement('afterend',aggregateLine); return aggregateLine; } function setListMergedInfo( line, activeCount, expanded=false, onToggle=null, cachedOnlyCount=0, dataDifferences=null ) { if (!line) return; const count=Number(activeCount) || 0; const cacheCount=Math.max(0,Number(cachedOnlyCount) || 0); const listCount=Math.max(0,count - cacheCount); const aggregateLine=getListAggregateLine(line); if (!aggregateLine) return; aggregateLine._vmToggleHandler=typeof onToggle === 'function' ? onToggle:null; if (count > 1) { const arrow=onToggle ? ` ${expanded ? '▲':'▼'}`:''; const topLine=makeElement('span','',`Zagregowano ${listCount} ${polishAdWord(listCount)}${arrow}`); Object.assign(topLine.style,{ display:'block', fontWeight:'700', whiteSpace:'nowrap' }); aggregateLine.replaceChildren(topLine); if (cacheCount > 0) { const hiddenWord=polishCountForm(cacheCount,'niewidoczny','niewidoczne','niewidocznych'); const sourceLine=makeElement('span','',`(+${cacheCount} ${hiddenWord} w tych wynikach)`); Object.assign(sourceLine.style,{ display:'block', marginTop:'1px', fontSize:'8px', fontWeight:'500', whiteSpace:'nowrap', opacity:'.92' }); aggregateLine.appendChild(sourceLine); } const differenceFields=[]; if (dataDifferences?.age) differenceFields.push('wiek'); if (dataDifferences?.city) differenceFields.push('miasto'); differenceFields.push(...getEscortStatDifferenceLabels(dataDifferences?.stats)); if (dataDifferences?.price) differenceFields.push('cena'); if (differenceFields.length) { const warningLine=makeElement('span','',`⚠ Niezgodne: ${differenceFields.join(', ')}`); if (dataDifferences?.price) { warningLine.title= `Porównano cenę za ${getEscortPriceDurationLabel(SETTINGS.searchResultPriceDuration)}.`; } Object.assign(warningLine.style,{ display:'block', width:'fit-content', maxWidth:'100%', marginTop:'2px', padding:'1px 3px', borderRadius:'3px', background:'#dc3545', color:'#fff', fontSize:'8px', fontWeight:'700', lineHeight:'1.2', whiteSpace:'nowrap', textShadow:'none' }); aggregateLine.appendChild(warningLine); } aggregateLine.style.display='inline-block'; aggregateLine.style.cursor=onToggle ? 'pointer':'default'; aggregateLine.style.textDecoration='none'; const toggleTitle=onToggle ? (expanded ? 'Kliknij, aby ukryć scalone anonse':'Kliknij, aby pokazać scalone anonse') :''; const cacheTitle=cacheCount > 0 ? `${cacheCount} ${polishAdWord(cacheCount)} ${cacheCount === 1 ? 'nie jest widoczny':'nie są widoczne'} w tych wynikach i nie ma kafelków na obecnie załadowanej liście.` :''; const differencesTitle=differenceFields.length ? `Zagregowane kafelki mają różne wartości: ${differenceFields.join(' i ')}.` :''; aggregateLine.title=[toggleTitle,cacheTitle,differencesTitle] .filter(Boolean) .join('\n'); } else { aggregateLine.replaceChildren(); aggregateLine.style.display='none'; aggregateLine.style.cursor='default'; aggregateLine.style.textDecoration='none'; aggregateLine.title=''; } } function renderListFavoriteStar(card,favorite) { let star=card.querySelector('.vm-list-favorite-star'); if (!favorite) {detachHelperNode(star); return;} if (star) return; const name=card.querySelector('.item-name, .item-info'); if (!name) return; star=makeElement('span','vm-list-favorite-star','★'); star.title='W Ulubionych - anons lub jego profil'; star.setAttribute('role','img'); star.setAttribute('aria-label',star.title); Object.assign(star.style,{color:'#ffd54f',marginRight:'4px',textShadow:'0 1px 2px #000',fontSize:'24px',lineHeight:'1'}); name.prepend(star); } function getListCardDataLine(card) { let line=card.querySelector('.item-escorti-stats'); if (line) return line; const info=card.querySelector('.item-info'); if (!info) return null; line=makeElement('span'); line.className='item-escorti-stats'; Object.assign(line.style,{ display:'block', marginTop:'2px', fontSize:'9px', lineHeight:'1.15', fontWeight:'600', color:'#ffffff', whiteSpace:'nowrap', textShadow:'0 1px 2px rgba(0,0,0,.75)' }); line.textContent='… ogł • od … • … opinii'; info.appendChild(line); return line; } function getListCardPriceLine(card) { let line=card.querySelector('.vm-escort-tile-price'); if (line) return line; if (getComputedStyle(card).position === 'static') { card.style.position='relative'; } line=makeElement('span'); line.className='vm-escort-tile-price'; Object.assign(line.style,{ display:'block', position:'absolute', top:SETTINGS.showRelatedAdsTileLink ? '22px':'4px', right:'2px', zIndex:'20', width:'fit-content', maxWidth:'100%', margin:'0', padding:'2px 5px', border:'1px solid rgba(255,255,255,.65)', borderRadius:'4px', background:'rgba(20,20,20,.72)', color:'#ffffff', fontSize:'9px', lineHeight:'1.2', fontWeight:'700', whiteSpace:'nowrap', textShadow:'0 1px 2px rgba(0,0,0,.55)', boxSizing:'border-box', boxShadow:'0 1px 3px rgba(0,0,0,.35)', pointerEvents:'none' }); card.appendChild(line); return line; } function formatEscortTilePrice(price) { if ( !price || price.amount == null || price.amount === '' || !Number.isFinite(Number(price.amount)) ) { return ''; } const amount=new Intl.NumberFormat('pl-PL',{ minimumFractionDigits:0, maximumFractionDigits:2 }).format(Number(price.amount)); const currencyCode=String(price.currency || '').toUpperCase(); const currency={ PLN:'zł', EUR:'€', USD:'$', GBP:'£' }[currencyCode] || price.currency || ''; return `${amount}${currency ? ` ${currency}`:''}`; } function formatEscortAggregatePrices(prices) { const groups=new Map(),unique=new Set(); let count=0; for (const price of prices) { if (!['string','number'].includes(typeof price?.amount) || String(price.amount).trim() === '') continue; const formatted=formatEscortTilePrice(price); if (!formatted) continue; count++; unique.add(formatted); const currency=String(price.currency || '').trim().toUpperCase(),amount=Number(price.amount); const range=groups.get(currency); groups.set(currency,range ? [Math.min(range[0],amount),Math.max(range[1],amount)]:[amount,amount]); } const compact=[...groups].map(([currency,[min,max]]) => { const low=formatEscortTilePrice({amount:min,currency}),high=formatEscortTilePrice({amount:max,currency}); return low === high ? high:`${formatEscortTilePrice({amount:min})}–${high}`; }).join(' • '); return {compact,full:[...unique].join(' • '),count}; } function formatEscortAggregateField(descriptor,rawValues) { if (descriptor.kind === 'price') return formatEscortAggregatePrices(rawValues); const format=value => {try {return formatEscortUniversalDataValue(descriptor,value,false);} catch {return '';}}; const present=value => value != null && (typeof value !== 'string' || value.trim() !== '') && (typeof value !== 'number' || Number.isFinite(value)) && (typeof value !== 'object' || Object.keys(value).length > 0); const sources=rawValues.map(value => (Array.isArray(value) ? value.flat():[value]).filter(value => present(value) && !!format(value))); const values=sources.flat(); const unique=[...new Set(values.map(format).filter(Boolean))]; let compact=unique.join(' • '); const date=['date','datetime','relative-time'].includes(descriptor.kind); const identity=descriptor.path.some(key => /phone|telefon|(?:^|[_-])id$|Id$|Ids$/i.test(key)); const stat=descriptor.path[0] === 'stats' && /^(?:wiek|age|wzrost|height|waga|weight|bmi|ocena|rating|score)$/i.test(descriptor.path[1] || ''); if (values.length && (date || (!identity && (descriptor.kind === 'number' || stat)))) { const units=new Set(); const ordered=values.map(value => { let number=NaN; if (date) { const text=String(value),parts=text.match(/^(\d{2})[.\/-](\d{2})[.\/-](\d{4})(?:,?\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/); number=typeof value === 'number' ? value:parts ? Date.UTC(+parts[3],+parts[2] - 1,+parts[1],+parts[4] || 0,+parts[5] || 0,+parts[6] || 0):Date.parse(text); } else { const parsed=String(value).trim().match(/^([+-]?\d[\d\s]*(?:[.,]\d+)?)\s*([^\d]*)$/); if (parsed) {number=Number(parsed[1].replace(/\s/g,'').replace(',','.')); units.add(parsed[2].trim().toLowerCase());} } return {value,number}; }).sort((a,b) => a.number - b.number); if (ordered.every(item => Number.isFinite(item.number)) && units.size <= 1) { const first=ordered[0],last=ordered[ordered.length - 1]; const low=format(first.value),high=format(last.value),suffix=!date && low.match(/^[+-]?[\d\s.,]+(.*)$/)?.[1]; compact=descriptor.kind === 'relative-time' || first.number === last.number || low === high ? high : `${suffix && high.toLowerCase().endsWith(suffix.toLowerCase()) ? low.slice(0,-suffix.length).trimEnd():low}–${high}`; } } return {compact:compact.length > 90 ? `${compact.slice(0,89)}…`:compact,full:unique.join(' • '), count:sources.filter(items => items.some(value => !!format(value))).length}; } function getEscortAggregateAdData(member) { const data=member?.adData?.status === 'ok' ? member.adData:getStoredEscortAdData(member?.adId); return data?.status === 'ok' ? data:null; } function renderEscortListPrice(line,adData,durationValue,members=null) { if (!line) return; const aggregate=Array.isArray(members) && members.length > 1; Object.assign(line.style,{pointerEvents:aggregate ? 'auto':'none',overflow:aggregate ? 'hidden':'',textOverflow:aggregate ? 'ellipsis':''}); const duration=normalizeEscortPriceDuration(durationValue); if (duration === 'hide') { line.style.display='none'; line.textContent=''; line.title=''; return; } line.style.display='block'; const durationLabel=getEscortPriceDurationLabel(duration); if (aggregate) { const data=members.map(getEscortAggregateAdData),value=formatEscortAggregatePrices(data.map(item => item?.prices?.[duration])); line.textContent=value.compact || (members.some(member => member?.adDataLoading) ? '…':data.every(Boolean) ? 'brak':'brak danych'); line.style.background=value.compact ? getEscortPagePinkColor():'rgba(20,20,20,.72)'; line.title=`Cena za ${durationLabel}\nDane dla ${value.count}/${members.length} anonsów\nWartości: ${value.full || 'b/d'}`; return; } const price=adData?.status === 'ok' && adData.prices ? adData.prices[duration] :null; if (!adData) { line.textContent='…'; line.style.background='rgba(20,20,20,.72)'; line.title=`Trwa pobieranie ceny za ${durationLabel}`; return; } if (adData.status !== 'ok') { line.textContent='brak danych'; line.style.background='rgba(120,35,45,.88)'; line.title='Nie udało się odczytać cennika z pojedynczego anonsu'; return; } const formattedPrice=formatEscortTilePrice(price); if (!formattedPrice) { line.textContent='brak'; line.style.background='rgba(20,20,20,.72)'; line.title=`W anonsie nie podano ceny za ${durationLabel}`; return; } line.textContent=formattedPrice; line.style.background=getEscortPagePinkColor(); line.title=`Cena podana w anonsie za ${durationLabel}`; } function getListCardExtraDataLine(card) { let host=card.querySelector('.vm-escort-tile-extra-data-host'); if (host) return host; if (getComputedStyle(card).position === 'static') card.style.position='relative'; host=makeElement('div'); host.className='vm-escort-tile-extra-data-host'; const baseTop=SETTINGS.showRelatedAdsTileLink ? 22:4; const extraTop=baseTop + (isEscortSearchPriceDisplayed() ? 19:0); Object.assign(host.style,{ display:'flex', position:'absolute', top:`${extraTop}px`, right:'2px', zIndex:'20', maxWidth:'78%', margin:'0', padding:'0', flexDirection:'column', alignItems:'flex-end', gap:'2px', boxSizing:'border-box', pointerEvents:'none' }); card.appendChild(host); return host; } function updateEscortListActiveCounts(buckets,getCount) { for (const bucket of buckets) { const count=getCount(bucket); for (const member of bucket.members) { if (member.activeAdCount === count) continue; member.activeAdCount=count; renderEscortListExtraData(member.extraLine,member); } } } function getEscortTileProfileFieldValue(state,descriptor) { const key=descriptor?.path?.[0]; if (!key) return null; if (key === 'activeCount') return state?.activeAdCount ?? null; if (key === 'totalAds') return state?.result?.adLinks != null && String(state.result.adLinks).trim() !== '' && Number.isFinite(Number(state.result.adLinks)) ? Number(state.result.adLinks):null; if (key === 'profileDates') return state?.result?.creationDate ? [state.result.creationDate]:null; if (key === 'cities') return state?.adData?.location?.city ? [state.adData.location.city]:null; if (key === 'online') return state?.result ? true:null; if (key === 'checkedAt') return state?.result?.checkedAt || null; return getEscortDataValueByPath(state?.result || {},descriptor.path); } function getEscortTileFieldValue(state,descriptor) { if (!descriptor) return null; if (descriptor.source === 'garso') { const stats=getCachedGarsoStatsForAds([state?.adUrl || `https://pl.escort.club/anons/${state?.adId}.html`],getEscortAdPhoneDigits(state?.adData)); return getGarsoDataValue(stats,descriptor); } if (descriptor.source === 'profile') { return getEscortTileProfileFieldValue( state, descriptor ); } if (descriptor.source === 'escorti') { if ( descriptor.path?.[0] === 'historicalCityCount' ) { return getEscortiHistoricalCityCount( state?.result ); } return getEscortDataValueByPath( state?.result || {}, descriptor.path ); } const cachedAd= getStoredEscortAdData(state?.adId); if (descriptor.path?.[0] === 'lastVisitedAt') return cachedAd?.lastVisitedAt || null; return getEscortDataFieldRawValue(state?.adData?.status === 'ok' ? state.adData:(cachedAd || {}),descriptor); } function renderEscortListExtraData(host,state,fieldValues=getSearchResultExtraFields(),members=state?.aggregateMembers) { if (!host) return; const fields=normalizeSearchResultExtraFields(fieldValues); if (!fields.length) { host.replaceChildren(); host.style.display='none'; return; } host.style.display='flex'; host.replaceChildren(); for (const field of fields) { const descriptor=getEscortDataFieldDescriptor(field); if (!descriptor) continue; const aggregate=Array.isArray(members) && members.length > 1 ? descriptor.source === 'profile' && descriptor.path[0] === 'activeCount' ? {compact:String(members.length),full:String(members.length),count:members.length} : formatEscortAggregateField(descriptor,members.map(member => getEscortTileFieldValue(member,descriptor))):null; const raw=aggregate ? null:getEscortTileFieldValue(state,descriptor); const compact=aggregate ? aggregate.compact:formatEscortUniversalDataValue(descriptor,raw,true); const full=aggregate ? `${descriptor.label}\nDane dla ${aggregate.count}/${members.length} anonsów\nWartości: ${aggregate.full || 'b/d'}` :formatEscortUniversalDataValue(descriptor,raw,false); const line=makeElement('span',"vm-escort-tile-extra-data",compact || 'b/d',{title:full || 'b/d'}); Object.assign(line.style,{ display:'block', width:'fit-content', maxWidth:'100%', margin:'0', padding:'2px 5px', overflow:'hidden', border:'1px solid rgba(255,255,255,.55)', borderRadius:'4px', background:compact ? 'rgba(20,20,20,.78)':'rgba(100,70,80,.72)', color:'#ffffff', fontSize:'9px', lineHeight:'1.2', fontWeight:'700', whiteSpace:'nowrap', textOverflow:'ellipsis', textShadow:'0 1px 2px rgba(0,0,0,.55)', boxSizing:'border-box', boxShadow:'0 1px 3px rgba(0,0,0,.35)', pointerEvents:'auto' }); host.appendChild(line); } if (!host.childElementCount) host.style.display='none'; } function renderListEscortiResult(line,result,stale=false) { if (!line) return; line.style.color='#ffffff'; line.title=stale ? 'Dane z lokalnego cache - trwa odświeżanie w tle':'Dane z Escorti'; if (result?.status === 'phone-missing') { line.textContent='Brak numeru'; line.style.color='#ffcf9f'; line.title='Nie można wyszukać w Escorti bez numeru telefonu'; return; } if (!result || result.status === 'timeout' || result.status === 'error') { line.textContent='Błąd'; line.style.color='#ffb3b3'; line.title='Nie udało się pobrać danych z Escorti'; return; } if (result.profiles === 0) { line.textContent='Nie znaleziono na Escorti'; line.style.color='#b8dcff'; line.title='Nie znaleziono profilu w Escorti'; return; } if (result.profiles > 1 && !result.merged) { line.textContent='Kilka profili'; line.title=`Znaleziono ${result.profiles} profile w Escorti`; return; } const ads=typeof result.adLinks === 'number' ? result.adLinks:'?'; const date=result.creationDate ? formatShortDate(result.creationDate):'?'; const opinions=typeof result.garsoTopics === 'number' ? result.garsoTopics:'?'; const opinionsText=typeof opinions === 'number' ? `${opinions} ${getGarsoTopicWord(opinions)}` :'? tematów'; line.textContent=`${ads} ogł • od ${date} • ${opinionsText}`; if (result.merged && result.profiles > 1) { line.title=`Scalono ${result.profiles} profile Escorti; duplikaty anonsów i tematów policzono tylko raz${result.partial ? ' (część profili miała błąd odczytu)':''}`; } } async function getOrStartListCheck(adId,searchValue,searchMode=getEscortListSearchMode(),cancelToken=null,requestContext=null) { requestContext ||= createRequestContext('escorti.list'); cancelToken?.throwIfCancelled(); const normalized=searchMode === 'phone-escorti' ? normalizeEscortCachedPhone(searchValue) || '':String(searchValue || '').trim().toLowerCase(); if (searchMode === 'phone-escorti' && !normalized) return {status:'phone-missing'}; await ensurePersistentCacheKeys([listCacheKey(adId)]); cancelToken?.throwIfCancelled(); const requestKey=`list:${searchMode}:${normalized}`; const result=await runSharedHelperRequest(requestKey,token => checkEscortiBackground(searchValue,{timeoutMs:60000,mergeProfiles:true,preferDirect:true,cancelToken:token,requestContext,reuseRecentProfileData:true}),cancelToken); cancelToken?.throwIfCancelled(); if (result?.status === 'ok') setListCache(adId,result,searchMode); return result; } function normalizeEscortListRouteKey(value) { try { const url=new URL(value,location.href); url.hash=''; url.pathname=url.pathname .replace(/\/(?:page|strona)\d+\.html\/?$/i,'/') .replace(/\/(?:page|strona)-\d+\/?$/i,'/') .replace(/\/(?:page|strona)\/\d+\/?$/i,'/'); for (const key of ['page','p','strona']) { url.searchParams.delete(key); } const sorted=[...url.searchParams.entries()] .sort(([ak,av],[bk,bv]) => ak.localeCompare(bk) || av.localeCompare(bv)); url.search=''; for (const [key,value] of sorted) { url.searchParams.append(key,value); } return `${url.hostname.toLowerCase()}${url.pathname}${url.search}`; } catch { return null; } } function getEscortListPageNumber(value) { try { const url=new URL(value,location.href); const pathPatterns=[ /\/(?:page|strona)(\d+)\.html\/?$/i, /\/(?:page|strona)-(\d+)\/?$/i, /\/(?:page|strona)\/(\d+)\/?$/i ]; for (const pattern of pathPatterns) { const match=url.pathname.match(pattern); if (match) return Number(match[1]); } for (const key of ['page','p','strona']) { const valueFromQuery=url.searchParams.get(key); if (/^\d+$/.test(valueFromQuery || '')) { return Number(valueFromQuery); } } return 1; } catch { return 1; } } function getEscortPaginationLinkPageNumber(link,href,currentPage) { const parsed=getEscortListPageNumber(href); if (parsed > 1) return parsed; const text=normalizeEscortAdText(link?.textContent); if (/^\d+$/.test(text)) return Number(text); const hint=normalizeEscortAdText([ link?.getAttribute?.('aria-label'), link?.getAttribute?.('title'), text ] .filter(Boolean).join(' ')).toLocaleLowerCase('pl-PL'); if (/następ|nastep|dalej|next|›|»/.test(hint)) return currentPage + 1; return parsed; } function findNextEscortListPageUrl(doc,baseUrl) { if (!doc) return null; const currentPage=getEscortListPageNumber(baseUrl); const routeKey=normalizeEscortListRouteKey(baseUrl); const normalizeCandidate=(link,relaxed=false) => { const href=link?.getAttribute?.('href'); if (!href) return null; try { const url=new URL(href,baseUrl); if (!isEscortClubHost(url.hostname)) return null; const page=getEscortPaginationLinkPageNumber(link,url.href,currentPage); if (!Number.isFinite(page) || page <= currentPage) return null; const sameRoute=normalizeEscortListRouteKey(url.href) === routeKey; if (!sameRoute && !relaxed) return null; return {href:url.href,page}; } catch { return null; } }; const directNextSelectors=[ 'link[rel="next"][href]', 'a[rel="next"][href]', '.pagination .next a[href]', '.pagination a.next[href]', '.pager .next a[href]', '.pager a.next[href]', 'a[aria-label*="następ" i][href]', 'a[title*="następ" i][href]', 'a[aria-label*="next" i][href]', 'a[title*="next" i][href]' ]; const isPaginationElement=element => !!element && ( element.matches?.('link[rel="next"], a[rel="next"]') || !!element.closest?.('.pagination, .pager, .pages, .ads-nav') ); for (const selector of directNextSelectors) { const el=doc.querySelector(selector); const candidate=normalizeCandidate(el,isPaginationElement(el)); if (candidate) return candidate.href; } const paginationLinks=[ ...doc.querySelectorAll( '.pagination a[href], .pager a[href], .pages a[href], .ads-nav a[href], a[href*="page"][href], a[href*="strona"][href]' ) ]; const textualNext=paginationLinks.find(a => { const text=(a.textContent || '') .replace(/\s+/g,' ') .trim() .toLowerCase(); return /^(następna|następny|dalej|next|›|»|>)$/.test(text); }); const textualCandidate=normalizeCandidate(textualNext,isPaginationElement(textualNext)); if (textualCandidate) return textualCandidate.href; const numbered=paginationLinks .map(a => normalizeCandidate(a,isPaginationElement(a))) .filter(Boolean) .filter(item => item.page > currentPage) .sort((a,b) => a.page - b.page); return numbered[0]?.href || null; } function normalizeEscortSectionHeading(value) { return normalizeText(value) .normalize('NFKD') .replace(/\p{M}/gu,''); } function isEscortMainResultsHeading(value) { return /anonse erotyczne|^escort in\b/i.test(normalizeEscortSectionHeading(value)); } function isEscortSearchPath(pathname=location.pathname) { return !getEscortAdIdFromPath(pathname) && /^\/(?:szukaj|search|anonse|personal-ads)(?:\/|$)/i.test(String(pathname || '')); } function getEscortSelectOptionData(select) { if (!(select instanceof HTMLSelectElement)) return []; return [...select.options].map(option => ({ value:String(option.value ?? ''), label:String(option.textContent || '').replace(/\s+/g,' ').trim() })).filter(option => option.label); } function findEscortSearchLocationControls(doc=document) { const searchBox=[...doc.querySelectorAll('.search-box')].find(box => ['country','province','city'] .every(key => box.querySelector(`select[name="${key}"]`)) && box.querySelector('.btn-search[type="submit"]')); const searchButton=searchBox?.querySelector('.btn-search[type="submit"]'); if (!searchButton || !searchBox) return null; return { searchBox, searchButton, submitItem:searchButton.closest('.form-item.-submit'), ...Object.fromEntries(['country','province','city','district'].map(key => [key,searchBox.querySelector(`select[name="${key}"]`)])) }; } function findEscortLocationOption(select,value,label='') { if (!(select instanceof HTMLSelectElement)) return null; const wantedValue=String(value ?? ''); const wantedLabel=String(label || '').replace(/\s+/g,' ').trim() .toLocaleLowerCase('pl-PL'); return [...select.options].find(option => wantedValue && String(option.value) === wantedValue ) || [...select.options].find(option => wantedLabel && String(option.textContent || '') .replace(/\s+/g,' ') .trim() .toLocaleLowerCase('pl-PL') === wantedLabel ) || null; } function getEscortLocationSelectSignature(select) { return getEscortSelectOptionData(select) .map(option => `${option.value}:${option.label}`) .join('|'); } function setEscortLocationControl(select,value,label='',strongChange=false) { const option=findEscortLocationOption(select,value,label); if (!option) return false; select.disabled=false; const changed=String(select.value ?? '') !== String(option.value ?? ''); select.value=option.value; if (changed) { dispatchEscortFilterControlEvents(select); if (strongChange) { try { const jq=window.jQuery; if (jq) jq(select).trigger('change'); } catch {} } } refreshEscortFilterSelect(select); return true; } function clearEscortLocationControl(select) { const empty=[...(select?.options || [])].find(option => !String(option.value || '')); if (!empty || String(select.value || '') === String(empty.value || '')) return false; select.value=empty.value; dispatchEscortFilterControlEvents(select); refreshEscortFilterSelect(select); return true; } function waitForEscortLocationOptions(controlName,predicate,timeoutMs=7000) { return new Promise((resolve,reject) => { const startedAt=Date.now(); const check=() => { const controls=findEscortSearchLocationControls(); const select=controls?.[controlName]; if (select && predicate(select,controls)) { clearInterval(timer); resolve(controls); return; } if (Date.now() - startedAt >= timeoutMs) { clearInterval(timer); reject(new Error( controlName === 'district' ? 'Escort.club nie udostępnił listy dzielnic.' :(controlName === 'city' ? 'Escort.club nie udostępnił listy miast.' :'Escort.club nie udostępnił listy województw.') )); } }; const timer=setInterval(check,100); check(); }); } async function loadEscortLocationOptions(locationData,options={}) { const selected=normalizeDefaultSearchLocation(locationData); const forceCountryChange=options?.forceCountryChange === true; let controls=findEscortSearchLocationControls(); if (!controls?.country || !controls?.province || !controls?.city) { throw new Error( 'Kontrolki lokalizacji są dostępne na stronie wyszukiwania Escort.club.' ); } if (options.useKnownIds && !selected.districtValue && !selected.districtLabel && ESCORT_RADIUS_CITIES.some(city => String(city[4]) === selected.cityValue && String(ESCORT_RADIUS_PROVINCE_IDS[city[1]]) === selected.provinceValue)) { // Formularz zostanie od razu wysłany; zdarzenia change uruchomiłyby ponowne pobieranie list. for (const key of ['country','province','city','district']) { const control=controls[key],value=selected[`${key}Value`]; if (!control) continue; if (![...control.options].some(option => option.value === value)) { control.appendChild(makeElement('option','',selected[`${key}Label`] || '',{value})); } control.value=value; control.disabled=false; refreshEscortFilterSelect(control); } return controls; } const previousCountry=String(controls.country.value || ''); const provinceSignature=getEscortLocationSelectSignature(controls.province); if (!setEscortLocationControl( controls.country, selected.countryValue, selected.countryLabel, forceCountryChange )) { throw new Error('Nie znaleziono wybranego państwa w wyszukiwarce Escort.club.'); } if (previousCountry !== String(controls.country.value || '')) { try { controls=await waitForEscortLocationOptions( 'province', select => getEscortLocationSelectSignature(select) !== provinceSignature, 3500 ); } catch { controls=findEscortSearchLocationControls() || controls; } } if (!selected.provinceValue && !selected.provinceLabel) return controls; if (!findEscortLocationOption( controls.province, selected.provinceValue, selected.provinceLabel )) { if (previousCountry === String(controls.country.value || '')) { dispatchEscortFilterControlEvents(controls.country); refreshEscortFilterSelect(controls.country); } controls=await waitForEscortLocationOptions( 'province', select => !!findEscortLocationOption( select, selected.provinceValue, selected.provinceLabel ), 5000 ); } const previousProvince=String(controls.province.value || ''); const citySignature=getEscortLocationSelectSignature(controls.city); if (!setEscortLocationControl( controls.province, selected.provinceValue, selected.provinceLabel )) { throw new Error('Nie znaleziono wybranego województwa w wyszukiwarce Escort.club.'); } const cityAlreadyAvailable=selected.cityValue || selected.cityLabel ? !!findEscortLocationOption( controls.city, selected.cityValue, selected.cityLabel ) :getEscortSelectOptionData(controls.city).some(option => option.value); if (!cityAlreadyAvailable || previousProvince !== String(controls.province.value || '')) { if ( !cityAlreadyAvailable && previousProvince === String(controls.province.value || '') ) { dispatchEscortFilterControlEvents(controls.province); refreshEscortFilterSelect(controls.province); } controls=await waitForEscortLocationOptions( 'city', select => { if (selected.cityValue || selected.cityLabel) { return !!findEscortLocationOption( select, selected.cityValue, selected.cityLabel ); } return getEscortLocationSelectSignature(select) !== citySignature && getEscortSelectOptionData(select).some(option => option.value); } ); } if (selected.cityValue || selected.cityLabel) { const previousCity=String(controls.city.value || ''); const districtSignature=getEscortLocationSelectSignature(controls.district); if (!setEscortLocationControl( controls.city, selected.cityValue, selected.cityLabel )) { throw new Error('Nie znaleziono wybranego miasta w wyszukiwarce Escort.club.'); } controls=findEscortSearchLocationControls() || controls; if (selected.districtValue || selected.districtLabel) { if (!controls.district) { throw new Error('Escort.club nie udostępnił pola dzielnicy.'); } const districtAlreadyAvailable=!!findEscortLocationOption( controls.district, selected.districtValue, selected.districtLabel ); if ( !districtAlreadyAvailable || previousCity !== String(controls.city.value || '') ) { if ( !districtAlreadyAvailable && previousCity === String(controls.city.value || '') ) { dispatchEscortFilterControlEvents(controls.city); refreshEscortFilterSelect(controls.city); } controls=await waitForEscortLocationOptions( 'district', select => !!findEscortLocationOption( select, selected.districtValue, selected.districtLabel ) || ( getEscortLocationSelectSignature(select) !== districtSignature && getEscortSelectOptionData(select).some(option => option.value) ) ); } if (!setEscortLocationControl( controls.district, selected.districtValue, selected.districtLabel )) { throw new Error('Nie znaleziono wybranej dzielnicy w wyszukiwarce Escort.club.'); } } else if (controls.district) { if (previousCity !== String(controls.city.value || '')) { try { controls=await waitForEscortLocationOptions( 'district', select => getEscortLocationSelectSignature(select) !== districtSignature, 2500 ); } catch { controls=findEscortSearchLocationControls() || controls; } } clearEscortLocationControl(controls.district); } } else { if (clearEscortLocationControl(controls.city)) controls=findEscortSearchLocationControls() || controls; clearEscortLocationControl(controls.district); } return controls; } function resolveEscortLocationLabels(value,doc=document) { const selected=normalizeDefaultSearchLocation(value),controls=findEscortSearchLocationControls(doc); const saved=(SETTINGS.savedLocationGroups || []).flat().concat(SETTINGS.searchLocations || []) .find(item => getEscortLocationKey(item) === getEscortLocationKey(selected)); let sameParent=String(controls?.country?.value || '') === selected.countryValue; for (const key of ['province','city','district']) { const id=selected[key + 'Value'],label=key + 'Label'; selected[label]=[selected[label],saved?.[label],sameParent ? findEscortLocationOption(controls?.[key],id)?.textContent?.trim():''] .find(text => text && text !== id) || ''; sameParent &&= String(controls?.[key]?.value || '') === id; } const province=ESCORT_RADIUS_PROVINCE_IDS.findIndex(id => String(id) === selected.provinceValue); if (province >= 0) { selected.provinceLabel ||= ESCORT_RADIUS_PROVINCES[province].replace(/^./,letter => letter.toLocaleUpperCase('pl-PL')); selected.cityLabel ||= ESCORT_RADIUS_CITIES.find(city => city[1] === province && String(city[4]) === selected.cityValue)?.[0] || ''; } return selected; } function getSavedLocationButtonLabel(locationData) { const selected=normalizeDefaultSearchLocation(locationData); const province=selected.provinceLabel || selected.provinceValue || ''; const city=selected.cityLabel || selected.cityValue || ''; const district=selected.districtLabel || selected.districtValue || ''; if (city && district) return `${city}, ${district}`; return city || province || 'Lokalizacja'; } function getEscortFullLocationLabel(locationData,separator=' › ') { const selected=normalizeDefaultSearchLocation(locationData); return ['province','city','district'].map(key => selected[key + 'Label'] || selected[key + 'Value']) .filter(Boolean).join(separator) || 'Lokalizacja'; } function getEscortLocationGroupLabel(locations) { return normalizeSearchLocations(locations) .map(getSavedLocationButtonLabel) .join(' + '); } function getEscortLocationKey(locationData) { const location=normalizeDefaultSearchLocation(locationData); return ['country','province','city','district'].map(key => location[key + 'Value'] || location[key + 'Label']) .join('|').toLocaleLowerCase('pl-PL'); } function isUsefulEscortLocation(locationData) { const location=normalizeDefaultSearchLocation(locationData); return !!( location.provinceValue || location.provinceLabel || location.cityValue || location.cityLabel ); } function readEscortLocationFromControls(controls=findEscortSearchLocationControls()) { if (!controls?.province || !controls?.city) return null; const fields={}; for (const key of ['province','city','district']) { const select=controls[key]; if (!(select instanceof HTMLSelectElement) || !select.value) continue; fields[key + 'Value']=select.value; fields[key + 'Label']=select.selectedOptions?.[0]?.textContent; } const locationData=normalizeDefaultSearchLocation(fields); return isUsefulEscortLocation(locationData) ? locationData:null; } function getEscortActiveMultiLocations() { if (SETTINGS.multiLocationEnabled === false) return []; try { return normalizeSearchLocations(JSON.parse(sessionStorage.getItem(ESCORT_MULTI_LOCATION_SESSION_KEY))); } catch { return []; } } function saveEscortActiveMultiLocations(locations,resetResultCounts=true) { const normalized=normalizeSearchLocations(locations); try { if (normalized.length) { sessionStorage.setItem( ESCORT_MULTI_LOCATION_SESSION_KEY, JSON.stringify(normalized) ); } else { sessionStorage.removeItem(ESCORT_MULTI_LOCATION_SESSION_KEY); } } catch {} if (resetResultCounts) clearEscortMultiLocationResultCounts(); return normalized; } function addEscortActiveMultiLocation(locationData) { if (!isUsefulEscortLocation(locationData)) return getEscortActiveMultiLocations(); const current=getEscortActiveMultiLocations(); const key=getEscortLocationKey(locationData); if (!current.some(item => getEscortLocationKey(item) === key)) { if (current.length >= ESCORT_MULTI_LOCATION_MAX) { showWatchToast(`Możesz wybrać maksymalnie ${ESCORT_MULTI_LOCATION_MAX} lokalizacji. Najpierw usuń jedną z listy.`,true); return null; } current.push(normalizeDefaultSearchLocation(locationData)); } return saveEscortActiveMultiLocations(current); } function removeEscortActiveMultiLocation(locationData) { const key=getEscortLocationKey(locationData); return saveEscortActiveMultiLocations( getEscortActiveMultiLocations().filter(item => getEscortLocationKey(item) !== key) ); } function getEscortSavedLocationGroups() { return normalizeSavedLocationGroups( SETTINGS.savedLocationGroups ); } function getEscortLocationGroupKey(locations) { return normalizeSearchLocations(locations) .map(getEscortLocationKey) .sort() .join('||'); } function saveEscortSavedLocationGroups(groups) { const normalized=normalizeSavedLocationGroups(groups); updateSettings({ searchLocations:[], savedLocationGroups:normalized }); return normalized; } function addEscortSavedLocationGroup(locations) { const normalizedGroup=normalizeSearchLocations(locations); if (!normalizedGroup.length) return getEscortSavedLocationGroups(); const current=getEscortSavedLocationGroups(); const key=getEscortLocationGroupKey(normalizedGroup); if (!current.some(group => getEscortLocationGroupKey(group) === key)) { current.push(normalizedGroup); } return saveEscortSavedLocationGroups(current); } function removeEscortSavedLocationGroup(locations) { const key=getEscortLocationGroupKey(locations); return saveEscortSavedLocationGroups( getEscortSavedLocationGroups().filter(group => getEscortLocationGroupKey(group) !== key ) ); } function getEscortMultiLocationsForCurrentPage() { const currentLocation=readEscortLocationFromControls(); const active=getEscortActiveMultiLocations(); if (active.length > 1) { if (!currentLocation) return active; const currentKey=getEscortLocationKey(currentLocation); if (!active.some(item => getEscortLocationKey(item) === currentKey)) { return active; } return [ currentLocation, ...active.filter(item => getEscortLocationKey(item) !== currentKey) ]; } return currentLocation ? [currentLocation]:active; } function polishLocationWord(count) { return polishCountForm(count,'miasto','miasta','miast'); } function refreshEscortMultiLocationUi() { for (const refresh of escortMultiLocationUiRefreshers) { try {refresh();} catch {} } } function clearEscortMultiLocationResultCounts() { escortMultiLocationResultCounts.clear(); escortMultiLocationPageCounts.clear(); refreshEscortMultiLocationUi(); } function setEscortMultiLocationResultCount(locationData,count,doc=document) { const key=getEscortLocationKey(locationData); if (!key) return; const resolved=resolveEscortLocationLabels(locationData,doc),active=getEscortActiveMultiLocations(); if (active.some(item => getEscortLocationKey(item) === key && JSON.stringify(item) !== JSON.stringify(resolved))) { saveEscortActiveMultiLocations(active.map(item => getEscortLocationKey(item) === key ? resolved:item),false); } Object.assign(locationData,resolved); const normalizedCount=Math.max(0,Number(count) || 0); escortMultiLocationResultCounts.set(key,normalizedCount); escortMultiLocationPageCounts.set(key,getEscortResultsPageCount(doc)); refreshEscortMultiLocationUi(); } function getEscortMultiLocationResultCount(locationData) { const key=getEscortLocationKey(locationData); return escortMultiLocationResultCounts.has(key) ? escortMultiLocationResultCounts.get(key) :null; } function getEscortMultiLocationKnownTotal(locations) { const group=normalizeSearchLocations(locations); if (group.length <= 1) return null; const counts=group.map(locationData => getEscortMultiLocationResultCount(locationData) ); if ( counts.length !== group.length || counts.some(value => value == null || !Number.isFinite(Number(value)) || Number(value) < 0 ) ) { return null; } return counts.reduce( (sum,value) => sum + Number(value), 0 ); } function getEscortResultsPageCount(doc=document) { const counters=[...doc.querySelectorAll('.pagination, .pager, .pages, .ads-nav')]; let pages=1; for (const counter of counters) { const total=(counter.textContent || '').match(/\b\d+\s*(?:z|\/)\s*(\d+)\b/i); if (total) pages=Math.max(pages,Number(total[1])); for (const link of counter.querySelectorAll('a[href]')) { const url=new URL(link.getAttribute('href'),location.href); if (isEscortClubHost(url.hostname) && /\/(?:anonse|search)\//.test(url.pathname)) pages=Math.max(pages,getEscortListPageNumber(url.href)); } } return pages; } function formatEscortResultPages(text,pages) { return `${text} (${pages} ${polishCountForm(pages,'strona','strony','stron')})`; } function markEscortResultCounter(element) { if (!element) return; element.classList.add('vm-escort-results-count'); element.dataset.vmOriginalText ??= element.textContent; element.textContent=formatEscortResultPages(element.dataset.vmOriginalText,getEscortResultsPageCount()); } function setEscortMultiResultCountersVisible(visible) { for (const element of document.querySelectorAll( '.search-links .sub-label, .ads-nav p, .vm-escort-results-count' )) { const textValue=normalizeEscortAdText( element.textContent ); const isResultCounter= /^(?:Lista\s+wyników?|Wyświetlane|Załadowano)\b/i .test(textValue); if (!isResultCounter) continue; if (visible) { if ( element.dataset.vmMultiResultCounterHidden === '1' ) { element.style.removeProperty('display'); delete element.dataset.vmMultiResultCounterHidden; } } else { element.dataset.vmMultiResultCounterHidden='1'; element.style.setProperty( 'display', 'none', 'important' ); } } } function setEscortMultiNoResultsMessageVisible(visible) { const section=getEscortMainResultsSection(document); const root=section || document; const matches=[...root.querySelectorAll('*')].filter(element => /^(?:brak\s+(?:anons(?:ów|ow)|ogłoszeń|ogloszen)|no\s+(?:ads|adverts|advertisements)(?:\s+found)?)[.!]?$/i.test( normalizeEscortAdText(element.textContent) ) ); const matchingSet=new Set(matches); const outerMatches=matches.filter(element => { let parent=element.parentElement; while (parent && parent !== root) { if (matchingSet.has(parent)) return false; parent=parent.parentElement; } return true; }); for (const element of outerMatches) { if (!visible) { if (element.dataset.vmMultiNoResultsHidden !== '1') { element.dataset.vmMultiNoResultsHidden='1'; element.dataset.vmMultiNoResultsDisplay= element.style.getPropertyValue('display') || ''; element.dataset.vmMultiNoResultsDisplayPriority= element.style.getPropertyPriority('display') || ''; } element.style.setProperty('display','none','important'); } else if (element.dataset.vmMultiNoResultsHidden === '1') { const display=element.dataset.vmMultiNoResultsDisplay || ''; const priority=element.dataset.vmMultiNoResultsDisplayPriority || ''; if (display) element.style.setProperty('display',display,priority); else element.style.removeProperty('display'); delete element.dataset.vmMultiNoResultsHidden; delete element.dataset.vmMultiNoResultsDisplay; delete element.dataset.vmMultiNoResultsDisplayPriority; } } } function ensureEscortMultiLocationUiStyle() { if (document.getElementById('vm-escort-multi-location-style')) return; appendHelperStyle('vm-escort-multi-location-style',` .search-box form, .search-box .row { flex-wrap:wrap; } #${ESCORT_MULTI_LOCATION_UI_ID} .vm-multi-submit-button.-end { display:block; min-height:40px; border:2px solid var(--vm-ui-accent,#f54da3) !important; border-radius:6px !important; background:var(--vm-ui-bg,#fff) !important; color:var(--vm-ui-text,#332c31) !important; -webkit-text-fill-color:currentColor !important; text-shadow:none !important; box-shadow:none !important; font-size:12px !important; line-height:1.05 !important; white-space:normal !important; overflow:hidden !important; } .vm-multi-end-inline { width:150px !important; min-width:150px !important; flex:0 0 150px; } #${ESCORT_MULTI_LOCATION_UI_ID} .vm-multi-submit-button.-end:hover { background:var(--vm-ui-bg-hover,color-mix(in srgb,var(--vm-ui-accent,#f54da3) 7%,transparent)) !important; } .vm-radius-link-row { margin:2px auto 7px; text-align:right; } #vm-escort-radius-open { width:auto; min-height:0; margin:0; padding:0; border:0; background:none; box-shadow:none; color:var(--vm-ui-text-muted,#756b72); font:400 12px/1.4 var(--vm-ui-font-family,Lato,Arial,sans-serif); text-decoration:underline; text-underline-offset:2px; cursor:pointer; } #vm-escort-radius-open:hover, #vm-escort-radius-open:focus-visible { color:var(--vm-ui-accent,#f54da3); } .vm-multi-panel { box-sizing:border-box; width:100%; max-width:var(--vm-multi-max-width,1120px); font:11px/1.35 var(--vm-ui-font-family,Lato,Arial,sans-serif); } #${ESCORT_MULTI_LOCATION_UI_ID}.vm-multi-panel { margin:8px auto 3px; } [data-vm-saved-locations-ui="1"].vm-multi-panel { margin:0 auto 8px; } .vm-multi-section { flex-direction:column; align-items:stretch; gap:7px; padding:10px 12px; border-radius:5px; } .vm-multi-section.-active { display:none; border:1px solid var(--vm-ui-accent,#f54da3); background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 7%,transparent); color:var(--vm-ui-text,#332c31); } .vm-multi-section.-saved { margin-top:7px; border:1px solid var(--vm-ui-border,#c8d0d7); background:var(--vm-ui-bg-alt,#f5f7f8); color:var(--vm-ui-text,#3f4850); } .vm-multi-top-row { display:flex; align-items:center; justify-content:space-between; gap:10px; } .vm-multi-top-row.-active, .vm-multi-top-row.-saved { flex-wrap:wrap; } .vm-multi-label { font-size:11px; letter-spacing:.03em; } .vm-multi-label.-active { color:var(--vm-ui-accent,#f54da3); } .vm-multi-label.-saved { color:var(--vm-ui-text-muted,#52606b); } .vm-multi-active-box { display:grid; grid-template-columns:repeat(auto-fit,minmax(185px,1fr)); align-items:stretch; gap:7px; } .vm-multi-saved-box { display:flex; align-items:center; flex-wrap:wrap; gap:6px; } .vm-multi-save-current { padding:6px 10px; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:5px; background:var(--vm-ui-accent,#f54da3); color:var(--vm-ui-accent-contrast,#fff); cursor:pointer; font:800 10px var(--vm-ui-font-family,Lato,Arial,sans-serif); box-shadow:var(--vm-ui-shadow,0 1px 2px rgba(0,0,0,.12)); } .vm-multi-active-card { position:relative; display:flex; flex-direction:column; justify-content:center; min-width:0; min-height:48px; padding:7px 31px 7px 10px; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:4px; background:var(--vm-ui-bg,#fff); box-shadow:var(--vm-ui-shadow,0 1px 2px rgba(0,0,0,.05)); } .vm-multi-active-text { color:var(--vm-ui-text,#2f292d); font-size:12px; font-weight:700; line-height:1.25; overflow-wrap:anywhere; } .vm-multi-count { margin-top:3px; color:var(--vm-ui-accent,#f54da3); font-size:10px; font-weight:700; } .vm-multi-count.-pending { color:var(--vm-ui-text-faint,#91878d); } .vm-multi-active-remove { position:absolute; top:50%; right:8px; transform:translateY(-50%); width:20px; height:20px; padding:0; border:1px solid var(--vm-ui-accent,#f54da3); border-radius:50%; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-accent,#f54da3); cursor:pointer; font:800 14px/17px var(--vm-ui-font-family,Arial,sans-serif); } .vm-multi-saved-chip { display:inline-flex; align-items:center; gap:6px; padding:6px 7px 6px 10px; border:1px solid var(--vm-ui-border-strong,#aeb8c1); border-radius:4px; background:var(--vm-ui-bg,#fff); white-space:normal; box-shadow:var(--vm-ui-shadow,0 1px 1px rgba(0,0,0,.04)); } .vm-multi-saved-action { padding:0; border:0; background:transparent; color:var(--vm-ui-text,#35424d); cursor:pointer; font:700 11px var(--vm-ui-font-family,Lato,Arial,sans-serif); white-space:normal; text-align:left; } .vm-multi-saved-remove { width:18px; height:18px; flex:0 0 18px; padding:0; border:0; border-radius:50%; background:var(--vm-ui-bg-hover,#eef1f3); color:var(--vm-ui-text-muted,#707b84); cursor:pointer; font:800 12px/16px var(--vm-ui-font-family,Arial,sans-serif); } `); } function normalizeRadiusCityName(value) { return String(value || '').toLowerCase().replace(/\s*\(\d+\)\s*$/,'').normalize('NFD') .replace(/[\u0300-\u036f]/g,'').replace(/ł/g,'l').replace(/[^a-z0-9]/g,''); } function escortRadiusDistanceKm(center,city) { const rad=Math.PI / 180,dLat=(city[2] - center.lat) * rad,dLng=(city[3] - center.lng) * rad; const a=Math.sin(dLat / 2) ** 2 + Math.cos(center.lat * rad) * Math.cos(city[2] * rad) * Math.sin(dLng / 2) ** 2; return 12742.0176 * Math.asin(Math.sqrt(Math.min(1,Math.max(0,a)))); } function getEscortRadiusCities(center,radiusKm) { if (![center?.lat,center?.lng,radiusKm].every(Number.isFinite) || Math.abs(center.lat) > 90 || Math.abs(center.lng) > 180 || radiusKm < 1 || radiusKm > 200) return []; return ESCORT_RADIUS_CITIES.map(city => ({city,distance:escortRadiusDistanceKm(center,city)})) .filter(entry => entry.distance <= radiusKm).sort((a,b) => a.distance - b.distance || a.city[0].localeCompare(b.city[0],'pl')); } function resolveEscortRadiusCities(cities) { return cities.map(city => normalizeDefaultSearchLocation({ provinceValue:ESCORT_RADIUS_PROVINCE_IDS[city[1]],provinceLabel:ESCORT_RADIUS_PROVINCES[city[1]], cityValue:city[4],cityLabel:city[0] })); } async function openEscortRadiusMap(onApply) { const L=await loadEscortRadiusLeaflet(); if (document.getElementById('vm-escort-radius-overlay')) return; if (typeof L === 'undefined') throw new Error('Nie udało się załadować mapy. Zaktualizuj skrypt i odśwież stronę.'); if (!document.getElementById('vm-escort-radius-style')) { appendHelperStyle('vm-escort-radius-style',GM_getResourceText('escortRadiusLeafletCss') + ` #vm-escort-radius-overlay .vm-ui-dialog { width:min(980px,calc(100vw - 24px)); padding:12px; } #vm-escort-radius-overlay .vm-ui-title { font-size:18px; margin:0 0 8px; } #vm-escort-radius-overlay .vm-radius-toolbar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:7px; } #vm-escort-radius-overlay .vm-radius-toolbar label { margin:0; } #vm-escort-radius-overlay .vm-radius-toolbar input[type=number] { width:78px; margin-left:6px; } #vm-escort-radius-overlay .vm-radius-slider { flex:1 1 180px; min-width:120px; max-width:420px; height:20px; margin:0; padding:0; border:0; background:transparent; accent-color:var(--vm-ui-accent,#f54da3); cursor:pointer; } #vm-escort-radius-map { height:clamp(220px,46vh,440px); width:100%; border:1px solid var(--vm-ui-border,#bbb); border-radius:6px; background:#ddd; color:#222; } #vm-escort-radius-map .leaflet-control-attribution { color:#333; } #vm-escort-radius-map .vm-radius-pin { background:none; border:0; font-size:30px; line-height:36px; text-align:center; } #vm-escort-radius-overlay .vm-radius-cities { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:3px 32px; max-height:150px; overflow:auto; margin-top:6px; } #vm-escort-radius-overlay .vm-radius-city { display:grid; grid-template-columns:auto minmax(0,1fr) 64px; align-items:center; gap:6px; margin:0; font-size:12px; cursor:pointer; } #vm-escort-radius-overlay .vm-radius-city > span { min-width:0; overflow-wrap:anywhere; } #vm-escort-radius-overlay .vm-radius-city small { text-align:right; color:var(--vm-ui-text-muted,#666); white-space:nowrap; font-variant-numeric:tabular-nums; } #vm-escort-radius-overlay .vm-radius-status { margin:5px 0; font-size:12px; min-height:1.4em; } #vm-escort-radius-overlay .vm-radius-status:empty { display:none; } #vm-escort-radius-overlay .vm-ui-actions { margin-top:8px; padding-top:8px; } `); } const native=readEscortLocationFromControls(); const nativeCity=ESCORT_RADIUS_CITIES.find(city => normalizeRadiusCityName(city[0]) === normalizeRadiusCityName(native?.cityLabel) && (!native?.provinceLabel || normalizeRadiusCityName(ESCORT_RADIUS_PROVINCES[city[1]]) === normalizeRadiusCityName(native.provinceLabel))); let center=nativeCity ? {lat:nativeCity[2],lng:nativeCity[3]}:{lat:52,lng:19}; let hasCenter=!!nativeCity,revision=0,closed=false,applying=false,map=null,selected=new Set(),matches=[]; const {overlay,box}=createWatchDialogBase('vm-escort-radius-overlay','Miasta w promieniu'); const controller=createDialogCloseController(overlay,{closeOnBackdrop:false,closeOnEscape:false, onClose:() => {closed=true; map?.remove();}}); const toolbar=makeElement('div','vm-radius-toolbar'); const radiusLabel=makeElement('label','','Promień:'); const radius=makeElement('input','vm-ui-field',null,{type:'number',min:'1',max:'200',step:'1',value:'20',required:true}); radius.setAttribute('aria-label','Promień wyszukiwania w kilometrach'); radiusLabel.append(radius,makeElement('span','',' km')); const slider=makeElement('input','vm-radius-slider'); Object.assign(slider,{type:'range',min:radius.min,max:radius.max,step:radius.step,value:radius.value}); slider.setAttribute('aria-label','Promień wyszukiwania w kilometrach - suwak'); toolbar.append(radiusLabel,slider); const mapHost=makeElement('div'); mapHost.id='vm-escort-radius-map'; const help=makeElement('div','vm-ui-help','Kliknij mapę lub przeciągnij pineskę. Promień jest liczony w linii prostej do środka miasta.'); const count=makeElement('div','vm-radius-status'); count.setAttribute('role','status'); const list=makeElement('div','vm-radius-cities'); const status=makeElement('div','vm-radius-status'); status.setAttribute('role','status'); const actions=makeElement('div','vm-ui-actions'); const cancel=makeButton('vm-ui-button','Anuluj'),apply=makeButton('vm-ui-button -primary','Zapisz'); controller.bindButton(cancel); actions.append(cancel,apply); box.append(toolbar,mapHost,help,count,list,status,actions); try {map=L.map(mapHost,{scrollWheelZoom:true}).setView(center,nativeCity ? 9:6);} catch (error) {controller.close('error'); throw error;} L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:18, attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> · <a href="https://www.geonames.org/" title="Współrzędne miast, zaokrąglone do 4 miejsc">GeoNames</a> (<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>)'}).addTo(map) .on('tileerror',() => {if (!closed) status.textContent='Nie udało się wczytać podkładu mapy. Sprawdź połączenie z internetem.';}); const accent=getEscortPagePinkColor(); const marker=L.marker(center,{draggable:true,title:'Centrum promienia - przeciągnij pineskę', icon:L.divIcon({className:'vm-radius-pin',html:'📍',iconSize:[32,38],iconAnchor:[16,36]})}).addTo(map); const circle=L.circle(center,{radius:Number(radius.value) * 1000,color:accent,weight:2,fillOpacity:.10,interactive:false}).addTo(map); const dots=L.layerGroup().addTo(map); const updateCount=() => { count.textContent=`W promieniu: ${matches.length} ${polishLocationWord(matches.length)}`; apply.disabled=applying || !hasCenter || !selected.size || selected.size > ESCORT_MULTI_LOCATION_MAX || !radius.checkValidity(); }; const render=(fit=false) => { if (closed || applying) return; matches=hasCenter ? getEscortRadiusCities(center,Number(radius.value)):[]; selected=new Set(matches.slice(0,ESCORT_MULTI_LOCATION_MAX).map(entry => entry.city)); list.replaceChildren(); dots.clearLayers(); for (const {city,distance} of matches) { const row=makeElement('label','vm-radius-city'),check=makeElement('input'); check.type='checkbox'; check.checked=selected.has(city); row.title=ESCORT_RADIUS_PROVINCES[city[1]]; check.addEventListener('change',() => { if (applying) return; if (check.checked && selected.size >= ESCORT_MULTI_LOCATION_MAX) { check.checked=false; status.textContent=`Możesz wybrać maksymalnie ${ESCORT_MULTI_LOCATION_MAX} miast. Najpierw odznacz inne.`; return; } if (check.checked) selected.add(city); else selected.delete(city); status.textContent=''; updateCount(); }); row.append(check,makeElement('span','',city[0]),makeElement('small','',`${distance.toLocaleString('pl-PL',{maximumFractionDigits:1})} km`)); list.appendChild(row); L.circleMarker([city[2],city[3]],{radius:4,color:accent,fillOpacity:.8,interactive:false}).addTo(dots); } updateCount(); status.textContent=!radius.checkValidity() ? 'Podaj promień od 1 do 200 km.':matches.length > ESCORT_MULTI_LOCATION_MAX ? `Domyślnie wybrano ${ESCORT_MULTI_LOCATION_MAX} najbliższych miast. Możesz zmienić zaznaczenie.`:hasCenter && !matches.length ? 'Brak miast z bazy w wybranym promieniu.':''; marker.setOpacity(hasCenter ? 1:0); const showCircle=hasCenter && radius.checkValidity(); circle.setStyle({opacity:showCircle ? 1:0,fillOpacity:showCircle ? .10:0}); if (showCircle) { circle.setLatLng(center).setRadius(Number(radius.value) * 1000); if (fit) map.fitBounds(circle.getBounds(),{padding:[18,18],maxZoom:14}); } }; const setCenter=point => { if (closed || applying) return; center={lat:point.lat,lng:((point.lng + 180) % 360 + 360) % 360 - 180}; hasCenter=true; revision++; marker.setLatLng(center); render(true); }; map.on('click',event => setCenter(event.latlng)); marker.on('dragstart',() => revision++).on('dragend',() => setCenter(marker.getLatLng())); radius.addEventListener('input',() => {if (radius.checkValidity()) slider.value=radius.value; render(true);}); slider.addEventListener('input',() => {radius.value=slider.value; render(true);}); apply.addEventListener('click',async () => { if (apply.disabled) return; applying=true; apply.disabled=true; radius.disabled=slider.disabled=true; for (const input of list.querySelectorAll('input')) input.disabled=true; map.dragging.disable(); marker.dragging.disable(); revision++; const chosen=[...selected]; status.textContent='Zapisywanie wybranych miast…'; try { const locations=resolveEscortRadiusCities(chosen); if (closed) return; cancel.disabled=true; if (await onApply(locations) === false) {status.textContent=''; return;} controller.close('apply'); } catch (error) { if (!closed) status.textContent=error?.message || 'Nie udało się wybrać miast.'; } finally { if (!closed) { applying=false; radius.disabled=slider.disabled=false; cancel.disabled=false; for (const input of list.querySelectorAll('input')) input.disabled=false; map.dragging.enable(); marker.dragging.enable(); updateCount(); } } }); render(!!nativeCity); if (!navigator.geolocation) {status.textContent='Przeglądarka nie udostępnia geolokalizacji. Wskaż pineskę na mapie.'; return;} const started=revision; status.textContent='Ustalanie lokalizacji…'; try {navigator.geolocation.getCurrentPosition(position => { if (!closed && revision === started) setCenter({lat:position.coords.latitude,lng:position.coords.longitude}); },error => { if (!closed && revision === started) status.textContent=error.code === 1 ? 'Brak zgody na lokalizację. Wskaż centrum na mapie lub pozostaw miasto z wyszukiwarki.' :'Nie udało się ustalić lokalizacji. Możesz wskazać pineskę na mapie.'; },{enableHighAccuracy:true,timeout:10000,maximumAge:60000});} catch {status.textContent='Geolokalizacja jest niedostępna. Wskaż pineskę na mapie.';} } async function chooseEscortRadiusLocations(locations,currentLocations,hasSearch) { let mode='replace'; if (hasSearch) mode=await chooseWatchOptionDialog({ id:'vm-escort-radius-mode-overlay',title:'Jak zastosować wybrane miasta?', info:'Zastąpić lokalizacje bieżącego wyszukiwania czy dodać wybrane miasta do multiwyszukiwania?', choices:[['replace','Zastąp bieżące wyszukiwanie'],['append','Dodaj miasta do multiwyszukiwania']], primaryValue:'append' }); if (!mode) return null; const merged=[...new Map((mode === 'append' ? [...currentLocations,...locations]:locations) .map(item => [getEscortLocationKey(item),item])).values()]; if (merged.length > ESCORT_MULTI_LOCATION_MAX) throw new Error( `Łącznie wybrano ${merged.length} lokalizacji. Limit to ${ESCORT_MULTI_LOCATION_MAX}. Odznacz część miast lub zastąp bieżące wyszukiwanie.` ); return merged; } function initEscortMultiLocationControls() { if (!IS_ESCORT_CLUB) return; ensureEscortMultiLocationUiStyle(); if (document.getElementById(ESCORT_MULTI_LOCATION_UI_ID)) return; if ( SETTINGS.multiLocationEnabled === false && SETTINGS.saveSearchLocationsEnabled === false ) return; const insert=() => { if (document.getElementById(ESCORT_MULTI_LOCATION_UI_ID)) return true; const controls=findEscortSearchLocationControls(); if (!controls?.searchBox || !controls?.submitItem || !controls?.searchButton) return false; if ( SETTINGS.saveSearchLocationsEnabled !== false && location.pathname === '/' ) { const searchTitle=[...document.querySelectorAll('h1,h2,h3,h4')] .find(element => normalizeText(element.textContent) === 'wyszukaj ogłoszenia'); if (searchTitle) { let ancestor=controls.searchBox.parentElement; let compacted=false; for (let depth=0; ancestor && ancestor !== document.body && depth < 7; depth++) { if (ancestor.contains(searchTitle)) { const computed=getComputedStyle(ancestor); const paddingTop=parseFloat(computed.paddingTop) || 0; if (paddingTop >= 45) { ancestor.style.setProperty( 'padding-top', `${Math.max(20,paddingTop - 65)}px`, 'important' ); compacted=true; break; } } ancestor=ancestor.parentElement; } if (!compacted) { searchTitle.style.setProperty('margin-top','-55px','important'); } } controls.searchBox.style.removeProperty('margin-top'); } const form=controls.searchBox.closest('form'); const hasInitialSearch=isEscortSearchPath() || location.pathname === '/searchgate.php' || !!favoriteSearchEditSession; const initialSearchLocation=hasInitialSearch ? readEscortLocationFromControls(controls):null; let multiSubmitInProgress=false; const nativeSearchButtonLabel= String(controls.searchButton.textContent || 'Szukaj').trim() || 'Szukaj'; const nativeSearchButtonTitle=controls.searchButton.getAttribute('title') || ''; const submitNativeSearch=() => { multiSubmitInProgress=true; if (favoriteSearchEditSession) { favoriteSearchEditSession.currentLocation=readEscortLocationFromControls(); persistFavoriteSearchSession(); } if (form?.requestSubmit) { form.requestSubmit(controls.searchButton); } else { controls.searchButton.click(); } }; controls.searchButton.addEventListener('click',() => { if (multiSubmitInProgress || favoriteSearchEditSession) return; saveEscortActiveMultiLocations([]); },{capture:true}); form?.addEventListener('submit',event => { if (multiSubmitInProgress) { multiSubmitInProgress=false; return; } if (event.submitter === controls.searchButton) { if (favoriteSearchEditSession) updateFavoriteSearchEditLocation(); else saveEscortActiveMultiLocations([]); } },{capture:true}); let radiusLink=null; let multiSubmitItem=null; let endMultiButton=null; if (SETTINGS.multiLocationEnabled !== false) { multiSubmitItem=controls.submitItem.cloneNode(false); multiSubmitItem.removeAttribute('id'); const multiSearchButton=controls.searchButton.cloneNode(false); multiSearchButton.type='button'; multiSearchButton.removeAttribute('id'); multiSearchButton.innerHTML='Dodaj do<br>multiwyboru'; multiSearchButton.style.cssText += ';font-size:10px!important;line-height:1.05!important;white-space:normal!important;overflow:hidden!important;'; multiSearchButton.title= 'Dodaje wybraną lokalizację do multiwyboru i od razu uruchamia wyszukiwanie.'; multiSearchButton.addEventListener('click',() => { const latestControls=findEscortSearchLocationControls() || controls; const locationData=readEscortLocationFromControls(latestControls); if (!locationData) { showWatchToast('Najpierw wybierz województwo lub miasto.',true); return; } if (!addEscortActiveMultiLocation(locationData)) return; render(); submitNativeSearch(); }); multiSubmitItem.appendChild(multiSearchButton); controls.submitItem.insertAdjacentElement('afterend',multiSubmitItem); if (CURRENT_HOST === 'pl.escort.club') { radiusLink=prepareHelperStyleApi(makeElement('div','vm-multi-panel vm-radius-link-row')); const mapButton=makeButton('','Wybierz miasta w promieniu',{id:'vm-escort-radius-open',title:'Wybierz centrum na mapie i wyszukaj miasta w zadanym promieniu.'}); mapButton.addEventListener('click',async () => { try {await openEscortRadiusMap(async locations => { const active=getEscortActiveMultiLocations(); const previous=favoriteSearchEditSession?.currentLocation || initialSearchLocation; const chosen=await chooseEscortRadiusLocations(locations, active.length ? active:previous ? [previous]:[],hasInitialSearch || active.length > 0); if (!chosen) return false; await loadEscortLocationOptions(chosen[0],{useKnownIds:true}); saveEscortActiveMultiLocations(chosen); render(); submitNativeSearch(); });} catch (error) {showWatchToast(error.message,true);} }); radiusLink.appendChild(mapButton); } endMultiButton=makeButton('vm-ui-button vm-multi-submit-button -end'); endMultiButton.innerHTML='Zakończ tryb<br>multiwyboru'; endMultiButton.title= 'Usuwa aktywny zestaw lokalizacji i wraca do zwykłego wyszukiwania jednej lokalizacji.'; } const panel=prepareHelperStyleApi( makeElement('div') ); panel.id=ESCORT_MULTI_LOCATION_UI_ID; const searchBoxComputed=getComputedStyle(controls.searchBox); const searchBoxMaxWidth=controls.searchBox.style.maxWidth || searchBoxComputed.maxWidth || '1120px'; panel.classList.add('vm-multi-panel'); panel.style.setProperty('--vm-multi-max-width',searchBoxMaxWidth === 'none' ? '1120px':searchBoxMaxWidth); const activeSection=makeElement('div','vm-multi-section -active'); const activeTopRow=makeElement('div','vm-multi-top-row -active'); const activeLabel=makeElement('strong','vm-multi-label -active','MULTIWYBÓR LOKALIZACJI:'); activeTopRow.append(activeLabel); const activeBox=makeElement('div','vm-multi-active-box'); activeSection.append(activeTopRow,activeBox); if (endMultiButton) { endMultiButton.classList.add('vm-multi-end-inline'); activeTopRow.appendChild(endMultiButton); } panel.appendChild(activeSection); const savedPanel=prepareHelperStyleApi( makeElement('div') ); savedPanel.dataset.vmSavedLocationsUi='1'; savedPanel.classList.add('vm-multi-panel'); savedPanel.style.setProperty('--vm-multi-max-width',searchBoxMaxWidth === 'none' ? '1120px':searchBoxMaxWidth); const savedSection=makeElement('div','vm-multi-section -saved'); savedSection.style.display=SETTINGS.saveSearchLocationsEnabled === false ? 'none':'flex'; const savedTopRow=makeElement('div','vm-multi-top-row -saved'); const savedLabel=makeElement('strong','vm-multi-label -saved','ZAPISANE LOKALIZACJE:'); const saveCurrent=makeButton('vm-multi-save-current','★ Zapisz bieżącą lokalizację'); savedTopRow.append(savedLabel,saveCurrent); const savedBox=makeElement('div','vm-multi-saved-box'); savedSection.append(savedTopRow,savedBox); savedPanel.appendChild(savedSection); const makeActiveChip=locationData => { const card=makeElement('div','vm-multi-active-card'); const text=makeElement('div','vm-multi-active-text',getEscortFullLocationLabel(locationData)); const count=getEscortMultiLocationResultCount(locationData); const countLine=makeElement('div','',count == null ? '…' :`${count} ${polishAdWord(count)}`,{className:`vm-multi-count${count == null ? ' -pending':''}`}); const remove=makeButton('','×',{title:'Usuń z multiwyszukiwania i odśwież wyniki'}); remove.className='vm-multi-active-remove'; remove.addEventListener('click',async () => { const remaining=removeEscortActiveMultiLocation(locationData); render(); if (!remaining.length) { location.reload(); return; } try { const latestControls=findEscortSearchLocationControls() || controls; const currentLocation=readEscortLocationFromControls(latestControls); const currentKey=currentLocation ? getEscortLocationKey(currentLocation) :''; const target=remaining.find(item => getEscortLocationKey(item) === currentKey ) || remaining[0]; if (currentKey && getEscortLocationKey(target) === currentKey) { location.reload(); } else { await loadEscortLocationOptions(target); submitNativeSearch(); } } catch (error) { showWatchToast( error?.message || 'Nie udało się odświeżyć wyników multiwyszukiwania.', true ); } }); card.append(text,countLine,remove); return card; }; const loadSavedGroup=async locations => { const group=normalizeSearchLocations(locations); if (!group.length) return; try { if (group.length > 1) { saveEscortActiveMultiLocations(group); await loadEscortLocationOptions(group[0]); render(); submitNativeSearch(); return; } await loadEscortLocationOptions(group[0]); render(); showWatchToast('Wstawiono zapisaną lokalizację do pól wyszukiwania.'); } catch (error) { showWatchToast( error?.message || 'Nie udało się wczytać zapisanej lokalizacji.', true ); } }; const makeSavedGroupChip=locations => { const group=normalizeSearchLocations(locations); const chip=makeElement('span','vm-multi-saved-chip'); const action=makeButton('',getEscortLocationGroupLabel(group),{title:group.length > 1 ? 'Uruchom zapisane multiwyszukiwanie' :'Wstaw zapisaną lokalizację do pól wyszukiwania'}); action.className='vm-multi-saved-action'; action.addEventListener('click',() => loadSavedGroup(group)); const remove=makeButton('','×',{title:'Usuń zapisaną lokalizację'}); remove.className='vm-multi-saved-remove'; remove.addEventListener('click',event => { event.stopPropagation(); removeEscortSavedLocationGroup(group); render(); }); chip.append(action,remove); return chip; }; const render=() => { const active=getEscortActiveMultiLocations(); activeBox.replaceChildren(); activeSection.style.display= SETTINGS.multiLocationEnabled !== false && active.length ? 'flex':'none'; if (active.length) { activeLabel.textContent=`MULTIWYBÓR LOKALIZACJI (${active.length}):`; active.forEach(locationData => activeBox.appendChild(makeActiveChip(locationData)) ); } savedBox.replaceChildren(); if (SETTINGS.saveSearchLocationsEnabled !== false) { const groups=getEscortSavedLocationGroups(); if (!groups.length) { const empty=makeElement('span','','brak zapisanych lokalizacji'); empty.style.color='var(--vm-ui-text-muted, #7d8790)'; savedBox.appendChild(empty); } else { groups.forEach(group => savedBox.appendChild(makeSavedGroupChip(group)) ); } } saveCurrent.textContent=active.length ? '★ Zapisz bieżącą multilokalizację' :'★ Zapisz bieżącą lokalizację'; saveCurrent.title=active.length ? `Zapisz zestaw: ${getEscortLocationGroupLabel(active)}` :'Zapisz lokalizację wybraną w polach Escort.club'; const soloSearchMode= SETTINGS.multiLocationEnabled !== false && active.length > 0; controls.submitItem.style.display=soloSearchMode ? 'none':''; controls.searchButton.textContent=nativeSearchButtonLabel; controls.searchButton.style.removeProperty('white-space'); controls.searchButton.style.removeProperty('line-height'); if (nativeSearchButtonTitle) { controls.searchButton.title=nativeSearchButtonTitle; } else { controls.searchButton.removeAttribute('title'); } panel.style.display=activeSection.style.display !== 'none' ? 'block' :'none'; savedPanel.style.display=savedSection.style.display !== 'none' ? 'block' :'none'; }; endMultiButton?.addEventListener('click',() => { saveEscortActiveMultiLocations([]); render(); location.reload(); }); saveCurrent.addEventListener('click',() => { const active=getEscortActiveMultiLocations(); if (active.length) { addEscortSavedLocationGroup(active); render(); showWatchToast('Zapisano bieżącą multilokalizację.'); return; } const latestControls=findEscortSearchLocationControls() || controls; const locationData=readEscortLocationFromControls(latestControls); if (!locationData) { showWatchToast('Najpierw wybierz województwo lub miasto.',true); return; } addEscortSavedLocationGroup([locationData]); render(); showWatchToast('Zapisano lokalizację.'); }); escortMultiLocationUiRefreshers.add(render); controls.searchBox.insertAdjacentElement('beforebegin',savedPanel); controls.searchBox.insertAdjacentElement('afterend',panel); if (radiusLink) { radiusLink.style.setProperty('--vm-multi-max-width',searchBoxMaxWidth === 'none' ? '1120px':searchBoxMaxWidth); const filters=document.getElementById('vm-escort-saved-filters-quick')?.closest('.vm-escort-results-controls-shell'); (filters || panel).insertAdjacentElement(filters ? 'beforebegin':'afterend',radiusLink); } render(); return true; }; if (insert()) return; let attempts=0; const timer=setInterval(() => { attempts++; if (insert() || attempts >= 40) clearInterval(timer); },250); } function getEscortMainResultsSection(doc=document) { const sections=[...doc.querySelectorAll('section.content-sec.-index')]; return sections.find(section => { const heading=section.querySelector('h1'); return isEscortMainResultsHeading(heading?.textContent || ''); }) || (IS_ESCORT_CLUB && CURRENT_HOST !== 'pl.escort.club' && !getEscortAdIdFromPath() ? sections.find(section => section.querySelector('h1') && section.querySelector('.item-col.col')):null) || null; } function isEscortResultsPage(doc=document,pageUrl=location.href) { let pathname=location.pathname; try { pathname=new URL(pageUrl,location.href).pathname; } catch {} if (getEscortAdIdFromPath(pathname)) return false; if (isEscortSearchPath(pathname)) return true; return !!getEscortMainResultsSection(doc); } function getEscortSectionTitleCandidates(doc=document) { const selectors=[ 'h1','h2','h3','h4','h5','h6', '.title','.heading','.section-title','.section-heading', '.block-title','.box-title','.content-title','.content-heading', '.list-title','.ads-title', '[class*="section-title"]','[class*="section-heading"]', '[class*="block-title"]','[class*="content-title"]', 'strong','b','p','span','div' ]; const out=[]; for (const el of doc.querySelectorAll(selectors.join(','))) { if ( el.closest( [ '.vm-helper-ui', '.vm-favorites-ui', '[data-vm-style-api="2"]', '[data-vm-favorites-style-api]', '[data-vm-watch-wrapper="1"]' ].join(', ') ) ) { continue; } const raw=(el.textContent || '').replace(/\s+/g,' ').trim(); if (!raw || raw.length > 90) continue; const normalized=normalizeEscortSectionHeading(raw); if ( !/^(?:polecane|wyroznione|popularne miasta)(?:\s|$)/i.test(normalized) && !isEscortMainResultsHeading(normalized) ) { continue; } const hasMatchingChild=[...el.children].some(child => { const childText=(child.textContent || '').replace(/\s+/g,' ').trim(); if (!childText || childText.length > 90) return false; const childNormalized=normalizeEscortSectionHeading(childText); return /^(?:polecane|wyroznione|popularne miasta)(?:\s|$)/i.test(childNormalized) || isEscortMainResultsHeading(childNormalized); }); if (hasMatchingChild) continue; out.push(el); } return out; } function findEscortSectionLabels(pattern,doc=document) { return getEscortSectionTitleCandidates(doc) .filter(el => pattern.test( normalizeEscortSectionHeading(el.textContent || '') )); } function isElementAfter(reference,el) { return !!( reference && el && reference !== el && (reference.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) ); } function isElementBefore(reference,el) { return !!( reference && el && reference !== el && (reference.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING) ); } function getEscortSectionBoundary(label,doc=document) { const labels=getEscortSectionTitleCandidates(doc); const currentIndex=labels.indexOf(label); if (currentIndex >= 0) { return labels.slice(currentIndex + 1)[0] || null; } return labels.find(candidate => isElementAfter(label,candidate)) || null; } function hideEscortSectionByHeading({ headingPattern, targetSelector, targetMapper=el => el, hiddenDatasetKey='vmEscortSectionHidden' }) { const labels=findEscortSectionLabels(headingPattern,document); for (const label of labels) { if ( label.closest( '.vm-helper-ui, .vm-favorites-ui, [data-vm-style-api="2"], ' + '[data-vm-favorites-style-api], [data-vm-watch-wrapper="1"]' ) ) { continue; } if (label.closest(`[data-${hiddenDatasetKey.replace(/[A-Z]/g,m => '-' + m.toLowerCase())}="1"]`)) { continue; } const nextBoundary=getEscortSectionBoundary(label,document); const isInsideRange=el => { if (!isElementAfter(label,el)) return false; if (!nextBoundary) return true; return isElementBefore(nextBoundary,el); }; const targetElements=targetSelector ? [...document.querySelectorAll(targetSelector)] .filter(isInsideRange) .map(targetMapper) .filter(Boolean) :[]; let wrapper=label.parentElement; let hidden=false; while ( wrapper && wrapper !== document.body && wrapper !== document.documentElement ) { const containsTarget=targetElements.length > 0 && targetElements.some(el => wrapper.contains(el)); const containsBoundary=nextBoundary ? wrapper.contains(nextBoundary) :false; if (containsTarget && !containsBoundary) { wrapper.style.setProperty('display','none','important'); wrapper.dataset[hiddenDatasetKey]='1'; hidden=true; break; } wrapper=wrapper.parentElement; } if (hidden) continue; label.style.setProperty('display','none','important'); label.dataset[hiddenDatasetKey]='1'; for (const el of targetElements) { el.style.setProperty('display','none','important'); el.dataset[hiddenDatasetKey]='1'; } } } function applyEscortSectionVisibility() { trimFavoriteSearchEditPage(); for (const searchBox of document.querySelectorAll('.search-box')) { for (let element=searchBox; element; element=element.parentElement) element.classList.add('vm-escort-search-background'); } const isResultsPage=isEscortResultsPage(); const isSingleAdPage=!!getEscortAdIdFromPath(location.pathname); if (isSingleAdPage && SETTINGS.hideSingleAdActions) { for (const actions of document.querySelectorAll('.content-actions-col')) { actions.style.setProperty('display','none','important'); actions.dataset.vmSingleAdActionsHidden='1'; } } if (isResultsPage && SETTINGS.hideRecommendedSearchSection) { for (const section of document.querySelectorAll('section.dates-sec')) { const title=normalizeEscortSectionHeading( section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || '' ); if (/^(?:polecane|recommended escorts)(?:\s|$)/i.test(title)) { section.style.setProperty('display','none','important'); section.dataset.vmRecommendedSectionHidden='1'; } } } if (isSingleAdPage && SETTINGS.hideRecommendedAdSection) { for (const section of document.querySelectorAll('section.dates-sec')) { const title=normalizeEscortSectionHeading( section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || '' ); if (/^(?:polecane|recommended escorts)(?:\s|$)/i.test(title)) { section.style.setProperty('display','none','important'); section.dataset.vmRecommendedAdSectionHidden='1'; } } hideEscortSectionByHeading({ headingPattern:/^(?:polecane|recommended escorts)(?:\s|$)/i, targetSelector:'a:is([href*="/anons/"],[href*="/personal-ads/"])', targetMapper:link => link.closest('.item-col, .col') || link, hiddenDatasetKey:'vmRecommendedAdSectionHidden' }); } if (isSingleAdPage && SETTINGS.hideSingleAdContactButtons) { for (const contactBox of document.querySelectorAll('.content-contact')) { const containsTargetButton=contactBox.querySelector( 'a[href*="action=tipTokens"], a[href*="action=sendMessage"]' ); if (!containsTargetButton) continue; contactBox.style.setProperty('display','none','important'); contactBox.dataset.vmSingleAdContactButtonsHidden='1'; } } if (isResultsPage && SETTINGS.hideFeaturedSearchSection) { for (const section of document.querySelectorAll('section.vip-sec')) { const title=normalizeEscortSectionHeading( section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || '' ); if (/^(?:wyroznione|escorts promoted)(?:\s|$)/i.test(title)) { section.style.setProperty('display','none','important'); section.dataset.vmFeaturedSectionHidden='1'; } } } if ( SETTINGS.hidePopularCitiesSection && (isResultsPage || isSingleAdPage) ) { const cityList=document.querySelector('#cityList'); const section=cityList?.closest('section.search-sec'); if (section) { section.style.setProperty('display','none','important'); section.dataset.vmPopularCitiesSectionHidden='1'; } else { hideEscortSectionByHeading({ headingPattern:/^(?:popularne miasta|popular cities)(?:\s|$)/i, targetSelector:'a[href*="/anonse/"]', targetMapper:link => link, hiddenDatasetKey:'vmPopularCitiesSectionHidden' }); } } } function initEscortSectionVisibility() { const style=makeElement('style'); style.textContent='.vm-escort-search-background,.vm-escort-search-background::before,.vm-escort-search-background::after { background-image:none !important; }'; document.head.appendChild(style); applyEscortSectionVisibility(); if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', applyEscortSectionVisibility, {once:true} ); } window.addEventListener('load',applyEscortSectionVisibility,{once:true}); if ( favoriteSearchEditSession || SETTINGS.hideRecommendedSearchSection || SETTINGS.hideRecommendedAdSection || SETTINGS.hideSingleAdContactButtons || SETTINGS.hideSingleAdActions || SETTINGS.hideFeaturedSearchSection || SETTINGS.hidePopularCitiesSection ) { let updateScheduled=false; const observer=new MutationObserver(() => { if (updateScheduled) return; updateScheduled=true; requestAnimationFrame(() => { updateScheduled=false; applyEscortSectionVisibility(); }); }); observer.observe(document.documentElement,{ childList:true, subtree:true }); } } function hideEscortMainResultsTitle(listContext=null) { const heading=listContext?.heading || findEscortMainResultsHeading(); const titleCol=heading?.closest('.title-col.col, .title-col'); if (!titleCol) return; titleCol.style.display='none'; titleCol.dataset.vmEscortMainTitleHidden='1'; } function findEscortResultsBreadcrumb(heading=findEscortMainResultsHeading()) { if (!heading) return null; const isBeforeHeading=element => !!( element && element !== heading && (element.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING) ); const preferred=[...document.querySelectorAll( '.breadcrumb, .breadcrumbs, [class*="breadcrumb"], [class*="breadcrumbs"]' )].filter(element => isBeforeHeading(element) && /escort\.club/i.test(element.textContent || '') && /anonse erotyczne/i.test(element.textContent || '') ); let container=preferred.at(-1) || null; if (!container) { const escortLinks=[...document.querySelectorAll('a')].filter(link => normalizeEscortAdText(link.textContent).toLocaleLowerCase('pl-PL') === 'escort.club' && isBeforeHeading(link) ); for (const link of escortLinks.reverse()) { let parent=link.parentElement; for (let depth=0; parent && depth < 4; depth++,parent=parent.parentElement) { const text=normalizeEscortAdText(parent.textContent); if (/anonse erotyczne/i.test(text) && text.length < 240) { container=parent; break; } } if (container) break; } } return container; } function updateEscortMultiResultsBreadcrumb( locations, forcedTotal=null ) { const group=normalizeSearchLocations(locations); const resultCounter=document.querySelector( '.vm-escort-results-count, .search-links > .sub-label' ); if (group.length <= 1) { setEscortMultiResultCountersVisible(true); if (resultCounter?.dataset.vmOriginalText != null) { resultCounter.textContent= formatEscortResultPages(resultCounter.dataset.vmOriginalText,getEscortResultsPageCount()); } return; } const forced=forcedTotal == null || forcedTotal === '' ? NaN:Number(forcedTotal); const total= Number.isFinite(forced) && forced >= 0 ? forced :getEscortMultiLocationKnownTotal(group); if (resultCounter && total != null) { markEscortResultCounter(resultCounter); const counts=group.map(value => escortMultiLocationPageCounts.get(getEscortLocationKey(value))); const text=`Lista wyników: ${total} sex ${polishCountForm(total,'ogłoszenie','ogłoszenia','ogłoszeń')}`; resultCounter.textContent=counts.every(Number.isFinite) ? formatEscortResultPages(text,counts.reduce((sum,n) => sum + n,0)):text; } const newLabel=`Anonse erotyczne ${getEscortLocationGroupLabel(group)}`; setEscortMultiResultCountersVisible(false); if (resultCounter && total != null) { resultCounter.style.removeProperty('display'); delete resultCounter.dataset.vmMultiResultCounterHidden; } const container=findEscortResultsBreadcrumb(); if (!container) return; const targetChild=[...container.querySelectorAll('a, span, li, p')] .reverse() .find(element => /anonse erotyczne/i.test(element.textContent || '') && normalizeEscortAdText(element.textContent).length < 180 ); if (targetChild) { targetChild.textContent=newLabel; return; } const walker=document.createTreeWalker(container,NodeFilter.SHOW_TEXT); let node=walker.nextNode(); while (node) { if (/anonse erotyczne/i.test(node.nodeValue || '')) { node.nodeValue=String(node.nodeValue || '').replace( /Anonse erotyczne.*$/i, newLabel ); return; } node=walker.nextNode(); } } function findEscortMainResultsHeading(doc=document) { const section=getEscortMainResultsSection(doc); if (section) return section.querySelector('h1'); return getEscortSectionTitleCandidates(doc) .find(el => isEscortMainResultsHeading(el.textContent || '')) || null; } function getEscortClubListContext(doc=document,pageUrl=location.href) { const cardSelector='.item-col.col > a:is([href*="/anons/"],[href*="/personal-ads/"])'; let pagePath=location.pathname; try {pagePath=new URL(pageUrl,location.href).pathname;} catch {} if (getEscortAdIdFromPath(pagePath)) return null; const mainSection=getEscortMainResultsSection(doc); if (mainSection) { return { anchors:[...mainSection.querySelectorAll(cardSelector)], adsNav:mainSection.querySelector('.ads-nav'), heading:mainSection.querySelector('h1') }; } if (isEscortSearchPath(pagePath)) { const heading=findEscortMainResultsHeading(doc); if (!heading) return null; const labels=getEscortSectionTitleCandidates(doc); const headingIndex=labels.indexOf(heading); const nextBoundary=headingIndex >= 0 ? labels.slice(headingIndex + 1)[0] || null :labels.find(el => isElementAfter(heading,el)) || null; const isInsideTargetSection=el => { if (!isElementAfter(heading,el)) return false; if (!nextBoundary) return true; return isElementBefore(nextBoundary,el); }; return { anchors:[...doc.querySelectorAll(cardSelector)].filter(isInsideTargetSection), adsNav:[...doc.querySelectorAll('.ads-nav')].find(isInsideTargetSection) || null, heading }; } const root=doc.querySelector('section.content-sec.-index') || doc; return { anchors:[...root.querySelectorAll(cardSelector)], adsNav:root.querySelector('.ads-nav'), heading:root.querySelector('h1') }; } function waitForEscortClubListContext(timeoutMs=10000) { return new Promise(resolve => { let observer=null,deadline=null,tick=null,poll=null,done=false; const finish=context => { if (done) return; done=true; observer?.disconnect(); clearTimeout(deadline); clearTimeout(tick); clearInterval(poll); document.removeEventListener('DOMContentLoaded',schedule); resolve(context); }; const check=() => { tick=null; const context=getEscortClubListContext(); if (context && (context.anchors?.length || getEscortActiveMultiLocations().length > 1 || (document.readyState !== 'loading' && getEscortListTotalResultCount(document,context) === 0))) finish(context); }; const schedule=() => {if (!done && !tick) tick=setTimeout(check,50);}; applyEscortSectionVisibility(); check(); if (done) return; if (typeof MutationObserver === 'function') { observer=new MutationObserver(schedule); observer.observe(document.body || document.documentElement,{childList:true,subtree:true}); } else poll=setInterval(check,500); document.addEventListener('DOMContentLoaded',schedule); deadline=setTimeout(() => finish(getEscortClubListContext()),Math.max(0,timeoutMs)); }); } function getEscortListCardContainer(anchors) { const cards=anchors .map(anchor => anchor.closest('.item-col.col')) .filter(Boolean); if (!cards.length) return null; const firstParent=cards[0].parentElement; if (!firstParent) return null; return firstParent; } function escortAdDataCacheKey(adId) { return `vm_escort_ad_data_${adId}`; } function normalizeEscortAdText(value) { return String(value || '').replace(/\s+/g,' ').trim(); } function normalizeEscortAdMultilineText(value) { return String(value || '') .replace(/\r\n?/g,'\n') .replace(/[^\S\r\n]+/g,' ') .replace(/ *\n */g,'\n') .replace(/\n{3,}/g,'\n\n') .trim(); } function extractEscortClubDescription(doc) { const box=doc?.querySelector('.content-desc'); if (!box) return null; let source=box.querySelector( '.tab-content .tab-pane.show.active, ' + '.tab-content .tab-pane.active, ' + '.tab-content .tab-pane' ); if (!source) { source=box.cloneNode(true); source.querySelectorAll('.label, .nav, script, style').forEach(el => detachHelperNode(el)); } else { source=source.cloneNode(true); source.querySelectorAll('script, style').forEach(el => detachHelperNode(el)); } source.querySelectorAll('br').forEach(br => br.replaceWith('\n')); return normalizeEscortAdMultilineText(source.textContent) || null; } function readEscortPhoneFromDocument(doc) { for (const link of doc.querySelectorAll('[data-show-phone][data-phone-id], .adsPhone a[href]')) { const href=String(link.getAttribute('href') || '').trim(); const candidates=[ /^tel:/i.test(href) ? href.replace(/^tel:/i,''):'', link.getAttribute('data-phone'), link.textContent ]; for (const value of candidates) { const digits=normalizeEscortCachedPhone(value); if (!digits) continue; return { phone:formatEscortPhoneForDisplay(value), phoneDigits:digits, phoneFetchStatus:'page', phoneCheckedAt:Date.now() }; } } return null; } function extractEscortClubPhoneId(doc) { const phoneLink=doc?.querySelector( '[data-show-phone][data-phone-id], [data-phone-id]' ); const phoneId=String(phoneLink?.getAttribute('data-phone-id') || '').trim(); return /^\d+$/.test(phoneId) ? phoneId:null; } function extractEscortClubAgencyOrSalonPhone(doc) { if (!doc) return false; const contactContainers=new Set(); const phoneLink=doc.querySelector( '.adsPhone [data-show-phone], .adsPhone a[href^="tel:"], ' + '[data-show-phone][data-phone-id]' ); const closestContactContainer=phoneLink?.closest('.content-contact'); if (closestContactContainer) contactContainers.add(closestContactContainer); for (const phoneContainer of doc.querySelectorAll('.adsPhone')) { const contactContainer=phoneContainer.closest('.content-contact'); contactContainers.add(contactContainer || phoneContainer); } return [...contactContainers].some(container => /\(\s*numer\s+telefonu\b[^)]*\)/i.test( normalizeEscortAdText(container.textContent) ) ); } function parseEscortClubPhoneResponse(response) { const raw=response?.responseText || ''; let payload=response?.response; if (!payload || typeof payload !== 'object') { try { payload=JSON.parse(raw); } catch { throw new Error(`Nieprawidłowa odpowiedź numeru: ${raw.slice(0,120) || 'pusta'}`); } } const phone=normalizeEscortAdText(payload?.phone); const phoneDigits=normalizeEscortCachedPhone(phone); if (!phone || !phoneDigits) { throw new Error(payload?.error || payload?.message || 'Odpowiedź nie zawiera numeru'); } return {phone,phoneDigits}; } async function fetchEscortClubPhone(phoneId,cancelToken=null,requestContext=null) { if (!/^\d+$/.test(String(phoneId || ''))) { throw new Error('Brak prawidłowego data-phone-id'); } const response=await gmRequest({ method:'POST', url:ESCORT_PHONE_ENDPOINT, cancelToken, timeout:ESCORT_AD_DATA_FETCH_TIMEOUT_MS, requestContext:{...requestContext,...createRequestContext(requestContext?.source || 'escort-phone',requestContext)}, headers:{ 'Accept':'application/json, text/javascript, */*; q=0.01', 'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With':'XMLHttpRequest' }, data:new URLSearchParams({id:String(phoneId)}).toString() }); if (response.status && (response.status < 200 || response.status >= 400)) { throw new Error(`HTTP ${response.status}`); } return parseEscortClubPhoneResponse(response); } function extractEscortClubAvailability(doc) { if (!doc) return {}; const box=[...doc.querySelectorAll('.sub-info-box')] .find(candidate => /^(?:godziny dostępności|availability|working hours|working time)$/i.test(normalizeText( candidate.querySelector('.label')?.textContent ))); if (!box) return {}; const availability={}; for (const row of box.querySelectorAll('.sub-info-items .sub-info-elem')) { const day=normalizeEscortAdText(row.querySelector('.sub-label')?.textContent) .replace(/:\s*$/,''); const hours=normalizeEscortAdText(row.querySelector('.desc')?.textContent); if (day && hours) availability[day]=hours; } return availability; } function normalizeEscortAvailabilityToken(value) { const token=normalizeEscortAdText(value) .toLocaleLowerCase('pl-PL') .normalize('NFD') .replace(/[\u0300-\u036f]/g,'') .replace(/ł/g,'l') .replace(/:\s*$/,'') .trim(); return ({monday:'poniedzialek',tuesday:'wtorek',wednesday:'sroda',thursday:'czwartek',friday:'piatek',saturday:'sobota',sunday:'niedziela'})[token] || token; } function getEscortCurrentWeekdayToken(now=new Date()) { try { return normalizeEscortAvailabilityToken( new Intl.DateTimeFormat('pl-PL',{weekday:'long'}).format(now) ); } catch { return ['niedziela','poniedzialek','wtorek','sroda','czwartek','piatek','sobota'][now.getDay()]; } } function getEscortAvailabilityNow(availability,now=new Date()) { if (!availability || typeof availability !== 'object') return null; const wantedDay=getEscortCurrentWeekdayToken(now); const entry=Object.entries(availability).find(([day]) => normalizeEscortAvailabilityToken(day) === wantedDay ); if (!entry) return null; const hoursText=String(entry[1] || ''); const normalized=normalizeEscortAvailabilityToken(hoursText); if (!normalized) return null; if (/niedostepn|not available|unavailable|closed/.test(normalized)) return false; if (/^(?:caly czas|cala dobe|all day|all the time|24\s*h)$/.test(normalized)) return true; const nowMinutes=now.getHours() * 60 + now.getMinutes(); const ranges=[...hoursText.matchAll( /(\d{1,2})[.:](\d{2})\s*(?:do|to|[-–\u2014])\s*(\d{1,2})[.:](\d{2})/gi )].map(match => ({ start:Number(match[1]) * 60 + Number(match[2]), end:Number(match[3]) * 60 + Number(match[4]), valid:Number(match[1]) < 24 && Number(match[3]) < 24 && Number(match[2]) < 60 && Number(match[4]) < 60 })).filter(range => range.valid); if (!ranges.length) return null; return ranges.some(({start,end}) => { if (start === end) return true; if (start < end) return nowMinutes >= start && nowMinutes <= end; return nowMinutes >= start || nowMinutes <= end; }); } function initEscortClubCurrentAvailabilityDot() { if (!getEscortAdIdFromPath(location.pathname)) return; if (!document.getElementById('vm-escort-current-availability-style')) { appendHelperStyle('vm-escort-current-availability-style',` .vm-escort-current-availability-open::before, .vm-escort-current-availability-closed::before, .vm-escort-location-current-availability-open::after, .vm-escort-location-current-availability-closed::after { content: '●'; display: inline-block; font-size: 19px; line-height: 1; vertical-align: -2px; } .vm-escort-current-availability-open::before, .vm-escort-current-availability-closed::before { margin-right: 6px; } .vm-escort-location-current-availability-open::after, .vm-escort-location-current-availability-closed::after { margin-left: 6px; } .vm-escort-current-availability-open::before, .vm-escort-location-current-availability-open::after { color: #1aa34a; } .vm-escort-current-availability-closed::before, .vm-escort-location-current-availability-closed::after { color: #d32f2f; } `); } const apply=() => { const box=findEscortSubInfoBox('godziny dostępności'); if (!box) return false; const status=getEscortAvailabilityNow(extractEscortClubAvailability(document)); const wantedDay=getEscortCurrentWeekdayToken(); for (const row of box.querySelectorAll('.sub-info-elem')) { const label=row.querySelector('.sub-label'); if (!label) continue; label.classList.remove( 'vm-escort-current-availability-open', 'vm-escort-current-availability-closed' ); label.removeAttribute('data-vm-current-availability'); if (normalizeEscortAvailabilityToken(label.textContent) !== wantedDay || status == null) continue; label.classList.add(status ? 'vm-escort-current-availability-open' :'vm-escort-current-availability-closed'); label.dataset.vmCurrentAvailability=status ? 'open':'closed'; label.title=status ? 'Aktualnie czynne':'Aktualnie zamknięte'; } const locationLabel=document.querySelector( '.content-info-col.-desc .content-name > .content-location .sub-label, .content-name > .content-location .sub-label' ); if (locationLabel) { locationLabel.classList.remove( 'vm-escort-location-current-availability-open', 'vm-escort-location-current-availability-closed' ); if (status != null) { locationLabel.classList.add(status ? 'vm-escort-location-current-availability-open' :'vm-escort-location-current-availability-closed'); locationLabel.title=status ? 'Aktualnie czynne':'Aktualnie zamknięte'; } } return true; }; apply(); let attempts=0; const readyTimer=setInterval(() => { if (apply() || ++attempts >= 40) clearInterval(readyTimer); },250); setInterval(apply,60000); } function getFavoriteAvailabilityNow(item,now=new Date(),details=getWatchedItemCachedAdDetails(item,true)) { if (!details.length) return null; const statuses=details.map(detail => getEscortAvailabilityNow(detail.adData?.availability,now) ); if (statuses.some(status => status === true)) return true; if (statuses.length && statuses.every(status => status === false)) return false; return null; } function getFavoriteAvailabilityTodayTooltip(item,now=new Date(),details=getWatchedItemCachedAdDetails(item,true)) { const wantedDay=getEscortCurrentWeekdayToken(now); const values=[...new Set( details.map(detail => { const availability=detail.adData?.availability; if (!availability || typeof availability !== 'object') return ''; const entry=Object.entries(availability).find(([day]) => normalizeEscortAvailabilityToken(day) === wantedDay ); return normalizeEscortAdText(entry?.[1]); }).filter(Boolean) )]; if (!values.length) return ''; return `Dzisiaj: ${values.join(' • ')}`; } function applyEscortClubTagHighlights(targetDocument=document) { if (targetDocument === document && !getEscortAdIdFromPath(location.pathname)) return false; if (SETTINGS.showTagHighlights === false) return false; const groups=normalizeTagHighlightGroups(SETTINGS.tagHighlightGroups); const fallback=groups.at(-1); const tagMap=new Map(); for (const group of groups.slice(0,-1)) { for (const tag of group.tags) { tagMap.set(normalizeEscortChangeComparisonValue(tag),group); } } const elements=[...targetDocument.querySelectorAll('.sub-info-box .tags-box .tag')]; if (!elements.length) return false; for (const element of elements) { const label=normalizeEscortAdText(element.textContent); const group=tagMap.get(normalizeEscortChangeComparisonValue(label)) || fallback; const color=normalizeFavoriteCategoryColor(group?.color) || DEFAULT_TAG_HIGHLIGHT_GROUPS.at(-1).color; element.dataset.vmTagHighlightGroup=group?.id || 'other'; element.style.setProperty('background-color',color,'important'); element.style.setProperty('border-color',color,'important'); element.style.setProperty('color','#fff','important'); element.style.setProperty('box-shadow',`inset 0 0 0 1px ${color}`,'important'); element.title=`Grupa: ${group?.name || 'pozostałe'}`; } return true; } function ensureEscortClubTagsSettingsGear() { if (!getEscortAdIdFromPath(location.pathname)) { return false; } if (document.getElementById('vm-escort-tags-settings-gear')) { return true; } const box=findEscortSubInfoBox('tagi'); const label=box?.querySelector(':scope > .label, .label'); if (!label) return false; const gear=makeSettingsTabGearButton( 'ad', 'Ustawienia - Pojedynczy anons' ); gear.id='vm-escort-tags-settings-gear'; Object.assign(gear.style,{ width:'18px', height:'18px', marginLeft:'5px', border:'0', borderRadius:'4px', background:'transparent', color:'currentColor', verticalAlign:'middle' }); const svg=gear.querySelector('svg'); if (svg) { svg.setAttribute('width','13'); svg.setAttribute('height','13'); } label.style.display='flex'; label.style.alignItems='center'; label.style.gap='0'; label.appendChild(gear); return true; } function initEscortClubTagHighlighting() { if (SETTINGS.showTagHighlights === false) { ensureEscortClubTagsSettingsGear(); return; } const apply=() => { const highlighted=applyEscortClubTagHighlights(); ensureEscortClubTagsSettingsGear(); return highlighted; }; apply(); let attempts=0; const timer=setInterval(() => { if (apply() || ++attempts >= 40) clearInterval(timer); },250); } function extractEscortClubTags(doc) { if (!doc) return []; const box=[...doc.querySelectorAll('.sub-info-box')] .find(candidate => normalizeText( candidate.querySelector('.label')?.textContent ) === 'tagi'); if (!box) return []; const tags=[]; const seen=new Set(); for (const element of box.querySelectorAll('.tags-box .tag')) { const tag=normalizeEscortAdText(element.textContent); const key=tag.toLocaleLowerCase('pl-PL'); if (!tag || seen.has(key)) continue; seen.add(key); tags.push(tag); } return tags; } function extractEscortClubLocation(doc) { const location={ country:null, province:null, city:null, district:null, area:null, addressText:null, addressParts:[] }; if (!doc) return location; const locationBox=[...doc.querySelectorAll('.content-name .content-location, .content-info-col .content-location')] .find(box => box.querySelector('.sub-label')); const label=locationBox?.querySelector('.sub-label') || null; if (label) { location.addressText=normalizeEscortAdText(label.textContent) || null; for (const link of label.querySelectorAll('a[href]')) { const text=normalizeEscortAdText(link.textContent); if (!text) continue; try { const url=new URL(link.getAttribute('href'),'https://pl.escort.club/'); const path=url.pathname; if (/^\/(?:anonse\/towarzyskie|personal-ads)\/poland\/?$/i.test(path)) { location.country=text; continue; } if (/^\/(?:szukaj|search)\/?$/i.test(path) && url.searchParams.has('province')) { location.province=text; continue; } if (/^\/(?:anonse\/towarzyskie|personal-ads)\/[^/]+\/?$/i.test(path)) { if (url.searchParams.has('district')) { location.district=text; } else if (!/^poland$/i.test(path.split('/').filter(Boolean).pop() || '')) { location.city=text; } } } catch {} } } const mapLink=doc.querySelector( '.content-location #showMap[adres], .content-location .content-map[adres]' ); if (mapLink) { location.addressParts=String(mapLink.getAttribute('adres') || '') .split(',') .map(part => normalizeEscortAdText(part)) .filter(Boolean); } location.city=location.city || extractEscortClubCity(doc); if (location.addressParts.length) { if (!location.province) location.province=location.addressParts[0] || null; if (!location.city) location.city=location.addressParts[1] || null; if (!location.district && location.addressParts.length >= 4) { location.district=location.addressParts[2] || null; } const areaIndex=location.district ? 3:2; if (location.addressParts.length > areaIndex) { location.area=location.addressParts.slice(areaIndex).join(', ') || null; } } return location; } function extractEscortClubProfileStats(doc) { const stats={}; if (!doc) return stats; for (const elem of doc.querySelectorAll('.content-hours .stats-box .stat-elem')) { const label=normalizeEscortAdText(elem.querySelector('.sub-label')?.textContent) .replace(/:\s*$/,''); const value=normalizeEscortAdText(elem.querySelector('.sub-desc')?.textContent); if (!label) continue; stats[label]=value || null; } return stats; } function extractEscortClubStructuredAdData(doc) { if (!doc) return {}; for (const script of doc.querySelectorAll('script[type="application/ld+json"]')) { try { const parsed=JSON.parse(script.textContent || 'null'); const queue=Array.isArray(parsed) ? [...parsed]:[parsed]; while (queue.length) { const item=queue.shift(); if (!item || typeof item !== 'object') continue; if (Array.isArray(item['@graph'])) queue.push(...item['@graph']); const types=Array.isArray(item['@type']) ? item['@type']:[item['@type']]; if (!types.some(type => String(type || '').toLowerCase() === 'classifiedad')) { continue; } return { datePosted:item.datePosted || null, structuredCity:item.location?.address?.addressLocality || item.location?.name || null }; } } catch {} } return {}; } function normalizeEscortClubPriceDurationKey(value) { const normalized=normalizeEscortAdText(value) .toLowerCase() .replace(/,/g,'.') .replace(/:\s*$/,''); if (!normalized) return null; if (/\bnoc\b|ca(?:ł|l)[aą]?\s+noc/.test(normalized)) return 'night'; if (/\bp[oó]ł\s*(?:godz|godzin)/.test(normalized)) return '30'; const minutesMatch=normalized.match(/(\d+(?:\.\d+)?)\s*(?:min|minut)/); if (minutesMatch) { const minutes=Math.round(Number(minutesMatch[1])); return Number.isFinite(minutes) && minutes > 0 ? String(minutes):null; } const hoursMatch=normalized.match(/(\d+(?:\.\d+)?)\s*(?:h\b|godz|godzin)/); if (hoursMatch) { const minutes=Math.round(Number(hoursMatch[1]) * 60); return Number.isFinite(minutes) && minutes > 0 ? String(minutes):null; } if (/^(?:jedna\s+)?godzina$/.test(normalized)) return '60'; return null; } function parseEscortClubPrice(value) { const raw=normalizeEscortAdText(value).replace(/\u00a0/g,' '); if (!raw) return null; const match=raw.match(/(\d[\d\s]*(?:[.,]\d+)?)\s*(PLN|zł|EUR|€|USD|\$|GBP|£)?/i); if (!match) return null; const amount=Number(match[1].replace(/\s/g,'').replace(',','.')); if (!Number.isFinite(amount)) return null; const currencyToken=String(match[2] || '').toUpperCase(); const currency=currencyToken === 'ZŁ' ? 'PLN' :currencyToken === '€' ? 'EUR' :currencyToken === '$' ? 'USD' :currencyToken === '£' ? 'GBP' :currencyToken || null; return {amount,currency}; } function extractEscortClubPrices(doc) { const prices={}; if (!doc) return prices; for (const elem of doc.querySelectorAll( '.contant-prices .stat-elem, .content-prices .stat-elem' )) { const rawLabel=normalizeEscortAdText( elem.querySelector('.sub-label')?.textContent ).replace(/:\s*$/,''); const durationKey=normalizeEscortClubPriceDurationKey(rawLabel); const parsedPrice=parseEscortClubPrice( elem.querySelector('.sub-desc')?.textContent ); if (!durationKey) continue; prices[durationKey]=parsedPrice || { amount:null, currency:null }; } return prices; } function formatEscortClubDatePosted(value) { const raw=String(value || '').trim(); if (!raw) return ''; const match=raw.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!match) return raw; const date=new Date(Number(match[1]),Number(match[2]) - 1,Number(match[3])); if ( Number.isNaN(date.getTime()) || date.getFullYear() !== Number(match[1]) || date.getMonth() !== Number(match[2]) - 1 || date.getDate() !== Number(match[3]) ) { return raw; } try { return new Intl.DateTimeFormat('pl-PL',{ day:'numeric', month:'long', year:'numeric' }).format(date); } catch { return raw; } } function initEscortClubDatePostedDisplay() { if (!getEscortAdIdFromPath(location.pathname)) return; if (isEscortCachedArchiveRuntime()) return; const insertDate=() => { if (document.getElementById('vm-escort-date-posted')) return true; const profileHeading=document.querySelector( '.content-info-col.-desc > .content-name.-desc-name > h1, ' + '.content-info-col.-desc > .content-name > h1' ); if (!profileHeading) return false; profileHeading.style.display='block'; const profileColumn=profileHeading.closest('.content-info-col.-desc'); const profileWrapper=profileColumn?.closest('.content-info-wrapper.-left'); const galleryColumn=profileWrapper?.querySelector('.content-gallery-col'); if (profileWrapper) profileWrapper.style.alignItems='flex-start'; if (profileColumn) { profileColumn.style.alignSelf='flex-start'; profileColumn.style.verticalAlign='top'; } if (galleryColumn) { galleryColumn.style.alignSelf='flex-start'; galleryColumn.style.verticalAlign='top'; } if (profileWrapper && profileColumn && galleryColumn && window.matchMedia?.('(min-width: 768px)').matches) { const halvePx=(element,property,cssProperty) => { const value=parseFloat(getComputedStyle(element)[property]); if (!Number.isFinite(value) || value <= 0) return; element.style.setProperty(cssProperty,`${Math.max(0,value / 2)}px`); }; halvePx(profileWrapper,'columnGap','column-gap'); halvePx(galleryColumn,'paddingRight','padding-right'); halvePx(profileColumn,'paddingLeft','padding-left'); } const datePosted=formatEscortClubDatePosted( extractEscortClubStructuredAdData(document).datePosted ); if (!datePosted) return true; const row=makeElement('span'); row.id='vm-escort-date-posted'; row.setAttribute('aria-label',`Data publikacji anonsu: ${datePosted}`); Object.assign(row.style,{ display:'block', boxSizing:'border-box', position:'static', float:'none', clear:'both', width:'100%', margin:'0 0 18px', padding:'4px 0', background:'transparent', color:getEscortPagePinkColor(), fontSize:'14px', lineHeight:'1.35', textAlign:'left', whiteSpace:'normal' }); const publishedLine=makeElement('span'); Object.assign(publishedLine.style,{ display:'flex', position:'static', float:'none', alignItems:'baseline', gap:'6px', whiteSpace:'nowrap' }); const label=makeElement('span','','Opublikowano:'); const date=makeElement('strong','',datePosted); date.style.color=getEscortPagePinkColor(); publishedLine.append(label,date); row.appendChild(publishedLine); const pendingLastVisit=document.getElementById(ESCORT_LAST_VISIT_ID); if (pendingLastVisit) row.appendChild(pendingLastVisit); const visitSummary=document.getElementById(ESCORT_VISIT_SUMMARY_ID); if (visitSummary) { row.style.marginBottom='5px'; visitSummary.insertAdjacentElement('beforebegin',row); } else { profileHeading.insertAdjacentElement('afterbegin',row); } return true; }; if (insertDate()) return; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded',insertDate,{once:true}); } else { window.addEventListener('load',insertDate,{once:true}); } } function extractEscortClubTitle(doc) { const heading=doc?.querySelector( '.content-info-col .content-name h1, .content-name.-desc-name h1' ); if (!heading) return null; const source=heading.cloneNode(true); source.querySelectorAll( '#vm-escort-date-posted, #vm-escort-visit-summary, #vm-escort-last-visited' ).forEach(element => detachHelperNode(element)); return normalizeEscortAdText(source.textContent) || null; } function extractEscortClubAdData(doc,adId,adUrl) { const structured=extractEscortClubStructuredAdData(doc); const location=extractEscortClubLocation(doc); if (!location.city && structured.structuredCity) { location.city=normalizeEscortAdText(structured.structuredCity) || null; } return { adId:String(adId), adUrl, phoneId:extractEscortClubPhoneId(doc), ...readEscortPhoneFromDocument(doc), isAgencyOrSalonPhone:extractEscortClubAgencyOrSalonPhone(doc), title:extractEscortClubTitle(doc), imageUrl:extractWatchPageImage(doc,adUrl), description:extractEscortClubDescription(doc), availability:extractEscortClubAvailability(doc), tags:extractEscortClubTags(doc), prices:extractEscortClubPrices(doc), datePosted:structured.datePosted || null, location, stats:extractEscortClubProfileStats(doc) }; } function normalizeEscortChangeComparisonValue(value) { return normalizeEscortAdText(value) .normalize('NFKC') .toLocaleLowerCase('pl-PL'); } function getEscortObjectEntryByNormalizedKey(object,key) { const wanted=normalizeEscortChangeComparisonValue(key).replace(/:\s*$/,''); return Object.entries(object || {}).find(([candidate]) => normalizeEscortChangeComparisonValue(candidate).replace(/:\s*$/,'') === wanted ) || null; } function formatEscortPhoneForDisplay(value) { const raw=normalizeEscortAdText(value).replace(/^tel:\s*/i,''); const digits=normalizeEscortCachedPhone(raw); if (!digits) return raw; if (digits.length === 9) { return '+48 ' + digits.replace(/(\d{3})(\d{3})(\d{3})/,'$1 $2 $3'); } return raw.startsWith('+') ? raw:`+${digits}`; } function getEscortComparableAddress(data) { if (!data || typeof data !== 'object') return null; if (Object.prototype.hasOwnProperty.call(data,'address')) { return normalizeEscortAdText(data.address) || null; } const location=data.location || {}; const addressText=normalizeEscortAdText(location.addressText); if (addressText) return addressText; const addressParts=Array.isArray(location.addressParts) ? location.addressParts.map(normalizeEscortAdText).filter(Boolean) :[]; if (addressParts.length) return addressParts.join(', '); return [ location.country, location.province, location.city, location.district, location.area ].map(normalizeEscortAdText).filter(Boolean).join(', ') || null; } function normalizeEscortImageComparisonValue( value ) { const raw=String(value || '').trim(); if (!raw) return ''; try { const url=new URL( raw, location.href ); url.search=''; url.hash=''; return `${url.hostname.toLowerCase()}${ url.pathname }`; } catch { return raw .replace(/[?#].*$/,'') .trim() .toLocaleLowerCase('pl-PL'); } } function createEscortVisitSnapshot(data,previousSnapshot=null) { const currentPhoneDigits=getEscortAdPhoneDigits(data); const previousPhoneDigits=getEscortAdPhoneDigits(previousSnapshot); const hasCurrentPhone=!!currentPhoneDigits; return { title: normalizeEscortAdText( data?.title ) || null, imageUrl: String(data?.imageUrl || '').trim() || String( previousSnapshot?.imageUrl || '' ).trim() || null, address:getEscortComparableAddress(data), phone:hasCurrentPhone ? (data.phone || formatEscortPhoneForDisplay(currentPhoneDigits)) :(previousPhoneDigits ? previousSnapshot.phone:null), phoneDigits:hasCurrentPhone ? currentPhoneDigits :(previousPhoneDigits || null), description:data?.description || null, availability:{...(data?.availability || {})}, prices:Object.fromEntries(Object.entries(data?.prices || {}).map( ([key,value]) => [key,value && typeof value === 'object' ? {...value}:value] )), stats:{...(data?.stats || {})}, tags:Array.isArray(data?.tags) ? [...data.tags]:[] }; } function readEscortPhoneFromCurrentPage() { return readEscortPhoneFromDocument(document); } async function resolveEscortPhoneForCurrentPage(phoneId) { for (let attempt=0; attempt < 9; attempt++) { const fromPage=readEscortPhoneFromCurrentPage(); if (fromPage) return fromPage; await new Promise(resolve => setTimeout(resolve,200)); } if (!phoneId) return null; const adId=getEscortAdIdFromPath(location.pathname); const cached=[escortAdDataMemoryCache.get(String(adId)),getStoredEscortAdData(adId)] .find(data => String(data?.phoneId || '') === String(phoneId) && isEscortAdPhoneFresh(data)); if (cached) return {phone:cached.phone,phoneDigits:getEscortAdPhoneDigits(cached),phoneFetchStatus:'cached', phoneCheckedAt:getEscortAdPhoneCheckedAt(cached)}; try { const fetched=await fetchEscortClubPhone(phoneId); return { ...fetched, phoneFetchStatus:'ok', phoneCheckedAt:Date.now() }; } catch (error) { log('Nie udało się odświeżyć numeru podczas zapisu wizyty',error); return null; } } function escortPricesEquivalent(previous,current) { if (!previous || !current) return false; const previousHasAmount=previous.amount != null && Number.isFinite(Number(previous.amount)); const currentHasAmount=current.amount != null && Number.isFinite(Number(current.amount)); if (previousHasAmount !== currentHasAmount) return false; if ( previousHasAmount && Number(previous.amount) !== Number(current.amount) ) { return false; } const previousCurrency=normalizeEscortAdText(previous.currency).toUpperCase(); const currentCurrency=normalizeEscortAdText(current.currency).toUpperCase(); return previousCurrency === currentCurrency; } function formatEscortChangedPrice(price) { return formatEscortTilePrice(price) || 'brak'; } function compareEscortAdVisitData(previous,current) { const changes={ title:null, image:null, address:null, prices:[], availability:[], tags:null, description:null, phone:null, stats:[], labels:[] }; if (!previous || !current) return changes; const previousTitle=normalizeEscortAdText(previous.title); const currentTitle=normalizeEscortAdText(current.title); if ( (previousTitle || currentTitle) && normalizeEscortChangeComparisonValue(previousTitle) !== normalizeEscortChangeComparisonValue(currentTitle) ) { changes.title={ previous:previousTitle || null, current:currentTitle || null }; changes.labels.push('nazwę anonsu'); } const previousImage= normalizeEscortImageComparisonValue( previous.imageUrl ); const currentImage= normalizeEscortImageComparisonValue( current.imageUrl ); if ( previousImage && currentImage && previousImage !== currentImage ) { changes.image={ previous: String(previous.imageUrl || ''), current: String(current.imageUrl || '') }; changes.labels.push( 'główne zdjęcie' ); } const previousAddress=getEscortComparableAddress(previous); const currentAddress=getEscortComparableAddress(current); if ( (previousAddress || currentAddress) && normalizeEscortChangeComparisonValue(previousAddress) !== normalizeEscortChangeComparisonValue(currentAddress) ) { changes.address={ previous:previousAddress, current:currentAddress }; changes.labels.push('adres'); } const previousPrices=previous.prices || {}; const currentPrices=current.prices || {}; const priceDurationKeys=new Set([ ...Object.keys(previousPrices), ...Object.keys(currentPrices) ]); for (const durationKey of priceDurationKeys) { const previousHasLine=Object.prototype.hasOwnProperty.call( previousPrices, durationKey ); const currentHasLine=Object.prototype.hasOwnProperty.call( currentPrices, durationKey ); const previousPrice=previousHasLine ? previousPrices[durationKey]:null; const currentPrice=currentHasLine ? currentPrices[durationKey]:null; if ( previousHasLine && currentHasLine && escortPricesEquivalent(previousPrice,currentPrice) ) { continue; } changes.prices.push({ durationKey, previous:previousPrice, current:currentPrice, type:!previousHasLine ? 'added' :(!currentHasLine ? 'removed':'changed') }); } if (changes.prices.length) { changes.labels.push( changes.prices.some(change => change.type !== 'changed') ? 'cennik' :'cenę' ); } for (const [day,currentHours] of Object.entries(current.availability || {})) { const previousEntry=getEscortObjectEntryByNormalizedKey(previous.availability,day); const previousHours=previousEntry?.[1]; if ( !previousHours || normalizeEscortChangeComparisonValue(previousHours) === normalizeEscortChangeComparisonValue(currentHours) ) { continue; } changes.availability.push({ day, previous:previousHours, current:currentHours }); } if (changes.availability.length) changes.labels.push('godziny dostępności'); if (Array.isArray(previous.tags) && Array.isArray(current.tags)) { const previousTags=new Map(previous.tags.map(tag => [ normalizeEscortChangeComparisonValue(tag), normalizeEscortAdText(tag) ]).filter(([key]) => key)); const currentTags=new Map(current.tags.map(tag => [ normalizeEscortChangeComparisonValue(tag), normalizeEscortAdText(tag) ]).filter(([key]) => key)); const added=[...currentTags] .filter(([key]) => !previousTags.has(key)) .map(([,tag]) => tag); const removed=[...previousTags] .filter(([key]) => !currentTags.has(key)) .map(([,tag]) => tag); if (added.length || removed.length) { changes.tags={added,removed}; changes.labels.push('tagi'); } } if ( previous.description && current.description && normalizeEscortChangeComparisonValue(previous.description) !== normalizeEscortChangeComparisonValue(current.description) ) { changes.description={ previous:previous.description, current:current.description }; changes.labels.push('opis'); } const previousPhoneDigits=getEscortAdPhoneDigits(previous); const currentPhoneDigits=getEscortAdPhoneDigits(current); if ( previousPhoneDigits && currentPhoneDigits && previousPhoneDigits !== currentPhoneDigits ) { changes.phone={ previous:previous.phone || previousPhoneDigits, current:current.phone || currentPhoneDigits }; changes.labels.push('numer telefonu'); } for (const [currentLabel,currentValue] of Object.entries(current.stats || {})) { const previousEntry=getEscortObjectEntryByNormalizedKey( previous.stats, currentLabel ); if (!previousEntry) continue; const previousValue=previousEntry[1]; if ( normalizeEscortChangeComparisonValue(previousValue) === normalizeEscortChangeComparisonValue(currentValue) ) { continue; } changes.stats.push({ label:currentLabel, previous:previousValue, current:currentValue }); } if (changes.stats.length) { const onlyBreastChanged=changes.stats.length === 1 && normalizeEscortChangeComparisonValue(changes.stats[0].label) === 'biust'; changes.labels.push(onlyBreastChanged ? 'rozmiar biustu':'dane profilu'); } return changes; } function ensureEscortVisitChangeStyles() { if (document.getElementById(ESCORT_CHANGE_STYLE_ID)) return; const changeRedColor='#b42318',changeBg='rgba(180, 35, 24, .13)',changeText='#7a1c12'; appendHelperStyle(ESCORT_CHANGE_STYLE_ID,[ '.vm-escort-changed-fragment {', ' padding: 0 4px !important;', ' border-radius: 2px !important;', ' background: ' + changeBg + ' !important;', ' box-shadow: inset 3px 0 0 ' + changeRedColor + ' !important;', ' color: ' + changeText + ' !important;', ' text-shadow: none !important;', '}', '.vm-escort-change-note {', ' box-sizing: border-box;', ' margin: 4px 0 8px;', ' padding: 5px 8px;', ' border-left: 3px solid ' + changeRedColor + ';', ' border-radius: 0 4px 4px 0;', ' background: ' + changeBg + ';', ' color: ' + changeText + ';', ' text-shadow: none;', ' font: 700 11px/1.35 Lato, Arial, sans-serif;', '}', '.vm-escort-description-change {', ' margin: 10px 0 0;', ' padding: 8px 10px 10px;', ' border: 1px solid ' + changeRedColor + ';', ' border-radius: 6px;', ' background: ' + changeBg + ';', ' color: ' + changeText + ';', ' font: 700 11px/1.35 Lato, Arial, sans-serif;', '}', '.vm-escort-description-change summary {', ' cursor: pointer;', ' color: ' + changeText + ';', ' font-weight: 700;', '}', '.vm-escort-description-current-changed {', ' padding-left: 8px;', ' box-shadow: inset 3px 0 0 ' + changeRedColor + ';', '}', '.vm-escort-description-previous {', ' max-height: 260px;', ' margin-top: 9px;', ' padding: 12px;', ' overflow: auto;', ' border-radius: 5px;', ' background: rgba(255, 255, 255, .98);', ' box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);', ' color: #2f2530;', ' font-weight: 400;', ' line-height: 1.5;', ' white-space: pre-wrap;', '}' ].join('\n')); } function addEscortPreviousValueNote(element,text) { if (!element || element.dataset.vmEscortChangeMarked === '1') return; element.dataset.vmEscortChangeMarked='1'; element.classList.add('vm-escort-changed-fragment'); const note=makeElement('div','vm-escort-change-note',text); element.insertAdjacentElement('afterend',note); } function addEscortPriceSectionChangeNote(priceBox,text) { if (!priceBox) { priceBox=document.getElementById('vm-escort-removed-price-section'); if (!priceBox) { const info=document.querySelector( '.content-info-col.-info .content-info, .content-info-col .content-info' ); if (!info) return; priceBox=makeElement('div'); priceBox.id='vm-escort-removed-price-section'; priceBox.style.marginTop='12px'; const removedLabel=makeElement('div','label','Zmiany w cenniku'); priceBox.appendChild(removedLabel); info.appendChild(priceBox); } } const label=priceBox.querySelector(':scope > .label, .label'); if (label) label.classList.add('vm-escort-changed-fragment'); let notes=priceBox.querySelector('.vm-escort-price-section-changes'); if (!notes) { notes=makeElement('div'); notes.className='vm-escort-price-section-changes'; if (label) label.insertAdjacentElement('afterend',notes); else priceBox.prepend(notes); } const note=makeElement('div','vm-escort-change-note',text); notes.appendChild(note); } function formatEscortChangedValue(value) { return normalizeEscortAdText(value) || 'brak'; } function renderEscortTitleChange(change) { if (!change) return; const heading=document.querySelector( '.content-info-col.-desc > .content-name.-desc-name > h1, ' + '.content-info-col.-desc > .content-name > h1' ); if (!heading) return; const currentKey=normalizeEscortChangeComparisonValue(change.current); const titleNode=currentKey ? [...heading.childNodes].find(node => node.nodeType === Node.TEXT_NODE && normalizeEscortChangeComparisonValue(node.textContent) === currentKey ) :null; if (titleNode) { const markedTitle=makeElement('span','vm-escort-changed-fragment'); markedTitle.style.marginRight='6px'; markedTitle.textContent=formatEscortChangedValue(change.current); titleNode.replaceWith(markedTitle); } if (!document.getElementById('vm-escort-title-change-note')) { const note=makeElement('div'); note.id='vm-escort-title-change-note'; note.className='vm-escort-change-note'; note.textContent= 'Nazwa zmieniona: ' + formatEscortChangedValue(change.previous) + ' → ' + formatEscortChangedValue(change.current); heading.insertAdjacentElement('afterend',note); } } function renderEscortAddressChange(change) { if (!change) return; const locationBox=document.querySelector( '.content-info-col.-desc .content-name > .content-location' ); addEscortPreviousValueNote( locationBox, 'Adres zmieniony: ' + formatEscortChangedValue(change.previous) + ' → ' + formatEscortChangedValue(change.current) ); } function findEscortSubInfoBox(labelText) { return [...document.querySelectorAll('.sub-info-box')] .find(candidate => normalizeText( candidate.querySelector('.label')?.textContent ) === labelText) || null; } function renderEscortTagChanges(change) { if (!change) return; let box=findEscortSubInfoBox('tagi'); if (!box) { const column=document.querySelector('.anons-info-sec .sub-info-col'); if (!column) return; box=makeElement('div'); box.className='sub-info-box -tags'; const label=makeElement('div','label','Tagi'); box.appendChild(label); column.appendChild(box); } const label=box.querySelector(':scope > .label, .label'); if (label) label.classList.add('vm-escort-changed-fragment'); let notes=box.querySelector('.vm-escort-tag-changes'); if (!notes) { notes=makeElement('div'); notes.className='vm-escort-tag-changes'; notes.style.marginTop='8px'; box.appendChild(notes); } const appendNote=text => { const note=makeElement('div','vm-escort-change-note',text); notes.appendChild(note); }; if (change.added?.length) { appendNote('Dodano tagi: ' + change.added.join(', ')); } if (change.removed?.length) { appendNote('Usunięto tagi: ' + change.removed.join(', ')); } } function findEscortStatRow(label) { const wanted=normalizeEscortChangeComparisonValue(label).replace(/:\s*$/,''); return [...document.querySelectorAll('.stats-box .stat-elem')] .find(row => normalizeEscortChangeComparisonValue( row.querySelector('.sub-label')?.textContent ).replace(/:\s*$/,'') === wanted ) || null; } function renderEscortVisitChanges(changes) { ensureEscortVisitChangeStyles(); renderEscortTitleChange( changes.title ); renderEscortAddressChange( changes.address ); if (changes.image) { const gallery= document.querySelector( '.content-gallery-col, .galleryContainer' ); if ( gallery && !gallery.querySelector( '.vm-escort-main-image-change' ) ) { const note=makeElement( 'div', 'vm-escort-main-image-change', 'Główne zdjęcie zmieniono od poprzedniej wizyty.' ); Object.assign(note.style,{ margin:'6px 0', padding:'5px 8px', borderLeft: '3px solid var(--vm-ui-danger, #ff4d4f)', background: 'color-mix(in srgb, var(--vm-ui-danger, #ff4d4f) 12%, transparent)', color: 'var(--vm-ui-danger, #ff4d4f)', font: '700 10px/1.35 Lato, Arial, sans-serif' }); gallery.appendChild(note); } } const priceBox=document.querySelector('.contant-prices, .content-prices'); for (const change of changes.prices) { const row=[...document.querySelectorAll( '.contant-prices .stat-elem, .content-prices .stat-elem' )].find(candidate => normalizeEscortClubPriceDurationKey( candidate.querySelector('.sub-label')?.textContent ) === change.durationKey ); const durationLabel=getEscortPriceDurationLabel(change.durationKey); const prefix=change.type === 'added' ? 'Dodano pozycję cennika' :(change.type === 'removed' ? 'Usunięto pozycję cennika' :'Cena zmieniona'); const message= prefix + ' (' + durationLabel + '): ' + formatEscortChangedPrice(change.previous) + ' → ' + formatEscortChangedPrice(change.current); if (row) addEscortPreviousValueNote(row,message); else addEscortPriceSectionChangeNote(priceBox,message); } const availabilityBox=findEscortSubInfoBox('godziny dostępności'); for (const change of changes.availability) { const row=[...(availabilityBox?.querySelectorAll('.sub-info-elem') || [])] .find(candidate => normalizeEscortChangeComparisonValue( candidate.querySelector('.sub-label')?.textContent ).replace(/:\s*$/,'') === normalizeEscortChangeComparisonValue(change.day).replace(/:\s*$/,'') ); addEscortPreviousValueNote( row, 'Godziny zmienione: ' + change.previous + ' → ' + change.current ); } renderEscortTagChanges(changes.tags); if (changes.description) { const description=document.querySelector('.content-desc'); if (description && !description.querySelector('.vm-escort-description-change')) { const currentText=description.querySelector('.tab-content') || description; currentText.classList.add('vm-escort-description-current-changed'); const details=makeElement('details','vm-escort-description-change'); const summary=makeElement('summary','','Opis zmieniony - pokaż poprzednią wersję'); const previous=makeElement('div','vm-escort-description-previous',changes.description.previous); details.append(summary,previous); description.appendChild(details); } } if (changes.phone) { for (const phoneBox of document.querySelectorAll('.adsPhone')) { addEscortPreviousValueNote( phoneBox, 'Numer telefonu zmieniony: ' + formatEscortPhoneForDisplay(changes.phone.previous) + ' → ' + formatEscortPhoneForDisplay(changes.phone.current) ); } } for (const change of changes.stats) { const isBreast= normalizeEscortChangeComparisonValue(change.label) === 'biust'; addEscortPreviousValueNote( findEscortStatRow(change.label), (isBreast ? 'Rozmiar biustu zmieniony: ' :'Zmiana „' + change.label + '”: ') + formatEscortChangedValue(change.previous) + ' → ' + formatEscortChangedValue(change.current) ); } } function polishElapsedValue(value,one,few,many) { return `${formatPolishCount(value,one,few,many)} temu`; } function formatEscortLastVisit(timestamp) { const elapsedMs=Date.now() - Number(timestamp); if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return ''; const minutes=Math.floor(elapsedMs / 60000); if (minutes < 1) return 'przed chwilą'; if (minutes < 60) return polishElapsedValue(minutes,'minutę','minuty','minut'); const hours=Math.floor(elapsedMs / 3600000); if (hours < 24) return polishElapsedValue(hours,'godzinę','godziny','godzin'); const days=Math.floor(elapsedMs / 86400000); if (days < 31) return polishElapsedValue(days,'dzień','dni','dni'); const months=Math.floor(days / 30.4375); if (months < 12) return polishElapsedValue(months,'miesiąc','miesiące','miesięcy'); const years=Math.floor(months / 12); return polishElapsedValue(years,'rok','lata','lat'); } function renderEscortVisitSummary(previous,changes,isFirstVisit=false) { if ((!previous && !isFirstVisit) || document.getElementById(ESCORT_LAST_VISIT_ID)) return; const profileHeading=document.querySelector( '.content-info-col.-desc > .content-name.-desc-name > h1, ' + '.content-info-col.-desc > .content-name > h1' ); if (!profileHeading) return; profileHeading.style.display='block'; const hasChanges=!isFirstVisit && changes.labels.length > 0; const pagePinkColor=getEscortPagePinkColor(); const changeRedColor='#b42318',changeBg='rgba(180, 35, 24, .13)',changeText='#7a1c12'; const visitLine=makeElement('span','',null,{id:ESCORT_LAST_VISIT_ID}); Object.assign(visitLine.style,{ display:'block', position:'static', float:'none', marginTop:'3px', color:pagePinkColor, font:'400 11px/1.35 Lato, Arial, sans-serif', opacity:'.72' }); if (isFirstVisit) { visitLine.textContent='Wyświetlasz pierwszy raz'; } else if (previous.lastVisitedAt) { visitLine.appendChild(document.createTextNode('Ostatnio odwiedzono: ')); const relative=makeElement('strong','',formatEscortLastVisit(previous.lastVisitedAt)); relative.style.fontWeight='600'; relative.title=new Intl.DateTimeFormat('pl-PL',{ dateStyle:'medium', timeStyle:'short' }).format(new Date(previous.lastVisitedAt)); visitLine.appendChild(relative); } else { visitLine.textContent='Porównano z wcześniejszym zapisem w cache.'; } const dateRow=document.getElementById('vm-escort-date-posted'); if (dateRow) { dateRow.appendChild(visitLine); dateRow.style.marginBottom=hasChanges ? '5px':'18px'; } else { Object.assign(visitLine.style,{ boxSizing:'border-box', margin:hasChanges ? '0 0 5px':'0 0 18px', padding:'4px 10px', borderLeft:'3px solid ' + pagePinkColor }); profileHeading.insertAdjacentElement('afterbegin',visitLine); } if (!hasChanges || document.getElementById(ESCORT_VISIT_SUMMARY_ID)) return; const row=makeElement('span'); row.id=ESCORT_VISIT_SUMMARY_ID; Object.assign(row.style,{ display:'block', boxSizing:'border-box', position:'static', float:'none', clear:'both', width:'100%', margin:'0 0 18px', padding:'0', border:'0', background:'transparent' }); const changedLine=makeElement('span'); Object.assign(changedLine.style,{ display:'inline-block', position:'static', float:'none', padding:'6px 10px', borderRadius:'0 4px 4px 0', borderLeft:'3px solid ' + changeRedColor, background:changeBg, color:changeText, textShadow:'none', font:'700 11px/1.35 Lato, Arial, sans-serif' }); changedLine.appendChild(document.createTextNode('Zmieniono: ')); const values=makeElement('strong','',changes.labels.join(', ')); changedLine.appendChild(values); row.appendChild(changedLine); if (dateRow) dateRow.insertAdjacentElement('afterend',row); else visitLine.insertAdjacentElement('afterend',row); } function mergeEscortDefinedObject(previous,current) { const merged={...(previous || {})}; for (const [key,value] of Object.entries(current || {})) { if (value == null || value === '') continue; if (Array.isArray(value) && value.length === 0) continue; merged[key]=value; } return merged; } function mergeEscortAdVisitData(previous,current,visitedAt) { const old=previous && typeof previous === 'object' ? previous:{}; const currentHasPhone=!!getEscortAdPhoneDigits(current); const previousPhone=current.phoneId && String(current.phoneId) !== String(old.phoneId || '') ? {}:old; const merged={ ...old, ...current, title:current.title || old.title || null, imageUrl:current.imageUrl || old.imageUrl || null, description:current.description || old.description || null, availability:{ ...(old.availability || {}), ...(current.availability || {}) }, tags:Array.isArray(current.tags) ? [...current.tags] :[...(old.tags || [])], prices:{...(current.prices || {})}, datePosted:current.datePosted || old.datePosted || null, location:mergeEscortDefinedObject(old.location,current.location), stats:Object.keys(current.stats || {}).length ? {...current.stats} :{...(old.stats || {})}, phoneId:current.phoneId || old.phoneId || null, phone:currentHasPhone ? current.phone:(previousPhone.phone || null), phoneDigits:currentHasPhone ? current.phoneDigits:(previousPhone.phoneDigits || null), phoneFetchStatus:currentHasPhone ? current.phoneFetchStatus :(previousPhone.phoneFetchStatus || null), phoneCheckedAt:currentHasPhone ? current.phoneCheckedAt :(previousPhone.phoneCheckedAt || null), visitSnapshot:createEscortVisitSnapshot(current,old.visitSnapshot), fetchTransport:'current-page', pageCheckedAt:visitedAt, lastVisitedAt:visitedAt }; return merged; } function initEscortClubVisitChangeTracking() { if (isEscortCachedArchiveRuntime()) return; if (!getEscortAdIdFromPath(location.pathname)) return; if (document.documentElement.dataset.vmEscortVisitTracking === '1') return; document.documentElement.dataset.vmEscortVisitTracking='1'; (async () => { let ready=false; for (let attempt=0; attempt < 40; attempt++) { ready= document.readyState !== 'loading' && !!document.querySelector( '.content-info-col .content-name h1, .content-name.-desc-name h1' ); if (ready) break; await new Promise(resolve => setTimeout(resolve,250)); } if (!ready) return; const adId=getAdIdFromUrl(); if (!adId) return; const adUrl='https://pl.escort.club/anons/' + adId + '.html'; const stored=getStoredEscortAdData(adId); const previousSnapshot=stored?.visitSnapshot || null; const current=CURRENT_HOST === 'pl.escort.club' ? extractEscortClubAdData(document,adId,adUrl) :await getEscortAdData(adId,adUrl,true); if (!current || (CURRENT_HOST !== 'pl.escort.club' && current.status !== 'ok')) return; const phone=await resolveEscortPhoneForCurrentPage(current.phoneId); if (phone) Object.assign(current,phone); const changes=compareEscortAdVisitData(previousSnapshot,current); const previousVisitInfo=(previousSnapshot || stored?.lastVisitedAt) ? { ...(previousSnapshot || {}), lastVisitedAt:stored?.lastVisitedAt || null } :null; renderEscortVisitSummary( previousVisitInfo, changes, !previousVisitInfo ); if (previousSnapshot) { renderEscortVisitChanges( changes ); } const visitedAt=Date.now(); const merged=mergeEscortAdVisitData(stored,current,visitedAt); const saved=setPersistentEscortAdData(adId,merged); if (saved) { escortAdDataMemoryCache.set(String(adId),saved); storeWatchedAdDataMirror(adId,saved); } })().catch(error => { log('Błąd porównania danych pojedynczego anonsu',error); }); } function isValidEscortAdTemplateCache(value,doc=document) { if (![1,2].includes(value?.schema) || !Number.isFinite(value.savedAt) || value.savedAt <= 0 || value.savedAt > Date.now() || typeof value.html !== 'string' || typeof value.css !== 'string' || !value.css.trim() || new TextEncoder().encode(JSON.stringify(value)).length > 100000 || /[<>\\]|url\s*\(|@import|image-set\s*\(|expression\s*\(|(?:^|[;{])\s*(?:behavior|content|src)\s*:/i.test(value.css)) return false; const holder=doc.createElement('template'); holder.innerHTML=value.html; const root=holder.content; const allowedTags=new Set(['MAIN','SECTION','DIV','H1','SPAN','A']); const labels=new Set(['Godziny dostępności','Tagi','Parametry','Ceny','O mnie','Więcej o mnie','Dodatkowe informacje']); for (const element of root.querySelectorAll('*')) { if (!allowedTags.has(element.tagName) || [...element.attributes].some(attribute => attribute.name !== 'class' || !/^[a-zA-Z0-9 _-]*$/.test(attribute.value))) return false; for (const child of element.childNodes) if (child.nodeType === 3 && child.textContent.trim() && (!element.classList.contains('label') || !labels.has(child.textContent.trim()))) return false; } if ([...root.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return false; return ['main .content-info-col .content-name h1','.content-location','.galleryContainer', '.content-desc .tab-content','.content-hours .stats-box','.content-prices .stats-box','.adsPhone a', '.sub-info-items','.tags-box',...(value.schema === 2 ? ['.content-side-col .content-sidebar-card', '.content-extra .sub-info-items','.content-extra .tags-box']:[])] .every(selector => !!root.querySelector(selector)) && [...root.querySelectorAll('.sub-info-box .label')] .some(label => label.textContent === 'Godziny dostępności') && [...root.querySelectorAll('.sub-info-box .label')] .some(label => label.textContent === 'Tagi'); } function createEscortAdTemplateCache(savedAt,appearance='') { const html=`<main class="vm-cached-ad-layout"> <section class="content-sec"> <div class="content-info-wrapper -left"> <div class="content-gallery-col"><div class="galleryContainer"></div></div> <div class="content-info-col -desc"> <div class="content-name -desc-name"><h1></h1><div class="content-location"></div></div> <div class="content-desc"><div class="label">O mnie</div><div class="tab-content"></div></div> </div> <div class="content-side-col content-info-col -info"> <div class="content-sidebar-card"> <div class="content-contact"><div class="adsPhone"><a></a></div></div> <div class="content-hours"><div class="label">Więcej o mnie</div><div class="stats-box"></div></div> </div> <div class="content-prices contant-prices"><div class="label">Ceny</div><div class="stats-box"></div></div> </div> </div> <section class="content-extra"> <div class="label content-extra-title">Dodatkowe informacje</div> <div class="content-extra-grid"> <div class="sub-info-box"><div class="label">Godziny dostępności</div><div class="sub-info-items"></div></div> <div class="sub-info-box"><div class="label">Tagi</div><div class="tags-box"></div></div> </div> </section> </section></main>`; const css=` * { box-sizing: border-box; } body { margin: 0; padding: 18px; color: #333; background-color: #fff; font: 14px/1.5 Arial,sans-serif; overflow-wrap: anywhere; } ${appearance} main { max-width: 1280px; margin: 0 auto; } h1 { margin: 0 0 14px; font-size: 32px; line-height: 1.2; font-weight: 400; } .content-info-wrapper { display: grid; grid-template-areas: "gallery info side"; grid-template-columns: minmax(0,.9fr) minmax(0,1.15fr) minmax(280px,1fr); align-items: start; gap: 32px; } .content-gallery-col,.content-info-col,.content-side-col { min-width: 0; } .content-gallery-col { grid-area: gallery; } .content-info-col.-desc { grid-area: info; } .content-side-col { grid-area: side; } .content-info-wrapper:not(:has(.content-gallery-col)) { grid-template-areas: "info side"; grid-template-columns: minmax(0,1.15fr) minmax(280px,1fr); } .galleryContainer { max-width: 340px; margin: 0 auto; } .galleryContainer img { display: block; width: 100%; height: auto; border-radius: 8px; } .content-location { margin: 20px 0; font-size: 17px; } .content-desc { margin-top: 34px; } .content-desc .tab-content { white-space: pre-wrap; } .content-sidebar-card { border: 1px solid rgba(128,128,128,.25); border-radius: 9px; } .content-contact { padding: 18px 20px; border-bottom: 1px solid rgba(128,128,128,.25); } .adsPhone { font-size: 20px; } .adsPhone a { color: inherit; text-decoration: none; } .content-hours { padding: 18px 20px; } .content-prices { padding: 20px; } .label { margin: 0 0 12px; font-weight: 600; } .stats-box,.sub-info-items { display: grid; gap: 10px; } .stat-elem,.sub-info-elem { display: flex; align-items: baseline; justify-content: space-between; gap: 8px 16px; } .stat-elem .sub-label,.sub-info-elem .sub-label { flex: 0 0 auto; max-width: 52%; } .stat-elem .sub-desc,.sub-info-elem .desc { min-width: 0; text-align: right; } .content-extra { margin-top: 48px; } .content-extra-title { margin-bottom: 22px; font-size: 24px; font-weight: 400; } .content-extra-grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.4fr); gap: 40px; } .content-extra .sub-info-items { max-width: 380px; } .tags-box { display: flex; flex-wrap: wrap; gap: 8px; } .tag { display: inline-block; max-width: 100%; padding: 6px 14px; border-radius: 999px; color: #6d2244; background-color: #ffe0ed; font-size: 12px; line-height: 1.4; text-align: center; } @media (max-width: 960px) { .content-info-wrapper { grid-template-areas: "gallery info" "side side"; grid-template-columns: minmax(0,1fr) minmax(0,1.3fr); gap: 24px; } .content-info-wrapper:not(:has(.content-gallery-col)) { grid-template-areas: "info" "side"; grid-template-columns: minmax(0,1fr); } .content-side-col { display: grid; grid-template-columns: minmax(0,1.3fr) minmax(0,1fr); align-items: start; gap: 24px; } } @media (max-width: 640px) { body { padding: 12px; } .content-info-wrapper,.content-side-col,.content-extra-grid { grid-template-columns: minmax(0,1fr); gap: 22px; } .content-info-wrapper { grid-template-areas: "gallery" "info" "side"; } .content-desc { margin-top: 24px; } .content-extra { margin-top: 32px; } .content-extra .sub-info-items { max-width: none; } } `; return {schema:2,savedAt,html,css}; } function readEscortAdTemplateCache() { try { const value=GM_getValue(ESCORT_AD_TEMPLATE_CACHE_KEY,null); if (!isValidEscortAdTemplateCache(value)) return null; if (value.schema === 2 && value.html === createEscortAdTemplateCache(value.savedAt).html) return value; // Stały szablon zachowuje motyw i datę. const appearance=[...value.css.matchAll(/(?:^|[}\n])\s*(body|h1)\s*\{([^{}]*)\}/g)] .map(([,selector,declarations]) => { const kept=declarations.split(';').filter(rule => /^\s*(?:color|background-color|font-family|font-size|font-weight|font-style|line-height|letter-spacing)\s*:/.test(rule)); return kept.length ? `${selector}{${kept.join(';')}}`:''; }).join('\n'); const updated=createEscortAdTemplateCache(value.savedAt,appearance); try {writeHelperStorageValue(ESCORT_AD_TEMPLATE_CACHE_KEY,updated);} catch {} return updated; } catch {return null;} } function saveEscortAdTemplateFromPage(doc=document) { if (!doc?.body || !doc.defaultView || isEscortCachedArchiveRuntime()) return false; try { const page=new URL(doc.URL || location.href),heading=doc.querySelector('.content-info-col .content-name h1, .content-name.-desc-name h1'); if (!isEscortClubHost(page.hostname) || !getEscortAdIdFromPath(page.pathname) || new URLSearchParams(page.hash.slice(1)).has('vm_cached_template') || doc.documentElement.dataset.vmCachedArchiveReady || !heading?.textContent.trim() || !doc.querySelector('.content-desc') || !doc.querySelector('.content-hours .stats-box, .contant-prices .stats-box, .content-prices .stats-box')) return false; const properties=['color','background-color','font-family','font-size','font-weight','font-style','line-height','letter-spacing']; const rules=[]; const transparent=value => !value || value === 'transparent' || /^(?:rgba\([^)]*,\s*0(?:\.0+)?\)|[a-z]+\([^)]*\/\s*0(?:\.0+)?%?\))$/i.test(value); for (const [selector,source] of [['body',doc.body],['h1',heading]]) { const computed=doc.defaultView.getComputedStyle(source),values=[]; for (const property of properties) { let value=computed.getPropertyValue(property).trim(); if (selector === 'body' && property === 'background-color' && transparent(value)) { value=doc.defaultView.getComputedStyle(doc.documentElement).getPropertyValue(property).trim(); if (transparent(value)) value='#fff'; } if (value && value.length < 240 && !/[<>\\{};@]|url\s*\(|image-set\s*\(|expression\s*\(/i.test(value)) values.push(`${property}:${value}`); } if (values.length) rules.push(`${selector}{${values.join(';')}}`); } const value=createEscortAdTemplateCache(Date.now(),rules.join('\n')); if (!isValidEscortAdTemplateCache(value,doc)) return false; const previous=readEscortAdTemplateCache(); if (previous?.html !== value.html || previous?.css !== value.css) writeHelperStorageValue(ESCORT_AD_TEMPLATE_CACHE_KEY,value); return true; } catch (error) { log('Nie udało się zapisać szablonu anonsu',error); return false; } } function getStoredEscortAdData(adId) { try { const value=readPersistentCacheValue(escortAdDataCacheKey(adId),null); if (!value || typeof value !== 'object' || value.status !== 'ok' || !value.checkedAt) { return null; } return sanitizeEscortAdDataPhone(value); } catch { return null; } } function formatEscortCachedArchiveDate(timestamp) { const value=Number(timestamp); if (!Number.isFinite(value) || value <= 0) return ''; try { return formatServerMediaDate(new Date(value)); } catch { return ''; } } function isEscortCachedArchiveRuntime() { if (escortCachedArchiveRuntimeOverride) return true; const raw=String(location.hash || '').replace(/^#/,''); if (!raw) return false; try { return new URLSearchParams(raw).get(ESCORT_CACHED_AD_ARCHIVE_VIEW_HASH_KEY) === '1'; } catch { return false; } } function getEscortCachedArchiveMarkerFromHash() { const raw=String(location.hash || '').replace(/^#/,''); if (!raw) return null; try { const params=new URLSearchParams(raw); const adId=params.get(ESCORT_CACHED_AD_HASH_KEY); return /^\d+$/.test(adId || '') ? adId:null; } catch { return null; } } function addEscortCachedArchiveMarkerToUrl(value) { try { const url=new URL(value,location.href); if (!isEscortClubHost(url.hostname)) return value; const adId=parseAdIdFromUrl(url.href); if (!adId || !getEscortAdIdFromPath(url.pathname)) return value; const parts=url.hash.slice(1).split('&').filter(part => part && !part.startsWith(`${ESCORT_CACHED_AD_HASH_KEY}=`) ); parts.push(`${ESCORT_CACHED_AD_HASH_KEY}=${encodeURIComponent(adId)}`); url.hash=parts.join('&'); return url.href; } catch { return value; } } function rememberEscortCachedAdNavigation(adId) { if (!adId) return; try { sessionStorage.setItem( ESCORT_CACHED_AD_SESSION_KEY, JSON.stringify({adId:String(adId),at:Date.now()}) ); } catch {} } function readRecentEscortCachedAdNavigation() { try { const value=JSON.parse(sessionStorage.getItem(ESCORT_CACHED_AD_SESSION_KEY) || 'null'); if (!value || !/^\d+$/.test(String(value.adId || ''))) return null; if (Date.now() - Number(value.at || 0) > 15000) return null; return String(value.adId); } catch { return null; } } function clearEscortCachedAdNavigationMarker() { try {sessionStorage.removeItem(ESCORT_CACHED_AD_SESSION_KEY);} catch {} if (!getEscortCachedArchiveMarkerFromHash()) return; try { history.replaceState( history.state, '', `${location.pathname}${location.search}` ); } catch {} } function installEscortCachedAdNavigationMarker() { if (document.documentElement.dataset.vmCachedAdNavigationMarker === '1') return; document.documentElement.dataset.vmCachedAdNavigationMarker='1'; const prepareAnchor=event => { const anchor=event.target?.closest?.('a[href]'); if (!anchor) return; let url; try {url=new URL(anchor.href,location.href);} catch {return;} const adId=parseAdIdFromUrl(url.href); if ( !isEscortClubHost(url.hostname) || !adId || !getEscortAdIdFromPath(url.pathname) ) return; anchor.href=addEscortCachedArchiveMarkerToUrl(url.href); if ( event.type === 'click' && event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey && String(anchor.target || '').toLowerCase() !== '_blank' ) { rememberEscortCachedAdNavigation(adId); } }; document.addEventListener('pointerdown',prepareAnchor,true); document.addEventListener('click',prepareAnchor,true); document.addEventListener('auxclick',prepareAnchor,true); } function formatEscortCachedArchiveDuration(key) { if (key === 'night') return 'Cała noc'; const minutes=Number(key); if (!Number.isFinite(minutes) || minutes <= 0) return String(key || 'Cena'); if (minutes < 60) return `${minutes} min`; return `${String(minutes / 60).replace('.',',')} h`; } function replaceEscortTemplateRows(container,entries,rowClass,valueClass) { if (!container) return; container.replaceChildren(); for (const [label,value] of entries || []) { if (value == null || String(value).trim() === '') continue; const row=container.ownerDocument.createElement('div'); row.className=rowClass; const labelNode=container.ownerDocument.createElement('span'); labelNode.className='sub-label'; labelNode.textContent=label; const valueNode=container.ownerDocument.createElement('span'); valueNode.className=valueClass; valueNode.textContent=String(value); row.append(labelNode,valueNode); container.appendChild(row); } } function getEscortCachedArchiveImageUrl(adId,cached) { const cachedUrl=String(cached?.imageUrl || '').trim(); if (cachedUrl) return cachedUrl; const targetId=String(adId); const watched=getWatchedItems().find(item => parseAdIdFromUrl(item.adUrl) === targetId && String(item.imageUrl || '').trim() ); if (watched?.imageUrl) return String(watched.imageUrl).trim(); const profileWithResolvedAd=getWatchedItems().find(item => item.sourceType === 'escorti-profile' && Array.isArray(item.resolvedAdUrls) && item.resolvedAdUrls.some(url => parseAdIdFromUrl(url) === targetId) && String(item.imageUrl || '').trim() ); return String(profileWithResolvedAd?.imageUrl || '').trim(); } function getEscortArchiveLocationText(cached) { const missing=value => /^(?:brak|brak danych|b[./]?d[.]?)$/i.test( normalizeEscortAdText(value) ); const cleanParts=value => String(value || '') .split(',') .map(normalizeEscortAdText) .filter(part => part && !missing(part)); const location=cached?.location || {}; const addressTextParts=cleanParts(location.addressText); if (addressTextParts.length) return addressTextParts.join(', '); const addressParts=Array.isArray(location.addressParts) ? location.addressParts .map(normalizeEscortAdText) .filter(part => part && !missing(part)) :[]; if (addressParts.length) return addressParts.join(', '); const parts=[ location.country, location.province, location.city, location.district, location.area ].map(normalizeEscortAdText).filter(part => part && !missing(part)); if (parts.length) return parts.join(', '); const fallback=cleanParts(cached?.address); return fallback.length ? fallback.join(', '):'Brak zapisanej lokalizacji'; } function replaceEscortArchivePublishedDate(doc,heading,cached) { detachHelperNode(doc.getElementById('vm-cached-ad-published')); const published=formatEscortClubDatePosted(cached.datePosted); const visitedAt=Number(cached.lastVisitedAt) || 0; const visitedDate=formatEscortCachedArchiveDate(visitedAt); const visitedAge=visitedAt ? formatEscortLastVisit(visitedAt):''; if (!published && !visitedDate && !visitedAge) return; const row=doc.createElement('div'); row.id='vm-cached-ad-published'; row.style.cssText= `margin:0 0 14px;padding:4px 0;` + `color:${getEscortPagePinkColor()};font-size:14px`; if (published) { const publishedLine=doc.createElement('div'); publishedLine.textContent=`Opublikowano: ${published}`; row.appendChild(publishedLine); } if (visitedDate || visitedAge) { const visitValue=[ visitedDate, visitedAge ? `(${visitedAge})`:'' ].filter(Boolean).join(' '); const visitedLine=doc.createElement('div'); visitedLine.textContent=`Ostatnio odwiedzono: ${visitValue}`; visitedLine.style.marginTop=published ? '2px':'0'; row.appendChild(visitedLine); } heading.insertAdjacentElement('afterend',row); } function renderEscortArchiveFallbackHelperUi(doc,adId,cached) { const heading=doc.querySelector('.content-info-col .content-name h1, .content-name.-desc-name h1'); const nameBox=heading?.closest('.content-name'),locationBox=nameBox?.querySelector('.content-location'); const previous=doc.getElementById('vm-escort-favorite-note-row'); previous?._vmDispose?.(); detachHelperNode(previous); const settings=getWatchSettings(); if (!heading || !locationBox || !settings.enabled) return; const id=String(adId),storedUrl=normalizeEscortiAdUrl(cached.adUrl || ''); const adUrl=parseAdIdFromUrl(storedUrl) === id ? storedUrl:`https://pl.escort.club/anons/${id}.html`; const getMatches=() => getFavoriteMatchesForCurrentEscortAd(id); let disposed=false; const change=async (itemId,action,value) => { if (disposed || !root.isConnected) return; try { await changeTrackedEntry(itemId,action,value,{getMatches,getContext:() => ({ sourceType:'escort-ad',resolutionMode:'single',adUrl,displayName:cached.title || `Anons ${id}`, imageUrl:getEscortCachedArchiveImageUrl(id,cached),phoneDigits:getEscortAdPhoneDigits(cached) || '' })}); } finally {refresh();} }; const {root,panel,list,render}=createTrackedEntriesBox(change,{ accent:getEscortPagePinkColor(),fallbackText:doc.defaultView.getComputedStyle(heading).color || '#222', emptyAdOwnerKey:`single:${id}` }); root.id='vm-escort-favorite-note-row'; root.classList.add('vm-ad-summary-box'); root.dataset.vmWatchWrapper='1'; panel.id='vm-escort-tracked-panel'; list.id='vm-escort-favorite-note-list'; const dispose=() => { disposed=true; watchPageButtonRefreshers.delete(refresh); const picker=doc.querySelector('.vm-favorites-category-picker'); if (picker?._vmAnchor && root.contains(picker._vmAnchor)) closeHelperDialog(picker,'archive-close'); }; const refresh=() => { if (disposed || !root.isConnected) {dispose(); return;} const current=getWatchSettings(); root.hidden=!current.enabled; if (root.hidden) return; const matches=getMatches(); render([getTrackedScopeItem(matches,'single'),...matches.filter(item => item.sourceType !== 'escort-ad')]); }; root._vmRender=refresh; root._vmDispose=dispose; nameBox.insertBefore(root,locationBox); watchPageButtonRefreshers.add(refresh); refresh(); for (const styleId of ['vm-tracked-note-category-style','vm-escort-helper-style-api']) cloneEscortHelperStyleIntoDocument(doc,styleId); } function applyEscortArchiveTheme(doc) { copyHelperThemeVariablesIntoDocument(doc,document); const root=doc.documentElement.style,view=doc.defaultView,heading=doc.querySelector('h1'); const bodyStyle=view.getComputedStyle(doc.body); const background=root.getPropertyValue('--vm-ui-bg').trim() || bodyStyle.backgroundColor; let text=root.getPropertyValue('--vm-ui-text').trim(),headingText=text; if (!text) { const canvas=doc.createElement('canvas'); canvas.width=canvas.height=1; const context=canvas.getContext('2d',{willReadFrequently:true}); const luminance=color => { context.fillStyle='#fff'; context.fillRect(0,0,1,1); context.fillStyle=color; context.fillRect(0,0,1,1); return [...context.getImageData(0,0,1,1).data].slice(0,3).reduce((sum,value,index) => { const channel=value/255; return sum+(channel<=.04045 ? channel/12.92:((channel+.055)/1.055)**2.4)*[.2126,.7152,.0722][index]; },0); }; const bg=luminance(background),contrast=color => { const fg=luminance(color); return (Math.max(bg,fg)+.05)/(Math.min(bg,fg)+.05); }; headingText=heading ? view.getComputedStyle(heading).color:bodyStyle.color; text=[bodyStyle.color,headingText,'#fff','#000'].find(color => contrast(color)>=4.5); if (contrast(headingText)<4.5) headingText=text; } for (const [name,value] of Object.entries({bg:background,text,'text-muted':text, 'panel-bg':background,'panel-text':text,'panel-muted':text,'input-bg':background,'input-text':text})) if (!root.getPropertyValue(`--vm-ui-${name}`).trim()) root.setProperty(`--vm-ui-${name}`,value); doc.body.style.color='var(--vm-ui-text)'; doc.body.style.backgroundColor='var(--vm-ui-bg)'; if (heading) heading.style.color=headingText; } function cloneEscortHelperStyleIntoDocument( targetDocument, styleId ) { if (!targetDocument) return; copyHelperThemeVariablesIntoDocument( targetDocument, document ); if ( targetDocument.getElementById(styleId) ) return; const source= document.getElementById(styleId); if (!source) return; const clone=targetDocument.createElement('style'); clone.id=styleId; clone.textContent=source.textContent; targetDocument.head?.appendChild(clone); } async function renderEscortArchiveFullFallbackUi( doc, adId, cached ) { if (!doc?.body) return false; try {await ensureEscortPageCache([String(adId)],getEscortAdPhoneDigits(cached) || '');} catch (error) {log('Nie udało się odczytać części danych panelu z cache',error);} if (!doc.defaultView?.frameElement?.isConnected) return false; copyHelperThemeVariablesIntoDocument( doc, document ); renderEscortArchiveFallbackHelperUi( doc, adId, cached ); const adUrl= `https://pl.escort.club/anons/${adId}.html`; const phone=getEscortAdPhoneDigits(cached) || ''; let researchPanel=null; let sidePanel=null; let sideToggle=null; const previousOverride= escortCachedArchiveRuntimeOverride; try { escortCachedArchiveRuntimeOverride=true; ensureEscortSummarySidePanelStyle(); researchPanel= createEscortResearchPanel( phone, adUrl, {archiveData:cached,archiveDocument:doc} ); sidePanel= document.getElementById( ESCORT_SUMMARY_SIDE_PANEL_ID ); sideToggle= document.getElementById( 'vm-escort-summary-side-toggle' ); } catch (error) { log( 'Nie udało się zbudować pełnego interfejsu archiwalnego z cache', error ); } finally { escortCachedArchiveRuntimeOverride= previousOverride; } document.body.classList.remove( ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS, ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS ); if (researchPanel) { detachHelperNode(researchPanel); doc.adoptNode(researchPanel); placeEscortResearchPanelAboveActionButtons( researchPanel, null, doc, {retry:false} ); } ensureGarsoSummaryCardsStyle(); for (const styleId of [ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID,'vm-escort-helper-style-api', 'vm-garso-summary-cards-style',ESCORT_COMPACT_SUMMARY_ID + '-style']) cloneEscortHelperStyleIntoDocument(doc,styleId); if ( SETTINGS.showSummarySidePanel && sidePanel && sideToggle ) { detachHelperNode(sidePanel); detachHelperNode(sideToggle); doc.adoptNode(sidePanel); doc.adoptNode(sideToggle); doc.body.append( sidePanel, sideToggle ); bindEscortSummarySidePanelControls( doc, sidePanel, sideToggle ); } else { detachHelperNode(sidePanel); detachHelperNode(sideToggle); } applyEscortClubTagHighlights(doc); return !!( researchPanel || sidePanel ); } function populateEscortArchiveTemplate(doc,adId,cached,closeView) { const title=normalizeEscortAdText(cached.title) || `Anons ${adId}`; const locationText=getEscortArchiveLocationText(cached); const savedDate=formatEscortCachedArchiveDate( cached.pageCheckedAt || cached.checkedAt || cached.lastVisitedAt ); doc.title=`${title} – zapisana wersja – Escort.club`; const heading=doc.querySelector( '.content-info-col .content-name h1, .content-name.-desc-name h1' ); if (!heading) return false; heading.textContent=title; replaceEscortArchivePublishedDate(doc,heading,cached); const locationLabel=doc.createElement('span'); locationLabel.className='sub-label vm-cached-archive-location'; locationLabel.textContent=locationText; doc.querySelector('.content-location').replaceChildren(locationLabel); const galleryColumn=doc.querySelector('.content-gallery-col'); const gallery=doc.querySelector('.galleryContainer'); const archiveImageUrl=getEscortCachedArchiveImageUrl(adId,cached); if (galleryColumn && gallery) { if (archiveImageUrl) { gallery.replaceChildren(); const link=doc.createElement('a'); link.className='simple-zoom-image'; link.href=archiveImageUrl; link.target='_blank'; link.rel='noopener'; const image=doc.createElement('img'); image.src=archiveImageUrl; image.alt=title; image.style.cssText='display:block;width:100%;height:auto'; image.addEventListener('error',() => detachHelperNode(galleryColumn),{once:true}); link.appendChild(image); gallery.appendChild(link); } else { detachHelperNode(galleryColumn); } } doc.querySelector('.content-desc .tab-content').textContent=normalizeEscortAdText(cached.description) ? String(cached.description):'Brak opisu w cache.'; replaceEscortTemplateRows( doc.querySelector('.sub-info-items'), Object.entries(cached.availability || {}).map(([day,value]) => [ `${day.replace(/:\s*$/,'')}:`, value ]), 'sub-info-elem', 'desc' ); const tagsContainer=doc.querySelector('.tags-box'); if (tagsContainer) { tagsContainer.replaceChildren(); for (const value of cached.tags || []) { const tag=normalizeEscortAdText(value); if (!tag) continue; const node=doc.createElement('span'); node.className='tag'; node.textContent=tag; tagsContainer.appendChild(node); } } const statsContainer=doc.querySelector('.content-hours .stats-box'); replaceEscortTemplateRows( statsContainer, Object.entries(cached.stats || {}).map(([label,value]) => [ `${label.replace(/:\s*$/,'')}:`, value ]), 'stat-elem', 'sub-desc' ); const priceContainer=doc.querySelector( '.contant-prices .stats-box, .content-prices .stats-box' ); replaceEscortTemplateRows( priceContainer, Object.entries(cached.prices || {}).map(([key,price]) => [ `${formatEscortCachedArchiveDuration(key)}:`, formatEscortTilePrice(price) || 'brak ceny' ]), 'stat-elem', 'sub-desc' ); const cachedPhoneDigits=getEscortAdPhoneDigits(cached); const phoneLink=doc.querySelector('.adsPhone a'); if (cachedPhoneDigits) { phoneLink.href=`tel:${cachedPhoneDigits}`; phoneLink.textContent=formatEscortPhoneForDisplay(cachedPhoneDigits); } else { detachHelperNode(phoneLink.closest('.content-contact')); } const main=doc.querySelector('main') || doc.body; const notice=doc.createElement('div'); notice.id='vm-cached-ad-template-notice'; notice.style.cssText= `margin:0 auto 22px;padding:12px 16px;max-width:1280px;` + `border:1px solid var(--vm-ui-border,rgba(128,128,128,.3));border-left:4px solid ${getEscortPagePinkColor()};border-radius:7px;` + `background:var(--vm-ui-bg,#fff);color:var(--vm-ui-text,#332c30);font-size:14px;font-weight:600`; notice.appendChild(doc.createTextNode( `Zapisana wersja anonsu${savedDate ? ` z ${savedDate}`:''} • dane z pamięci podręcznej` )); const close=doc.createElement('button'); close.type='button'; close.textContent='Przejdź na stronę główną'; close.style.cssText= `margin-left:14px;border:0;background:transparent;` + `color:${getEscortPagePinkColor()};font-size:14px;font-weight:800;cursor:pointer`; close.addEventListener('click',() => { try { doc.defaultView.top.location.href=ESCORT_CLUB_ORIGIN + '/'; } catch { closeView(); } }); notice.appendChild(close); const originalAdUrl= normalizeEscortiAdUrl(cached.adUrl || '') || (adId ? `https://pl.escort.club/anons/${adId}.html`:''); if (originalAdUrl) { const linkLine=doc.createElement('div'); linkLine.style.cssText= 'margin-top:6px;font-size:13px;font-weight:600;line-height:1.35'; linkLine.appendChild(doc.createTextNode('Link anonsu: ')); const link=doc.createElement('a'); link.id='vm-cached-ad-original-link'; link.href=originalAdUrl; link.target='_blank'; link.rel='noopener noreferrer'; link.textContent=originalAdUrl; link.style.cssText= `color:${getEscortPagePinkColor()};text-decoration:underline;` + `overflow-wrap:anywhere`; linkLine.appendChild(link); notice.appendChild(linkLine); } detachHelperNode(doc.getElementById('vm-cached-ad-template-notice')); main.insertBefore(notice,main.firstChild); return true; } function renderEscortCachedAdSnapshot(adId,cached) { const template=readEscortAdTemplateCache(); if (!template) { window.alert('Brak zapisanego szablonu anonsu. Gdy strona będzie działać, otwórz dowolny aktywny anons — szablon zapisze się automatycznie.'); return; } const previousView=document.getElementById('vm-cached-ad-view'); if (previousView?._vmClose) previousView._vmClose(); else detachHelperNode(previousView); const banner=document.getElementById('vm-cached-ad-unavailable-banner'); const button=banner?.querySelector('button'),label=button?.textContent,previousBannerDisplay=banner?.style.display; if (button) {button.disabled=true; button.textContent='Otwieranie zapisanej wersji…';} const iframe=makeElement('iframe'); iframe.id='vm-cached-ad-view'; iframe.title='Zapisana wersja anonsu'; iframe.setAttribute('sandbox','allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation'); Object.assign(iframe.style,{ position:'fixed',inset:'0',width:'100%',height:'100%',border:'0', zIndex:'2147483646',background:'#fff',visibility:'hidden' }); const previousOverflow=document.documentElement.style.overflow; document.documentElement.style.overflow='hidden'; let closed=false,ready=false,timer=null; const restore=() => {if (button) {button.disabled=false; button.textContent=label;}}; const closeView=() => { if (closed) return; closed=true; clearTimeout(timer); try {iframe.contentDocument?.getElementById('vm-escort-favorite-note-row')?._vmDispose?.();} catch {} document.documentElement.style.overflow=previousOverflow; detachHelperNode(iframe); if (banner) banner.style.display=previousBannerDisplay; restore(); }; iframe._vmClose=closeView; const fail=error => { if (closed || ready) return; closeView(); log('Nie udało się otworzyć szablonu z cache',error); window.alert('Nie udało się otworzyć zapisanego szablonu. Dane anonsu pozostały w cache.'); }; iframe.addEventListener('load',async () => { if (closed || ready) return; try { const doc=iframe.contentDocument; if (!doc?.querySelector('main')) return; doc.documentElement.dataset.vmCachedArchiveAdId=String(adId); applyEscortArchiveTheme(doc); if (!populateEscortArchiveTemplate(doc,adId,cached,closeView)) throw new Error('Niekompletny szablon'); const close=doc.createElement('button'); close.type='button'; close.textContent='Zamknij'; close.style.cssText='float:right;margin:0 0 8px 12px;padding:7px 12px;cursor:pointer;border:1px solid var(--vm-ui-border,#bbb);border-radius:5px;background:var(--vm-ui-bg-alt,var(--vm-ui-bg,#f5f5f5));color:var(--vm-ui-text,#222)'; close.addEventListener('click',closeView); (doc.getElementById('vm-cached-ad-template-notice') || doc.body).prepend(close); doc.documentElement.dataset.vmCachedArchiveReady='1'; ready=true; clearTimeout(timer); restore(); if (banner) banner.style.display='none'; iframe.style.visibility='visible'; try {await renderEscortArchiveFullFallbackUi(doc,adId,cached);} catch (error) {if (!closed) log('Nie udało się uzupełnić panelu archiwalnego z cache',error);} } catch (error) {fail(error);} }); iframe.addEventListener('error',fail,{once:true}); iframe.srcdoc='<!doctype html><html lang="pl"><head><meta charset="utf-8">' + '<meta name="viewport" content="width=device-width,initial-scale=1">' + '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; img-src https: data:; style-src \'unsafe-inline\'; base-uri \'none\'; form-action \'none\'">' + '<style>' + template.css + '</style></head><body>' + template.html + '</body></html>'; timer=setTimeout(() => fail(new Error('Przekroczono czas otwierania lokalnego szablonu')),10000); document.body.appendChild(iframe); } function renderEscortCachedAdUnavailableBanner(adId,cached) { if (document.getElementById('vm-cached-ad-unavailable-banner')) return; const savedAt=Number(cached.pageCheckedAt || cached.checkedAt || cached.lastVisitedAt || 0); const savedDate=formatEscortCachedArchiveDate(savedAt); const banner=prepareHelperStyleApi(makeElement('div')); banner.id='vm-cached-ad-unavailable-banner'; Object.assign(banner.style,{ position:'fixed', inset:'22px', zIndex:'2147483645', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', boxSizing:'border-box', padding:'32px', border:'1px solid var(--vm-ui-border,#ddd)', borderLeft:'8px solid var(--vm-ui-accent,#f54da3)', borderRadius:'12px', background:'var(--vm-ui-bg,rgba(255,255,255,.985))', color:'var(--vm-ui-text,#332c30)', boxShadow:'var(--vm-ui-shadow,0 12px 46px rgba(0,0,0,.34))', fontFamily:'Lato, Arial, sans-serif', textAlign:'center' }); const title=makeElement('div','','Anons jest obecnie niedostępny'); Object.assign(title.style,{ fontSize:'30px', fontWeight:'900', marginBottom:'18px' }); const actions=makeElement('div'); Object.assign(actions.style,{ display:'flex', flexWrap:'wrap', alignItems:'center', justifyContent:'center', gap:'12px' }); const button=makeButton('vm-ui-button -primary',`Pokaż zapisaną wersję${savedDate ? ` z ${savedDate}`:''}`); const homeButton=makeButton('vm-ui-button -accent','Przejdź do strony głównej'); for (const control of [button,homeButton]) { Object.assign(control.style,{padding:'12px 18px',borderRadius:'7px',fontSize:'15px',fontWeight:'900'}); } button.addEventListener('click',() => renderEscortCachedAdSnapshot(adId,cached)); homeButton.addEventListener('click',() => { location.href=ESCORT_CLUB_ORIGIN + '/'; }); actions.append(button,homeButton); banner.append(title,actions); document.body.appendChild(banner); } async function handleEscortCachedAdArchivePage() { if (!IS_ESCORT_CLUB) return false; const markerAdId=getEscortCachedArchiveMarkerFromHash(); const exactAdId=getEscortAdIdFromPath(); const redirectedAdId=location.pathname === '/' ? (markerAdId || readRecentEscortCachedAdNavigation()) :null; const candidateAdId=exactAdId || redirectedAdId; if (!candidateAdId) return false; if (exactAdId) { const hasAdContent=!!document.querySelector( '.content-info-col .content-name h1, .content-name.-desc-name h1' ); if (hasAdContent) { clearEscortCachedAdNavigationMarker(); return false; } await new Promise(resolve => setTimeout(resolve,500)); if (document.querySelector( '.content-info-col .content-name h1, .content-name.-desc-name h1' )) { clearEscortCachedAdNavigationMarker(); return false; } } let cached=null; try { cached=getStoredEscortAdData(candidateAdId); } catch (error) { log( `Błąd odczytu cache archiwalnego anonsu ${candidateAdId}`, error ); } if (!cached) { const cacheHost=getEscortCachedAdHost(candidateAdId); if (cacheHost && cacheHost !== CURRENT_HOST) { const banner=prepareHelperStyleApi(makeElement('div','vm-ui-panel')); const link=makeElement('a','vm-ui-button',`Pokaż zapisaną wersję z cache (${cacheHost})`,{href:`https://${cacheHost}${getEscortAdPath(candidateAdId,cacheHost)}#${ESCORT_CACHED_AD_HASH_KEY}=${candidateAdId}`}); banner.appendChild(link); document.body.prepend(banner); } clearEscortCachedAdNavigationMarker(); return false; } clearEscortCachedAdNavigationMarker(); renderEscortCachedAdUnavailableBanner(candidateAdId,cached); return true; } function getPersistentEscortAdData(adId) { const value=getStoredEscortAdData(adId); const age=Date.now() - Number(value?.checkedAt); return value?.status === 'ok' && Number(value.checkedAt) > 0 && age >= 0 && age < getListCacheTtlMs() ? value:null; } function setPersistentEscortAdData(adId,data,{preserveCheckedAt=false}={}) { if (!persistentCacheKeyIsLoaded(escortAdDataCacheKey(adId))) { ensurePersistentCacheKeys([escortAdDataCacheKey(adId)]).then(() => setPersistentEscortAdData(adId,data,{preserveCheckedAt})).catch(error => log('Nie udało się scalić zapisu cache',error)); return null; } if (!data) return null; try { const existing=getStoredEscortAdData(adId); const cacheId=String(adId); const pendingActivity=pendingEscortiActivityCacheWrites.get(cacheId) || {}; const escortiActivity={ ...(data?.escortiActivity || {}), ...(existing?.escortiActivity || {}), ...pendingActivity }; const value={ status:'ok', ...(existing?.lastVisitedAt ? {lastVisitedAt:existing.lastVisitedAt} :{}), ...(existing?.pageCheckedAt ? {pageCheckedAt:existing.pageCheckedAt} :{}), ...(existing?.visitSnapshot ? {visitSnapshot:existing.visitSnapshot} :{}), ...(existing?.watchSnapshot ? {watchSnapshot:existing.watchSnapshot} :{}), ...(existing?.watchSnapshotCheckedAt ? {watchSnapshotCheckedAt:existing.watchSnapshotCheckedAt} :{}), ...data, oldestPhotoDate:data.oldestPhotoDate || existing?.oldestPhotoDate || null, imageUrl:data.imageUrl || existing?.imageUrl || null, ...(Object.keys(escortiActivity).length ? {escortiActivity} :{}), checkedAt:preserveCheckedAt ? data.checkedAt:Date.now() }; writePersistentCacheValue(escortAdDataCacheKey(adId),value); registerEscortDataFieldsFromAdData(value); pendingEscortiActivityCacheWrites.delete(cacheId); return value; } catch (error) { log(`Nie udało się zapisać danych Escort.club dla anonsu ${adId}`,error); return null; } } const escortAdPhoneInflight=new Map(); function getEscortAdPhoneCheckedAt(data) { return Number(Object.prototype.hasOwnProperty.call(data || {},'phoneCheckedAt') ? data.phoneCheckedAt:data?.checkedAt) || 0; } function isEscortAdPhoneFresh(data,now=Date.now()) { const checkedAt=getEscortAdPhoneCheckedAt(data); return !['error','missing-id','invalid-cache'].includes(data?.phoneFetchStatus) && !!getEscortAdPhoneDigits(data) && checkedAt > 0 && checkedAt <= now && now - checkedAt < getListCacheTtlMs(); } function hasRecentEscortPhoneAttempt(data,now=Date.now()) { const attemptedAt=Number(data?.phoneAttemptedAt) || 0; const ttl=data?.phoneFetchStatus === 'error' ? Math.min(getListCacheTtlMs(),5 * 60 * 1000):getListCacheTtlMs(); return ['error','missing-id'].includes(data?.phoneFetchStatus) && attemptedAt > 0 && attemptedAt <= now && now - attemptedAt < ttl; } function preserveEscortAdPhoneData(data,previous) { if (getEscortAdPhoneDigits(data)) return data; if (String(data.phoneId || '') !== String(previous?.phoneId || '')) previous=null; return {...data,phone:previous?.phone || null,phoneDigits:getEscortAdPhoneDigits(previous), phoneFetchStatus:previous?.phoneFetchStatus === 'page' ? 'cached':previous?.phoneFetchStatus || 'not-requested',phoneCheckedAt:getEscortAdPhoneCheckedAt(previous), phoneAttemptedAt:Number(previous?.phoneAttemptedAt) || 0}; } function joinEscortAdDataOperation(operations,key,run,cancelToken=null,requestContext=null) { cancelToken?.throwIfCancelled(); let operation=operations.get(key); if (!operation || operation.token.cancelled) { operation={token:createOperationCancelToken('Dane anonsu Escort.club'),waiters:new Set(),promise:null}; operations.set(key,operation); operation.promise=Promise.resolve().then(() => { operation.token.throwIfCancelled(); return run(operation.token); }).finally(() => {if (operations.get(key) === operation) operations.delete(key);}); } else recordDiagnosticCacheDecision(requestContext,'joined'); const waiter={}; operation.waiters.add(waiter); return new Promise((resolve,reject) => { let settled=false,off=() => {}; const finish=(value,error=null) => { if (settled) return; settled=true; off(); operation.waiters.delete(waiter); if (error) reject(error); else resolve(value); }; off=cancelToken?.onCancel(reason => { finish(null,createOperationCancelledError(reason)); if (!operation.waiters.size) operation.token.cancel(reason); }) || (() => {}); operation.promise.then(value => finish(value),error => finish(null,error)); }); } function completeEscortAdPhone(id,data,forceRefresh,cancelToken,requestContext,followedIdChange=false) { if (!forceRefresh && (isEscortAdPhoneFresh(data) || hasRecentEscortPhoneAttempt(data))) return Promise.resolve(data); // Świeży numer ze strony nie wymaga POST. if (data.phoneFetchStatus === 'page' && isEscortAdPhoneFresh(data) && forceRefresh) return Promise.resolve(data); requestContext={...requestContext,cacheKind:'phone',cacheReason:forceRefresh ? 'forced':getEscortAdPhoneDigits(data) ? 'expired':'incomplete'}; return joinEscortAdDataOperation(escortAdPhoneInflight,`${id}:${data.phoneId || ''}`,async token => { const current=escortAdDataMemoryCache.get(id) || data; if (String(current.phoneId || '') !== String(data.phoneId || '')) return current; if (!forceRefresh && (isEscortAdPhoneFresh(current) || hasRecentEscortPhoneAttempt(current))) return current; let phone=null,status='missing-id'; if (data.phoneId) { try { phone=await fetchEscortClubPhone(data.phoneId,token,requestContext); status='ok'; } catch (error) { if (isOperationCancelledError(error)) throw error; if (isEscortRequestBlockedError(error)) return current; status='error'; log(`Błąd pobierania numeru dla anonsu ${id}`,error); } } token.throwIfCancelled(); const latest=escortAdDataMemoryCache.get(id) || data; if (String(latest.phoneId || '') !== String(data.phoneId || '')) return latest; const updated={...latest,...phone,phoneFetchStatus:status,phoneAttemptedAt:Date.now(), phoneCheckedAt:phone ? Date.now():getEscortAdPhoneCheckedAt(latest)}; const saved=setPersistentEscortAdData(id,updated,{preserveCheckedAt:true}) || updated; escortAdDataMemoryCache.set(id,saved); return saved; },cancelToken,requestContext).then(result => !followedIdChange && String(result.phoneId || '') !== String(data.phoneId || '') ? completeEscortAdPhone(id,result,forceRefresh,cancelToken,requestContext,true):result); } function getEscortAdData(adId,adUrl,forceRefresh=false,cancelToken=null,requestContext=null,{needPhone=true,maxAgeMs=getListCacheTtlMs(),minCheckedAt=0,beforeDispatch=null,freshnessRetry=false}={}) { cancelToken?.throwIfCancelled(); const id=String(adId || ''),requestedAt=Date.now(); minCheckedAt=Math.min(requestedAt,Math.max(0,Number(minCheckedAt) || 0)); requestContext ||= createRequestContext('escort.ad-data'); if (!persistentCacheKeyIsLoaded(escortAdDataCacheKey(id))) return ensurePersistentCacheKeys([escortAdDataCacheKey(id)]) .then(() => getEscortAdData(adId,adUrl,forceRefresh,cancelToken,requestContext,{needPhone,maxAgeMs,minCheckedAt,beforeDispatch,freshnessRetry})); adUrl=normalizeEscortiAdUrl(adUrl); if (!id || !adUrl || parseAdIdFromUrl(adUrl) !== id) return Promise.resolve({status:'error',data:null}); const activity=typeof readSharedEscortActivity === 'function' ? readSharedEscortActivity(adUrl):null; if (activity && !activity.active) return Promise.resolve({status:'offline',activity,data:null}); const finish=promise => promise.then(data => { cancelToken?.throwIfCancelled(); if (data?.status === 'ok' && (Number(data.checkedAt) < minCheckedAt || ((forceRefresh || Date.now() - Number(data.checkedAt) >= Math.max(0,Number(maxAgeMs) || 0)) && Number(data.checkedAt) < requestedAt))) { if (freshnessRetry) return {status:'error',data:null}; return getEscortAdData(id,adUrl,true,cancelToken,requestContext, {needPhone,maxAgeMs,minCheckedAt,beforeDispatch,freshnessRetry:true}); } if (!needPhone || data?.status !== 'ok') return data; return completeEscortAdPhone(id,data,forceRefresh,cancelToken,requestContext).then(result => isEscortAdPhoneFresh(result) ? result:{...result,phone:null,phoneDigits:null}); }).catch(error => { if (isOperationCancelledError(error)) return {status:'cancelled',data:null}; throw error; }); const allowedAge=Math.min(getListCacheTtlMs(),Math.max(0,Number(maxAgeMs) || 0)); const readCached=() => { const memory=sanitizeEscortAdDataPhone(escortAdDataMemoryCache.get(id)); const persistent=getPersistentEscortAdData(id),age=Date.now() - Number(memory?.checkedAt); const cached=memory?.status === 'ok' && Number(memory.checkedAt) > 0 && age >= 0 && age < getListCacheTtlMs() && (!persistent || Number(memory.checkedAt) >= Number(persistent.checkedAt)) ? memory:persistent; return {memory,cached:cached && cached.checkedAt >= minCheckedAt && Date.now() - cached.checkedAt < allowedAge ? cached:null}; }; const {memory,cached}=readCached(); if (!forceRefresh && cached) { escortAdDataMemoryCache.set(id,cached); return finish(Promise.resolve(cached)); } const stored=getStoredEscortAdData(id); requestContext={...requestContext,cacheKind:'details',cacheReason:forceRefresh && !freshnessRetry ? 'forced':stored || memory ? 'expired':'missing'}; const reuseCached=() => { const skipped=beforeDispatch?.(); if (skipped) return skipped; const latest=!forceRefresh && readCached().cached; if (!latest) return null; return {outcome:'cached',value:{sharedAdData:latest}}; }; return finish(joinEscortAdDataOperation(escortAdDataInflight,id,token => new Promise(resolve => { enqueueEscortAdDataJob(async () => { try { token.throwIfCancelled(); const result=reuseCached()?.value || await fetchEscortClubAdDocument(adUrl,token,requestContext,reuseCached, {minCheckedAt:forceRefresh ? requestedAt:Math.max(minCheckedAt,Date.now() - allowedAge)}); token.throwIfCancelled(); if (result?.sharedAdData) { const saved=setPersistentEscortAdData(id,result.sharedAdData,{preserveCheckedAt:true}) || result.sharedAdData; escortAdDataMemoryCache.set(id,saved); resolve(saved); return; } if (result?.sharedActivityRetry) {resolve(result); return;} if (result?.sharedActivityResult) { resolve({status:result.sharedActivityResult.unknown ? 'error':result.sharedActivityResult.active ? 'activity':'offline',activity:result.sharedActivityResult,data:null}); return; } if (!result?.ok) {resolve({status:'error',data:null}); return;} const finalUrl=new URL(result.finalUrl || adUrl,adUrl),finalAdId=parseAdIdFromUrl(finalUrl.href); if (!isEscortClubHost(finalUrl.hostname) || !getEscortAdIdFromPath(finalUrl.pathname) || finalAdId !== id) { resolve({status:'error',data:null}); return; } const extracted=extractEscortClubAdData(result.doc,id,finalUrl.href); const previous=escortAdDataMemoryCache.get(id) || memory || stored; const data={...preserveEscortAdPhoneData(extracted,previous),fetchTransport:result.transport || null}; const parsed={status:'ok',...data,checkedAt:Date.now()}; const saved=setPersistentEscortAdData(id,parsed,{preserveCheckedAt:true}) || parsed; if (typeof saveSharedEscortActivity === 'function') saveSharedEscortActivity(adUrl,{active:true},parsed.checkedAt); escortAdDataMemoryCache.set(id,saved); resolve(saved); } catch (error) { if (isOperationCancelledError(error)) {resolve({status:'cancelled',data:null}); return;} log(`Błąd pobierania danych Escort.club dla anonsu ${id}`,error); resolve({status:'error',data:null}); } }); }),cancelToken,requestContext)); } function prepareImportedEscortCard(card,baseUrl=location.href) { const makeAbsolute=value => { if (!value) return null; try { return new URL(value,baseUrl).href; } catch { return value; } }; for (const img of card.querySelectorAll('img')) { const lazySrc= img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.getAttribute('data-original'); if (!img.hasAttribute('loading')) img.loading='lazy'; img.decoding='async'; if (lazySrc) { img.setAttribute('src',makeAbsolute(lazySrc)); } else if (img.getAttribute('src')) { img.setAttribute('src',makeAbsolute(img.getAttribute('src'))); } const lazySrcset= img.getAttribute('data-srcset') || img.getAttribute('data-lazy-srcset'); if (lazySrcset) { img.setAttribute('srcset',lazySrcset); } } for (const source of card.querySelectorAll('source')) { const lazySrcset= source.getAttribute('data-srcset') || source.getAttribute('data-lazy-srcset'); if (lazySrcset) { source.setAttribute('srcset',lazySrcset); } } return card; } function wrapEscortListSelect(select,width) { const styleId='vm-escort-list-select-style'; if (!document.getElementById(styleId)) { appendHelperStyle(styleId,` .vm-escort-list-select-wrap { position:relative; display:inline-flex; flex:0 0 auto; height:26px; vertical-align:middle; } .vm-escort-list-select-wrap::after { content:''; position:absolute; top:11px; right:3px; border-left:3px solid transparent; border-right:3px solid transparent; border-top:4px solid var(--vm-ui-accent,${getEscortPagePinkColor()}); pointer-events:none; } :is(#vm-escort-custom-filters-control,#vm-escort-list-pages-control) .vm-escort-list-select-wrap > select { appearance:none !important; box-sizing:border-box; width:100% !important; min-width:0 !important; max-width:none !important; height:26px !important; min-height:26px !important; margin:0 !important; padding:0 10px 0 2px !important; border:1px solid var(--vm-ui-accent,${getEscortPagePinkColor()}) !important; border-radius:5px !important; background:var(--vm-ui-bg,#fff) !important; color:var(--vm-ui-text,#211d22) !important; font:400 11px var(--vm-ui-font-family,Lato,Arial,sans-serif) !important; box-shadow:none !important; cursor:pointer; } .vm-escort-list-select-wrap > select:focus-visible { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:2px; } .vm-escort-list-select-wrap > select:disabled { cursor:wait; opacity:.65; } `); } const wrap=makeElement('span','vm-escort-list-select-wrap'); wrap.style.width=`${width}px`; wrap.appendChild(select); return wrap; } function addEscortListPagesControl(listContext) { const existing=document.getElementById('vm-escort-list-pages-control'); if (existing) { const progress=document.getElementById('vm-escort-list-pages-progress'); const progressText=progress?.querySelector('.vm-list-pages-progress-text'); const cancelButton=progress?.querySelector('.vm-list-pages-cancel'); return { row:existing, select:existing.querySelector('select'), status:existing.querySelector('.vm-list-pages-status'), progress, setProgress(text,onCancel=null) { const value=String(text || ''); if (progressText) progressText.textContent=value; if (progress) progress.hidden=!value; if (cancelButton) { cancelButton.hidden=typeof onCancel !== 'function'; cancelButton.onclick=typeof onCancel === 'function' ? onCancel :null; } } }; } if (!listContext) return null; const targetContainer=getEscortListCardContainer(listContext.anchors || []); const earlyTitleCol=listContext.heading?.closest('.title-col'); const earlyTitleRow=earlyTitleCol?.parentElement; if (!targetContainer?.parentElement && !(earlyTitleCol && earlyTitleRow?.classList.contains('row'))) { return null; } const pagePinkColor=getEscortPagePinkColor(); const listPagesStyleId='vm-list-pages-select-style'; if (!document.getElementById(listPagesStyleId)) { appendHelperStyle(listPagesStyleId,` #vm-escort-list-pages-control .vm-list-pages-selection { display: inline-flex; align-items: center; flex: 0 0 auto; gap: 2px; box-sizing: border-box; height: 34px; margin: 0; padding: 0 5px 0 8px; border: 1px solid var(--vm-ui-accent,${pagePinkColor}); border-radius: 7px; color: var(--vm-ui-text,#211d22); white-space: nowrap; } #vm-favorite-search-button { align-self:center; height:auto !important; min-height:28px; padding:4px 9px; margin:0 0 0 12px; vertical-align:middle; white-space:nowrap; font:700 11px/13px var(--vm-ui-font-family,Lato,Arial,sans-serif); box-sizing:border-box; } .vm-favorite-search-slot { display:inline-flex; align-items:center; list-style:none; } .vm-favorite-search-slot::before, .vm-favorite-search-slot::after { content:none !important; } `); } const row=makeElement('div',"header-filter col",null,{id:'vm-escort-list-pages-control'}); Object.assign(row.style,{ display:'flex', alignItems:'center', justifyContent:'flex-end', flex:'0 0 auto', marginLeft:'auto', paddingLeft:'12px' }); const inner=makeElement('div','filter-box vm-list-pages-inner'); Object.assign(inner.style,{ display:'inline-flex', alignItems:'center', flexWrap:'nowrap', justifyContent:'flex-end', gap:'2px', fontFamily:'Lato,Arial,sans-serif', lineHeight:'1.2' }); const label=makeElement('span','sub-label','Pokaż'); Object.assign(label.style,{ color:'var(--vm-ui-text,#211d22)', fontSize:'12px', fontWeight:'400', whiteSpace:'nowrap' }); const selectionBox=makeElement('div','vm-list-pages-selection'); const select=makeElement('select','vm-list-pages-select'); select.setAttribute('aria-label','Pokaż na jednej liście'); const selectWrap=wrapEscortListSelect(select,59.5); const multiLocationCount=getEscortMultiLocationsForCurrentPage().length; const locationMultiplier=makeElement('span','vm-list-pages-location-multiplier',multiLocationCount > 1 ? `× ${multiLocationCount} ${polishLocationWord(multiLocationCount)}` :''); Object.assign(locationMultiplier.style,{ color:'var(--vm-ui-text-muted,#746b70)', fontSize:'11px', fontWeight:'700', whiteSpace:'nowrap' }); const selectedValue=normalizeListPagesToShow(LIST_PAGES_TO_SHOW); appendSelectOptions(select,LIST_PAGE_OPTIONS,selectedValue); const status=makeElement('span','vm-list-pages-status'); Object.assign(status.style,{ color:'var(--vm-ui-text-faint,#9a9297)', fontFamily:'Lato,Arial,sans-serif', fontSize:'10px', fontWeight:'400', whiteSpace:'nowrap' }); const progress=makeElement('div'); progress.id='vm-escort-list-pages-progress'; progress.setAttribute('aria-live','polite'); progress.hidden=true; const progressText=makeElement('span','vm-list-pages-progress-text'); const cancelButton=makeButton('vm-list-pages-cancel','Przerwij',{hidden:true}); progress.append(progressText,cancelButton); const setProgress=(text,onCancel=null) => { const value=String(text || ''); progressText.textContent=value; progress.hidden=!value; cancelButton.hidden=typeof onCancel !== 'function'; cancelButton.onclick=typeof onCancel === 'function' ? onCancel :null; }; select.addEventListener('change',() => { const newValue=normalizeListPagesToShow(select.value); const oldValue=normalizeListPagesToShow(LIST_PAGES_TO_SHOW); if (String(newValue) === String(oldValue)) { return; } select.disabled=true; status.textContent=''; setProgress('Ładowanie stron...'); if (!requestOneTimeListPagesReload(newValue)) { select.disabled=false; setProgress(''); status.textContent='Nie udało się przeładować listy.'; select.value=String(oldValue); return; } location.reload(); }); selectionBox.appendChild(label); selectionBox.appendChild(selectWrap); if (multiLocationCount > 1) selectionBox.appendChild(locationMultiplier); inner.appendChild(selectionBox); inner.appendChild(status); row.appendChild(inner); const titleCol=listContext.heading?.closest('.title-col'); const titleRow=titleCol?.parentElement; if (titleCol && titleRow?.classList.contains('row')) { titleCol.insertAdjacentElement('afterend',row); titleRow.insertAdjacentElement('afterend',progress); } else { Object.assign(row.style,{ width:'100%', justifyContent:'flex-start', margin:'0 0 10px', paddingLeft:'0' }); targetContainer.parentElement.insertBefore(row,targetContainer); row.insertAdjacentElement('afterend',progress); } return { row, select, status, progress, setProgress }; } function getEscortListSortField(field) { const duration=normalizeEscortPriceDuration(SETTINGS.searchResultPriceDuration); return {price:`ad:prices:${duration === 'hide' ? '60':duration}`,age:'ad:stats:wiek', published:'ad:datePosted',visited:'ad:lastVisitedAt',default:'none'}[field] || field; } function createEscortListSortControl(onChange) { const key='vm_escort_list_sort_v1'; const saved=readFavoriteViewPreferences(key); let config={field:saved.field || 'default',direction:saved.direction === 'desc' ? 'desc':'asc'}; const node=makeElement('div','vm-search-price-control'); const select=makeElement('select','vm-search-extra-select'); const direction=makeButton('vm-search-sort-direction'); select.setAttribute('aria-label','Sortuj wyniki według'); node.title='Sortuje wszystkie wczytane wyniki. Brakujące dane trafiają na koniec. Grupy pozostają razem.'; node.append(document.createTextNode('Sortuj:'),wrapEscortListSelect(select,84),direction); function sync() { select.value=config.field; select.title=select.selectedOptions?.[0]?.textContent || ''; const descending=config.direction === 'desc'; direction.textContent=descending ? '↓':'↑'; direction.title=descending ? 'Malejąco — zmień na rosnąco':'Rosnąco — zmień na malejąco'; direction.setAttribute('aria-label',direction.title); direction.disabled=config.field === 'default'; } function commit(nextDirection) { const next={field:select.value,direction:nextDirection}; try { saveFavoriteViewPreferences(key,next); config=next; } catch (error) { log('Nie udało się zapisać sortowania wyników',error); sync(); return; } sync(); onChange?.(); } function refresh() { const duration=getEscortListSortField('price').split(':').pop(); const standardFields=['ad:location:city','ad:title','escorti:adLinks','escorti:creationDate','escorti:garsoTopics']; const options=[['default','Domyślna kolejność'],['price',`Cena (${getEscortPriceDurationLabel(duration)})`], ['age','Wiek'],['published',getEscortDataFieldDescriptor('ad:datePosted').label],['visited','Ostatnia wizyta']]; const used=new Set(options.map(([field]) => getEscortListSortField(field))); for (const field of [...standardFields,...getSearchResultExtraFields()]) { if (used.has(field)) continue; used.add(field); options.push([field,field === 'ad:location:city' ? 'Miasto':getEscortDataFieldDescriptor(field)?.label || field]); } if (!options.some(([field]) => field === config.field)) { config.field=options.find(([field]) => getEscortListSortField(field) === getEscortListSortField(config.field))?.[0] || 'default'; try {saveFavoriteViewPreferences(key,config);} catch (error) {log('Nie udało się zapisać sortowania wyników',error);} } select.replaceChildren(); appendSelectOptions(select,options,config.field); sync(); } select.addEventListener('change',() => commit(config.direction)); direction.addEventListener('click',() => commit(config.direction === 'desc' ? 'asc':'desc')); refresh(); return {node,refresh,getConfig:() => ({...config})}; } function getEscortListSortValue(state,field) { if (field === 'default') return null; if (field === 'age') { const age=getEscortListCardAgeAndCity(state.card).age; if (age != null) return age; } const dataField=getEscortListSortField(field); const descriptor=getEscortDataFieldDescriptor(dataField); if (!descriptor) return null; let raw=getEscortTileFieldValue(state,descriptor); if (dataField === 'ad:location:city') raw=getEscortListCardAgeAndCity(state.card).city || raw; if (dataField === 'ad:title') raw=normalizeEscortAdText(state.card?.querySelector('.item-name')?.textContent) || raw; if (raw == null || raw === '' || (Array.isArray(raw) && !raw.length)) return null; if (typeof raw === 'number') return Number.isFinite(raw) ? raw:null; if (typeof raw === 'boolean') return Number(raw); return favoriteDataSortValue(null,dataField,formatEscortUniversalDataValue(descriptor,raw,false),{lastVisited:raw}); } function applyEscortResultSort(states,config={field:'default',direction:'asc'},getGroupAdIds=null) { const parents=new Map(),direction=config.direction === 'desc' ? -1:1,dataField=getEscortListSortField(config.field); for (const state of states) { const parent=state.card?.parentNode; if (!parent) continue; if (!parents.has(parent)) parents.set(parent,new Map()); const groups=parents.get(parent),key=state.group || state; if (!groups.has(key)) groups.set(key,[]); groups.get(key).push(state); } for (const [parent,groups] of parents) { const buckets=[...groups.values()].map(members => { members.sort((a,b) => a.index-b.index); const canonical=members.find(member => !member.hiddenInResults) || members[0]; let value=null; if (dataField === 'ad:lastVisitedAt') { value=getBucketLastVisitedAt({members},canonical.group ? getGroupAdIds?.(canonical.group):undefined); } else if (config.field !== 'default') { const visible=(canonical.aggregateMembers || members).filter(member => !member.hiddenInResults); for (const member of visible) { const next=getEscortListSortValue(member,config.field); if (compareFavoriteValues(next,value,direction) < 0) value=next; } if (dataField === 'profile:activeCount' && canonical.aggregateMembers?.length > 1) value=visible.length; } return {members,index:members[0].index,value}; }); buckets.sort((a,b) => compareFavoriteValues(a.value,b.value,direction) || a.index-b.index); const cards=buckets.flatMap(bucket => bucket.members.map(member => member.card)); const tracked=new Set(cards); let cursor=[...parent.childNodes].find(node => tracked.has(node)) || null; // Keep loader anchors and other site nodes attached while moving cards. for (const card of cards) { while (cursor && !tracked.has(cursor)) cursor=cursor.nextSibling; if (card === cursor) cursor=cursor.nextSibling; else parent.insertBefore(card,cursor); } } } function addEscortCustomFiltersControl(listContext,onChange,onPriceDurationChange,onExtraFieldChange,onSortChange) { const existing=document.getElementById('vm-escort-custom-filters-control'); if (existing) return null; const heading=listContext?.heading; const titleCol=heading?.closest('.title-col'); const titleRow=titleCol?.parentElement; if (!heading || !titleCol || !titleRow) return null; const pagePinkColor=getEscortPagePinkColor(); const styleId='vm-escort-custom-filters-style'; if (!document.getElementById(styleId)) { appendHelperStyle(styleId,` #vm-escort-custom-filters-control { display: flex; flex-wrap: wrap; align-items: center; flex: 0 1 auto; gap: 7px; margin-left: 0; padding-left: 10px; } #vm-escort-custom-filters-trigger { display: inline-flex !important; align-items: center; justify-content: center; flex: 0 0 auto !important; width: auto !important; min-width: max-content !important; max-width: none !important; height: 32px; margin: 0; padding: 7px 12px !important; border: 1px solid var(--vm-ui-accent, #f54da3) !important; background: var(--vm-ui-accent, #f54da3) !important; border-radius: 7px !important; color: var(--vm-ui-accent-contrast, #fff) !important; font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); font-size: 12px; font-weight: 700; line-height: 16px; white-space: nowrap !important; cursor: pointer; } #vm-escort-custom-filters-trigger * { color: inherit !important; fill: currentColor !important; stroke: currentColor !important; } #vm-escort-custom-filters-trigger:disabled { cursor: not-allowed !important; opacity: .55; } #vm-escort-custom-filters-control .vm-search-sort-direction { flex:0 0 28px; width:28px; height:26px; margin:0; padding:0; border:1px solid var(--vm-ui-accent, ${pagePinkColor}); border-radius:5px; background:var(--vm-ui-bg, #fff); color:var(--vm-ui-accent, ${pagePinkColor}); font:700 18px/24px var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor:pointer; } #vm-escort-custom-filters-control .vm-search-sort-direction:disabled { opacity:.4; cursor:default; } #vm-escort-custom-filters-control .vm-search-price-control { display: inline-flex; align-items: center; flex: 0 0 auto; gap: 6px; box-sizing: border-box; height: 34px; margin: 0 0 0 3px; padding: 0 5px 0 8px; border: 1px solid var(--vm-ui-accent, ${pagePinkColor}); border-radius: 7px; color: var(--vm-ui-text, #211d22); font:400 12px var(--vm-ui-font-family,Lato,Arial,sans-serif); white-space: nowrap; } #vm-escort-custom-filters-control .vm-search-extra-control { display: inline-flex; align-items: center; flex: 0 1 auto; gap: 4px; box-sizing: border-box; min-height: 34px; margin: 0; padding: 3px 6px 3px 8px; border: 1px solid var(--vm-ui-accent, ${pagePinkColor}); border-radius: 7px; color: var(--vm-ui-text, #211d22); font:400 12px var(--vm-ui-font-family,Lato,Arial,sans-serif); white-space: nowrap; } #vm-escort-custom-filters-control .vm-search-extra-add, #vm-escort-custom-filters-control .vm-search-extra-chip { box-sizing: border-box; height: 26px; margin: 0; border: 1px solid var(--vm-ui-border, rgba(0,0,0,.14)); border-radius: 5px; background: var(--vm-ui-bg, #fff); color: var(--vm-ui-text, #211d22); font:600 10px var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-escort-custom-filters-control .vm-search-extra-add { padding: 2px 7px; border-color: var(--vm-ui-accent, ${pagePinkColor}); color: var(--vm-ui-accent, ${pagePinkColor}); cursor: pointer; } #vm-escort-custom-filters-control .vm-search-extra-chip { display: inline-flex; align-items: center; gap: 3px; flex: 0 0 auto; max-width: 180px; padding: 1px 3px 1px 6px; } #vm-escort-custom-filters-control .vm-search-extra-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #vm-escort-custom-filters-control .vm-search-extra-chip button { flex: 0 0 auto; padding: 0 2px; border: 0; background: transparent; color: var(--vm-ui-text-muted, #777); font-size: 13px; line-height: 1; cursor: pointer; } #vm-custom-filter-pages-hint { box-sizing:border-box; width:100%; margin:4px 0 8px; padding:7px 10px; border-left:3px solid var(--vm-ui-accent, #f54da3); border-radius:4px; background:var(--vm-ui-bg-alt, #fcfafb); color:var(--vm-ui-text, #211d22); font:400 12px/1.4 var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-custom-filter-pages-hint[hidden] { display:none !important; } #vm-escort-custom-filters-panel { box-sizing: border-box; width: 100%; margin: 0 0 10px; padding: 5px 8px; border-top: 1px solid var(--vm-ui-accent, #ff4c99); background: var(--vm-ui-bg, #fff); box-shadow: var(--vm-ui-shadow, 0 3px 8px rgba(33, 29, 34, .08)); font-family:var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-escort-custom-filters-panel[hidden] { display: none !important; } #vm-escort-custom-filters-panel[data-busy="true"] { opacity: .58; } #vm-escort-custom-filters-panel .vm-custom-filter-intro { margin: 0 0 3px; padding: 0; color: var(--vm-ui-accent, ${pagePinkColor}); font-size: 11px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase; } #vm-escort-custom-filters-panel .vm-custom-filter-intro strong { font-weight: 700; } #vm-escort-custom-filters-panel .vm-custom-filter-intro.-show-only { margin-top: 5px; padding-top: 4px; border-top: 1px solid var(--vm-ui-accent, #ff4c99); font-weight: 700; } #vm-escort-custom-filters-panel .vm-custom-filter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 5px; align-items: start; } #vm-escort-custom-filters-panel .vm-custom-filter-group { min-width: 0; padding: 3px 6px 4px; border: 1px solid var(--vm-ui-border, #eee6eb); border-radius: 7px; background: var(--vm-ui-bg-alt, #fcfafb); } #vm-escort-custom-filters-panel .vm-custom-filter-group-title { margin: 0 0 2px; color: var(--vm-ui-accent, ${pagePinkColor}); font-size: 10px; font-weight: 700; line-height: 15px; } #vm-escort-custom-filters-panel .vm-custom-filter-group-body { display: grid; gap: 0; } #vm-escort-custom-filters-panel .vm-custom-filter-row { display: flex; align-items: center; min-width: 0; min-height: 22px; height: auto; margin: 0; padding: 0; line-height: 1.2; flex-wrap: wrap; gap: 4px; color: var(--vm-ui-text, #211d22); font-size: 11px; white-space: normal; } #vm-escort-custom-filters-panel .vm-custom-filter-check { flex: 0 0 auto; width: 14px; height: 14px; margin: 0 1px 0 0; accent-color: var(--vm-ui-accent, ${pagePinkColor}); cursor: pointer; } #vm-escort-custom-filters-panel .vm-custom-filter-number { box-sizing: border-box; width: 48px; height: 20px; padding: 0 3px; border: 0; border-bottom: 1px solid var(--vm-ui-accent, #ff4c99); border-radius: 0; outline: 0; background: transparent; color: var(--vm-ui-text, #211d22); font:700 11px var(--vm-ui-font-family,Lato,Arial,sans-serif); text-align: center; } #vm-escort-custom-filters-panel .vm-custom-filter-unit, #vm-escort-custom-filters-panel .vm-custom-filter-day, #vm-escort-custom-filters-panel .vm-custom-filter-time { box-sizing: border-box; width: 72px; height: 20px; padding: 0 3px; border: 0; border-bottom: 1px solid var(--vm-ui-accent, #ff4c99); border-radius: 0; outline: 0; background: transparent; color: var(--vm-ui-text, #211d22); font:11px var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor: pointer; } #vm-escort-custom-filters-panel .vm-custom-filter-day { width: 98px; } #vm-escort-custom-filters-panel .vm-custom-filter-time { width: 66px; font-weight: 700; text-align: center; } #vm-escort-custom-filters-panel .vm-custom-filter-text { box-sizing: border-box; width: min(190px, 100%); height: 20px; padding: 0 3px; border: 0; border-bottom: 1px solid var(--vm-ui-accent, #ff4c99); border-radius: 0; outline: 0; background: transparent; color: var(--vm-ui-text, #211d22); font:11px var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-escort-custom-filters-panel .vm-custom-filter-category { box-sizing: border-box; min-width: 0; max-width: 140px; height: 20px; margin: 0; padding: 0 3px; font: inherit; } #vm-escort-custom-filters-panel .vm-custom-filter-number:disabled, #vm-escort-custom-filters-panel .vm-custom-filter-unit:disabled, #vm-escort-custom-filters-panel .vm-custom-filter-day:disabled, #vm-escort-custom-filters-panel .vm-custom-filter-time:disabled, #vm-escort-custom-filters-panel .vm-custom-filter-text:disabled { border-bottom-color: var(--vm-ui-border, #d8d2d5); color: var(--vm-ui-text-faint, #aaa3a7); cursor: not-allowed; opacity: .7; } #vm-escort-custom-filters-panel .vm-custom-filter-footer { display: flex; align-items: center; gap: 10px; margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--vm-ui-border, #eee9ec); } #vm-escort-custom-filters-panel .vm-custom-filter-clear { padding: 0; border: 0; background: transparent; color: var(--vm-ui-accent, ${pagePinkColor}); font:700 11px var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor: pointer; } #vm-escort-custom-filters-panel .vm-custom-filter-status { color: var(--vm-ui-text-muted, #8d858a); font-size: 10px; } @media (max-width: 767px) { #vm-escort-custom-filters-control { flex-wrap: wrap; margin-left: 0; padding: 6px 0 0; } #vm-escort-custom-filters-panel .vm-custom-filter-grid { grid-template-columns: 1fr; } #vm-escort-custom-filters-panel .vm-custom-filter-row { min-height: 22px; height: auto; flex-wrap: wrap; white-space: normal; } } `); } let config=getEscortCustomFilters(); let filtersBusy=false; let listIncomplete=false; const control=prepareHelperStyleApi( makeElement('div') ); control.id='vm-escort-custom-filters-control'; control.className='header-filter col'; const trigger=makeElement('button'); trigger.id='vm-escort-custom-filters-trigger'; trigger.type='button'; trigger.className='btn btn-pink'; let priceDurationSelect=null; let priceControl=null; if (SETTINGS.showPricesInSearchResults) { priceControl=makeElement('label'); priceControl.className='vm-search-price-control'; priceControl.appendChild(document.createTextNode('Cena za')); priceDurationSelect=makeElement('select'); priceDurationSelect.className='vm-search-price-select'; priceDurationSelect.setAttribute('aria-label','Wyświetl cenę za wybrany czas'); const selectedDuration=normalizeEscortPriceDuration( SETTINGS.searchResultPriceDuration ); const compactLabels={hide:'—',15:'15′',30:'30′',60:'1h',120:'2h',night:'noc'}; appendSelectOptions(priceDurationSelect,ESCORT_PRICE_DISPLAY_OPTIONS.map(([value]) => [value,compactLabels[value]]),selectedDuration); priceDurationSelect.title=getEscortPriceDurationLabel(selectedDuration); priceControl.appendChild(wrapEscortListSelect(priceDurationSelect,32.4)); } const sortControl=createEscortListSortControl(onSortChange); const extraFieldControl=makeElement('div',"vm-search-extra-control"); extraFieldControl.appendChild(document.createTextNode('Wyświetl dodatkowo:')); const extraFieldAddButton=makeButton('vm-search-extra-add','Dodaj'); const extraFieldSelect=makeElement('select',"vm-search-extra-select"); const extraFieldSelectWrap=wrapEscortListSelect(extraFieldSelect,168); extraFieldSelectWrap.style.display='none'; extraFieldSelect.setAttribute('aria-label','Dodaj dodatkową informację na kafelkach'); extraFieldSelect.title='Opcje są tworzone automatycznie z danych, które Escort Helper już odczytał lub ma w cache. Dodanie pola nie uruchamia dodatkowego pobierania.'; const extraFieldChips=makeElement('div'); extraFieldChips.style.display='contents'; const saveSearchResultExtraFields=fields => { const previousFields=getSearchResultExtraFields(); const normalizedFields=normalizeSearchResultExtraFields(fields); try { updateSettings({ searchResultExtraFields:normalizedFields }); refreshExtraFieldOptions(); if (typeof onExtraFieldChange === 'function') onExtraFieldChange(normalizedFields); } catch (error) { SETTINGS.searchResultExtraFields=previousFields; log('Nie udało się zapisać dodatkowych informacji na kafelkach',error); } }; const refreshExtraFieldOptions=() => { sortControl.refresh(); const selectedFields=getSearchResultExtraFields(); const available=getEscortDataFieldOptions('none') .filter(([field]) => field !== 'none' && !selectedFields.includes(field)); extraFieldSelect.replaceChildren(); appendSelectOptions(extraFieldSelect,[['','wybierz…'],...available],''); extraFieldAddButton.disabled=!available.length || selectedFields.length >= SEARCH_RESULT_MAX_EXTRA_FIELDS; extraFieldAddButton.style.opacity=extraFieldAddButton.disabled ? '.45':'1'; extraFieldAddButton.style.cursor=extraFieldAddButton.disabled ? 'default':'pointer'; if (extraFieldAddButton.disabled) extraFieldSelectWrap.style.display='none'; extraFieldChips.replaceChildren(); for (const field of selectedFields) { const descriptor=getEscortDataFieldDescriptor(field); const chip=makeElement('span','vm-search-extra-chip'); const text=makeElement('span','',descriptor?.label || field,{title:descriptor?.label || field}); const remove=makeButton('','×',{title:'Usuń tę informację z kafelków'}); remove.addEventListener('click',() => { saveSearchResultExtraFields(getSearchResultExtraFields().filter(value => value !== field)); }); chip.append(text,remove); extraFieldChips.appendChild(chip); } }; extraFieldAddButton.addEventListener('click',() => { if (extraFieldAddButton.disabled) return; extraFieldSelectWrap.style.display=extraFieldSelectWrap.style.display === 'none' ? 'inline-block':'none'; if (extraFieldSelectWrap.style.display !== 'none') extraFieldSelect.focus(); }); extraFieldSelect.addEventListener('change',() => { const field=normalizeEscortDataFieldId(extraFieldSelect.value); if (!field || field === 'none') return; saveSearchResultExtraFields([...getSearchResultExtraFields(),field]); extraFieldSelect.value=''; extraFieldSelectWrap.style.display='none'; }); refreshExtraFieldOptions(); window.addEventListener('vmEscortDataCatalogChanged',refreshExtraFieldOptions); extraFieldControl.append(extraFieldAddButton,extraFieldSelectWrap,extraFieldChips); const loadAllHint=prepareHelperStyleApi(makeElement('div')); loadAllHint.id='vm-custom-filter-pages-hint'; loadAllHint.setAttribute('role','status'); loadAllHint.textContent='Filtry niestandardowe działają tylko na wczytanych anonsach. Warto wczytać wszystkie strony: „Pokaż” → „Wszystkie”.'; loadAllHint.hidden=true; const progress=makeElement('div','vm-custom-filter-progress'); progress.setAttribute('aria-live','polite'); progress.hidden=true; const progressText=makeElement('span','vm-custom-filter-progress-text'); const progressCancelButton=makeButton('vm-custom-filter-cancel','Przerwij',{hidden:true}); let progressBusy=false; progress.append(progressText,progressCancelButton); const panel=makeElement('div'); panel.id='vm-escort-custom-filters-panel'; panel.hidden=!config.expanded; const intro=makeElement('p','vm-custom-filter-intro','Ukrywaj'); const grid=makeElement('div','vm-custom-filter-grid'); const showOnlyIntro=makeElement('p','vm-custom-filter-intro -show-only','Pokazuj tylko'); const showOnlyGrid=makeElement('div','vm-custom-filter-grid'); const status=makeElement('span','vm-custom-filter-status'); status.setAttribute('aria-live','polite'); const controls={}; const dependentControls=[]; function makeFilterGroup(title,targetGrid) { const group=makeElement('section','vm-custom-filter-group'); const heading=makeElement('div','vm-custom-filter-group-title',title); const body=makeElement('div','vm-custom-filter-group-body'); group.append(heading,body); targetGrid.appendChild(group); return body; } function makeFilterControl(key,tag,className,label,attributes={}) { const control=makeElement(tag,'vm-custom-filter-' + className,null,attributes); control.setAttribute('aria-label',label); controls[key]=control; return control; } function makeFilterRow(key,text,fields,target,title='',tag='div') { const row=makeElement(tag,'vm-custom-filter-row'); if (title) row.title=title; const checkbox=makeElement('input','vm-custom-filter-check',null,{type:'checkbox',checked:!!config[key]}); controls[key]=checkbox; row.append(checkbox,document.createTextNode(text),...fields); for (const field of fields) { if (field?.matches?.('input,select')) dependentControls.push([checkbox,field]); } target.appendChild(row); } function makeFilterSelect(key,className,label,options,value=config[key]) { const select=makeFilterControl(key,'select',className,label); appendSelectOptions(select,options,value); return select; } function makeCategoryFilterRow(checkKey,categoryKey,target) { const categories=normalizeFavoriteCategories(SETTINGS.favoriteCategories); const current=normalizeCustomFilterCategory(config[categoryKey]); const options=[['','Dowolna'],...categories.map(category => [category,category])]; if (current && !categories.some(category => category.toLocaleLowerCase('pl-PL') === current.toLocaleLowerCase('pl-PL'))) { options.push([current,current]); } makeFilterRow(checkKey,'Ma przypisaną kategorię:',[ makeFilterSelect(categoryKey,'category','Wybierz kategorię',options,current) ],target,'Uwzględnia kategorie przypisane zarówno do konkretnego anonsu, jak i do powiązanego profilu.'); } function makeFilterNumberInput(key,label) { const [normalize,,min=0,max=100000]=ESCORT_CUSTOM_FILTER_FIELDS[key]; const decimal=normalize === normalizeCustomFilterNumber; const input=makeFilterControl(key,'input','number',label,{ type:'number',min:String(min),max:String(max),step:decimal ? '0.1':'1',inputMode:decimal ? 'decimal':'numeric',value:String(config[key]) }); input.dataset.filterKey=key; return input; } const filterGroups={ hp:makeFilterGroup('Garsoniera i profil Escorti.pl',grid), ha:makeFilterGroup('Anonse i agregacja',grid), hv:makeFilterGroup('Odwiedziny, notatki i kategorie',grid), hd:makeFilterGroup('Dostępność',grid), sp:makeFilterGroup('Garsoniera i profil Escorti.pl',showOnlyGrid), sa:makeFilterGroup('Anonse i agregacja',showOnlyGrid), sv:makeFilterGroup('Odwiedziny, notatki i kategorie',showOnlyGrid), sd:makeFilterGroup('Dostępność',showOnlyGrid) }; const rowBuilders={ s:(target,[key,text,title='']) => makeFilterRow(key,text,[],target,title,'label'), n:(target,[key,text,numberKey,suffix='',title='']) => makeFilterRow(key,text,[ makeFilterNumberInput(numberKey,`${text} ${suffix}`),document.createTextNode(suffix) ],target,title), a:(target,[key,text,numberKey,unitKey]) => makeFilterRow(key,text,[ makeFilterNumberInput(numberKey,text), makeFilterSelect(unitKey,'unit',`${text} - jednostka wieku`,ESCORT_CUSTOM_FILTER_AGE_UNIT_OPTIONS) ],target), b:(target,[key,minKey,maxKey]) => makeFilterRow(key,'Tylko z deklarowanym BMI: od ',[ makeFilterNumberInput(minKey,'BMI od'),document.createTextNode(' do '),makeFilterNumberInput(maxKey,'BMI do') ],target,'BMI jest liczone w locie z deklarowanych w anonsie pól Waga i Wzrost. BMI nie jest zapisywane w cache.'), c:(target,[key,categoryKey]) => makeCategoryFilterRow(key,categoryKey,target), v:(target,[key,text,dayKey,timeKey]) => makeFilterRow(key,text,[ makeFilterSelect(dayKey,'day',`${text} - dzień tygodnia`,ESCORT_CUSTOM_FILTER_WEEKDAYS), makeFilterControl(timeKey,'input','time',`${text} - godzina`,{type:'time',step:'60',value:normalizeCustomFilterTime(config[timeKey])}) ],target), d:(target,[key,text,queryKey,placeholder='wpisz tekst']) => { const input=makeFilterControl(queryKey,'input','text',text,{type:'search',value:config[queryKey] || '',placeholder,maxLength:200}); input.dataset.filterKey=queryKey; makeFilterRow(key,text,[input],target); } }; const filterLayout={ hp:[ ['s','hideWithoutOpinions','Bez opinii na Garsonierze'], ['n','hideProfileAdsOver','Anonse profilu na Escorti.pl: więcej niż','profileAdsLimit','','Łączna liczba anonsów znaleziona w historii profilu Escorti.pl.'], ['n','hideLocationChangesOver','Liczba zmian lokalizacji więcej niż:','locationChangesLimit','','Liczba kolejnych zmian miasta w chronologicznej historii profilu Escorti.pl. Domyślny limit: 1.'], ['a','hideYoungerThan','Profil młodszy niż','youngerValue','youngerUnit'], ['a','hideOlderThan','Profil starszy niż','olderValue','olderUnit'] ], ha:[ ['n','hideActiveAdsOver','Zagregowane anonse: więcej niż','activeAdsLimit'], ['s','hideAgencyPhone','Z numerem telefonu agencji/salonu'], ['d','hideCity','Miasto:','hideCityQuery','wpisz miasto'], ['n','hideTagCountOver','Liczba usług / tagów: więcej niż','tagCountLimit','','Domyślnie używa limitu z ustawień głównych; tutaj można ustawić inną wartość dla filtrowania.'], ['a','hideAdYoungerThan','Anons młodszy niż','adYoungerValue','adYoungerUnit'], ['a','hideAdOlderThan','Anons starszy niż','adOlderValue','adOlderUnit'], ['d','hideDescriptionMatch','Opis zawiera:','hideDescriptionQuery'] ], hv:[ ['s','hideVisited','Odwiedzone'],['s','hideUnvisited','Nieodwiedzone'], ['a','hideVisitedOlderThan','Anons odwiedzony dawniej niż','hideVisitedOlderValue','hideVisitedOlderUnit'], ['a','hideVisitedRecentThan','Anons odwiedzony w ciągu ostatnich','hideVisitedRecentValue','hideVisitedRecentUnit'], ['s','hideHasNote','Ma notatkę','Uwzględnia notatkę przypisaną do anonsu lub powiązanego profilu.'], ['c','hideHasCategory','hideCategory'] ], hd:[ ['s','hideAlwaysAvailable','Dostępne „cały czas”'], ['v','hideUnavailableAt','Niedostępne:','hideUnavailableDay','hideUnavailableTime'], ['v','hideAvailableAt','Dostępne:','hideAvailableDay','hideAvailableTime'] ], sp:[ ['s','showOnlyWithoutOpinions','Bez opinii na Garsonierze (tryb sapera)'], ['s','showOnlySingleProfileAd','Profil Escorti.pl z 1 anonsem'] ], sa:[ ['s','showOnlyAgencyPhone','Z numerem telefonu agencji/salonu'], ['b','showOnlyBmi','showOnlyBmiMin','showOnlyBmiMax'], ['d','showOnlyDescriptionMatch','Opis zawiera:','showOnlyDescriptionQuery'] ], sv:[ ['a','showOnlyVisitedOlderThan','Anons odwiedzony dawniej niż','showOnlyVisitedOlderValue','showOnlyVisitedOlderUnit'], ['a','showOnlyVisitedRecentThan','Anons odwiedzony w ciągu ostatnich','showOnlyVisitedRecentValue','showOnlyVisitedRecentUnit'], ['s','showOnlyHasNote','Ma notatkę','Uwzględnia notatkę przypisaną do anonsu lub powiązanego profilu.'], ['c','showOnlyHasCategory','showOnlyCategory'] ], sd:[ ['v','showOnlyUnavailableAt','Niedostępne:','showOnlyUnavailableDay','showOnlyUnavailableTime'], ['v','showOnlyAvailableAt','Dostępne:','showOnlyAvailableDay','showOnlyAvailableTime'] ] }; for (const [groupKey,rows] of Object.entries(filterLayout)) { for (const [type,...args] of rows) { rowBuilders[type](filterGroups[groupKey],args); } } const footer=makeElement('div','vm-custom-filter-footer'); const clearBtn=makeButton('vm-custom-filter-clear','Wyłącz wszystkie'); footer.append(clearBtn,status); panel.append(intro,grid,showOnlyIntro,showOnlyGrid,footer); control.append(trigger,sortControl.node); if (priceControl) control.appendChild(priceControl); control.appendChild(extraFieldControl); function readConfig() { return Object.fromEntries(Object.entries(ESCORT_CUSTOM_FILTER_FIELDS).map(([key,[normalize]]) => [ key,key === 'expanded' ? !panel.hidden:normalizeEscortCustomFilterField( key,normalize === Boolean ? controls[key].checked:controls[key].value,SETTINGS.alarmingTagCountLimit ) ])); } function updateTrigger() { const activeCount=getActiveEscortCustomFilterCount(config); loadAllHint.hidden=!listIncomplete || !activeCount; const arrow=panel.hidden ? '▼':'▲'; trigger.textContent=`Filtry niestandardowe${activeCount ? ` (${activeCount})`:''} ${arrow}`; trigger.setAttribute('aria-expanded',String(!panel.hidden)); trigger.setAttribute('aria-controls',panel.id); } function updateDisabledStates() { for (const checkbox of panel.querySelectorAll('.vm-custom-filter-check')) { checkbox.disabled=filtersBusy; } for (const [checkbox,dependent] of dependentControls) { dependent.disabled=filtersBusy || (!checkbox.checked && !dependent.matches('.vm-custom-filter-category')); } clearBtn.disabled=filtersBusy; } function setFiltersBusy(value) { if (filtersBusy === !!value) return; filtersBusy=!!value; trigger.disabled=filtersBusy; trigger.setAttribute('aria-disabled',String(filtersBusy)); panel.dataset.busy=String(filtersBusy); updateDisabledStates(); } function commit() { config=saveEscortCustomFilters(readConfig()); for (const input of panel.querySelectorAll('.vm-custom-filter-number')) { input.value=String(config[input.dataset.filterKey]); } updateDisabledStates(); updateTrigger(); if (typeof onChange === 'function') onChange(config); } for (const control of panel.querySelectorAll('.vm-custom-filter-check,.vm-custom-filter-number,.vm-custom-filter-unit,.vm-custom-filter-day')) { control.addEventListener('change',commit); } for (const category of panel.querySelectorAll('.vm-custom-filter-category')) { category.addEventListener('change',() => { category.closest('.vm-custom-filter-row').querySelector('.vm-custom-filter-check').checked=true; commit(); }); } for (const time of panel.querySelectorAll('.vm-custom-filter-time')) { time.addEventListener('change',() => { time.value=normalizeCustomFilterTime(time.value); commit(); }); } let descriptionCommitTimer=null; for (const input of panel.querySelectorAll('.vm-custom-filter-text')) { input.addEventListener('input',() => { clearTimeout(descriptionCommitTimer); descriptionCommitTimer=setTimeout(commit,180); }); input.addEventListener('change',commit); } trigger.addEventListener('click',() => { if (filtersBusy) return; panel.hidden=!panel.hidden; config=saveEscortCustomFilters(readConfig()); updateTrigger(); }); priceDurationSelect?.addEventListener('change',() => { const previousDuration=SETTINGS.searchResultPriceDuration; const duration=normalizeEscortPriceDuration(priceDurationSelect.value); try { updateSettings({ searchResultPriceDuration:duration }); priceDurationSelect.title=getEscortPriceDurationLabel(duration); sortControl.refresh(); if (typeof onPriceDurationChange === 'function') { onPriceDurationChange(duration); } } catch (error) { priceDurationSelect.value=previousDuration; priceDurationSelect.title=getEscortPriceDurationLabel(previousDuration); log('Nie udało się zapisać czasu ceny na kafelkach',error); } }); clearBtn.addEventListener('click',() => { for (const checkbox of panel.querySelectorAll('.vm-custom-filter-check')) { checkbox.checked=false; } commit(); }); updateDisabledStates(); updateTrigger(); if (favoriteSearchEditSession) favoriteSearchEditReadFilters=readConfig; titleCol.insertAdjacentElement('afterend',control); titleRow.insertAdjacentElement('afterend',panel); panel.insertAdjacentElement('afterend',progress); titleRow.insertAdjacentElement('afterend',loadAllHint); return { getSortConfig:sortControl.getConfig, setListIncomplete:value => {listIncomplete=!!value; updateTrigger();}, getConfig:() => ({...config}), setStatus:text => {status.textContent=text || '';}, setProgress:(text,busy=!!text) => { const value=text || ''; const nextBusy=!!value && !!busy; if (progressText.textContent === value && progressBusy === nextBusy) return; progressText.textContent=value; progress.hidden=!value; progressBusy=nextBusy; progressCancelButton.hidden=!progressBusy || typeof progressCancelButton.onclick !== 'function'; setFiltersBusy(progressBusy); }, setCancelHandler:handler => { progressCancelButton.hidden=!progressBusy || typeof handler !== 'function'; progressCancelButton.onclick=typeof handler === 'function' ? handler :null; } }; } function getEscortListTotalResultCount(doc=document,context=null) { if (!doc) return null; const parseCount=value => { const text=String(value || '').replace(/\u00a0/g,' '); const patterns=[ /wyświetlane[\s\S]{0,160}?spośród\s+(\d[\d\s.]*)/i, /wyswietlane[\s\S]{0,160}?sposrod\s+(\d[\d\s.]*)/i, /displayed[\s\S]{0,160}?\b(?:from|of)\s+(\d[\d\s.,]*)/i, /(?:lista\s+wynik(?:ów|ow)|result\s+list)\s*:\s*(\d[\d\s.,]*)/i ]; for (const pattern of patterns) { const match=text.match(pattern); if (!match) continue; const count=Number(match[1].replace(/[\s.,]/g,'')); if (Number.isFinite(count) && count >= 0) return count; } return null; }; const resolvedContext=context || getEscortClubListContext(doc,location.href); const navText=resolvedContext?.adsNav?.textContent || ''; const fromNav=parseCount(navText); if (fromNav != null) return fromNav; for (const element of doc.querySelectorAll('p, .search-box + *, .search-box ~ *')) { const text=element.textContent || ''; if (!/lista\s+wynik|wyświetlane|wyswietlane|result\s+list|displayed/i.test(text)) continue; const count=parseCount(text); if (count != null) return count; } const fromBody=parseCount(doc.body?.textContent || ''); if (fromBody != null) return fromBody; const emptyResultsRoot=getEscortMainResultsSection(doc) || doc; for (const element of emptyResultsRoot.querySelectorAll('*')) { if (/^(?:brak\s+(?:anons(?:ów|ow)|ogłoszeń|ogloszen)|no\s+(?:ads|adverts|advertisements)(?:\s+found)?)[.!]?$/i.test( normalizeEscortAdText(element.textContent))) return 0; } return null; } function buildEscortSearchRequestForLocation(locationData) { const controls=findEscortSearchLocationControls(); const form=controls?.searchBox?.closest('form'); if (!form) throw new Error('Nie znaleziono formularza wyszukiwania Escort.club.'); const data=new FormData(form); const selected=normalizeDefaultSearchLocation(locationData); const setField=(name,value) => { if (data.has(name) || form.querySelector(`[name="${name}"]`)) { data.set(name,String(value || '')); } }; setField('country',selected.countryValue || '33'); setField('province',selected.provinceValue); setField('city',selected.cityValue); setField('district',selected.districtValue); for (const name of ['page','p']) data.delete(name); if (controls.searchButton?.name) { data.set(controls.searchButton.name,controls.searchButton.value || ''); } const method=String(form.method || 'GET').toUpperCase(); const action=new URL(form.action || location.href,location.href); const options={ method, credentials:'include', redirect:'follow', cache:'no-store' }; if (method === 'GET') { const params=new URLSearchParams(); for (const [key,value] of data.entries()) { if (typeof value === 'string') params.append(key,value); } action.search=params.toString(); } else if ((form.enctype || '').toLowerCase().includes('multipart/form-data')) { options.body=data; } else { const params=new URLSearchParams(); for (const [key,value] of data.entries()) { if (typeof value === 'string') params.append(key,value); } options.body=params; options.headers={'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'}; } return {url:action.href,options}; } function stripEscortListPageFromUrl(value) { const url=new URL(value,location.href); url.pathname=url.pathname .replace(/\/(?:page|strona)\d+\.html\/?$/i,'/') .replace(/\/(?:page|strona)-\d+\/?$/i,'/') .replace(/\/(?:page|strona)\/\d+\/?$/i,'/'); for (const key of ['page','p','strona']) url.searchParams.delete(key); return url; } function buildEscortListPageUrlFromTemplate(baseUrl,pageNumber,templateUrl=location.href) { const page=Math.max(1,Number(pageNumber) || 1); const base=stripEscortListPageFromUrl(baseUrl); if (page <= 1) return base.href; const template=new URL(templateUrl,location.href); const pathname=template.pathname; const rootPath=base.pathname.replace(/\/$/,''); const compact=pathname.match(/\/(page|strona)\d+\.html\/?$/i); if (compact) { base.pathname=`${rootPath}/${compact[1]}${page}.html`; return base.href; } const dashed=pathname.match(/\/(page|strona)-\d+\/?$/i); if (dashed) { base.pathname=`${rootPath}/${dashed[1]}-${page}/`; return base.href; } const nested=pathname.match(/\/(page|strona)\/\d+\/?$/i); if (nested) { base.pathname=`${rootPath}/${nested[1]}/${page}/`; return base.href; } for (const key of ['page','p','strona']) { if (!template.searchParams.has(key)) continue; base.searchParams.set(key,String(page)); return base.href; } return null; } async function fetchEscortListPage(url,options={},cancelToken=null,requestContext=null) { while (true) { try { requestContext ||= createRequestContext('results.pagination'); const controller=new AbortController(); let releaseSlot=() => {},timer=null,response=null,trace=null,outcome='error'; const observeResponse=(result,complete=true) => releaseSlot.observeResponse ? releaseSlot.observeResponse(result,complete):observeEscortRequestResponse(url,result); const unsubscribe=cancelToken?.onCancel(() => controller.abort()) || (() => {}); try { cancelToken?.throwIfCancelled(); releaseSlot=await acquireEscortRequestSlot(url,cancelToken,false,options.method,requestContext); cancelToken?.throwIfCancelled(); releaseSlot.beforeDispatch?.(); timer=setTimeout(() => controller.abort(),ESCORT_AD_DATA_FETCH_TIMEOUT_MS); trace=recordDiagnosticRequest(url,options.method || 'GET',requestContext,'fetch'); const pendingResponse=fetch(url,{ credentials:'include', redirect:'follow', cache:'no-store', ...options, signal:controller.signal }); releaseSlot.markStarted?.(); response=await pendingResponse; trace?.(response.status,'headers',response); outcome='response'; recordDiagnosticResponse(url,response.status); let html='',blockedError=null; try {observeResponse({status:response.status,headers:response.headers,finalUrl:response.url},false);} catch (error) {blockedError=error;} try {html=await response.text();} catch (error) { if (blockedError) throw blockedError; if (response.ok) throw error; } if (blockedError) throw refineEscortRequestBlockedError(url,{responseText:html},blockedError); observeResponse({status:response.status,headers:response.headers,responseText:html,finalUrl:response.url}); if (!response.ok) throw new Error(`HTTP ${response.status}`); const fetchedUrl=response.url || url; return { doc:new DOMParser().parseFromString(html,'text/html'), url:fetchedUrl }; } catch (error) { outcome=cancelToken?.cancelled ? 'cancelled':controller.signal.aborted ? 'timeout':response?.status >= 400 ? 'response':'error'; throw error; } finally { trace?.(response?.status,outcome,response); clearTimeout(timer); unsubscribe(); releaseSlot(); } } catch (error) { if (cancelToken?.cancelled && !isOperationCancelledError(error)) throw createOperationCancelledError(cancelToken.reason); if (error?.retryable && (error.status === 503 || error.code === 'ESCORT_QUEUE_RESCHEDULE') && !cancelToken?.cancelled) continue; throw error; } } } async function loadAdditionalEscortListPages(initialContext,cancelToken=null,onProgress=null,onLocationPage=null,onCardsAppended=null) { const requested=normalizeListPagesToShow(LIST_PAGES_TO_SHOW); const currentLocation=readEscortLocationFromControls(); const multiLocations=getEscortMultiLocationsForCurrentPage(); // Bez lokalizacji w formularzu zachowaj paginację bieżącej listy: użyj jedynej // aktywnej lokalizacji lub technicznego ID, którego nie wysyłamy do formularza. const currentPageFallbackLocation=currentLocation || (multiLocations.length === 1 ? multiLocations[0]:null) || normalizeDefaultSearchLocation({cityValue:'__vm_current_list__',cityLabel:'Bieżąca lista'}); const locations=multiLocations.length ? multiLocations :[currentPageFallbackLocation]; const locationCount=Math.max(1,locations.length); const requestContext=createRequestContext('results.pagination'); const locationAdIds=new Map(); const locationTotalAds=new Map(); const rememberLocationTotal=(locationData,doc,context=null) => { if (!locationData || !doc) return null; const key=getEscortLocationKey(locationData); if (!key) return null; const total=getEscortListTotalResultCount(doc,context); if (total == null) return null; locationTotalAds.set(key,total); setEscortMultiLocationResultCount(locationData,total,doc); return total; }; const rememberLocationAds=(locationData,context) => { if (!locationData || !context?.anchors) return 0; const key=getEscortLocationKey(locationData); if (!key) return 0; if (!locationAdIds.has(key)) locationAdIds.set(key,new Set()); const ids=locationAdIds.get(key); for (const anchor of context.anchors) { const adId=parseAdIdFromUrl(anchor.getAttribute('href')); if (adId) ids.add(adId); } return ids.size; }; const selectedLocationKeys=new Set( locations.map(getEscortLocationKey).filter(Boolean) ); const currentLocationKey=getEscortLocationKey(currentPageFallbackLocation); const initialPageBelongsToSelection=!!currentLocationKey && selectedLocationKeys.has(currentLocationKey); const foreignInitialContainer=locationCount > 1 && !initialPageBelongsToSelection ? getEscortListCardContainer(initialContext?.anchors || []) :null; if (locationCount > 1 && !initialPageBelongsToSelection) { const foreignCards=new Set( (initialContext?.anchors || []) .map(anchor => anchor.closest('.item-col.col')) .filter(Boolean) ); for (const card of foreignCards) detachHelperNode(card); } else if (currentPageFallbackLocation) { rememberLocationTotal(currentPageFallbackLocation,document,initialContext); if (initialContext?.anchors?.length) { rememberLocationAds(currentPageFallbackLocation,initialContext); } } if ( (requested === 1 && locationCount <= 1) || (!initialContext?.anchors?.length && locationCount <= 1) ) { return { loadedPages:1, allPagesLoaded:getEscortListPageNumber(location.href) === 1 && !findNextEscortListPageUrl(document,location.href), addedCards:0, duplicateCards:0, locationCount:1, cancelled:false, locationAdCounts:Object.fromEntries( [...locationAdIds].map(([key,ids]) => [key,ids.size]) ), locationTotalAds:Object.fromEntries(locationTotalAds) }; } const initialAnchors=initialPageBelongsToSelection ? (initialContext.anchors || []) :[]; let targetContainer=getEscortListCardContainer(initialAnchors) || foreignInitialContainer; const ensureTargetContainer=fetchedContext => { if (targetContainer?.isConnected) return targetContainer; if (!fetchedContext?.anchors?.length) return null; const sourceContainer=getEscortListCardContainer(fetchedContext.anchors); if (!sourceContainer) return null; const importedContainer=document.importNode(sourceContainer,false); const mainSection=getEscortMainResultsSection(document); const adsNav=initialContext?.adsNav; if (adsNav?.parentElement) { adsNav.insertAdjacentElement('beforebegin',importedContainer); } else if (mainSection) { mainSection.appendChild(importedContainer); } else { return null; } targetContainer=importedContainer; return targetContainer; }; const knownAdIds=new Set( initialAnchors .map(anchor => parseAdIdFromUrl(anchor.getAttribute('href'))) .filter(Boolean) ); const startPage=getEscortListPageNumber(location.href); const maxPagesPerLocation=requested === 'all' ? Number.POSITIVE_INFINITY :Number(requested); const finitePlannedPages=requested === 'all' ? null :maxPagesPerLocation * locationCount; const currentKey=initialPageBelongsToSelection ? currentLocationKey :''; let loadedPages=initialPageBelongsToSelection ? 1:0; let addedCards=0; let duplicateCards=0; let reachedEnd=false; let stoppedByError=false; let cancelled=false; const locationStats=new Map(); const completedLocations=new Set(); const locationEnds=new Map(); const ensureLocationEnds=destination => { if (locationEnds.size) return; const ordered=[ ...locations.filter(item => getEscortLocationKey(item) === currentKey), ...locations.filter(item => getEscortLocationKey(item) !== currentKey) ]; for (const item of ordered) { const end=document.createComment('vm-location-end'); destination.appendChild(end); locationEnds.set(getEscortLocationKey(item),end); } }; const report=(locationData,locationPages,phase='loading') => { if (typeof onProgress !== 'function') return; const key=getEscortLocationKey(locationData); try { onProgress({ loadedPages, totalPages:finitePlannedPages, locationCount, locationLabel:getSavedLocationButtonLabel(locationData), locationPages, pagesPerLocation:requested, locationAds:locationAdIds.get(key)?.size || 0, phase, addedCards, duplicateCards }); } catch {} }; const appendContextCards=async (fetchedContext,fetchedUrl,locationData) => { if (!fetchedContext?.anchors?.length) return 0; const destination=ensureTargetContainer(fetchedContext); if (!destination) { log('Nie znaleziono miejsca do wstawienia kafelków z pozostałych lokalizacji'); return 0; } rememberLocationAds(locationData,fetchedContext); ensureLocationEnds(destination); const end=locationEnds.get(getEscortLocationKey(locationData)); let fragment=document.createDocumentFragment(); let imported=0; let importedSinceYield=0; const importedCards=[]; try { for (const anchor of fetchedContext.anchors) { cancelToken?.throwIfCancelled(); const href=anchor.getAttribute('href'); const adId=parseAdIdFromUrl(href); const sourceCard=anchor.closest('.item-col.col'); if (!adId || !sourceCard) continue; if (knownAdIds.has(adId)) { duplicateCards++; continue; } const importedCard=prepareImportedEscortCard( document.importNode(sourceCard,true), fetchedUrl ); fragment.appendChild(importedCard); importedCards.push(importedCard); knownAdIds.add(adId); addedCards++; imported++; importedSinceYield++; if (importedSinceYield >= 8) { importedSinceYield=0; destination.insertBefore(fragment,end); fragment=document.createDocumentFragment(); await yieldHelperWork(); } } } finally { destination.insertBefore(fragment,end); if (importedCards.length) await onCardsAppended?.(importedCards); } return imported; }; const loadLocation=async locationData => { const locationKey=getEscortLocationKey(locationData); const isCurrent=!!currentKey && locationKey === currentKey; let currentDoc; let currentUrl; let locationPages=0; try { cancelToken?.throwIfCancelled(); if (isCurrent) { currentDoc=document; currentUrl=location.href; locationPages=1; rememberLocationTotal(locationData,currentDoc,initialContext); rememberLocationAds(locationData,initialContext); locationStats.set(locationKey,locationPages); report(locationData,locationPages); } else { const request=buildEscortSearchRequestForLocation(locationData); let first=await fetchEscortListPage(request.url,request.options,cancelToken,requestContext); onLocationPage?.(locationData,first.doc,first.url,getEscortClubListContext(first.doc,first.url)); if (startPage > 1) { const directPageUrl=buildEscortListPageUrlFromTemplate( first.url, startPage, location.href ); if (directPageUrl) { first=await fetchEscortListPage(directPageUrl,{method:'GET'},cancelToken,requestContext); } else { let page=1; while (page < startPage) { const next=findNextEscortListPageUrl(first.doc,first.url); if (!next) break; first=await fetchEscortListPage(next,{method:'GET'},cancelToken,requestContext); page++; } } } currentDoc=first.doc; currentUrl=first.url; const firstContext=getEscortClubListContext(currentDoc,currentUrl); rememberLocationTotal(locationData,currentDoc,firstContext); await appendContextCards(firstContext,currentUrl,locationData); locationPages=1; loadedPages++; locationStats.set(locationKey,locationPages); report(locationData,locationPages); if (!firstContext?.anchors?.length) { if (startPage === 1 && locationTotalAds.get(locationKey) === 0) completedLocations.add(locationKey); return; } } while (locationPages < maxPagesPerLocation) { cancelToken?.throwIfCancelled(); const nextUrl=findNextEscortListPageUrl(currentDoc,currentUrl); if (!nextUrl) { reachedEnd=true; break; } const next=await fetchEscortListPage(nextUrl,{method:'GET'},cancelToken,requestContext); const fetchedContext=getEscortClubListContext(next.doc,next.url); if (!fetchedContext?.anchors?.length) { reachedEnd=true; break; } await appendContextCards(fetchedContext,next.url,locationData); currentDoc=next.doc; currentUrl=next.url; locationPages++; loadedPages++; locationStats.set(locationKey,locationPages); report(locationData,locationPages); if (locationPages >= 500) { stoppedByError=true; log('Przerwano doładowywanie po 500 stronach jednej lokalizacji.'); break; } await yieldHelperWork(); } if (startPage === 1 && !findNextEscortListPageUrl(currentDoc,currentUrl)) completedLocations.add(locationKey); } catch (error) { if (isOperationCancelledError(error) || cancelToken?.cancelled || error?.name === 'AbortError') { cancelled=true; return; } stoppedByError=true; log(`Nie udało się pobrać listy dla lokalizacji ${getSavedLocationButtonLabel(locationData)}`,error); } }; const currentFirst=locations.find(item => getEscortLocationKey(item) === currentKey); const orderedLocations=[currentFirst,...locations.filter(item => getEscortLocationKey(item) !== currentKey)].filter(Boolean); await watchMapLimit(orderedLocations,3,loadLocation); for (const end of locationEnds.values()) detachHelperNode(end); if (cancelToken?.cancelled) cancelled=true; refreshEscortMultiLocationUi(); return { loadedPages, allPagesLoaded:!cancelled && !stoppedByError && completedLocations.size === locationCount, addedCards, duplicateCards, locationCount, reachedEnd, stoppedByError, cancelled, locationStats:Object.fromEntries(locationStats), locationAdCounts:Object.fromEntries( [...locationAdIds].map(([key,ids]) => [key,ids.size]) ), locationTotalAds:Object.fromEntries(locationTotalAds) }; } function showEscortListInitializationError(error) { ensureSettingsUiStyles(); const existing=document.getElementById('vm-escort-list-init-error'); if (existing) return; const message=makeElement('div',"vm-escort-list-init-error",'Nie udało się uruchomić dodatków skryptu na tej liście anonsów. Odśwież stronę; jeśli problem się powtórzy, sprawdź konsolę przeglądarki.',{id:'vm-escort-list-init-error'}); if (error?.message) message.title=String(error.message); const heading=findEscortMainResultsHeading(); const host=heading?.parentElement || document.querySelector('main') || document.body; if (heading?.parentElement) heading.insertAdjacentElement('afterend',message); else host?.prepend(message); } function initHiddenResultCards() { if (document.documentElement.dataset.vmHiddenResults) return; document.documentElement.dataset.vmHiddenResults='1'; ensureHiddenItemsStyles(); const refresh=() => { for (const anchor of getEscortClubListContext()?.anchors || []) { const id=parseAdIdFromUrl(anchor.href); const card=anchor.closest('.item-col.col'); if (card && !card.dataset.vmHiddenManaged) card.classList.toggle('vm-hidden-result',getHiddenAdMatches(id,getHiddenResultProfileUrls(getListCache(id))).length > 0); } }; hiddenUiRefreshers.add(refresh); const schedule=createAnimationFrameScheduler(refresh); new MutationObserver(mutations => { if (mutations.some(mutation => [...mutation.addedNodes].some(node => node.nodeType === 1 && (node.closest('.item-col:not([data-vm-hidden-managed])') || node.querySelector('.item-col:not([data-vm-hidden-managed])'))))) schedule(); }).observe(document.documentElement,{childList:true,subtree:true}); refresh(); } function getActiveEscortCustomFilterCount(filters) { return Object.entries(DEFAULT_ESCORT_CUSTOM_FILTERS).filter(([key,fallback]) => typeof fallback === 'boolean' && key !== 'expanded' && filters[key] && (!['hideCity','hideDescriptionMatch','showOnlyDescriptionMatch'].includes(key) || normalizeCustomFilterQuery(filters[key.replace(/Match$/,'') + 'Query']))).length; } function getCustomFilterDataNeeds(filters) { const source=filters || {}; return { escorti:!!( source.hideWithoutOpinions || source.hideProfileAdsOver || source.hideLocationChangesOver || source.hideYoungerThan || source.hideOlderThan || source.hideHasNote || source.hideHasCategory || source.showOnlyHasNote || source.showOnlyHasCategory || source.showOnlyWithoutOpinions || source.showOnlySingleProfileAd ), escortAd:!!( source.hideAlwaysAvailable || source.hideAdYoungerThan || source.hideAdOlderThan || source.hideTagCountOver || source.hideAgencyPhone || source.hideUnavailableAt || source.hideAvailableAt || ( source.hideDescriptionMatch && normalizeCustomFilterQuery(source.hideDescriptionQuery) ) || source.showOnlyUnavailableAt || source.showOnlyAvailableAt || source.showOnlyAgencyPhone || source.showOnlyBmi || ( source.showOnlyDescriptionMatch && normalizeCustomFilterQuery(source.showOnlyDescriptionQuery) ) ), agencyPhone:!!( source.hideAgencyPhone || source.showOnlyAgencyPhone ) }; } function customFilterAgeToDays(value,unit) { const multipliers={ days:1, weeks:7, months:30.4375, years:365.25 }; return Math.max(1,Number(value) || 1) * (multipliers[unit] || 1); } function normalizeCustomFilterWeekdayToken(value) { return normalizeText(value) .replace(/:\s*$/,'') .normalize('NFD') .replace(/[\u0300-\u036f]/g,'') .replace(/ł/g,'l'); } function normalizeCustomFilterSearchText(value) { return normalizeText(value) .normalize('NFD') .replace(/[\u0300-\u036f]/g,'') .replace(/ł/g,'l'); } function customFilterDescriptionContains(description,query) { const needle=normalizeCustomFilterSearchText(query); return !!needle && normalizeCustomFilterSearchText(description).includes(needle); } function customFilterCityMatches(city,query) { const needle=normalizeCustomFilterSearchText(query); return !!needle && normalizeCustomFilterSearchText(city) === needle; } function customFilterTimeToMinutes(value) { const normalized=normalizeCustomFilterTime(value,''); if (!normalized) return null; const [hours,minutes]=normalized.split(':').map(Number); return hours * 60 + minutes; } function customFilterIsAlwaysAvailable(value) { const normalized=normalizeCustomFilterWeekdayToken(value); return normalized === 'caly czas' || normalized === 'cala dobe' || normalized === '24h' || normalized === '24 h'; } function customFilterIsExplicitlyUnavailable(value) { return normalizeCustomFilterWeekdayToken(value).includes('niedostepn'); } function customFilterHasAlwaysAvailable(availability) { return !!availability && typeof availability === 'object' && Object.values(availability).some(customFilterIsAlwaysAvailable); } function customFilterGetDayAvailability(availability,weekday) { if (!availability || typeof availability !== 'object') return null; const label=ESCORT_CUSTOM_FILTER_WEEKDAYS .find(([value]) => value === normalizeCustomFilterWeekday(weekday))?.[1]; const wanted=normalizeCustomFilterWeekdayToken(label); for (const [day,hours] of Object.entries(availability)) { if (normalizeCustomFilterWeekdayToken(day) === wanted) return String(hours || ''); } return null; } function customFilterGetAvailabilityAt(availability,weekday,time) { const hoursText=customFilterGetDayAvailability(availability,weekday); if (!hoursText) return null; if (customFilterIsAlwaysAvailable(hoursText)) return true; if (customFilterIsExplicitlyUnavailable(hoursText)) return false; const selectedMinutes=customFilterTimeToMinutes(time); if (selectedMinutes == null) return null; const ranges=[...hoursText.matchAll( /(\d{1,2})[.:](\d{2})\s*(?:do|to|[-–\u2014])\s*(\d{1,2})[.:](\d{2})/gi )]; if (!ranges.length) return null; return ranges.some(match => { const start=Number(match[1]) * 60 + Number(match[2]); const end=Number(match[3]) * 60 + Number(match[4]); if (start === end) return true; if (start < end) return selectedMinutes >= start && selectedMinutes <= end; return selectedMinutes >= start || selectedMinutes <= end; }); } function customFilterIsUnavailableAt(availability,weekday,time) { const available=customFilterGetAvailabilityAt(availability,weekday,time); return available == null ? null:!available; } function buildCustomFilterWatchMetadataIndex() { const byAdId=new Map(); const byProfileId=new Map(); const add=( map, key, item ) => { const normalizedKey= String(key || '').trim(); if (!normalizedKey) return; if (!map.has(normalizedKey)) { map.set( normalizedKey, new Set() ); } map.get(normalizedKey).add(item); }; for (const item of getWatchedItems()) { const categories= normalizeFavoriteAssignedCategories( item.categories ); if (!item.favorite && !item.note && !categories.length) { continue; } for (const url of getWatchedItemAdUrls( item, false )) { const adId= parseAdIdFromUrl(url); if (adId) { add( byAdId, adId, item ); } } for (const url of item.profileUrls || []) { const profileId= getEscortiProfileId(url); if (profileId) { add( byProfileId, profileId, item ); } } const watchProfiles= String(item.watchKey || '') .match(/^profiles:(.+)$/i)?.[1] ?.split(',') .map(value => value.trim()) .filter(value => /^\d+$/.test(value)) || []; for (const profileId of watchProfiles) { add( byProfileId, profileId, item ); } } return { byAdId, byProfileId }; } function getBucketWatchMetadata( bucket, index ) { const matches=new Set(); const collect=( map, key ) => { const values= map.get(String(key || '')); if (!values) return; for (const item of values) { matches.add(item); } }; for (const member of bucket.members) { if (member.adId) { collect( index.byAdId, member.adId ); } const result=member.result; const profileUrls=[ ...(Array.isArray( result?.profileUrls ) ? result.profileUrls :[]), result?.profileUrl, ...(Array.isArray( result?.profileSummaries ) ? result.profileSummaries .map(profile => profile?.profileUrl ) :[]) ].filter(Boolean); for (const url of profileUrls) { const profileId= getEscortiProfileId(url); if (profileId) { collect( index.byProfileId, profileId ); } } } const categories=new Map(); let hasNote=false; for (const item of matches) { if (String(item.note || '').trim()) { hasNote=true; } for (const category of normalizeFavoriteAssignedCategories( item.categories )) { const key= category.toLocaleLowerCase( 'pl-PL' ); if (!categories.has(key)) { categories.set( key, category ); } } } return { hasNote, favorite:[...matches].some(hasFavoriteStar), categories: [...categories.values()] }; } function customFilterCategoryMatches( categories, wanted ) { const list= normalizeFavoriteAssignedCategories( categories ); if (!list.length) return false; const target= normalizeCustomFilterCategory( wanted ); if (!target) return true; const targetKey= target.toLocaleLowerCase( 'pl-PL' ); return list.some( category => category.toLocaleLowerCase( 'pl-PL' ) === targetKey ); } function countEscortiProfileLocationChanges( profile ) { if (!profile || typeof profile !== 'object') { return null; } const history=( Array.isArray(profile.cityHistory) ? profile.cityHistory :[] ) .map(entry => ({ city: normalizeEscortCity( entry?.city ), time: profileDateToTime( entry?.date ) })) .filter(entry => entry.city) .sort((a,b) => (a.time || 0) - (b.time || 0) ); const cities= history.map(entry => entry.city); const currentCity= normalizeEscortCity( profile.currentCity ); if ( currentCity && ( !cities.length || normalizeEscortCityKey( cities[cities.length - 1] ) !== normalizeEscortCityKey( currentCity ) ) ) { cities.push(currentCity); } if (!cities.length) return 0; let changes=0; let previousKey=''; for (const city of cities) { const key= normalizeEscortCityKey(city); if (!key) continue; if ( previousKey && key !== previousKey ) { changes++; } previousKey=key; } return changes; } function getEscortiResultLocationChangeCount( result ) { if ( !result || result.status !== 'ok' ) { return null; } if (Number(result.profiles) === 0) { return 0; } const profiles= getEscortiProfileSummariesForResearch( result ); if (!profiles.length) { return null; } let total=0; let found=false; for (const profile of profiles) { const count= countEscortiProfileLocationChanges( profile ); if (count == null) continue; total += count; found=true; } return found ? total :null; } function collectBucketMemberRecords(bucket,extractor) { const records=[]; let unresolved=false; for (const member of bucket.members) { const extracted=member.adData?.status === 'ok' ? extractor(member.adData) || {}:{unresolved:true}; if (extracted.unresolved) unresolved=true; if (extracted.hasValue) records.push(extracted.value); } return {records,unresolved}; } function getBucketAvailabilityRecords(bucket) { return collectBucketMemberRecords(bucket,data => ({hasValue:true, value:data.availability && typeof data.availability === 'object' ? data.availability:{}})); } function getBucketDescriptionRecords(bucket) { return collectBucketMemberRecords(bucket,data => ({hasValue:true,value:String(data.description || '')})); } function getBucketAdAgeRecords(bucket) { const now=Date.now(); return collectBucketMemberRecords(bucket,data => { const datePostedTime=profileDateToTime(data.datePosted); return datePostedTime == null ? {unresolved:true} :{hasValue:true,value:Math.max(0,(now - datePostedTime) / (24 * 60 * 60 * 1000))}; }); } function getBucketTagCountRecords(bucket) { return collectBucketMemberRecords(bucket,data => Array.isArray(data.tags) ? {hasValue:true,value:data.tags.length}:{unresolved:true}); } function getBucketLastVisitedAt(bucket,adIds=[]) { let latest=0; const members=[...bucket.members].filter(member => !member.hiddenInResults); const ids=new Set([...members.map(member => member.adId),...adIds]); const visits=members.map(member => member.adData?.lastVisitedAt); for (const id of ids) visits.push(getStoredEscortAdData(id)?.lastVisitedAt); for (const value of visits) { const visitedAt=Number(value); if (Number.isFinite(visitedAt) && visitedAt > latest) latest=visitedAt; } return latest || null; } function getBucketBmiRecords(bucket) { return collectBucketMemberRecords(bucket,data => { const bmi=getDeclaredEscortBmi(data); return bmi == null ? {}:{hasValue:true,value:bmi}; }); } function getBucketAgencyPhoneRecords(bucket) { return collectBucketMemberRecords(bucket,data => typeof data.isAgencyOrSalonPhone === 'boolean' ? {hasValue:true,value:data.isAgencyOrSalonPhone}:{unresolved:true}); } function resolveBucketRecordMatch(bucketRecords,predicate) { let unresolved=bucketRecords.unresolved; for (const record of bucketRecords.records) { const result=predicate(record); if (result === true) return true; if (result == null) unresolved=true; } return unresolved ? null:false; } function getEscortBucketActiveAdCount(bucket,{phoneBasedAggregation,escortClubPhoneSearchAggregation,phoneCacheIndex,cacheIndex,phoneSearchAggregationIndex,getGroupAggregateAdIds}) { const ids=bucket.group ? getGroupAggregateAdIds(bucket.group) :new Set(bucket.members.map(member => member.adId).filter(Boolean)); for (const member of bucket.members) { const aggregationKeys=phoneBasedAggregation ? (getEscortAdPhoneDigits(member.adData) ? [`phone:${getEscortAdPhoneDigits(member.adData)}`]:[]) :getEscortProfileKeys(member.result); for (const aggregationKey of aggregationKeys) { const cachedIds=phoneBasedAggregation ? phoneCacheIndex.phoneToAdIds.get( aggregationKey.replace(/^phone:/,'') ) :cacheIndex.freshProfileToAdIds.get(aggregationKey); for (const adId of cachedIds || []) ids.add(adId); if (escortClubPhoneSearchAggregation) { const searchedIds=phoneSearchAggregationIndex.get( aggregationKey.replace(/^phone:/,'') )?.adIds; for (const adId of searchedIds || []) ids.add(adId); } } } return ids.size || null; } function getBucketCustomFilterMetrics( bucket, filters, watchMetadataIndex, activeAdCount, relatedAdIds ) { const results=bucket.members .map(member => member.result) .filter(result => result && result.status === 'ok'); let opinions=null; let profileAds=null; let locationChanges=null; let creationTime=null; for (const result of results) { const opinionCount=typeof result.garsoTopics === 'number' ? result.garsoTopics :(result.profiles === 0 ? 0:null); if (opinionCount != null) { opinions=opinions == null ? opinionCount:Math.max(opinions,opinionCount); } const adCount=typeof result.adLinks === 'number' ? result.adLinks :(result.profiles === 0 ? 0:null); if (adCount != null) { profileAds=profileAds == null ? adCount:Math.max(profileAds,adCount); } const resultLocationChanges= getEscortiResultLocationChangeCount( result ); if ( resultLocationChanges != null ) { locationChanges= locationChanges == null ? resultLocationChanges :Math.max( locationChanges, resultLocationChanges ); } const time=profileDateToTime(result.creationDate); if (time != null && (creationTime == null || time < creationTime)) { creationTime=time; } } const availability=getBucketAvailabilityRecords(bucket); const descriptions=getBucketDescriptionRecords(bucket); const adAges=getBucketAdAgeRecords(bucket); const tagCounts=getBucketTagCountRecords(bucket); const agencyPhones= getBucketAgencyPhoneRecords(bucket); const bmiValues= getBucketBmiRecords(bucket); const lastVisitedAt= getBucketLastVisitedAt(bucket,relatedAdIds); const watchMetadata= getBucketWatchMetadata( bucket, watchMetadataIndex ); const visitAgeDays=lastVisitedAt ? Math.max(0,(Date.now() - lastVisitedAt) / (24 * 60 * 60 * 1000)) :null; return { opinions, profileAds, locationChanges, activeAds:activeAdCount, hasNote: watchMetadata.hasNote, hasCategoryHide: customFilterCategoryMatches( watchMetadata.categories, filters.hideCategory ), hasCategoryShowOnly: customFilterCategoryMatches( watchMetadata.categories, filters.showOnlyCategory ), visited:!!lastVisitedAt, visitAgeDays, tagCountOver:resolveBucketRecordMatch( tagCounts, value => value > normalizeCustomFilterInteger(filters.tagCountLimit,SETTINGS.alarmingTagCountLimit,0,10000) ), cityMatchesHide:bucket.members.some(member => customFilterCityMatches( getEscortListCardAgeAndCity(member?.card).city, filters.hideCityQuery ) ), agencyPhone:resolveBucketRecordMatch( agencyPhones, value => value === true ), bmiInRange:resolveBucketRecordMatch( bmiValues, value => { const first= normalizeCustomFilterNumber( filters.showOnlyBmiMin, 0, 0, 100 ); const second= normalizeCustomFilterNumber( filters.showOnlyBmiMax, 100, 0, 100 ); const min= Math.min(first,second); const max= Math.max(first,second); return value >= min && value <= max; } ), alwaysAvailable:resolveBucketRecordMatch( availability, customFilterHasAlwaysAvailable ), unavailableAtHideTime:resolveBucketRecordMatch( availability, value => customFilterIsUnavailableAt( value, filters.hideUnavailableDay, filters.hideUnavailableTime ) ), availableAtHideAvailableTime:resolveBucketRecordMatch( availability, value => customFilterGetAvailabilityAt( value, filters.hideAvailableDay, filters.hideAvailableTime ) ), unavailableAtShowOnlyTime:resolveBucketRecordMatch( availability, value => customFilterIsUnavailableAt( value, filters.showOnlyUnavailableDay, filters.showOnlyUnavailableTime ) ), availableAtShowOnlyTime:resolveBucketRecordMatch( availability, value => customFilterGetAvailabilityAt( value, filters.showOnlyAvailableDay, filters.showOnlyAvailableTime ) ), descriptionMatchesHide:resolveBucketRecordMatch( descriptions, value => customFilterDescriptionContains( value, filters.hideDescriptionQuery ) ), descriptionMatchesShowOnly:resolveBucketRecordMatch( descriptions, value => customFilterDescriptionContains( value, filters.showOnlyDescriptionQuery ) ), adYoungerThan:resolveBucketRecordMatch( adAges, value => value < customFilterAgeToDays( filters.adYoungerValue, filters.adYoungerUnit ) ), adOlderThan:resolveBucketRecordMatch( adAges, value => value > customFilterAgeToDays( filters.adOlderValue, filters.adOlderUnit ) ), ageDays:creationTime == null ? null :Math.max(0,(Date.now() - creationTime) / (24 * 60 * 60 * 1000)) }; } function evaluateEscortCustomFilters(metrics,filters) { let shouldHide=false; let unresolved=false; const test=(enabled,value,predicate) => { if (!enabled) return; if (value == null) { unresolved=true; return; } if (predicate(value)) shouldHide=true; }; test(filters.hideWithoutOpinions,metrics.opinions,value => value === 0); test( filters.hideProfileAdsOver, metrics.profileAds, value => value > normalizeCustomFilterInteger(filters.profileAdsLimit,10) ); test( filters.hideLocationChangesOver, metrics.locationChanges, value => value > normalizeCustomFilterInteger( filters.locationChangesLimit, 1, 0, 100000 ) ); test( filters.hideActiveAdsOver, metrics.activeAds, value => value > normalizeCustomFilterInteger(filters.activeAdsLimit,1) ); test( filters.hideAgencyPhone, metrics.agencyPhone, value => value === true ); test( filters.hideCity && !!normalizeCustomFilterQuery(filters.hideCityQuery), metrics.cityMatchesHide, value => value === true ); if (filters.hideVisited && metrics.visited) shouldHide=true; if (filters.hideUnvisited && !metrics.visited) shouldHide=true; if (filters.hideVisitedOlderThan && metrics.visited && metrics.visitAgeDays > customFilterAgeToDays(filters.hideVisitedOlderValue,filters.hideVisitedOlderUnit)) shouldHide=true; if (filters.hideVisitedRecentThan && metrics.visited && metrics.visitAgeDays < customFilterAgeToDays(filters.hideVisitedRecentValue,filters.hideVisitedRecentUnit)) shouldHide=true; if ( filters.hideHasNote && metrics.hasNote ) { shouldHide=true; } test( filters.hideHasCategory, metrics.hasCategoryHide, value => value === true ); test(filters.hideTagCountOver,metrics.tagCountOver,value => value === true); test( filters.hideYoungerThan, metrics.ageDays, value => value < customFilterAgeToDays(filters.youngerValue,filters.youngerUnit) ); test( filters.hideOlderThan, metrics.ageDays, value => value > customFilterAgeToDays(filters.olderValue,filters.olderUnit) ); test( filters.hideAdYoungerThan, metrics.adYoungerThan, value => value === true ); test( filters.hideAdOlderThan, metrics.adOlderThan, value => value === true ); test( filters.hideAlwaysAvailable, metrics.alwaysAvailable, value => value === true ); test( filters.hideUnavailableAt, metrics.unavailableAtHideTime, value => value === true ); test( filters.hideAvailableAt, metrics.availableAtHideAvailableTime, value => value === true ); test( filters.hideDescriptionMatch && !!normalizeCustomFilterQuery(filters.hideDescriptionQuery), metrics.descriptionMatchesHide, value => value === true ); test( filters.showOnlyWithoutOpinions, metrics.opinions, value => value !== 0 ); test( filters.showOnlySingleProfileAd, metrics.profileAds, value => value !== 1 ); test( filters.showOnlyAgencyPhone, metrics.agencyPhone, value => value === false ); test( filters.showOnlyBmi, metrics.bmiInRange, value => value === false ); if ( filters.showOnlyHasNote && !metrics.hasNote ) { shouldHide=true; } test( filters.showOnlyHasCategory, metrics.hasCategoryShowOnly, value => value === false ); if ( filters.showOnlyVisitedOlderThan && ( !metrics.visited || metrics.visitAgeDays <= customFilterAgeToDays( filters.showOnlyVisitedOlderValue, filters.showOnlyVisitedOlderUnit ) ) ) shouldHide=true; if (filters.showOnlyVisitedRecentThan && (!metrics.visited || metrics.visitAgeDays >= customFilterAgeToDays(filters.showOnlyVisitedRecentValue,filters.showOnlyVisitedRecentUnit))) shouldHide=true; test( filters.showOnlyUnavailableAt, metrics.unavailableAtShowOnlyTime, value => value === false ); test( filters.showOnlyAvailableAt, metrics.availableAtShowOnlyTime, value => value === false ); test( filters.showOnlyDescriptionMatch && !!normalizeCustomFilterQuery(filters.showOnlyDescriptionQuery), metrics.descriptionMatchesShowOnly, value => value === false ); return {shouldHide,unresolved}; } function normalizeFavoriteSearch(value,onError=null) { if (!value || !Array.isArray(value.requests) || !value.requests.length) return null; try { const requests=value.requests.map(request => { const url=stripEscortListPageFromUrl(request.url); if (url.protocol !== 'https:' || !isEscortClubHost(url.hostname) || url.username || url.password || !(isEscortSearchPath(url.pathname) || url.pathname.startsWith('/anonse/') || url.pathname === '/searchgate.php' || url.pathname === '/')) throw new Error(`Nieobsługiwany adres formularza: ${url.origin}${url.pathname}`); url.hash=''; url.searchParams.sort(); const method=(request.method || request.options?.method) === 'POST' ? 'POST':'GET'; const body=new URLSearchParams(method === 'POST' ? (request.body ?? request.options?.body) || '':''); body.delete('page'); body.delete('p'); body.sort(); return {url:url.href,method,body:body.toString()}; }).sort((a,b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); return { requests:requests.filter((request,index) => !index || JSON.stringify(request) !== JSON.stringify(requests[index - 1])), filters:normalizeEscortCustomFilters(value.filters), listSearchMode:value.listSearchMode === 'phone-escorti' ? 'phone-escorti':'address-escorti', merge:!!value.merge, aggregationMode:normalizeEscortAggregationMode(value.aggregationMode), locations:normalizeSearchLocations(value.locations) }; } catch (error) {onError?.(error); return null;} } function getFavoriteSearchSession() {return favoriteSearchEditSession || favoriteSearchViewSession;} function persistFavoriteSearchSession() { const session=getFavoriteSearchSession(); if (!session) return; if (favoriteSearchEditSession && window.top !== window.self && JSON.parse(sessionStorage.getItem(FAVORITE_SEARCH_EDIT_KEY) || 'null')?.frameToken !== session.frameToken) return; sessionStorage.setItem(favoriteSearchEditSession ? FAVORITE_SEARCH_EDIT_KEY:FAVORITE_SEARCH_VIEW_KEY,JSON.stringify(session)); } function applyFavoriteSearchPageSettings() { const search=getFavoriteSearchSession()?.search; if (search) Object.assign(SETTINGS,{mergeEscortAds:search.merge,escortAggregationMode:search.aggregationMode, ...(search.requests.length > 1 ? {multiLocationEnabled:true}:{})}); } function getFavoriteSearchViewUrl(item) { const request=item.search?.requests?.[0]; if (!request) return ''; const url=new URL(request.method === 'POST' ? '/':request.url,request.url); url.hash=new URLSearchParams({[FAVORITE_SEARCH_VIEW_KEY]:item.id}).toString(); return url.href; } function initFavoriteSearchView() { if (!IS_ESCORT_CLUB || window.top !== window.self) return false; const url=new URL(location.href),hash=new URLSearchParams(url.hash.slice(1)),itemId=hash.get(FAVORITE_SEARCH_VIEW_KEY); try { if (itemId) sessionStorage.removeItem(FAVORITE_SEARCH_EDIT_KEY); else if (url.searchParams.has(FAVORITE_SEARCH_EDIT_PARAM) || sessionStorage.getItem(FAVORITE_SEARCH_EDIT_KEY)) return false; const session=itemId ? {search:getWatchedItems().find(item => item.id === itemId && item.sourceType === 'escort-search')?.search} :JSON.parse(sessionStorage.getItem(FAVORITE_SEARCH_VIEW_KEY) || 'null'); if (!session) return false; if (!itemId && !isEscortSearchPath(url.pathname) && url.pathname !== '/searchgate.php' && !(url.pathname === '/' && url.search) && !session.pending) { if (url.pathname === '/') sessionStorage.removeItem(FAVORITE_SEARCH_VIEW_KEY); return false; } session.search=normalizeFavoriteSearch(session.search); if (!session.search) throw new Error('Nie znaleziono zapisanego wyszukiwania. Otwórz je ponownie z Ulubionych.'); favoriteSearchViewSession=session; applyFavoriteSearchPageSettings(); if (itemId) { saveEscortActiveMultiLocations(getFavoriteSearchEditLocations(session.search)); hash.delete(FAVORITE_SEARCH_VIEW_KEY); url.hash=hash.toString(); history.replaceState(history.state,'',url.href); } session.pending=!!itemId && session.search.requests[0].method === 'POST'; persistFavoriteSearchSession(); if (session.pending) {navigateFavoriteSearchRequest(session.search.requests[0]); return true;} } catch (error) { favoriteSearchViewSession=null; sessionStorage.removeItem(FAVORITE_SEARCH_VIEW_KEY); showWatchToast(error?.message || 'Nie udało się otworzyć wyszukiwania.',true); } return false; } function makeFavoriteSearchEditButton(item,compact=false) { const button=makeButton((compact ? 'vm-favorites-icon-button vm-ui-button vm-ui-icon vm-ui-compact' :'vm-escort-inline-category-add vm-tracked-entry-action') + ' vm-favorite-search-edit-button','✎',{title:`Edytuj wyszukiwanie: ${item.displayName}`}); button.setAttribute('aria-label',button.title); button.addEventListener('click',event => { stopUiEvent(event); try {openFavoriteSearchEdit(item.id);} catch (error) {showWatchToast(error?.message || 'Nie udało się otworzyć edycji.',true);} }); return button; } function openFavoriteSearchEdit(itemId) { const item=getWatchedItems().find(entry => entry.id === itemId && entry.sourceType === 'escort-search'); const search=normalizeFavoriteSearch(item?.search); if (!search) throw new Error('Nie znaleziono zapisanego wyszukiwania.'); const now=Date.now(),prefix=FAVORITE_SEARCH_EDIT_KEY + ':'; for (const key of GM_listValues().filter(key => key.startsWith(prefix))) { if (!(GM_getValue(key,null)?.expiresAt > now)) deleteHelperStorageValue(key); } const token=`${now}-${Math.random().toString(36).slice(2)}`,key=prefix + token; writeHelperStorageValue(key,{itemId,search,baseline:favoriteSearchKey(search),closeKey:key,expiresAt:now + 600000}); const url=new URL('/',search.requests[0].url); url.searchParams.set(FAVORITE_SEARCH_EDIT_PARAM,token); let tab,listener; const cleanup=() => {if (listener != null) {GM_removeValueChangeListener(listener); listener=null;} deleteHelperStorageValue(key);}; try { listener=GM_addValueChangeListener(key,(_key,_old,value) => { if (value?.finished) {try {tab?.close?.();} finally {cleanup();}} }); tab=GM_openInTab(url.href,{active:true,insert:true,setParent:true}); tab.onclose=cleanup; } catch (error) {cleanup(); throw error;} } function navigateFavoriteSearchRequest(request,frame) { if (request.method !== 'POST') {if (frame) frame.src=request.url; else location.replace(request.url); return;} const form=makeElement('form','',null,{method:'POST',action:request.url,target:frame?.name || '_self',hidden:true}); for (const [name,value] of new URLSearchParams(request.body)) { const input=makeElement('input','',null,{type:'hidden',name,value}); form.appendChild(input); } document.body.appendChild(form); HTMLFormElement.prototype.submit.call(form); detachHelperNode(form); } function getFavoriteSearchEditLocations(search) { if (search.requests.length < 2) return []; return normalizeSearchLocations(search.requests.map(request => { const params=new URLSearchParams(new URL(request.url).search); for (const [key,value] of new URLSearchParams(request.body)) params.set(key,value); const selected={countryValue:params.get('country') || '33',provinceValue:params.get('province') || '', cityValue:params.get('city') || '',districtValue:params.get('district') || ''}; const saved=search.locations?.find(value => getEscortLocationKey(value) === getEscortLocationKey(selected)); return resolveEscortLocationLabels({...saved,...selected}); })); } function updateFavoriteSearchEditLocation(doc=null) { if (!favoriteSearchEditSession) return; const active=getEscortActiveMultiLocations(); if (active.length < 2) return; const current=readEscortLocationFromControls(doc ? findEscortSearchLocationControls(doc):undefined); if (!current) {if (!doc) saveEscortActiveMultiLocations([]); return;} const previous=doc ? current:favoriteSearchEditSession.currentLocation; if (!previous || (!doc && getEscortLocationKey(previous) === getEscortLocationKey(current))) return; saveEscortActiveMultiLocations(active.map(value => getEscortLocationKey(value) === getEscortLocationKey(previous) ? current:value),!doc); if (doc) refreshEscortMultiLocationUi(); else {favoriteSearchEditSession.currentLocation=current; persistFavoriteSearchSession();} } function captureFavoriteSearchEdit() { updateFavoriteSearchEditLocation(); const locations=getEscortMultiLocationsForCurrentPage(); const requests=(locations.length ? locations:[readEscortLocationFromControls() || {}]).map(buildEscortSearchRequestForLocation); const search=normalizeFavoriteSearch({...favoriteSearchEditSession.search,requests,locations, filters:favoriteSearchEditReadFilters ? favoriteSearchEditReadFilters():getEscortCustomFilters()}); if (!search) throw new Error('Nie udało się odczytać parametrów formularza wyszukiwania.'); return search; } function getFavoriteSearchLocationScope(request,doc,search=null) { const url=new URL(request.url),params=new URLSearchParams(url.search); if (request.method === 'POST') for (const [key,value] of new URLSearchParams(request.body)) params.set(key,value); const controls=findEscortSearchLocationControls(doc),scope={}; const selected=Object.fromEntries(['country','province','city','district'].map(key => [key + 'Value',params.get(key) || ''])); const saved=search?.locations?.find(value => getEscortLocationKey(value) === getEscortLocationKey(selected)); const labels=resolveEscortLocationLabels({...saved,...selected},doc); for (const key of ['country','province','city','district']) { const value=params.get(key); if (!value || value === '0') continue; const option=findEscortLocationOption(controls?.[key] || doc.querySelector(`select[name="${key}"]`),value)?.textContent?.trim(); const label=option && option !== value ? option:((params.get('country') || '33') === '33' ? labels[key + 'Label']:''); scope[key]=key === 'country' && value === '33' ? 'polska':label ? normalizeEscortCityKey(label):null; if (!scope[key]) throw new Error('Nie odczytano lokalizacji wyszukiwania. Otwórz „Edytuj” i zapisz wyszukiwanie ponownie.'); } const cityPath=url.pathname.match(/^\/(?:anonse\/[^/]+|personal-ads)\/([^/]+)\/?$/i)?.[1]; if (cityPath && !('city' in scope)) { if (cityPath.toLowerCase() === 'poland') {if (!('country' in scope)) scope.country='polska';} else {try {scope.cityPath=decodeURIComponent(cityPath);} catch {scope.cityPath=null;}} } return scope; } function matchesFavoriteSearchLocation(adData,scope) { const location=adData?.location; const slug=value=>normalizeEscortCityKey(value).replace(/ł/g,'l').replace(/[^\p{L}\p{N}]+/gu,'-').replace(/^-|-$/g,''); return !!location && Object.entries(scope).every(([key,value])=>value && (key === 'cityPath' ? slug(location.city) === slug(value):normalizeEscortCityKey(location[key]) === value)); } async function* iterateFavoriteSearchPages(search,cancelToken=null,onPage=null,requestContext=null) { let page=0; for (const request of search.requests) { let url=request.url,slots=0,locationScope=null; const seen=new Set(); while (url) { cancelToken?.throwIfCancelled(); if (seen.has(url)) throw new Error('Nie udało się odczytać całego wyszukiwania. Zawęź wyniki filtrami i spróbuj ponownie.'); seen.add(url); page++; onPage?.(page); const pageContext=createRequestContext('watch.search-pages',requestContext); const {doc,finalUrl}=await fetchWatchDocumentDirect(url,cancelToken,{ ...(seen.size === 1 ? {method:request.method,...(request.method === 'POST' ? {data:request.body, headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'}}:{})}:{}), requestContext:pageContext }); if (!isEscortClubHost(new URL(finalUrl).hostname)) throw new Error('Wyszukiwanie przekierowało poza Escort.club.'); const context=getEscortClubListContext(doc,finalUrl),total=getEscortListTotalResultCount(doc,context); if (!context?.anchors?.length && total !== 0) throw new Error('Nie rozpoznano wyników wyszukiwania. Sprawdź stronę Escort.club i spróbuj ponownie.'); slots += context?.anchors?.length || 0; url=findNextEscortListPageUrl(doc,finalUrl); if (!url && total != null && slots < total) throw new Error('Odczytano tylko część wyników. Zawęź wyszukiwanie lub spróbuj ponownie.'); locationScope ||= getFavoriteSearchLocationScope(request,doc,search); yield {context,finalUrl,page,locationScope}; } } cancelToken?.throwIfCancelled(); } function commitFavoriteSearchEdit(session,search) { const items=getWatchedItems(),index=items.findIndex(item => item.id === session.itemId && item.sourceType === 'escort-search'); if (index < 0) throw new Error('Ten profil został usunięty. Edycja nie została zapisana.'); const current=items[index],key=favoriteSearchKey(search); if (favoriteSearchKey(normalizeFavoriteSearch(current.search)) !== session.baseline) throw new Error('Wyszukiwanie zmieniono w innej karcie. Anuluj i otwórz edycję ponownie.'); if (items.some(item => item.id !== current.id && item.sourceType === 'escort-search' && favoriteSearchKey(normalizeFavoriteSearch(item.search)) === key)) throw new Error('Takie wyszukiwanie jest już zapisane jako inny profil.'); if (key === session.baseline) return updateWatchedItem(current.id,{search}) || current; items[index]=normalizeWatchedItem({...current,search,watchKey:key,resolvedAdUrls:[],state:null, lastCheckedAt:0,lastFullCheckedAt:0,lastError:null,hiddenLastCheckedAt:0,hiddenLastError:null}); saveWatchedItems(items); return items[index]; } function finishFavoriteSearchEdit() { const session=favoriteSearchEditSession; if (!session) return; favoriteSearchEditCancelToken?.cancel('Zakończono edycję wyszukiwania'); sessionStorage.removeItem(FAVORITE_SEARCH_EDIT_KEY); favoriteSearchEditSession=null; favoriteSearchEditReadFilters=null; detachHelperNode(document.getElementById('vm-favorite-search-edit-frame')); sessionStorage.removeItem(ESCORT_MULTI_LOCATION_SESSION_KEY); try {if (session.closeKey) writeHelperStorageValue(session.closeKey,{finished:true,expiresAt:Date.now() + 60000});} finally {window.close();} } function trimFavoriteSearchEditPage() { if (!favoriteSearchEditSession) return; const searchBox=findEscortSearchLocationControls()?.searchBox; const results=getEscortMainResultsSection() || getEscortClubListContext()?.heading?.closest('section'); const keep='#vm-favorite-search-edit-bar, #advansed-search, .vm-ui-overlay, .vm-multi-panel, .select2-container, .select2-dropdown, .bs-container'; const hideOutside=(boundary,direction) => { for (let node=boundary; node && node !== document.body; node=node.parentElement) { for (let sibling=node[direction]; sibling; sibling=sibling[direction]) { if (!sibling.matches('script, style, link') && !sibling.matches(keep) && !sibling.querySelector(keep)) sibling.classList.add('vm-search-edit-outside'); } } }; hideOutside(searchBox,'previousElementSibling'); hideOutside(results,'nextElementSibling'); } function mountFavoriteSearchEditBar() { const session=favoriteSearchEditSession,origin=location.origin; const item=getWatchedItems().find(entry => entry.id === session.itemId); const shell=prepareHelperStyleApi(makeElement('div')); shell.id='vm-favorite-search-edit-shell'; const bar=makeElement('div'); bar.id='vm-favorite-search-edit-bar'; bar.setAttribute('role','region'); bar.setAttribute('aria-label','Edycja wyszukiwania jako profilu'); const title=makeElement('strong','',`Edytujesz wyszukiwanie zapisane jako profil: ${item?.displayName || ''}`); const save=makeButton('vm-ui-button -primary','Zapisz wyszukiwanie'); const cancel=makeButton('vm-ui-button','Anuluj'); const status=makeElement('span'); status.setAttribute('role','status'); const frame=makeElement('iframe'); frame.id='vm-favorite-search-edit-frame'; frame.name=FAVORITE_SEARCH_EDIT_KEY + ':' + session.frameToken; frame.title='Wyszukiwarka - edycja profilu'; const style=makeElement('style'); style.textContent=` html.vm-search-edit-shell, html.vm-search-edit-shell body { overflow:hidden !important; } html.vm-search-edit-shell body > :not(#vm-favorite-search-edit-shell):not(#vm-garso-watch-toast):not(style):not(script) { display:none !important; } #vm-favorite-search-edit-shell { position:fixed; inset:0; z-index:2147483647; display:flex; flex-direction:column; padding:7px; box-sizing:border-box; background:var(--vm-ui-accent,${getEscortPagePinkColor()}); color:var(--vm-ui-accent-contrast,#fff); font:700 16px/1.2 var(--vm-ui-font-family,Arial,sans-serif); } #vm-favorite-search-edit-bar { display:flex; flex:0 0 auto; flex-wrap:wrap; align-items:center; gap:6px 10px; padding:4px 10px 10px; } #vm-favorite-search-edit-bar strong { flex:1 1 320px; font:inherit; } #vm-favorite-search-edit-bar .vm-ui-button { flex:0 0 auto; min-height:30px; padding:4px 10px; border-color:var(--vm-ui-accent-contrast,#fff); background:transparent; color:inherit; font:inherit; } #vm-favorite-search-edit-bar .vm-ui-button.-primary { background:var(--vm-ui-accent-contrast,#fff); color:var(--vm-ui-accent,${getEscortPagePinkColor()}); } #vm-favorite-search-edit-bar .vm-ui-button:disabled { opacity:.55; cursor:wait; } #vm-favorite-search-edit-bar .vm-ui-button:focus-visible { outline-color:var(--vm-ui-accent-contrast,#fff); } #vm-favorite-search-edit-bar [role=status] { flex:1 1 100%; font-size:12px; } #vm-favorite-search-edit-bar [role=status]:empty { display:none; } #vm-favorite-search-edit-frame { display:block; flex:1 1 0; min-height:0; width:100%; border:0; background:var(--vm-ui-bg,#fff); } `; document.head.appendChild(style); bar.append(title,save,cancel,status); shell.append(bar,frame); document.documentElement.classList.add('vm-search-edit-shell'); let ready=false,waiting=false,busy=false,pageId='',requestId=0,timer; const setBusy=value => {busy=value; frame.inert=value; save.disabled=value || !ready; if (!value) status.textContent='';}; const loading=() => { favoriteSearchEditCancelToken?.cancel('Wyszukiwarka jest przeładowywana'); ready=false; waiting=false; setBusy(false); frame.setAttribute('aria-busy','true'); clearTimeout(timer); }; const receive=async event => { const data=event.data; if (favoriteSearchEditSession !== session || event.origin !== origin || event.source !== frame.contentWindow || data?.type !== FAVORITE_SEARCH_EDIT_KEY || data.token !== session.frameToken) return; if (data.state === 'loading') {loading(); return;} if (data.state === 'ready' || data.state === 'error') { if (busy) return; clearTimeout(timer); pageId=data.pageId; ready=data.state === 'ready'; setBusy(false); frame.setAttribute('aria-busy','false'); if (!ready) showWatchToast(data.error || 'Nie udało się przygotować wyszukiwarki.',true); return; } if (data.state !== 'capture' || !waiting || data.pageId !== pageId || data.requestId !== requestId) return; waiting=false; clearTimeout(timer); const token=favoriteSearchEditCancelToken=createOperationCancelToken('Zapis wyszukiwania'); try { if (data.error) throw new Error(data.error); const search=normalizeFavoriteSearch(data.search); if (!search) throw new Error('Nie udało się odczytać parametrów wyszukiwania.'); token.throwIfCancelled(); if (favoriteSearchEditSession !== session) return; const saved=commitFavoriteSearchEdit(session,search); cancel.disabled=true; status.textContent='Wyszukiwanie zapisane. Aktualizowanie anonsów profilu…'; await refreshFavoriteItemData(saved,true); finishFavoriteSearchEdit(); } catch (error) { if (favoriteSearchEditSession === session && !isOperationCancelledError(error)) { showWatchToast(error?.message || 'Nie udało się zapisać wyszukiwania.',true); setBusy(false); cancel.disabled=false; } } }; window.addEventListener('message',receive); cancel.addEventListener('click',() => {clearTimeout(timer); finishFavoriteSearchEdit();}); save.addEventListener('click',() => { if (save.disabled || favoriteSearchEditSession !== session) return; setBusy(true); waiting=true; frame.contentWindow.postMessage({type:FAVORITE_SEARCH_EDIT_KEY,token:session.frameToken,state:'capture',pageId,requestId:++requestId},origin); timer=setTimeout(() => { waiting=false; setBusy(false); showWatchToast('Wyszukiwarka nie odpowiada. Poczekaj na załadowanie wyników i spróbuj zapisać ponownie.',true); },10000); }); frame.addEventListener('load',() => { try { if (frame.contentWindow.location.href === 'about:blank') return; if (frame.contentWindow.location.origin !== origin) throw new Error(); if (ready && frame.contentDocument?.documentElement.dataset.vmSearchEditPage !== pageId) loading(); } catch { loading(); showWatchToast('Wyszukiwarka przeszła na inną domenę lub ramka została zablokowana. Anuluj edycję i otwórz ją ponownie.',true); } }); window.addEventListener('pagehide',() => { clearTimeout(timer); favoriteSearchEditCancelToken?.cancel('Zamknięto kartę edycji'); window.removeEventListener('message',receive); }); window.addEventListener('pageshow',event => {if (event.persisted) location.reload();}); loading(); document.body.appendChild(shell); navigateFavoriteSearchRequest(session.search.requests[0],frame); } function initFavoriteSearchEditFrame() { const session=favoriteSearchEditSession,origin=location.origin,pageId=crypto.randomUUID(); document.documentElement.dataset.vmSearchEditPage=pageId; let active=true; favoriteSearchEditFrameNotify=(state,details={}) => { if (active) window.parent.postMessage({...details,type:FAVORITE_SEARCH_EDIT_KEY,token:session.frameToken,pageId,state},origin); }; favoriteSearchEditFrameNotify('loading'); const style=makeElement('style'); style.textContent=` html.vm-favorite-search-edit .item-col.col, html.vm-favorite-search-edit .item-col.col * { cursor:not-allowed !important; } html.vm-favorite-search-edit :is(.vm-search-edit-outside, #vm-favorite-search-button, #${HELPER_MENU_ID}, #${GLOBAL_QUICK_SEARCH_ID}, #${WATCH_BAR_ID}, #${WATCH_BAR_SPACER_ID}) { display:none !important; } `; document.head.appendChild(style); document.documentElement.classList.add('vm-favorite-search-edit'); trimFavoriteSearchEditPage(); const blockCard=event => { if (event.target.closest?.('.item-col.col')) {event.preventDefault(); event.stopImmediatePropagation();} }; for (const type of ['click','auxclick','contextmenu']) window.addEventListener(type,blockCard,true); window.addEventListener('message',event => { const data=event.data; if (!active || event.origin !== origin || event.source !== window.parent || data?.type !== FAVORITE_SEARCH_EDIT_KEY || data.token !== session.frameToken || data.pageId !== pageId || data.state !== 'capture') return; try { const search=captureFavoriteSearchEdit(),limitError=getFavoriteSearchLimitError(); if (limitError) throw new Error(limitError); session.search=search; persistFavoriteSearchSession(); favoriteSearchEditFrameNotify('capture',{requestId:data.requestId,search}); } catch (error) {favoriteSearchEditFrameNotify('capture',{requestId:data.requestId,error:error.message});} }); window.addEventListener('pagehide',() => { favoriteSearchEditFrameNotify('loading'); active=false; try { if (JSON.parse(sessionStorage.getItem(FAVORITE_SEARCH_EDIT_KEY) || 'null')?.frameToken !== session.frameToken) return; if (favoriteSearchEditReadFilters) session.search=captureFavoriteSearchEdit(); persistFavoriteSearchSession(); } catch {} }); window.addEventListener('pageshow',event => {if (event.persisted) location.reload();}); } function initFavoriteSearchEdit() { if (!IS_ESCORT_CLUB) return false; const url=new URL(location.href),token=url.searchParams.get(FAVORITE_SEARCH_EDIT_PARAM); const child=window.top !== window.self; if (child && !window.name.startsWith(FAVORITE_SEARCH_EDIT_KEY + ':')) return false; try { const fresh=!child && token && token !== 'shell'; const key=fresh ? FAVORITE_SEARCH_EDIT_KEY + ':' + token:FAVORITE_SEARCH_EDIT_KEY; const session=fresh ? GM_getValue(key,null):JSON.parse(sessionStorage.getItem(key) || 'null'); if (fresh) deleteHelperStorageValue(key); if (!child && !token && (!session || getEscortAdIdFromPath(url.pathname))) return false; if (!session?.itemId || !normalizeFavoriteSearch(session.search) || (fresh && !(session.expiresAt > Date.now()))) throw new Error('Brak aktualnej sesji edycji. Otwórz edycję ponownie z profilu.'); if (child && (window.name !== FAVORITE_SEARCH_EDIT_KEY + ':' + session.frameToken || window.parent.location.origin !== url.origin || new URL(window.parent.location.href).searchParams.get(FAVORITE_SEARCH_EDIT_PARAM) !== 'shell')) return false; favoriteSearchEditSession=session; applyFavoriteSearchPageSettings(); if (child) { session.currentLocation=readEscortLocationFromControls(); initFavoriteSearchEditFrame(); return false; } session.frameToken ||= crypto.randomUUID(); persistFavoriteSearchSession(); if (fresh) saveEscortActiveMultiLocations(getFavoriteSearchEditLocations(session.search)); url.searchParams.set(FAVORITE_SEARCH_EDIT_PARAM,'shell'); history.replaceState(history.state,'',url.href); mountFavoriteSearchEditBar(); return true; } catch (error) { favoriteSearchEditSession=null; if (!child) sessionStorage.removeItem(FAVORITE_SEARCH_EDIT_KEY); else try { if (window.parent.location.origin === url.origin && new URL(window.parent.location.href).searchParams.get(FAVORITE_SEARCH_EDIT_PARAM) === 'shell') window.parent.postMessage({type:FAVORITE_SEARCH_EDIT_KEY,token:window.name.slice(FAVORITE_SEARCH_EDIT_KEY.length + 1), state:'error',error:error?.message || 'Nie udało się uruchomić edycji.'},url.origin); } catch {} showWatchToast(error?.message || 'Nie udało się uruchomić edycji.',true); return child; } return false; } function favoriteSearchKey(search) { if (!search) return 'search:null'; const criteria={...search}; delete criteria.locations; return `search:${JSON.stringify(criteria)}`; } function getFavoriteSearchBuckets(states,search) { const buckets=new Set(); const byKey=new Map(); const phoneBased=['exact-phone','phone-escort-search'].includes(search.aggregationMode); for (const member of states) { const phone=getEscortAdPhoneDigits(member.adData); const keys=!search.merge ? []:phoneBased ? (phone ? [`phone:${phone}`]:[]):getEscortProfileKeys(member.result); const found=[...new Set(keys.map(key => byKey.get(key)).filter(Boolean))]; const bucket=found[0] || {members:[],keys:new Set()}; for (const other of found.slice(1)) { bucket.members.push(...other.members); for (const key of other.keys) {bucket.keys.add(key); byKey.set(key,bucket);} buckets.delete(other); } bucket.members.push(member); for (const key of keys) {bucket.keys.add(key); byKey.set(key,bucket);} buckets.add(bucket); } return [...buckets]; } async function resolveFavoriteSearch(item,cancelToken=null,onProgress=null,requestContext=null) { const operationContext=createRequestContext('watch.saved-search',requestContext); await persistentCacheInitialization; await ensureFullPersistentCache(); cancelToken?.throwIfCancelled(); const search=normalizeFavoriteSearch(item.search); if (!search) throw new Error('Brak prawidłowej definicji wyszukiwania. Zapisz wyszukiwanie ponownie.'); const states=new Map(),known=new Set(getWatchedItemHistoricalAdUrls(item).map(parseAdIdFromUrl)); const filters=search.filters; const filtered=Object.entries(filters).some(([key,value]) => /^(hide|showOnly)/.test(key) && value === true); const phoneBased=['exact-phone','phone-escort-search'].includes(search.aggregationMode); const needs=getCustomFilterDataNeeds(filters); const needsEscorti=needs.escorti || (((search.merge && filtered) || filters.hideActiveAdsOver) && !phoneBased) || getWatchedItems().some(entry => entry.id !== item.id && entry.hidden && entry.profileUrls.length); const needsAggregationPhone=(search.merge && filtered && phoneBased) || (filters.hideActiveAdsOver && phoneBased); const needsEscortiPhone=needsEscorti && search.listSearchMode === 'phone-escorti'; const needsAd=needs.escortAd || needsAggregationPhone || needsEscortiPhone; const excluded=state => getHiddenAdMatches(state.adId,getHiddenResultProfileUrls(state.result)).some(entry => entry.id !== item.id); for await (const {context,finalUrl,page,locationScope} of iterateFavoriteSearchPages(search,cancelToken,page => { onProgress?.(`Sprawdzanie wyszukiwania · strona ${page}…`); touchWatchCheckLock(); },operationContext)) { const newStates=[]; for (const anchor of context?.anchors || []) { const adUrl=normalizeEscortiAdUrl(anchor.getAttribute('href'),finalUrl); const adId=parseAdIdFromUrl(adUrl); if (!adId) continue; if (states.has(adId)) {states.get(adId).locationScopes.push(locationScope);continue;} const state={adId,adUrl,locationScopes:[locationScope],newCandidate:!known.has(adId),card:anchor.closest('.item-col.col'),result:getListCache(adId,search.listSearchMode),adData:getStoredEscortAdData(adId)}; states.set(adId,state); if (!excluded(state)) newStates.push(state); } await watchMapLimit(newStates,2,async state => { cancelToken?.throwIfCancelled(); if (state.newCandidate || needsAd || needsEscorti) onProgress?.(`Sprawdzanie filtrów · anons ${state.adId} · strona ${page}…`); const cardContext=createRequestContext('watch.search-ad',operationContext); const needPhone=!!(needsAggregationPhone || (needsEscortiPhone && !isListCacheFresh(state.result))); const needAd=needs.escortAd || needPhone; if (needAd) state.adData=await getEscortAdData(state.adId,state.adUrl,state.newCandidate,cancelToken,cardContext,{needPhone}); if (state.newCandidate) { state.verified=needAd ? {url:state.adUrl,active:state.adData?.status === 'ok',adData:state.adData, city:state.adData?.location?.city,imageUrl:state.adData?.imageUrl} :await inspectEscortClubAd(state.adUrl,cancelToken,true,cardContext); cancelToken?.throwIfCancelled(); if (!state.verified.active || !state.verified.adData) return; state.adData=state.verified.adData; } if (needsEscorti && !isListCacheFresh(state.result)) { const query=search.listSearchMode === 'phone-escorti' ? getEscortAdPhoneDigits(state.adData):state.adUrl; if (!query) throw new Error('Brak numeru telefonu potrzebnego do sprawdzenia filtrów. Spróbuj ponownie po odczytaniu danych anonsu.'); state.result=await getOrStartListCheck(state.adId,query,search.listSearchMode,cancelToken,cardContext); } if (needsEscorti && state.result?.status !== 'ok') throw new Error('Nie udało się sprawdzić danych Escorti.pl potrzebnych do filtrowania. Spróbuj ponownie.'); }); } // Odrzucony kandydat pozostaje nieznany; wróci przy zwykłym sprawdzeniu. const candidates=[...states.values()].filter(state => !excluded(state) && (!state.newCandidate || (state.verified?.active && state.locationScopes.some(scope=>matchesFavoriteSearchLocation(state.verified.adData,scope))))); const watchMetadata=buildCustomFilterWatchMetadataIndex(); const cacheIndex=buildEscortListCacheIndex(); const phoneIndex=phoneBased ? buildEscortAdPhoneCacheIndex():null; const phoneResults=new Map(); const accepted=[]; for (const bucket of getFavoriteSearchBuckets(candidates,search)) { cancelToken?.throwIfCancelled(); if (filters.hideActiveAdsOver && search.aggregationMode === 'phone-escort-search') { for (const member of bucket.members) { const phone=getEscortAdPhoneDigits(member.adData); if (phone && !phoneResults.has(phone)) { const found=await fetchEscortClubPhoneSearchAds(phone,cancelToken, createRequestContext('watch.search-phone',operationContext)); phoneResults.set(phone,{adIds:new Set(found.adUrls.map(parseAdIdFromUrl).filter(Boolean))}); } } } const activeAdCount=getEscortBucketActiveAdCount(bucket,{phoneBasedAggregation:phoneBased,escortClubPhoneSearchAggregation:search.aggregationMode === 'phone-escort-search',phoneCacheIndex:phoneIndex,cacheIndex,phoneSearchAggregationIndex:phoneResults}); const metrics=getBucketCustomFilterMetrics(bucket,filters,watchMetadata,activeAdCount); const {shouldHide,unresolved}=evaluateEscortCustomFilters(metrics,filters); if (shouldHide) continue; if (unresolved) throw new Error('Brakuje danych do sprawdzenia filtrów niestandardowych. Poczekaj na dane lub zawęź wyszukiwanie i spróbuj ponownie.'); accepted.push(...bucket.members); } onProgress?.(`Pasujące anonse: ${accepted.length} · sprawdzono wszystkie strony.`); return {profileUrls:[],adUrls:accepted.map(state => state.adUrl),displayName:item.displayName,imageUrl:item.imageUrl || '',profileInfos:[], verifiedAdResults:accepted.map(state=>state.verified).filter(Boolean)}; } function getFavoriteSearchLimitError(locations=getEscortMultiLocationsForCurrentPage()) { const total=locations.length > 1 ? getEscortMultiLocationKnownTotal(locations):getEscortListTotalResultCount(); if (total == null) return 'Nie odczytano jeszcze liczby ogłoszeń dla wszystkich wybranych lokalizacji. Poczekaj na załadowanie liczników wyników lub odśwież stronę.'; return total > FAVORITE_SEARCH_MAX_ADS ? `Za dużo wyników: ${total} ogłoszeń. Limit zapisu wyszukiwania to ${FAVORITE_SEARCH_MAX_ADS} ogłoszeń łącznie dla wybranych lokalizacji. Zawęź wyszukiwanie filtrami serwisu.`:''; } function addFavoriteSearchButton(listContext,row) { if (!row || document.getElementById('vm-favorite-search-button')) return; const locations=getEscortMultiLocationsForCurrentPage(); const multi=locations.length > 1; updateFavoriteSearchEditLocation(document); const requests=multi ? locations.map(buildEscortSearchRequestForLocation):[{url:location.href}]; const makeSearch=() => normalizeFavoriteSearch({requests,locations,filters:getEscortCustomFilters(),listSearchMode:getEscortListSearchMode(),merge:SETTINGS.mergeEscortAds,aggregationMode:SETTINGS.escortAggregationMode},error => showWatchToast(`Nie udało się zapisać parametrów wyszukiwania. ${error?.message || error}`,true)); const button=makeButton('vm-ui-button','★ Dodaj wyszukiwanie jako profil'); button.id='vm-favorite-search-button'; button.title=`Zapisuje wyszukiwanie z najwyżej ${FAVORITE_SEARCH_MAX_ADS} ogłoszeniami łącznie dla wybranych lokalizacji, po uwzględnieniu filtrów serwisu.`; const refresh=() => {button.style.display=getWatchSettings().enabled ? '':'none';}; hiddenUiRefreshers.add(refresh); refresh(); button.addEventListener('click',async () => { if (!getWatchSettings().enabled || button.disabled) return; if (document.getElementById('vm-favorite-search-overlay')) return; const limitError=getFavoriteSearchLimitError(locations); if (limitError) {showWatchToast(limitError,true); return;} const search=makeSearch(); if (!search) return; button.disabled=true; try { const watchKey=favoriteSearchKey(search); const existing=getWatchedItems().find(item => item.watchKey === watchKey); if (existing?.favorite) { showWatchToast('Ten wpis jest już w Ulubionych.'); return; } const {overlay,box}=createWatchDialogBase('vm-favorite-search-overlay','Zapisz wyszukiwanie jako profil'); const controller=createDialogCloseController(overlay); const label=makeElement('label','vm-ui-stack','Nazwa profilu'); const name=makeElement('input','vm-ui-field',null,{maxLength:120,value:`Wyszukiwanie · ${normalizeEscortAdText(listContext.heading?.textContent || document.title).slice(0,100)}`}); label.appendChild(name); const info=makeElement('p','vm-ui-help','Zapamiętam lokalizacje i filtry, także niestandardowe. Po zapisaniu sprawdzę anonse w tle; ukryte pominę. Zmiana filtrów na stronie nie zmieni tego profilu.'); const status=makeElement('p','vm-ui-help',`Lokalizacje: ${multi ? locations.length:1}`); status.setAttribute('role','status'); const save=styleDialogActionButton(makeButton('','Zapisz profil'),true); const cancel=makeButton('vm-ui-button','Anuluj'); controller.bindButton(cancel); const actions=makeElement('div','vm-ui-actions'); actions.append(cancel,save); box.append(label,info,status,actions); save.addEventListener('click',() => { if (save.disabled || !overlay.isConnected) return; const displayName=normalizeEscortAdText(name.value); if (!displayName) {name.focus(); status.textContent='Wpisz nazwę profilu.'; return;} if (!getWatchSettings().enabled) return; save.disabled=true; const item=saveFavoriteItemFromContext({sourceType:'escort-search',resolutionMode:'search-escort',watchKey,search,displayName,resolvedAdUrls:existing?.resolvedAdUrls || []},{favorite:true},'person'); controller.close('save'); initLocalWatchSystem(); showWatchToast('Profil zapisany. Anonse są sprawdzane w tle. Alerty włączysz w Ulubionych.'); refreshFavoriteItemData(item,true).finally(() => refreshWatchPageButtons()); }); } finally {button.disabled=false;} }); const breadcrumb=findEscortResultsBreadcrumb(listContext.heading); if (breadcrumb) { const container=breadcrumb.querySelector('ol,ul') || breadcrumb; const slot=container.matches('ol,ul') ? makeElement('li','vm-favorite-search-slot'):container; if (slot !== container) container.appendChild(slot); slot.appendChild(button); } else row.prepend(button); } async function initEscortClubListPage() { initHiddenResultCards(); const shouldWaitForList= isEscortSearchPath() || location.pathname.startsWith('/anonse/'); let listContext=shouldWaitForList ? await waitForEscortClubListContext(10000) :getEscortClubListContext(); applyEscortSectionVisibility(); if (!listContext) { recordParserIssue( 'Escort.club - lista wyników', 'Nie znaleziono głównej sekcji listy anonsów.' ); log('Nie znaleziono sekcji "Anonse erotyczne ..." na stronie wyszukiwania Escort.club'); return; } hideEscortMainResultsTitle(listContext); markEscortResultCounter(document.querySelector('.vm-escort-results-count, .search-links > .sub-label')); updateEscortMultiResultsBreadcrumb(getEscortMultiLocationsForCurrentPage()); const initialLocations=getEscortMultiLocationsForCurrentPage(); const activeMultiLocations=getEscortActiveMultiLocations(); const currentLocation=readEscortLocationFromControls(); if (activeMultiLocations.length && currentLocation) { const currentTotal=getEscortListTotalResultCount(document,listContext); if (currentTotal != null) { setEscortMultiLocationResultCount(currentLocation,currentTotal); } } const initialMultiLocationCount=Math.max( 1, initialLocations.length ); if (!listContext.anchors.length && initialMultiLocationCount <= 1 && !favoriteSearchEditSession && getEscortListTotalResultCount(document,listContext) !== 0) { recordParserIssue( 'Escort.club - kafelki', 'Sekcja listy istnieje, ale nie znaleziono linków kafelków anonsów.' ); log('Nie znaleziono kafelków anonsów Escort.club w analizowanej sekcji'); return; } const listPagesControl=addEscortListPagesControl(listContext); try {addFavoriteSearchButton(listContext,listPagesControl?.row);} catch (error) {log('Nie udało się przygotować zapisu wyszukiwania',error);} let customFilterCallbacksReady=false; const customFilterController=addEscortCustomFiltersControl( listContext, config => { if (customFilterCallbacksReady) { ensureListDataNeeds(); applyCustomFilters(config); } }, duration => { if (customFilterCallbacksReady) { renderAllEscortListPrices(duration); ensureListDataNeeds(); } }, field => { if (customFilterCallbacksReady) { renderAllEscortListExtraData(field); ensureListDataNeeds(); } }, () => { if (customFilterCallbacksReady) ensureListDataNeeds(); } ); const requestedPages=normalizeListPagesToShow(LIST_PAGES_TO_SHOW); const activeLocationCount=initialMultiLocationCount; const listRequestContext=createRequestContext('results'); const needsAdditionalListLoading=requestedPages !== 1 || activeLocationCount > 1; const listPagesCancelToken=needsAdditionalListLoading ? createOperationCancelToken('Ładowanie wielu stron/lokalizacji Escort.club') :null; if (listPagesControl && needsAdditionalListLoading) { if (listPagesControl.status) listPagesControl.status.textContent=''; listPagesControl.setProgress?.( activeLocationCount > 1 ? `Ładowanie: 1/${requestedPages === 'all' ? '?':Number(requestedPages) * activeLocationCount} stron • ${activeLocationCount} ${polishLocationWord(activeLocationCount)}` :'Ładowanie stron...', () => listPagesCancelToken?.cancel('Przerwano ładowanie stron/lokalizacji') ); if (listPagesControl.select) { listPagesControl.select.disabled=true; } } let loadResult={loadedPages:1,locationCount:initialMultiLocationCount,allPagesLoaded:false}; const {adsNav}=listContext; const anchors=[]; const initialPageSelected=initialLocations.length <= 1 || initialLocations.some(item => getEscortLocationKey(item) === getEscortLocationKey(currentLocation)); const aggregationMode=normalizeEscortAggregationMode( SETTINGS.escortAggregationMode ); const phoneBasedAggregation= aggregationMode === 'exact-phone' || aggregationMode === 'phone-escort-search'; const escortClubPhoneSearchAggregation= aggregationMode === 'phone-escort-search'; const listSearchMode=getEscortListSearchMode(); async function buildIndexInBatches(builder,keys) { let index=builder([]); for (let start=0; start < keys.length; start += 24) { index=builder(keys.slice(start,start + 24),index); if (start + 24 < keys.length) await yieldHelperWork(); } return index; } await ensureEscortPageCache(listContext.anchors.map(anchor => parseAdIdFromUrl(anchor.href)).filter(Boolean)); const cacheIndex=await buildIndexInBatches(buildEscortListCacheIndex,getEscortListCacheKeys()); const indexedListKeys=new Set(getEscortListCacheKeys()); const phoneCacheIndex=phoneBasedAggregation ? await buildIndexInBatches(buildEscortAdPhoneCacheIndex,getEscortAdDataCacheKeys()) :{phoneToAdIds:new Map(),adIdToPhone:new Map()}; const phoneSearchAggregationIndex=new Map(); const cardStates=new Map(); const aggregationKeyToGroup=new Map(); let nextGroupId=1; let listInitComplete=false; const dirtyGroups=new Set(); const dirtyStates=new Set(); let watchMetadataIndex=null; let listRefreshPending=false; let listRefreshFrame=null,listRefreshTimer=null,listRefreshRunning=false; let cacheChangeTimer=null,cacheChangeRunning=false,cacheChangePending=false; let listProgressTimer=null; function deferListView(state=null) { if (!document.hidden) return false; if (state) dirtyStates.add(state); listRefreshPending=true; return true; } function scheduleListProgress() { if (listInitComplete && !deferListView() && !listProgressTimer) listProgressTimer=setTimeout(updateListDataProgress,100); } function clearListViewTimers() { cancelAnimationFrame(listRefreshFrame); clearTimeout(listRefreshTimer); clearTimeout(listProgressTimer); listRefreshFrame=listRefreshTimer=listProgressTimer=null; } function flushListView() { clearListViewTimers(); if (!listRefreshPending || deferListView() || cacheChangePending || cacheChangeRunning) return; listRefreshRunning=true; try { for (const state of dirtyStates) { renderState(state); renderEscortListPrice(state.priceLine,state.adData,SETTINGS.searchResultPriceDuration,state.aggregateMembers); applyListMerge(state); } dirtyStates.clear(); applyCustomFilters(); listRefreshPending=false; } finally {listRefreshRunning=false;} } function scheduleListRefresh() { if (!listInitComplete) return; listRefreshPending=true; if (document.hidden || listRefreshRunning || listRefreshFrame !== null) return; listRefreshFrame=requestAnimationFrame(flushListView); listRefreshTimer=setTimeout(flushListView,100); } const listDataCancelToken=createOperationCancelToken( 'Masowe pobieranie danych na liście anonsów' ); let massDataCancelled=false; customFilterController?.setCancelHandler(() => { if (massDataCancelled) return; massDataCancelled=true; listDataCancelToken.cancel('Przerwano masowe pobieranie danych'); for (const state of cardStates.values()) { state.dataCancelled=true; state.checkDone=true; state.adDataRetryScheduled=false; } customFilterController.setStatus( 'Pobieranie przerwane. Odśwież stronę, aby wznowić.' ); applyCustomFilters(customFilterController.getConfig()); updatePageAggregateSummary(); }); const originalSummaryLine=adsNav ? [...adsNav.querySelectorAll('p')].find(p => /(?:Wyświetlane|Lista\s+wyników?)/i.test(p.textContent || '') ) :null; const originalSummaryText=(originalSummaryLine?.textContent || '') .replace(/\s+/g,' ') .trim(); const totalAdsMatch=originalSummaryText.match(/spośród\s+(\d[\d\s.]*)/i); const totalAds=totalAdsMatch ? Number(totalAdsMatch[1].replace(/[\s.]/g,'')) :null; function polishPageWord(n) { return polishCountForm(n,'strona','strony','stron'); } function repeatedAdsPhrase(n) { return polishCountForm( n, 'powtarzający się anons', 'powtarzające się anonse', 'powtarzających się anonsów' ); } function updateLoadedSummaryLine() { if (!originalSummaryLine) return; const loadedCount=anchors.length; const requested=normalizeListPagesToShow(LIST_PAGES_TO_SHOW); const allPagesLoaded=requested === 'all' && loadResult.allPagesLoaded; const pagesText=allPagesLoaded ? 'wszystkie strony' :`${loadResult.loadedPages} ${polishPageWord(loadResult.loadedPages)}`; const multiLocations=getEscortMultiLocationsForCurrentPage(); const multiLocation=multiLocations.length > 1 || Number(loadResult.locationCount) > 1; if (multiLocation) { const totalsByKey=loadResult.locationTotalAds || {}; const expectedLocations=Math.max( 1, Number(loadResult.locationCount) || multiLocations.length || 1 ); let totals=Object.values(totalsByKey) .map(value => Number(value)) .filter(value => Number.isFinite(value) && value >= 0); if (totals.length !== expectedLocations) { const activeLocations=getEscortActiveMultiLocations(); const fallbackTotals=activeLocations.map(locationData => { const value=Number(getEscortMultiLocationResultCount(locationData)); return Number.isFinite(value) && value >= 0 ? value:null; }); if ( fallbackTotals.length === expectedLocations && fallbackTotals.every(value => value != null) ) { totals=fallbackTotals; } } const allTotalsKnown=totals.length === expectedLocations; const summedTotal=totals.reduce((sum,value) => sum + value,0); updateEscortMultiResultsBreadcrumb( multiLocations.length > 1 ? multiLocations :getEscortMultiLocationsForCurrentPage(), allTotalsKnown ? summedTotal:null ); setEscortMultiResultCountersVisible(false); return; } setEscortMultiResultCountersVisible(true); originalSummaryLine.innerHTML=Number.isFinite(totalAds) && totalAds > 0 ? `Załadowano <strong>${loadedCount}</strong> z <strong>${totalAds}</strong> ${totalAds === 1 ? 'anonsu':'anonsów'} • ${pagesText}.` :`Załadowano <strong>${loadedCount}</strong> ${polishAdWord(loadedCount)} • ${pagesText}.`; originalSummaryLine.title= 'Licznik obejmuje kafelki rzeczywiście załadowane na tej liście przez Escort.club i skrypt.'; } let aggregateSummaryLine=adsNav?.querySelector('.vm-escorti-page-aggregate-summary') || null; if (!aggregateSummaryLine && originalSummaryLine) { aggregateSummaryLine=makeElement('p'); aggregateSummaryLine.className='vm-escorti-page-aggregate-summary'; Object.assign(aggregateSummaryLine.style,{marginTop:'2px'}); originalSummaryLine.insertAdjacentElement('afterend',aggregateSummaryLine); } customFilterController?.setListIncomplete(needsAdditionalListLoading); function getListDataNeeds() { const filters=customFilterController?.getConfig() || {}; const sortField=customFilterController?.getSortConfig().field; const fields=[...getSearchResultExtraFields()]; if (sortField?.startsWith('escorti:')) fields.push(sortField); const extraFields=fields.map(getEscortDataFieldDescriptor).filter(Boolean); const options={showPrice:isEscortSearchPriceDisplayed(),showEscorti:SETTINGS.showEscortiTileData, mergeAds:SETTINGS.mergeEscortAds,phoneBasedAggregation,escortClubPhoneSearchAggregation}; return {...getEscortListDataRequirements(filters,sortField,extraFields,options),sortField,extraFields}; } function updateListDataProgress(states=cardStates.values()) { clearTimeout(listProgressTimer); listProgressTimer=null; if (!listInitComplete || deferListView()) return; const progress=getEscortListDataProgress(states,getListDataNeeds(),phoneSearchAggregationIndex); const queue=getEscortRequestQueueStatus(); customFilterController?.setProgress(formatEscortListDataProgress(progress,queue,massDataCancelled), progress.pending > 0 && !massDataCancelled); } function updatePageAggregateSummary() { if (!listInitComplete || deferListView()) return; const allStates=[...cardStates.values()]; const states=allStates.filter(state => !state.hiddenInResults); const hiddenText=allStates.length !== states.length ? ` • ukryte z Twojej listy: <strong>${allStates.length - states.length}</strong>`:''; const pendingCount=states.filter(state => !state.dataCancelled && ( !state.checkDone || ( SETTINGS.mergeEscortAds && phoneBasedAggregation && ( !state.adData || state.adDataLoading || ( escortClubPhoneSearchAggregation && isEscortClubPhoneSearchAggregationPending(state) ) ) )) ).length; updateListDataProgress(states); if (massDataCancelled) { customFilterController?.setStatus( 'Pobieranie przerwane. Odśwież stronę, aby wznowić.' ); } if (!aggregateSummaryLine) return; if (!SETTINGS.mergeEscortAds) { aggregateSummaryLine.innerHTML= 'Agregacja wyłączona' + hiddenText + (pendingCount > 0 ? ' • trwa sprawdzanie…':'.'); return; } const repeatedCount=states.filter(state => state.mergedHidden).length; const afterAggregation=states.length - repeatedCount; aggregateSummaryLine.innerHTML= `Po agregacji: <strong>${afterAggregation}</strong> ${polishAdWord(afterAggregation)} ` + `• ukryto <strong>${repeatedCount}</strong> ${repeatedAdsPhrase(repeatedCount)}` + hiddenText + (pendingCount > 0 ? ' • trwa sprawdzanie…':'.'); aggregateSummaryLine.title= 'Ukryte powtarzające się anonse to kafelki połączone przez agregację z innym kafelkiem na liście.'; } function ensureMergedCardHighlightStyles() { if (document.getElementById('vm-escorti-merged-card-styles')) return; appendHelperStyle('vm-escorti-merged-card-styles',` .vm-escorti-merged-expanded-card { position: relative !important; } .vm-escorti-merged-expanded-card::after { content: ''; position: absolute; inset: 0; box-sizing: border-box; border: 4px solid #00d8f0; background: rgba(0, 216, 240, .10); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .20), 0 0 8px rgba(0, 216, 240, .35); pointer-events: none; z-index: 15; } `); } function clearMergedCardHighlight(state) { state.card.classList.remove('vm-escorti-merged-expanded-card'); } function highlightMergedCard(state) { ensureMergedCardHighlightStyles(); state.card.classList.add('vm-escorti-merged-expanded-card'); } function restoreCard(state) { clearMergedCardHighlight(state); state.mergedHidden=false; state.card.style.display=state.customHidden ? 'none':state.originalDisplay; } function hideMergedCard(state) { clearMergedCardHighlight(state); state.card.style.display='none'; state.mergedHidden=true; } function showMergedCard(state) { state.card.style.display=state.customHidden ? 'none':state.originalDisplay; if (state.customHidden) clearMergedCardHighlight(state); else highlightMergedCard(state); state.mergedHidden=true; } function renderState(state) { if (state.result) registerEscortDataFieldsFromEscortiResult(state.result); if (deferListView(state)) return; if (state.result) renderListEscortiResult(state.line,state.result,!!state.stale); renderEscortListExtraData(state.extraLine,state,getSearchResultExtraFields()); } function getCanonical(group) { const members=[...group.members].filter(member => !member.hiddenInResults); return members.reduce( (best,item) => (!best || item.index < best.index ? item:best), null ); } function getStateAggregationKeys(state) { if (phoneBasedAggregation) { const phone=getStatePhoneForAggregation(state); return phone ? [`phone:${phone}`]:[]; } return getEscortProfileKeys(state?.result); } function getGroupAggregateAdIds(group) { const adIds=new Set( [...group.members] .filter(member => !member.hiddenInResults) .map(member => member.adId) .filter(Boolean) ); for (const aggregationKey of group.aggregationKeys) { const cachedIds=phoneBasedAggregation ? phoneCacheIndex.phoneToAdIds.get( aggregationKey.replace(/^phone:/,'') ) :cacheIndex.freshProfileToAdIds.get(aggregationKey); for (const adId of cachedIds || []) { adIds.add(adId); } if (escortClubPhoneSearchAggregation) { const searchedIds=phoneSearchAggregationIndex.get( aggregationKey.replace(/^phone:/,'') )?.adIds; for (const adId of searchedIds || []) adIds.add(adId); } } return new Set([...adIds].filter(id => !getHiddenAdMatches(id,getHiddenResultProfileUrls(cacheIndex.byAdId.get(id))).length)); } function refreshGroupDisplay(group) { if (!group) return; if (deferListView()) {dirtyGroups.add(group); return;} const previousMembers=new Set(group.members || []); if (group.canonical) previousMembers.add(group.canonical); for (const member of previousMembers) delete member.aggregateMembers; if (!group.members) group.members=new Set(); for (const member of group.members) { if (!member.hiddenInResults) continue; clearMergedCardHighlight(member); member.mergedHidden=false; setListMergedInfo(member.line,0); } const members=[...group.members].filter(member => !member.hiddenInResults).sort((a,b) => a.index - b.index); const canonical=SETTINGS.mergeEscortAds ? getCanonical(group):null; group.canonical=canonical; if (!canonical) { for (const member of previousMembers) { if (!SETTINGS.mergeEscortAds) restoreCard(member); setListMergedInfo(member.line,0); renderEscortListPrice(member.priceLine,member.adData,SETTINGS.searchResultPriceDuration,member.aggregateMembers); renderEscortListExtraData(member.extraLine,member,getSearchResultExtraFields()); } return; } for (const member of members) { member.group=group; if (member === canonical) { restoreCard(member); if (group.expanded && members.length > 1) { highlightMergedCard(member); } } else if (group.expanded) { showMergedCard(member); } else { hideMergedCard(member); } } if (group.expanded) { let insertAfter=canonical.card; for (const member of members) { if (member === canonical) continue; insertAfter.insertAdjacentElement('afterend',member.card); insertAfter=member.card; } } const aggregateAdIds=new Set([...getGroupAggregateAdIds(group)].map(adId => String(adId || '')).filter(Boolean)); const aggregateCount=aggregateAdIds.size; const aggregateMembers=new Map(); for (const member of members) { const adId=String(member.adId || ''); if (adId && aggregateAdIds.has(adId) && !aggregateMembers.has(adId)) aggregateMembers.set(adId,member); } const cachedOnlyCount=Math.max(0,aggregateCount - aggregateMembers.size),cachedOnlyAdData=[]; for (const adId of aggregateAdIds) { if (aggregateMembers.has(adId)) continue; const adData=getStoredEscortAdData(adId); aggregateMembers.set(adId,{adId,adUrl:`https://pl.escort.club/anons/${adId}.html`,adData, result:cacheIndex.byAdId.get(adId) || null,activeAdCount:aggregateCount}); if (adData?.status === 'ok') cachedOnlyAdData.push(adData); } canonical.aggregateMembers=[...aggregateMembers.values()]; const dataDifferences=getEscortListGroupDataDifferences( members, cachedOnlyAdData ); setListMergedInfo( canonical.line, aggregateCount, !!group.expanded, members.length > 1 ? () => { group.expanded=!group.expanded; refreshGroupDisplay(group); updatePageAggregateSummary(); } :null, cachedOnlyCount, dataDifferences ); for (const member of previousMembers) { renderEscortListPrice(member.priceLine,member.adData,SETTINGS.searchResultPriceDuration,member.aggregateMembers); if (member === canonical) renderState(member); else { setListMergedInfo(member.line,0); renderEscortListExtraData(member.extraLine,member,getSearchResultExtraFields()); } } if (aggregateCount > 1 && canonical.line) { const additionalSource=escortClubPhoneSearchAggregation ? 'z wyszukiwarki Escort.club lub aktualnego lokalnego cache; bez tworzenia kafelków' :'z aktualnego lokalnego cache; bez tworzenia kafelków'; canonical.line.title= `${canonical.line.title || 'Dane z Escorti'}; ` + `na tej stronie: ${members.length}, w liczniku agregacji: ${aggregateCount}` + (cachedOnlyCount > 0 ? ` (dodatkowo ${cachedOnlyCount} ${additionalSource})` :''); } } function applyListMerge(state) { if (!SETTINGS.mergeEscortAds || !state) return; if (deferListView(state)) return; if ( !phoneBasedAggregation && (!state.result || state.result.status !== 'ok' || state.result.profiles === 0) ) { return; } const aggregationKeys=getStateAggregationKeys(state); if (!aggregationKeys.length) return; const touchedGroups=[...new Set( aggregationKeys .map(key => aggregationKeyToGroup.get(key)) .filter(Boolean) )]; let group; if (!touchedGroups.length) { group={ id:nextGroupId++, members:new Set(), aggregationKeys:new Set(), expanded:false }; } else { group=touchedGroups[0]; for (const other of touchedGroups.slice(1)) { if (other === group) continue; dirtyGroups.delete(other); group.expanded=!!(group.expanded || other.expanded); for (const member of other.members) group.members.add(member); for (const key of other.aggregationKeys) group.aggregationKeys.add(key); } } group.members.add(state); for (const key of aggregationKeys) group.aggregationKeys.add(key); for (const key of group.aggregationKeys) aggregationKeyToGroup.set(key,group); for (const member of group.members) member.group=group; dirtyGroups.add(group); scheduleListRefresh(); } function getCustomFilterBuckets() { const buckets=[]; const seenGroups=new Set(); for (const state of cardStates.values()) { if (state.hiddenInResults) continue; if (state.group) { if (seenGroups.has(state.group)) continue; seenGroups.add(state.group); buckets.push({group:state.group,members:[...state.group.members].filter(member => !member.hiddenInResults)}); } else { buckets.push({group:null,members:[state]}); } } return buckets; } function getBucketActiveAdCount(bucket) { return getEscortBucketActiveAdCount(bucket,{phoneBasedAggregation,escortClubPhoneSearchAggregation,phoneCacheIndex,cacheIndex,phoneSearchAggregationIndex,getGroupAggregateAdIds}); } function getStatePhoneForAggregation(state) { return isEscortAdPhoneFresh(state?.adData) ? getEscortAdPhoneDigits(state.adData):''; } function isEscortClubPhoneSearchAggregationPending(state) { if (!escortClubPhoneSearchAggregation) return false; if (!state?.adData || state.adDataLoading) return true; const phone=getStatePhoneForAggregation(state); if (!phone) return false; const entry=phoneSearchAggregationIndex.get(phone); return !entry || !!entry.loading; } function ensureEscortClubPhoneSearchAggregation(state) { if ( !escortClubPhoneSearchAggregation || !state || massDataCancelled || listDataCancelToken.cancelled || !getListDataNeeds().phoneSearch ) return null; const phone=getStatePhoneForAggregation(state); if (!phone) { state.phoneSearchAggregationLoading=false; return null; } const existing=phoneSearchAggregationIndex.get(phone); if (existing) { state.phoneSearchAggregationLoading=!!existing.loading; return existing.promise; } const entry={ adIds:new Set(), loading:true, error:null, promise:null }; phoneSearchAggregationIndex.set(phone,entry); for (const member of cardStates.values()) { if (getStatePhoneForAggregation(member) === phone) { member.phoneSearchAggregationLoading=true; } } updatePageAggregateSummary(); const requestContext=createRequestContext('results.phone-search',listRequestContext); entry.promise=(async () => { const search=await fetchEscortClubPhoneSearchAds( phone, listDataCancelToken, requestContext ); const searchItems=search.adUrls .map(url => ({ url, adId:parseAdIdFromUrl(url) })) .filter(item => item.adId); for (const item of searchItems) entry.adIds.add(item.adId); await watchMapLimit(searchItems,4,async item => { try { listDataCancelToken.throwIfCancelled(); const result=await getEscortAdData( item.adId, item.url, false, listDataCancelToken, requestContext, {needPhone:getListDataNeeds().phone} ); if (result?.status === 'ok') { updateEscortAdPhoneCacheIndex( phoneCacheIndex, item.adId, result ); } else entry.incomplete=true; return result; } catch (error) { if (isOperationCancelledError(error)) throw error; entry.incomplete=true; log( `Nie udało się pobrać danych anonsu ${item.adId} podczas agregowania przez wyszukiwarkę Escort.club`, error ); return {status:'error'}; } }); })() .catch(error => { if (isOperationCancelledError(error) || listDataCancelToken.cancelled) { entry.error='cancelled'; return; } entry.error=formatHelperDiagnosticError(error); log( `Błąd agregowania przez wyszukiwarkę Escort.club dla numeru ${phone}`, error ); }) .finally(() => { entry.loading=false; for (const member of cardStates.values()) { if (getStatePhoneForAggregation(member) !== phone) continue; member.phoneSearchAggregationLoading=false; if (member.group) dirtyGroups.add(member.group); } scheduleListRefresh(); }); return entry.promise; } function ensureEscortAdDataForState(state,forceRefresh=false) { if (state?.hiddenInResults) return; if (!state || massDataCancelled || listDataCancelToken.cancelled) { if (state) state.dataCancelled=true; return; } if (state.adDataLoading) return; const needPhone=getListDataNeeds().phone; const previousAdData=!forceRefresh && needPhone && state.adDataPrefetchStarted && state.adData?.status === 'ok' ? state.adData:null; if (forceRefresh) { if (state.agencyPhoneRefreshStarted || state.agencyPhoneRefreshDone) return; state.agencyPhoneRefreshStarted=true; } else { if (state.adDataPrefetchStarted && !(needPhone && !state.adDataPhoneRequested && !isEscortAdPhoneFresh(state.adData))) return; state.adDataPrefetchStarted=true; } if (needPhone) state.adDataPhoneRequested=true; state.adDataAttempts=(state.adDataAttempts || 0) + 1; const requestContext=createRequestContext('results.ad-data',listRequestContext); state.adDataLoading=true; getEscortAdData( state.adId, state.adUrl, forceRefresh, listDataCancelToken, requestContext, {needPhone} ) .then(result => { if (result?.status === 'cancelled') { state.dataCancelled=true; state.adDataCancelled=true; return; } state.adData=result?.status === 'ok' ? result :previousAdData || {status:'error'}; }) .catch(error => { if (isOperationCancelledError(error)) { state.dataCancelled=true; state.adDataCancelled=true; return; } state.adData=previousAdData || {status:'error'}; }) .finally(() => { state.adDataLoading=false; state.adDataFinished=true; if (forceRefresh) {state.agencyPhoneRefreshStarted=false; state.agencyPhoneRefreshDone=true;} updateEscortAdPhoneCacheIndex( phoneCacheIndex, state.adId, state.adData ); if (state.group) dirtyGroups.add(state.group); if (phoneBasedAggregation) { applyListMerge(state); if (escortClubPhoneSearchAggregation) { ensureEscortClubPhoneSearchAggregation(state); } } if (!deferListView(state)) { renderEscortListPrice( state.priceLine, state.adData, SETTINGS.searchResultPriceDuration, state.aggregateMembers ); renderEscortListExtraData( state.extraLine, state, getSearchResultExtraFields() ); } if ( !forceRefresh && !massDataCancelled && !listDataCancelToken.cancelled && state.adData?.status !== 'ok' && state.adDataAttempts < 2 ) { state.adDataRetryScheduled=true; setTimeout(() => { state.adDataRetryScheduled=false; state.adDataPrefetchStarted=false; ensureEscortAdDataForState(state); },1200); } if (state.adData?.status === 'ok' && getListDataNeeds().phone && !state.adDataPhoneRequested) ensureEscortAdDataForState(state); if (!forceRefresh && state.adData?.status === 'ok' && getListDataNeeds().agencyPhone && typeof state.adData.isAgencyOrSalonPhone !== 'boolean') ensureEscortAdDataForState(state,true); scheduleListRefresh(); }); } function renderAllEscortListPrices() { if (!SETTINGS.showPricesInSearchResults) return; const baseTop=SETTINGS.showRelatedAdsTileLink ? 22:4; const extraTop=baseTop + (isEscortSearchPriceDisplayed() ? 19:0); for (const state of cardStates.values()) { renderEscortListPrice( state.priceLine, state.adData, SETTINGS.searchResultPriceDuration, state.aggregateMembers ); if (state.priceLine) state.priceLine.style.top=`${baseTop}px`; if (state.extraLine) state.extraLine.style.top=`${extraTop}px`; } const groups=new Set( [...cardStates.values()].map(state => state.group).filter(Boolean) ); for (const group of groups) refreshGroupDisplay(group); } function renderAllEscortListExtraData(fieldValues=getSearchResultExtraFields()) { const fields=normalizeSearchResultExtraFields(fieldValues); for (const state of cardStates.values()) { if (!fields.length) { detachHelperNode(state.extraLine); state.extraLine=null; continue; } if (!state.extraLine) state.extraLine=getListCardExtraDataLine(state.card); renderEscortListExtraData(state.extraLine,state,fields); } } function applyCustomFilters(config=customFilterController?.getConfig()) { if (deferListView()) return; watchMetadataIndex ||= buildCustomFilterWatchMetadataIndex(); const favoritesEnabled=getWatchSettings().enabled; for (const state of cardStates.values()) { const hidden=getHiddenAdMatches(state.adId,getHiddenResultProfileUrls(state.result)).length > 0; state.card.classList.toggle('vm-hidden-result',hidden); const favorite=favoritesEnabled && getBucketWatchMetadata({members:[state]},watchMetadataIndex).favorite; if (state.favorite !== favorite) { state.favorite=favorite; renderListFavoriteStar(state.card,favorite); } if (state.hiddenInResults !== hidden) { state.hiddenInResults=hidden; if (state.group) dirtyGroups.add(state.group); else restoreCard(state); } } if (getSearchResultExtraFields().includes('profile:activeCount')) { updateEscortListActiveCounts(getCustomFilterBuckets(),getBucketActiveAdCount); } const filters=config || getEscortCustomFilters(); const activeFilterCount=getActiveEscortCustomFilterCount(filters); if (!activeFilterCount) { for (const state of cardStates.values()) { if (!state.customHidden) continue; state.customHidden=false; if (state.group) dirtyGroups.add(state.group); else restoreCard(state); } for (const group of dirtyGroups) refreshGroupDisplay(group); dirtyGroups.clear(); customFilterController?.setStatus(massDataCancelled ? 'Pobieranie przerwane. Odśwież stronę, aby wznowić.':''); applyEscortResultSort(cardStates.values(),customFilterController?.getSortConfig(),getGroupAggregateAdIds); updatePageAggregateSummary(); return; } const needsEscortAdData=getCustomFilterDataNeeds(filters).escortAd; let hiddenCount=0; let pendingCount=0; let failedDataCount=0; const needsVisitData=filters.hideVisited || filters.hideUnvisited || filters.hideVisitedOlderThan || filters.hideVisitedRecentThan || filters.showOnlyVisitedOlderThan || filters.showOnlyVisitedRecentThan; for (const bucket of getCustomFilterBuckets()) { const metrics= getBucketCustomFilterMetrics( bucket, filters, watchMetadataIndex, getBucketActiveAdCount(bucket), needsVisitData && bucket.group ? getGroupAggregateAdIds(bucket.group):undefined ); const {shouldHide,unresolved}=evaluateEscortCustomFilters(metrics,filters); const visibilityChanged=bucket.members.some(member => member.customHidden !== shouldHide); for (const member of bucket.members) member.customHidden=shouldHide; if (bucket.group && (visibilityChanged || dirtyGroups.has(bucket.group))) { refreshGroupDisplay(bucket.group); } else if (!bucket.group && visibilityChanged && bucket.members[0]) { restoreCard(bucket.members[0]); } if (shouldHide) { hiddenCount++; } else if (unresolved) { const failed=needsEscortAdData && bucket.members.some(member => member.adData?.status === 'error' && !member.adDataLoading && !member.adDataRetryScheduled && (member.adDataAttempts || 0) >= 2 ); if (failed) failedDataCount++; else pendingCount++; } } dirtyGroups.clear(); if (customFilterController) { if (massDataCancelled) { customFilterController.setStatus( 'Pobieranie przerwane. Odśwież stronę, aby wznowić.' ); } else if (!activeFilterCount) { customFilterController.setStatus(''); } else { customFilterController.setStatus( `Ukryto: ${hiddenCount}` + (pendingCount ? ` • oczekuje na dane: ${pendingCount}`:'') + (failedDataCount ? ` • brak danych: ${failedDataCount}`:'') ); } } applyEscortResultSort(cardStates.values(),customFilterController?.getSortConfig(),getGroupAggregateAdIds); updatePageAggregateSummary(); } function scheduleListNetworkCheck(state) { if ( !state || state.hiddenInResults || state.checkDone || state.checkScheduled || massDataCancelled || listDataCancelToken.cancelled ) { if (state && (massDataCancelled || listDataCancelToken.cancelled)) { state.dataCancelled=true; state.checkDone=true; } return; } state.checkScheduled=true; const requestContext=createRequestContext('results.escorti',listRequestContext); if (!state.result && !deferListView(state)) state.line.textContent='… ogł • od … • … opinii'; enqueueListJob(async () => { try { listDataCancelToken.throwIfCancelled(); const newlyResolvedCache=cacheIndex.byAdId.get(state.adId); if (isListCacheFresh(newlyResolvedCache)) { state.result=newlyResolvedCache; state.stale=false; renderState(state); applyListMerge(state); return; } let searchValue=state.adUrl; if (listSearchMode === 'phone-escorti') { state.escortiPhoneLoading=true; state.adDataPhoneRequested=true; const adDataResult=await getEscortAdData( state.adId, state.adUrl, false, listDataCancelToken, createRequestContext('results.escorti-phone',requestContext), {needPhone:true} ); state.escortiPhoneLoading=false; listDataCancelToken.throwIfCancelled(); state.adData=adDataResult?.status === 'ok' ? adDataResult :{status:'error'}; updateEscortAdPhoneCacheIndex( phoneCacheIndex, state.adId, state.adData ); if (!deferListView(state)) renderEscortListPrice( state.priceLine, state.adData, SETTINGS.searchResultPriceDuration, state.aggregateMembers ); searchValue=isEscortAdPhoneFresh(state.adData) ? getEscortAdPhoneDigits(state.adData):''; if (!searchValue) { state.result={status:'phone-missing'}; state.stale=false; renderState(state); return; } } const result=await getOrStartListCheck( state.adId, searchValue, listSearchMode, listDataCancelToken, requestContext ); listDataCancelToken.throwIfCancelled(); state.result=result; state.stale=false; updateEscortListCacheIndex(cacheIndex,state.adId,result); renderState(state); applyListMerge(state); } catch (e) { if (isOperationCancelledError(e) || listDataCancelToken.cancelled) { state.dataCancelled=true; state.result=state.result || {status:'cancelled'}; return; } log(`Błąd Escorti dla anonsu ${state.adId}`,e); state.result={status:'error'}; state.stale=false; renderState(state); } finally { state.escortiPhoneLoading=false; state.checkDone=true; if (state.group) dirtyGroups.add(state.group); scheduleListRefresh(); } }); } async function initializeListCards(newAnchors) { await ensureEscortPageCache(newAnchors.map(anchor => parseAdIdFromUrl(anchor.href)).filter(Boolean)); await buildIndexInBatches((keys,index) => buildEscortListCacheIndex(keys,index || cacheIndex),getEscortListCacheKeys().filter(key => !indexedListKeys.has(key))); for (const key of getEscortListCacheKeys()) indexedListKeys.add(key); const newStates=[]; let batchStarted=performance.now(),batchCount=0; for (const anchor of newAnchors) { const adUrl=new URL(anchor.getAttribute('href'),location.href).href; const adId=parseAdIdFromUrl(adUrl); const card=anchor.closest('.item-col.col'); if (!adId || !card || cardStates.has(adId)) continue; const index=anchors.length; anchors.push(anchor); if (SETTINGS.showRelatedAdsTileLink) { addRelatedAdsTileLinkButton(card,adId,adUrl); } else { detachHelperNode(card.querySelector('.vm-related-ads-open-button')); } const line=getListCardDataLine(card); if (line) { line.style.display=SETTINGS.showEscortiTileData ? 'block' :'none'; } const priceLine=SETTINGS.showPricesInSearchResults ? getListCardPriceLine(card) :null; const extraLine=getSearchResultExtraFields().length ? getListCardExtraDataLine(card) :null; const state={ adId, adUrl, index, anchor, card, line, priceLine, extraLine, originalDisplay:card.style.display, result:null, stale:false, group:null, mergedHidden:false, customHidden:false, hiddenInResults:getHiddenAdMatches(adId,getHiddenResultProfileUrls(cacheIndex.byAdId.get(adId))).length > 0, adData:getStoredEscortAdData(adId), adDataPrefetchStarted:false, adDataPhoneRequested:false, adDataAttempts:0, adDataRetryScheduled:false, adDataLoading:false, phoneSearchAggregationLoading:false, checkDone:false, checkScheduled:false, dataCancelled:false }; cardStates.set(adId,state); newStates.push(state); card.dataset.vmHiddenManaged='1'; card.classList.toggle('vm-hidden-result',state.hiddenInResults); if (!deferListView(state)) { renderEscortListPrice( state.priceLine, state.adData, SETTINGS.searchResultPriceDuration, state.aggregateMembers ); renderEscortListExtraData( state.extraLine, state, getSearchResultExtraFields() ); } const cache=cacheIndex.byAdId.get(adId) || null; if (cache) { state.result=cache; state.stale=!isListCacheFresh(cache); renderState(state); if (!state.stale) { applyListMerge(state); state.checkDone=true; } } batchCount++; if (performance.now() - batchStarted >= 8 || batchCount >= 24) { batchCount=0; await yieldHelperWork(); batchStarted=performance.now(); } } return newStates; } await initializeListCards(initialPageSelected ? listContext.anchors:[]); customFilterCallbacksReady=true; listInitComplete=true; const unsubscribeQueueProgress=subscribeEscortRequestQueueStatus(scheduleListProgress); window.addEventListener('pagehide',event => { listRefreshPending=true; if (cacheChangeTimer) cacheChangePending=true; clearListViewTimers(); clearTimeout(cacheChangeTimer); cacheChangeTimer=null; if (!event.persisted) { unsubscribeQueueProgress(); document.removeEventListener('visibilitychange',resumeListView); window.removeEventListener('pageshow',resumeListView); } }); const refreshListAfterCacheChange=async () => { clearTimeout(cacheChangeTimer); cacheChangeTimer=null; if (document.hidden || cacheChangeRunning) {cacheChangePending=true; return;} cacheChangePending=false; cacheChangeRunning=true; try { await ensureEscortPageCache([...cardStates.keys()]); if (document.hidden) {cacheChangePending=true; return;} const rebuilt=await buildIndexInBatches(buildEscortListCacheIndex,getEscortListCacheKeys()); if (document.hidden) {cacheChangePending=true; return;} Object.assign(cacheIndex,rebuilt); const expandedKeys=new Set([...aggregationKeyToGroup].filter(([,group]) => group.expanded).map(([key]) => key)); aggregationKeyToGroup.clear(); dirtyGroups.clear(); const fields=getSearchResultExtraFields(); // Przebudowa grup musi być synchroniczna: odświeżenie w połowie pokazywało częściowe liczniki. for (const state of cardStates.values()) { state.adData=getStoredEscortAdData(state.adId); state.result=cacheIndex.byAdId.get(state.adId) || null; state.stale=!isListCacheFresh(state.result); state.group=null; delete state.aggregateMembers; if (state.result) renderState(state); else if (state.line) state.line.textContent=''; if (phoneBasedAggregation) updateEscortAdPhoneCacheIndex(phoneCacheIndex,state.adId,state.adData); } for (const state of cardStates.values()) { applyListMerge(state); if (state.group && [...state.group.aggregationKeys].some(key => expandedKeys.has(key))) state.group.expanded=true; if (state.group) continue; restoreCard(state); setListMergedInfo(state.line,0); renderEscortListPrice(state.priceLine,state.adData,SETTINGS.searchResultPriceDuration); renderEscortListExtraData(state.extraLine,state,fields); } applyCustomFilters(); dirtyStates.clear(); listRefreshPending=false; clearListViewTimers(); } catch (error) {log('Nie udało się odświeżyć listy po zmianie cache',error);} finally { cacheChangeRunning=false; if (!document.hidden) { if (cacheChangePending) cacheChangeTimer=setTimeout(refreshListAfterCacheChange,100); else if (listRefreshPending) scheduleListRefresh(); } } }; document.addEventListener('vm-helper-cache-change',event => { if (!(event.detail?.keys || []).some(key => /^(?:vm_escort_ad_data_|vm_escorti_list_)\d+$/.test(key))) return; if (document.hidden || cacheChangeRunning) {cacheChangePending=true; return;} if (!cacheChangeTimer) cacheChangeTimer=setTimeout(refreshListAfterCacheChange,100); }); function resumeListView() { if (document.hidden) {listRefreshPending=true; clearListViewTimers(); return;} if (cacheChangePending) void refreshListAfterCacheChange(); else if (listRefreshPending) scheduleListRefresh(); } document.addEventListener('visibilitychange',resumeListView); window.addEventListener('pageshow',resumeListView); hiddenUiRefreshers.add(() => { watchMetadataIndex=null; applyCustomFilters(); ensureListDataNeeds(); }); applyCustomFilters(customFilterController?.getConfig()); if (typeof requestIdleCallback === 'function') requestIdleCallback(() => ensureListDataNeeds(),{timeout:250}); else setTimeout(ensureListDataNeeds,32); loadResult=await loadAdditionalEscortListPages( listContext, listPagesCancelToken, progressInfo => { if (!listPagesControl || !needsAdditionalListLoading) return; const totalText=progressInfo.totalPages ? `${progressInfo.loadedPages}/${progressInfo.totalPages}` :`${progressInfo.loadedPages}`; const perLocationText=progressInfo.pagesPerLocation === 'all' ? `${progressInfo.locationPages} str.` :`${progressInfo.locationPages}/${progressInfo.pagesPerLocation}`; listPagesControl.setProgress?.( `Ładowanie: ${totalText} stron • ${progressInfo.locationLabel} ${perLocationText}`, () => listPagesCancelToken?.cancel('Przerwano ładowanie stron/lokalizacji') ); }, (_location,doc) => updateFavoriteSearchEditLocation(doc), async cards => { const newStates=await initializeListCards(cards.flatMap(card => [...card.querySelectorAll(':scope > a:is([href*="/anons/"],[href*="/personal-ads/"])')])); applyCustomFilters(); ensureListDataNeeds(newStates); } ); listPagesControl?.setProgress?.(''); customFilterController?.setListIncomplete(!loadResult.allPagesLoaded); if (listPagesControl?.status) { if (loadResult.cancelled) { listPagesControl.status.textContent= `przerwano po ${loadResult.loadedPages} ${polishPageWord(loadResult.loadedPages)}`; } else { listPagesControl.status.textContent=''; } if (listPagesControl.select) { listPagesControl.select.disabled=false; } } listContext=getEscortClubListContext(); hideEscortMainResultsTitle(listContext); const breadcrumbMultiLocations= getEscortMultiLocationsForCurrentPage(); const breadcrumbTotals=Object.values( loadResult.locationTotalAds || {} ) .map(value => Number(value)) .filter(value => Number.isFinite(value) && value >= 0 ); const breadcrumbExpectedLocations=Math.max( 1, Number(loadResult.locationCount) || breadcrumbMultiLocations.length || 1 ); const breadcrumbTotal= breadcrumbMultiLocations.length > 1 && breadcrumbTotals.length === breadcrumbExpectedLocations ? breadcrumbTotals.reduce( (sum,value) => sum + value, 0 ) :null; updateEscortMultiResultsBreadcrumb( breadcrumbMultiLocations, breadcrumbTotal ); const multiSelectionActive=activeMultiLocations.length > 0; const currentPageTotal=getEscortListTotalResultCount( document, listContext ); const hasMultiResults=breadcrumbTotal != null ? breadcrumbTotal > 0 :activeMultiLocations.length === 1 && currentPageTotal != null ? currentPageTotal > 0 :!!listContext?.anchors?.length; if (multiSelectionActive) { setEscortMultiNoResultsMessageVisible(!hasMultiResults); } if (!listContext?.anchors?.length) { if ((breadcrumbTotal ?? currentPageTotal) !== 0) { recordParserIssue( 'Escort.club - lista po doładowaniu', 'Po doładowaniu stron nie znaleziono kafelków anonsów.' ); log('Po doładowaniu nie znaleziono kafelków anonsów Escort.club'); } return; } updateLoadedSummaryLine(); applyCustomFilters(); function ensureListDataNeeds(states=cardStates.values()) { const {escortAd:needsAd,escorti:needsEscorti,agencyPhone}=getListDataNeeds(); for (const state of states) { const hidden=getHiddenAdMatches(state.adId,getHiddenResultProfileUrls(state.result)).length > 0; if (state.hiddenInResults !== hidden) { state.hiddenInResults=hidden; if (state.group) dirtyGroups.add(state.group); } if (state.hiddenInResults) continue; if (needsAd) ensureEscortAdDataForState(state,!!(state.adDataPrefetchStarted && agencyPhone && state.adData?.status === 'ok' && typeof state.adData.isAgencyOrSalonPhone !== 'boolean')); if (needsEscorti) { if (state.checkSkipped) {state.checkSkipped=false; state.checkDone=false;} scheduleListNetworkCheck(state); } else if (!state.checkScheduled && !state.checkDone) { state.checkSkipped=true; state.checkDone=true; } } scheduleListRefresh(); } } const IMAGE_TOOLS_ID='vm-gallery-image-tools'; function getEscortListDataRequirements(filters,sortField,extraFields,{showPrice,showEscorti,mergeAds,phoneBasedAggregation,escortClubPhoneSearchAggregation}) { const filterNeeds=getCustomFilterDataNeeds(filters); const phoneAggregation=phoneBasedAggregation && (mergeAds || extraFields.some(field => field.id === 'profile:activeCount')); return { escorti:!!(showEscorti || filterNeeds.escorti || (mergeAds && !phoneBasedAggregation) || extraFields.some(field => field.source === 'escorti' || field.source === 'profile')), escortAd:!!(filterNeeds.escortAd || showPrice || ['price','published'].includes(sortField) || phoneAggregation || extraFields.some(field => !['escorti','profile','garso'].includes(field.source) || field.path?.[0] === 'cities')), agencyPhone:filterNeeds.agencyPhone, phone:!!(phoneAggregation || extraFields.some(field => field.source === 'ad' && field.path?.[0] === 'phone')), phoneSearch:!!(phoneAggregation && escortClubPhoneSearchAggregation) }; } function getEscortListDataProgress(states,needs,phoneSearchIndex=new Map()) { const progress={total:0,completed:0,pending:0,failed:0,cancelled:0,escortPending:0}; for (const state of states) { if (state.hiddenInResults) continue; progress.total++; const adBusy=!!(state.adDataLoading || state.adDataRetryScheduled || state.agencyPhoneRefreshStarted); const escortiBusy=!!(state.checkScheduled && !state.checkDone); const requiresAd=needs.escortAd || adBusy; const requiresEscorti=needs.escorti || escortiBusy; const entry=phoneSearchIndex.get(getEscortAdPhoneDigits(state.adData)); const adPending=requiresAd && (adBusy || (!state.adDataPrefetchStarted && !state.adDataFinished) || (needs.agencyPhone && state.adData?.status === 'ok' && typeof state.adData.isAgencyOrSalonPhone !== 'boolean' && !state.agencyPhoneRefreshDone) || (needs.phone && state.adData?.status === 'ok' && !isEscortAdPhoneFresh(state.adData) && !state.adDataPhoneRequested)); const escortiPending=requiresEscorti && (escortiBusy || (!state.checkDone && !(state.result?.status === 'ok' && !state.stale))); const phonePending=!!state.phoneSearchAggregationLoading || !!(needs.phoneSearch && entry?.loading); const adMissing=requiresAd && (state.adDataCancelled || state.adData?.status !== 'ok' || (needs.agencyPhone && typeof state.adData.isAgencyOrSalonPhone !== 'boolean') || (needs.phone && !isEscortAdPhoneFresh(state.adData))); const missing=adMissing || (requiresEscorti && (state.result?.status !== 'ok' || (state.dataCancelled && state.stale))) || (needs.phoneSearch && (!entry || entry.error || entry.incomplete)); if (!adPending && !escortiPending && !phonePending && !missing) progress.completed++; else if (state.dataCancelled) progress.cancelled++; else if (adPending || escortiPending || phonePending) { progress.pending++; if (adPending || phonePending || state.escortiPhoneLoading) progress.escortPending++; } else progress.failed++; } if (!needs.escortAd && !needs.escorti && !progress.pending) progress.total=progress.completed=progress.failed=progress.cancelled=0; return progress; } function formatEscortListDataProgress(progress,queue={},cancelled=false) { if (!progress.total || progress.completed === progress.total) return ''; const pause=progress.escortPending > 0 && (queue.reloadRequired || queue.blockedUntil > Date.now()); const label=cancelled ? 'Uzupełnianie przerwane':pause ? 'Uzupełnianie danych wstrzymane': progress.pending ? 'Uzupełnianie danych':'Uzupełnianie zakończone z brakami'; return `${label}: ${progress.completed}/${progress.total} kafelków` + (progress.pending ? ` • oczekuje: ${progress.pending}`:'') + (progress.failed ? ` • brak danych: ${progress.failed}`:'') + (progress.cancelled ? ` • przerwano: ${progress.cancelled}`:'') + (pause ? queue.reloadRequired ? ' • HTTP 403 — odśwież stronę':` • pauza Escort.club do ${new Date(queue.blockedUntil).toLocaleTimeString('pl-PL')}`:''); } const IMAGE_SERVER_DATE_ID='vm-gallery-image-server-date'; const LIGHTBOX_IMAGE_URL_ID='vm-lightbox-image-url'; const mediaServerDateCache=new Map(); function isEscortGalleryPhotoUrl(value) { if (!value) return false; try { const url=new URL(value,location.href); return /(^|\.)escort\.club$/i.test(url.hostname) && /\/galleries\//i.test(url.pathname) && /\.(?:jpe?g|png|webp|avif)(?:$|[?#])/i.test(url.href); } catch {return false;} } function isEscortGalleryVideoUrl(value) { if (!value) return false; try { const url=new URL(value,location.href); return /(^|\.)escort\.club$/i.test(url.hostname) && /\/videos\//i.test(url.pathname) && /\.(?:mp4|webm|m4v|mov)(?:$|[?#])/i.test(url.href); } catch {return false;} } function buildConfiguredImageSearchUrl(urlPrefix,imageUrl) { return `${urlPrefix}${encodeURIComponent(imageUrl)}`; } function getEnabledImageSearchEngines() { return normalizeImageSearchEngines(SETTINGS.imageSearchEngines) .filter(engine => engine.enabled !== false); } function getActiveGallerySlide() { const slider=document.querySelector('.content-gallery-col .galleryContainer #lightSlider'); if (!slider) return null; const active=slider.querySelector(':scope > li.lslide.active, :scope > li.active'); if (active) return active; const wrapper=slider.closest('.lSSlideWrapper'); if (!wrapper) return null; const wr=wrapper.getBoundingClientRect(); let best=null,bestArea=0; for (const li of slider.querySelectorAll(':scope > li')) { const r=li.getBoundingClientRect(); const w=Math.max(0,Math.min(r.right,wr.right) - Math.max(r.left,wr.left)); const h=Math.max(0,Math.min(r.bottom,wr.bottom) - Math.max(r.top,wr.top)); if (w * h > bestArea) {bestArea=w * h; best=li;} } return best; } function getActiveGalleryMedia() { const slide=getActiveGallerySlide(); if (!slide) return null; const anchor=slide.querySelector('a.simple-zoom-image'); if (!anchor) return null; const isVideo=anchor.classList.contains('video-layer'); const images=[...anchor.querySelectorAll('img')]; const img=isVideo ? images.find(candidate => /\/thumbs\//i.test(candidate.currentSrc || candidate.src || '')) || images.at(-1) :images[0]; if (!img) return null; let mediaUrl=anchor.href || ''; if (isVideo) { if (!isEscortGalleryVideoUrl(mediaUrl)) return null; } else { if (!isEscortGalleryPhotoUrl(mediaUrl)) mediaUrl=img.currentSrc || img.src || ''; if (!isEscortGalleryPhotoUrl(mediaUrl)) return null; } const r=img.getBoundingClientRect(); if (r.width < 100 || r.height < 100) return null; return { slide, img, mediaUrl, mediaType:isVideo ? 'video':'photo' }; } function getActiveGalleryPhoto(activeMedia=getActiveGalleryMedia()) { if (!activeMedia || activeMedia.mediaType !== 'photo') return null; return { ...activeMedia, imageUrl:activeMedia.mediaUrl }; } function createImageToolButton(engine) { const btn=makeButton('',`${engine.name} ↗`,{title:`Otwórz ${engine.name} z adresem aktualnego zdjęcia`}); Object.assign(btn.style,{ display:'block', width:'58px', maxWidth:'58px', padding:'3px', border:'1px solid var(--vm-ui-panel-border, rgba(255,255,255,.75))', borderRadius:'4px', background:'var(--vm-ui-panel-bg, rgba(20,20,20,.82))', color:'var(--vm-ui-panel-text, #fff)', fontSize:'9px', lineHeight:'1.15', fontWeight:'700', fontFamily:'var(--vm-ui-font-family, Arial, sans-serif)', textAlign:'center', cursor:'pointer', boxShadow:'var(--vm-ui-shadow, 0 1px 5px rgba(0,0,0,.45))', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', transition:'border-color .12s ease, background .12s ease, color .12s ease' }); btn.addEventListener('mouseenter',() => { btn.style.borderColor= 'var(--vm-ui-accent, #f54da3)'; btn.style.background= 'var(--vm-ui-panel-bg-alt, var(--vm-ui-panel-bg, rgba(20,20,20,.82)))'; }); btn.addEventListener('mouseleave',() => { btn.style.borderColor= 'var(--vm-ui-panel-border, rgba(255,255,255,.75))'; btn.style.background= 'var(--vm-ui-panel-bg, rgba(20,20,20,.82))'; }); const open=event => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); const imageUrl=btn.closest(`#${IMAGE_TOOLS_ID}`)?.dataset.imageUrl; if (imageUrl && engine.urlPrefix) GM_openInTab(buildConfiguredImageSearchUrl(engine.urlPrefix,imageUrl),{active:true,insert:true}); }; btn.addEventListener('mousedown',e => {e.preventDefault(); e.stopPropagation();},true); btn.addEventListener('click',open,true); return btn; } function getOrCreateImageTools() { let tools=document.getElementById(IMAGE_TOOLS_ID); if (tools) return tools; tools=prepareHelperStyleApi( makeElement('div') ); tools.id=IMAGE_TOOLS_ID; Object.assign(tools.style,{ position:'absolute',display:'none',zIndex:'1000',flexDirection:'column', alignItems:'stretch',gap:'3px',pointerEvents:'auto' }); for (const engine of getEnabledImageSearchEngines()) tools.appendChild(createImageToolButton(engine)); document.body.appendChild(tools); return tools; } function parseResponseHeader(headers,name) { const safeName=String(name || '').replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); const match=String(headers || '').match(new RegExp(`^${safeName}:\\s*(.+)$`,'im')); return match ? match[1].trim():null; } function formatServerMediaDate(date) { return new Intl.DateTimeFormat('pl-PL',{ day:'2-digit', month:'2-digit', year:'numeric' }).format(date); } function formatMinimumMediaAge(date) { const ms=Math.max(0,Date.now() - date.getTime()); const days=Math.floor(ms / 86400000); if (days < 1) return '<1 dnia'; if (days < 31) return `${days} dni`; const months=Math.floor(days / 30.4375); if (months < 12) return `${months} mies.`; const years=Math.floor(months / 12); const restMonths=months % 12; return restMonths ? `${years} r. ${restMonths} mies.`:`${years} r.`; } function getMediaServerDate(mediaUrl) { if (mediaServerDateCache.has(mediaUrl)) return mediaServerDateCache.get(mediaUrl); const promise=gmRequest({ method:'HEAD', url:mediaUrl, timeout:12000 }).then(response => { const value=parseResponseHeader(response.responseHeaders,'Last-Modified'); if (!value) return null; const timestamp=Date.parse(value); return Number.isFinite(timestamp) ? new Date(timestamp):null; }).catch(error => { log('Nie udało się odczytać daty pliku galerii z serwera',error); return null; }); mediaServerDateCache.set(mediaUrl,promise); return promise; } function getOrCreateImageServerDateLabel() { let label=document.getElementById(IMAGE_SERVER_DATE_ID); if (label) return label; label=prepareHelperStyleApi( makeElement('div') ); label.id=IMAGE_SERVER_DATE_ID; Object.assign(label.style,{ position:'absolute', display:'none', zIndex:'999', padding:'3px 6px', border:'1px solid var(--vm-ui-panel-border, rgba(255,255,255,.35))', borderRadius:'4px', background:'var(--vm-ui-panel-bg, rgba(20,20,20,.72))', color:'var(--vm-ui-panel-text, #fff)', fontSize:'9px', lineHeight:'1.15', fontWeight:'600', fontFamily:'var(--vm-ui-font-family, Arial, sans-serif)', whiteSpace:'nowrap', pointerEvents:'auto', cursor:'help', boxShadow:'var(--vm-ui-shadow, 0 1px 4px rgba(0,0,0,.3))' }); document.body.appendChild(label); return label; } async function updateMediaServerDateLabel(active) { const label=getOrCreateImageServerDateLabel(); if (!active) { label.style.display='none'; label.dataset.mediaUrl=''; label.dataset.loaded=''; return; } const {slide,img,mediaUrl,mediaType}=active; if (label.parentElement !== slide) slide.appendChild(label); if (getComputedStyle(slide).position === 'static') slide.style.position='relative'; const sameLoadedMedia=label.dataset.mediaUrl === mediaUrl && label.dataset.loaded === '1'; label.style.display='block'; const positionLabel=() => { const sr=slide.getBoundingClientRect(); const ir=img.getBoundingClientRect(); label.style.left=`${Math.round(Math.max(4,ir.left - sr.left + 8))}px`; label.style.top=`${Math.round(Math.max(4,ir.top - sr.top + 8))}px`; }; if (sameLoadedMedia) { positionLabel(); return; } label.dataset.mediaUrl=mediaUrl; label.dataset.loaded='0'; label.textContent='data: …'; label.title=`Odczytuję nagłówek Last-Modified z serwera ${mediaType === 'video' ? 'filmu':'obrazu'}.`; positionLabel(); const serverDate=await getMediaServerDate(mediaUrl); if (label.dataset.mediaUrl !== mediaUrl) return; label.dataset.loaded='1'; if (!serverDate) { label.textContent='data serwera: brak'; label.title=`Serwer nie zwrócił nagłówka Last-Modified dla tego ${mediaType === 'video' ? 'filmu':'zdjęcia'}.`; positionLabel(); return; } label.textContent=`${formatServerMediaDate(serverDate)} (${formatMinimumMediaAge(serverDate)})`; label.title= `Data pochodzi z nagłówka HTTP Last-Modified serwera ${mediaType === 'video' ? 'filmu':'obrazu'}.\n` + `Wiek w nawiasie jest wiekiem minimalnym: materiał mógł powstać wcześniej niż wskazuje data pliku na serwerze.\n` + `Last-Modified nie jest datą wykonania materiału.`; positionLabel(); } function updateGalleryImageTools() { const tools=getOrCreateImageTools(); const activeMedia=getActiveGalleryMedia(); const active=getActiveGalleryPhoto(activeMedia); updateMediaServerDateLabel(activeMedia); if (!active || !SETTINGS.showImageSearchButtons || !getEnabledImageSearchEngines().length) { tools.style.display='none'; tools.dataset.imageUrl=''; return; } const {slide,img,imageUrl}=active; if (tools.parentElement !== slide) slide.appendChild(tools); if (getComputedStyle(slide).position === 'static') slide.style.position='relative'; tools.dataset.imageUrl=imageUrl; tools.style.display='flex'; const sr=slide.getBoundingClientRect(); const ir=img.getBoundingClientRect(); const tr=tools.getBoundingClientRect(); tools.style.left=`${Math.round(Math.max(4,ir.right - sr.left - tr.width - 4))}px`; tools.style.top=`${Math.round(Math.max(4,ir.top - sr.top + 8))}px`; } function updateLightboxImageUrl() { const lightbox=document.querySelector('#lightbox'); const existing=document.getElementById(LIGHTBOX_IMAGE_URL_ID); if (!lightbox) return; if (getComputedStyle(lightbox).display === 'none') { if (existing) existing.style.display='none'; return; } const img=lightbox.querySelector('.lb-image'); const dataContainer=lightbox.querySelector('.lb-dataContainer'); if (!img || !dataContainer) return; const imageUrl=img.currentSrc || img.src || ''; if (!/^https:\/\/static\.escort\.club\//i.test(imageUrl)) { if (existing) existing.style.display='none'; return; } let box=existing; if (!box) { box=prepareHelperStyleApi( makeElement('div') ); box.id=LIGHTBOX_IMAGE_URL_ID; Object.assign(box.style,{ display:'block',clear:'both',width:'100%',boxSizing:'border-box',padding:'7px 4px 5px', fontFamily:'Arial,sans-serif',fontSize:'11px',lineHeight:'1.25',textAlign:'left',wordBreak:'break-all' }); const link=makeElement('a','',null,{target:'_blank',rel:'noopener noreferrer'}); Object.assign(link.style,{color:'var(--vm-ui-accent, #f54da3)',textDecoration:'underline',cursor:'pointer'}); link.addEventListener('click',event => { const href=link.href; if (!href) return; stopUiEvent(event); GM_openInTab(href,{active:true,insert:true}); }); box.appendChild(link); dataContainer.appendChild(box); } else if (box.parentElement !== dataContainer) { dataContainer.appendChild(box); } const link=box.querySelector('a'); box.style.display='block'; link.href=imageUrl; link.textContent=imageUrl; link.title='Otwórz samo zdjęcie w nowej karcie'; } function initEscortGalleryWatcher() { if (!getEscortAdIdFromPath(location.pathname)) return; if (!document.getElementById('vm-gallery-navigation-contrast')) { appendHelperStyle('vm-gallery-navigation-contrast',` .content-gallery-col .lSAction > a:not(.disabled), #lightbox .lSAction > a:not(.disabled), #lightbox .lb-nav a.lb-prev:not(.disabled), #lightbox .lb-nav a.lb-next:not(.disabled) { filter:drop-shadow(0 0 1px #000) drop-shadow(0 0 3px #000) !important; opacity:1 !important; }`); } const gallery=document.querySelector('.content-gallery-col'); const initialPhoneContainer=getEscortPhoneLink()?.closest('.adsPhone'); if (!gallery && !initialPhoneContainer) return; let observedLightbox=null; let observedPhoneContainer=null; let observedTargetsBound=false; const scheduleGalleryUpdate=createAnimationFrameScheduler(updateGalleryImageTools); const scheduleLightboxUpdate=createAnimationFrameScheduler(updateLightboxImageUrl); const isOwnScriptElement=target => { const element=target?.nodeType === Node.ELEMENT_NODE ? target :target?.parentElement; return !!element?.closest( `#${IMAGE_TOOLS_ID}, #${IMAGE_SERVER_DATE_ID}, ` + `#${LIGHTBOX_IMAGE_URL_ID}, .vm-escort-copy-phone` ); }; const isOnlyOwnScriptMutation=mutation => { if (isOwnScriptElement(mutation.target)) return true; if (mutation.type !== 'childList') return false; const changedNodes=[ ...mutation.addedNodes, ...mutation.removedNodes ]; return changedNodes.length > 0 && changedNodes.every(isOwnScriptElement); }; const observer=new MutationObserver(mutations => { let galleryChanged=false; let lightboxChanged=false; let phoneChanged=false; for (const mutation of mutations) { if (isOnlyOwnScriptMutation(mutation)) continue; if ( observedPhoneContainer && ( mutation.target === observedPhoneContainer || observedPhoneContainer.contains(mutation.target) ) ) { phoneChanged=true; } else if ( observedLightbox && (mutation.target === observedLightbox || observedLightbox.contains(mutation.target)) ) { lightboxChanged=true; } else { galleryChanged=true; } } if (galleryChanged) scheduleGalleryUpdate(); if (lightboxChanged) scheduleLightboxUpdate(); if (phoneChanged) checkAndAddButtons(); }); const refreshObservedTargets=() => { const lightbox=document.querySelector('#lightbox'); const phoneContainer=getEscortPhoneLink()?.closest('.adsPhone') || null; if ( observedTargetsBound && lightbox === observedLightbox && phoneContainer === observedPhoneContainer ) return; observer.disconnect(); if (gallery) { observer.observe(gallery,{ subtree:true, childList:true, attributes:true, attributeFilter:['class','src','srcset','href'] }); } observedLightbox=lightbox; if (observedLightbox) { observer.observe(observedLightbox,{ subtree:true, childList:true, attributes:true, attributeFilter:['class','src','style'] }); scheduleLightboxUpdate(); } observedPhoneContainer=phoneContainer; if (observedPhoneContainer) { observer.observe(observedPhoneContainer,{ subtree:true, childList:true, characterData:true, attributes:true, attributeFilter:['href','data-phone-id','data-show-phone'] }); } observedTargetsBound=true; }; if (gallery) { gallery.addEventListener('load',scheduleGalleryUpdate,true); gallery.addEventListener('transitionend',scheduleGalleryUpdate,true); gallery.addEventListener('click',event => { scheduleGalleryUpdate(); const galleryAnchor=event.target.closest?.('a.simple-zoom-image'); if (galleryAnchor) { requestAnimationFrame(refreshObservedTargets); } },true); } window.addEventListener('resize',scheduleGalleryUpdate,{passive:true}); refreshObservedTargets(); if (gallery) scheduleGalleryUpdate(); } function getAdIdFromUrl() { return getEscortAdIdFromPath(); } function buildGarsoAdLinkSearchTerm(adId) { const normalizedAdId=String(adId || '').match(/^\d+$/)?.[0]; if (!normalizedAdId) return ''; return `"escort.club / anons / ${normalizedAdId}.html"`; } function normalizeGarsoSearchTerm(term) { const raw=String(term || '').trim(); const candidate=raw.replace(/^["“”„]+|["“”„]+$/g,''); if (!/^(?:https?:\/\/)?(?:(?:[a-z]{2}|www)\.)?escort\.club\/(?:anons|personal-ads)\/\d+\.html(?:[?#]|\/?$)/i.test(candidate)) return raw; const url=normalizeEscortiAdUrl(/^https?:\/\//i.test(candidate) ? candidate:`https://${candidate}`); return url ? buildGarsoAdLinkSearchTerm(parseAdIdFromUrl(url)):raw; } function buildGarsoPhoneMaskSearchTerm(searchTerm) { const digits=normalizeEscortCachedPhone(searchTerm); return digits?.length === 9 ? `"${digits.slice(0,3)}-${digits.slice(3,6)}-xxx"`:''; } function getGarsoSearchTerms(term,searchMode='') { if (searchMode !== 'phone') return [term]; const phone=formatEscortPhoneForClipboard(term,'local-hyphens'); return phone ? [`"${phone}"`,buildGarsoPhoneMaskSearchTerm(phone)].filter(Boolean):[]; } function getGarsoSearchQuery(term,searchMode='') { const terms=(Array.isArray(term) ? term:getGarsoSearchTerms(term,searchMode)).filter(Boolean); if (searchMode === 'phone') terms.reverse(); return {term:terms.join(' '),andorType:terms.length > 1 ? 'or':'and'}; } function queryFirstEscortElement(selectors,root=document) { for (const selector of selectors) { const element=root.querySelector(selector); if (element) return element; } return null; } function getEscortPhoneLink() { return queryFirstEscortElement([ '.content-info-col.-info .adsPhone a[href^="tel:"]', '.content-info-col.-info .adsPhone a[data-show-phone][data-phone-id]', '.content-info-col.-info a[href^="tel:"]', '.content-info-col.-info a[data-show-phone][data-phone-id]', '.content-info-col.-info .adsPhone a[href]', '.adsPhone a[href^="tel:"]', '.content-info .adsPhone a[data-show-phone][data-phone-id]', '.adsPhone a[data-show-phone][data-phone-id]', '[data-show-phone][data-phone-id]', '.adsPhone a[href]' ]); } function getEscortButtonsContainer(phoneLink=getEscortPhoneLink()) { const phoneContact=phoneLink?.closest('.content-contact'); if (phoneContact?.closest('.content-info-col.-info')) return phoneContact; const desktopInfo=document.querySelector('.content-info-col.-info'); if (desktopInfo) { const desktopPhone=queryFirstEscortElement([ '.adsPhone a[data-show-phone][data-phone-id]', '.adsPhone a[href^="tel:"]', '.adsPhone a[href]' ],desktopInfo); return desktopPhone?.closest('.content-contact') || desktopInfo.querySelector('.content-info') || desktopInfo; } if (phoneContact) return phoneContact; return queryFirstEscortElement([ '.content-info .content-contact', '.content-info-col .content-info', '.content-info-col' ]); } function getEscortTipMessageButtonsContainer(scope=document) { return [...scope.querySelectorAll('.content-contact')].find(container => container.querySelector('a[href*="action=tipTokens"]') && container.querySelector('a[href*="action=sendMessage"]') ) || null; } function placeEscortResearchPanelAboveActionButtons( panel, fallbackContainer=null, scopeDocument=document, {retry=true}={} ) { if (!panel || !scopeDocument) return false; const getPlacement=() => { const infoColumn= scopeDocument.querySelector( '.content-info-col.-info' ) || fallbackContainer?.closest?.( '.content-info-col.-info' ) || scopeDocument.querySelector( '.content-info-col' ); if (!infoColumn) return null; const actionButtons= getEscortTipMessageButtonsContainer( infoColumn ); const moreAboutSection= infoColumn.querySelector( '.content-hours' ); const reference= actionButtons || moreAboutSection; if (reference?.parentNode) { return { parent:reference.parentNode, reference }; } return { parent: infoColumn.querySelector( '.content-info' ) || infoColumn, reference:null }; }; const place=() => { const placement=getPlacement(); if (!placement?.parent) return false; if ( panel.parentNode !== placement.parent || panel.nextSibling !== placement.reference ) { placement.parent.insertBefore( panel, placement.reference ); } return true; }; if (place()) return true; if (!retry) return false; let attempts=0; const timer=setInterval(() => { attempts++; if ( place() || attempts >= 40 ) { clearInterval(timer); } },250); return false; } function getFormattedPhoneNumber(phoneEl=getEscortPhoneLink()) { if (!phoneEl) return null; const href=String(phoneEl.getAttribute('href') || '').trim(); const candidates=[ /^tel:/i.test(href) ? href.replace(/^tel:/i,''):'', phoneEl.getAttribute('data-phone'), phoneEl.textContent ]; for (const value of candidates) { const digits=normalizeEscortCachedPhone(value); if (!digits) continue; return digits.length === 9 ? digits.replace(/(\d{3})(\d{3})(\d{3})/,'$1-$2-$3') :`+${digits}`; } return null; } function formatEscortPhoneForClipboard( value, format=SETTINGS.phoneClipboardFormat ) { const digits=normalizeEscortCachedPhone(value); if (!digits) return null; if (digits.length !== 9) return `+${digits}`; const groups=digits.match(/\d{3}/g); if (!groups) return digits; return normalizePhoneClipboardFormat(format) === 'international-spaces' ? `+48 ${groups.join(' ')}` :groups.join('-'); } function copyEscortPhoneToClipboard(value) { try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(value,'text'); return Promise.resolve(); } } catch {} if (navigator.clipboard?.writeText) { return navigator.clipboard.writeText(value); } return new Promise((resolve,reject) => { const textarea=makeElement('textarea','',null,{value}); textarea.setAttribute('readonly',''); Object.assign(textarea.style,{ position:'fixed', left:'-9999px', top:'0' }); document.body.appendChild(textarea); textarea.select(); try { if (!document.execCommand('copy')) throw new Error('copy failed'); resolve(); } catch (error) { reject(error); } finally { detachHelperNode(textarea); } }); } function renderEscortPhoneCopyButton(phone) { if (SETTINGS.showPhoneCopyButton === false) { for (const button of document.querySelectorAll( '.vm-escort-copy-phone' )) { detachHelperNode(button); } return; } const digits=normalizeEscortCachedPhone(phone); if (!digits) return; const formatted=formatEscortPhoneForClipboard(digits); if (!formatted) return; for (const phoneBox of document.querySelectorAll('.adsPhone')) { const phoneLink=phoneBox.querySelector( '[data-show-phone], a[href^="tel:"]' ); if (!phoneLink) continue; const phoneContact=phoneBox.closest('.content-contact') || phoneBox; for (const node of [...phoneContact.childNodes]) { if ( node !== phoneLink && node.nodeType === Node.TEXT_NODE && /^\s*Telefon\s*:\s*$/i.test(node.textContent || '') ) { node.textContent=''; } } for (const label of phoneContact.querySelectorAll( '.label, strong, b, label, span' )) { if ( label !== phoneLink && !label.contains(phoneLink) && /^\s*Telefon\s*:\s*$/i.test(label.textContent || '') ) { label.style.display='none'; } } let copyButton=phoneBox.querySelector('.vm-escort-copy-phone'); if (!copyButton) { copyButton=makeElement('button'); copyButton.type='button'; copyButton.className='vm-escort-copy-phone'; copyButton.textContent='Kopiuj'; Object.assign(copyButton.style,{ display:'inline-flex', alignItems:'center', justifyContent:'center', marginLeft:'8px', padding:'4px 7px', border:`1px solid ${getEscortPagePinkColor()}`, borderRadius:'6px', background:'transparent', color:getEscortPagePinkColor(), fontSize:'10px', fontWeight:'600', lineHeight:'1', whiteSpace:'nowrap', verticalAlign:'middle', cursor:'pointer' }); copyButton.addEventListener('click',event => { stopUiEvent(event); const value=copyButton.dataset.copyValue; if (!value) return; copyEscortPhoneToClipboard(value).then(() => { copyButton.textContent='Skopiowano'; clearTimeout(copyButton._vmRestoreTimer); copyButton._vmRestoreTimer=setTimeout(() => { copyButton.textContent='Kopiuj'; },1200); }).catch(() => { copyButton.textContent='błąd kopiowania'; }); }); } copyButton.dataset.copyValue=formatted; copyButton.title=`Skopiuj ${formatted}`; if (phoneLink.nextElementSibling !== copyButton) { phoneLink.insertAdjacentElement('afterend',copyButton); } } } function normalizeGarsoCacheSearchTerm(searchMode,searchTerm) { if (searchMode === 'phone') { return normalizeEscortCachedPhone(searchTerm) || ''; } if (searchMode === 'address') { const match=String(searchTerm || '').match(/(?:anons|personal-ads)\s*\/\s*(\d+)\.html/i); if (match) return match[1]; } return normalizeText(searchTerm); } function garsoSearchCountCacheKey(searchMode,searchTerm) { if (searchMode !== 'phone' && searchMode !== 'address') return null; const normalizedTerm=normalizeGarsoCacheSearchTerm(searchMode,searchTerm); if (!normalizedTerm) return null; const versionSuffix=['phone','address'].includes(searchMode) ? `_v${GARSO_PHONE_SEARCH_CACHE_VERSION}` :''; return 'vm_garso_search_count_' + `${searchMode}${versionSuffix}_${encodeURIComponent(normalizedTerm)}`; } function getGarsoSearchCountCache(searchMode,searchTerm) { const key=garsoSearchCountCacheKey(searchMode,searchTerm); if (!key) return null; try { const value=readPersistentCacheValue(key,null); const count=Number(value?.count); const checkedAt=Number(value?.checkedAt); const topicLinksCheckedAt=Number(value?.topicLinksCheckedAt); const topicLinks=Array.isArray(value?.topicLinks) ? mergeUniqueGarsoTopics(value.topicLinks) :[]; const phoneSearchVariant=['full','masked','both'].includes(value?.phoneSearchVariant) ? value.phoneSearchVariant:null; const variantsKnown=searchMode !== 'phone' || !!phoneSearchVariant; const topicLinksFresh=variantsKnown && Number.isFinite(topicLinksCheckedAt) && topicLinksCheckedAt > 0 && Date.now() - topicLinksCheckedAt < GARSO_TOPIC_LINKS_CACHE_TTL_MS; if (!Number.isFinite(count) || count < 0 || !Number.isFinite(checkedAt) || checkedAt <= 0) { return null; } return { count, checkedAt, phoneSearchVariant, topicLinks:topicLinksFresh ? topicLinks:[], topicLinksCheckedAt:topicLinksFresh ? topicLinksCheckedAt:null, topicLinksFresh, fresh:variantsKnown && Date.now() - checkedAt < getGarsoCacheTtlMs() }; } catch (error) { log('Błąd odczytu wyniku Garsoniery z cache',error); return null; } } function setGarsoSearchCountCache( searchMode, searchTerm, count, { phoneSearchVariant=null, topicLinks, topicLinksCheckedAt=null }={} ) { const key=garsoSearchCountCacheKey(searchMode,searchTerm); const normalizedCount=Number(count); if (!key || !Number.isFinite(normalizedCount) || normalizedCount < 0) return null; try { const checkedAt=Date.now(); const previous=readPersistentCacheValue(key,null); const storedTopicLinks=Array.isArray(topicLinks) ? mergeUniqueGarsoTopics(topicLinks) :Array.isArray(previous?.topicLinks) ? previous.topicLinks :null; const storedTopicLinksCheckedAt=Array.isArray(topicLinks) ? Number(topicLinksCheckedAt) || checkedAt :Number(previous?.topicLinksCheckedAt) || null; const topicLinksData=Array.isArray(storedTopicLinks) && storedTopicLinksCheckedAt > 0 ? { topicLinks:storedTopicLinks, topicLinksCheckedAt:storedTopicLinksCheckedAt } :{}; writePersistentCacheValue(key,{ count:normalizedCount, checkedAt, ...(searchMode === 'phone' && ['full','masked','both'].includes(phoneSearchVariant) ? {phoneSearchVariant}:{}), ...topicLinksData }); return checkedAt; } catch (error) { log('Błąd zapisu wyniku Garsoniery do cache',error); return null; } } function getGarsoDataFields() { return [['mean','średnia ogólna'],['median','mediana'],['last5Mean','średnia ostatnich 5 recenzji'], ['reviewCount','liczba ocenionych recenzji'],...CATEGORY_DEFS.map(def => [def.key,def.label])]; } function getGarsoDataValue(stats,descriptor) { const key=descriptor.path[0]; return stats?.categories?.[key]?.mean ?? stats?.[key] ?? null; } function getCachedGarsoStatsForAds(urls,fallbackPhone='') { const candidates=new Map(); const add=(mode,term) => { if (!term) return; const key=garsoSummaryCacheKey(mode,term); if (!candidates.has(key)) candidates.set(key,getGarsoSummaryCache(mode,term)); }; add('phone',fallbackPhone); for (const url of urls || []) { const id=parseAdIdFromUrl(url); if (!id) continue; const phone=getEscortAdPhoneDigits(getStoredEscortAdData(id) || getWatchedAdDataMirror(id)) || fallbackPhone; add('address',url); add('phone',phone); add('combined',buildCombinedGarsoSummaryTerm(phone,url)); } return [...candidates.values()].filter(Boolean).sort((a,b) => (Number(b.result.stats.reviewCount) || 0) - (Number(a.result.stats.reviewCount) || 0) || b.checkedAt - a.checkedAt )[0]?.result.stats || null; } function garsoSummaryCacheKey(searchMode,searchTerm) { if (!['phone','address','combined'].includes(searchMode)) return null; const normalizedTerm=normalizeGarsoCacheSearchTerm(searchMode,searchTerm); if (!normalizedTerm) return null; const versionSuffix=['phone','combined'].includes(searchMode) ? `_v${GARSO_PHONE_SEARCH_CACHE_VERSION}` :''; return 'vm_garso_summary_' + `${searchMode}${versionSuffix}_${encodeURIComponent(normalizedTerm)}`; } function getGarsoTopicsSignature(topics) { return (topics || []) .filter(topic => topic?.url) .map(topic => normalizeTopicUrl(topic.url)) .filter(Boolean) .sort() .join('|'); } function getGarsoTopicDeduplicationKey(topicUrl) { if (!topicUrl) return ''; const normalizedUrl=normalizeTopicUrl(topicUrl); if (!normalizedUrl) return ''; try { const url=new URL(normalizedUrl,GARSO_BASE_URL); const pathId=url.pathname.match(/\/topic\/(\d+)(?:[-/]|$)/i)?.[1]; const queryId=url.searchParams.get('showtopic'); const topicId=pathId || (/^\d+$/.test(queryId || '') ? queryId:''); if (topicId) return `topic:${topicId}`; return `url:${url.href.replace(/\/$/,'')}`; } catch { return `url:${normalizedUrl.replace(/\/$/,'')}`; } } function mergeUniqueGarsoTopics(...topicGroups) { const topicsByKey=new Map(); for (const topic of topicGroups.flat()) { if (!topic?.url) continue; const url=normalizeTopicUrl(topic.url); if (!url) continue; const deduplicationKey=getGarsoTopicDeduplicationKey(url); if (!deduplicationKey) continue; const current=topicsByKey.get(deduplicationKey); const candidate={ title:normalizeEscortAdText(topic?.title), subtitle:normalizeEscortAdText(topic?.subtitle), url }; if (!current) { topicsByKey.set(deduplicationKey,candidate); continue; } if (candidate.title.length > current.title.length) { current.title=candidate.title; } if (candidate.subtitle.length > current.subtitle.length) { current.subtitle=candidate.subtitle; } } return [...topicsByKey.values()]; } function buildCombinedGarsoSummaryTerm(phoneTerm,addressTerm) { const phoneKey=normalizeGarsoCacheSearchTerm('phone',phoneTerm); const addressKey=normalizeGarsoCacheSearchTerm('address',addressTerm); return `${phoneKey || 'bez-numeru'}|${addressKey || 'bez-anonsu'}`; } function compactGarsoSummaryResult(result) { const sourceStats=result?.stats && typeof result.stats === 'object' ? result.stats :{}; const {reviews:_unusedReviews,...stats}=sourceStats; const topics=(Array.isArray(result?.topics) ? result.topics:[]) .map(topic => ({ title:normalizeEscortAdText(topic?.title), subtitle:normalizeEscortAdText(topic?.subtitle), url:topic?.url ? normalizeTopicUrl(topic.url):'' })) .filter(topic => topic.url); const topicInfo=(Array.isArray(result?.topicInfo) ? result.topicInfo:[]) .map(info => ({ title:normalizeEscortAdText(info?.title), subtitle:normalizeEscortAdText(info?.subtitle), url:info?.url ? normalizeTopicUrl(info.url):'', maxPage:Number.isFinite(Number(info?.maxPage)) ? Number(info.maxPage) :null, pagesRead:(Array.isArray(info?.pagesRead) ? info.pagesRead:[]) .map(Number) .filter(page => Number.isFinite(page) && page > 0), posts:Math.max(0,Number(info?.posts) || 0), error:info?.error ? String(info.error):null })); const excludedTopics=(Array.isArray(result?.excludedTopics) ? result.excludedTopics :[]) .map(topic => ({ title:normalizeEscortAdText(topic?.title), url:topic?.url ? normalizeTopicUrl(topic.url):'' })) .filter(topic => topic.url); const screenedTopicCount=Number.isFinite(Number(result?.screenedTopicCount)) ? Math.max(0,Number(result.screenedTopicCount)) :topicInfo.length; const excludedTopicCount=Math.max( excludedTopics.length, Number.isFinite(Number(result?.excludedTopicCount)) ? Math.max(0,Number(result.excludedTopicCount)) :0 ); const failedTopicCount=Number.isFinite(Number(result?.failedTopicCount)) ? Math.max(0,Number(result.failedTopicCount)) :0; return { searchTerm:String(result?.searchTerm || ''), sourceCheckedAt:Number(result?.sourceCheckedAt) || null, sourceCheckAttemptAt:Number(result?.sourceCheckAttemptAt) || null, unverifiedTopicCount:Math.max(0,Number(result?.unverifiedTopicCount) || 0), topics, topicInfo, excludedTopics, screenedTopicCount, excludedTopicCount, failedTopicCount, postCount:Number.isFinite(Number(result?.postCount)) ? Math.max(0,Number(result.postCount)) :Math.max(0,Number(result?.allPosts?.length) || 0), stats }; } function getGarsoAnalysisPages(maxPage) { return Array(Math.min(maxPage,30)).fill().map((_,index) => maxPage <= 30 || index < 10 ? index + 1:maxPage - 29 + index); } function hasGarsoAnalysisPages({maxPage,pagesRead}={}) { if (!Number.isSafeInteger(maxPage) || maxPage < 1 || !Array.isArray(pagesRead)) return false; if (pagesRead.length === maxPage) return pagesRead.every((page,index) => page === index + 1); const selected=getGarsoAnalysisPages(maxPage); return pagesRead.length === selected.length && pagesRead.every((page,index) => page === selected[index]); } function isCompleteGarsoSummary(result,expectedTopics=[]) { const info=Array.isArray(result?.topicInfo) ? result.topicInfo:[]; const expectedCount=mergeUniqueGarsoTopics(expectedTopics).length; const screenedCount=Number(result?.screenedTopicCount); const failedCount=Number(result?.failedTopicCount); return Number.isFinite(screenedCount) && screenedCount >= expectedCount && (!Number.isFinite(failedCount) || failedCount === 0) && info.every(topic => !topic?.error && hasGarsoAnalysisPages(topic)); } function isGarsoTopicCountCheckRecent(timestamp) { const age=Date.now() - Number(timestamp); return Number(timestamp) > 0 && age >= 0 && age < GARSO_TOPIC_COUNT_CHECK_INTERVAL_MS; } function getGarsoSummaryCache(searchMode,searchTerm) { const key=garsoSummaryCacheKey(searchMode,searchTerm); if (!key) return null; try { const value=readPersistentCacheValue(key,null); if ( Number(value?.parserVersion) !== GARSO_SUMMARY_PARSER_VERSION ) { return null; } const checkedAt=Number(value?.checkedAt); if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null; if (!value?.summary?.stats || !Array.isArray(value.summary.topicInfo)) { return null; } const result=compactGarsoSummaryResult(value.summary); const recentCheck=isGarsoTopicCountCheckRecent(result.sourceCheckAttemptAt) && value.topicCacheRevision === GM_getValue(GARSO_TOPIC_REVISION_KEY,''); return { checkedAt, recentCheck, fresh:recentCheck && !result.unverifiedTopicCount, topicSignature:String(value.topicSignature || getGarsoTopicsSignature(result.topics)), result }; } catch (error) { log('Błąd odczytu analizy Garso z cache',error); return null; } } function setGarsoSummaryCache(searchMode,searchTerm,topics,result) { const key=garsoSummaryCacheKey(searchMode,searchTerm); if (!key || !result?.stats) return; try { const checkedAt=Number(result.sourceCheckedAt) || Date.now(); const summary=compactGarsoSummaryResult(result); writePersistentCacheValue(key,{ checkedAt, parserVersion:GARSO_SUMMARY_PARSER_VERSION, topicCacheRevision:GM_getValue(GARSO_TOPIC_REVISION_KEY,''), topicSignature:getGarsoTopicsSignature(topics || summary.topics), summary }); return checkedAt; } catch (error) { log('Błąd zapisu analizy Garso do cache',error); return null; } } function deleteGarsoSummaryCache(searchMode,searchTerm) { const key=garsoSummaryCacheKey(searchMode,searchTerm); if (key) deletePersistentCacheValue(key); } function garsoExtendedCacheKey(adId) { const normalizedAdId=String(adId || '').match(/^\d+$/)?.[0]; return normalizedAdId ? `vm_garso_extended_${normalizedAdId}` :null; } function normalizeGarsoExtendedSearchMode(value) { return ['phone','address','combined'].includes(value) ? value :'address'; } function getGarsoExtendedCache(adId) { const key=garsoExtendedCacheKey(adId); if (!key) return null; try { const value=readPersistentCacheValue(key,null); if (!value || typeof value !== 'object' || Number(value.phoneScanVersion) !== GARSO_EXTENDED_PHONE_SCAN_VERSION) return null; const checkedAt=Number(value?.checkedAt); if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null; const searchMode=normalizeGarsoExtendedSearchMode( value?.searchMode ); const adUrls=[...new Set( (Array.isArray(value?.adUrls) ? value.adUrls:[]) .map(url => normalizeEscortiAdUrl(url)) .filter(Boolean) )]; const results=(Array.isArray(value?.results) ? value.results:[]) .filter(result => result?.searchTerm && Number(result?.count) > 0) .map(result => ({ status:'ok', count:Number(result.count), searchTerm:String(result.searchTerm),andorType:result.andorType === 'or' ? 'or':'and', adUrl:normalizeEscortiAdUrl(result.adUrl) || null, signature:String(result.signature || '') })); return { checkedAt, fresh:Date.now() - checkedAt < getGarsoCacheTtlMs(), adUrls, results, searchMode, phoneTermsKey:String(value.phoneTermsKey || ''), phoneScanVersion:Number(value.phoneScanVersion) || 0 }; } catch (error) { log('Błąd odczytu rozszerzonego wyniku Garso z cache',error); return null; } } function setGarsoExtendedCache( adId, adUrls, results, searchMode='address', phoneTermsKey='' ) { const key=garsoExtendedCacheKey(adId); if (!key) return; const mode=normalizeGarsoExtendedSearchMode(searchMode); const normalizedAdUrls=[...new Set( (adUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(Boolean) )]; const normalizedResults=(results || []) .filter(result => result?.status === 'ok' && Number(result?.count) > 0) .map(result => ({ count:Number(result.count), searchTerm:String(result.searchTerm || ''),andorType:result.andorType === 'or' ? 'or':'and', adUrl:normalizeEscortiAdUrl(result.adUrl) || null, signature:getGarsoExtendedResultSignature(result) })) .filter(result => result.searchTerm); try { const checkedAt=Date.now(); writePersistentCacheValue(key,{ checkedAt, adUrls:normalizedAdUrls, searchMode:mode, phoneTermsKey, phoneScanVersion:GARSO_EXTENDED_PHONE_SCAN_VERSION, results:normalizedResults }); } catch (error) { log('Błąd zapisu rozszerzonego wyniku Garso do cache',error); } } function createGarsoHttpError(status) { const error=new Error(`Garso: HTTP ${status}`); error.status=Number(status) || 0; if ([401,403].includes(error.status)) garsoAuthPromise=null; return error; } function getGarsoAuth(cancelToken=null,immediate=false) { if (!immediate && garsoAuthPromise) { return garsoAuthPromise.then(auth => { cancelToken?.throwIfCancelled(); return auth; }); } const requestPromise=immediate ? gmRequest({ method:'GET', url:GARSO_BASE_URL, cancelToken }) :garsoTopicRequest({ method:'GET', url:GARSO_BASE_URL, cancelToken }); const authPromise=requestPromise.then(response => { if (response.status && (response.status < 200 || response.status >= 400)) { throw createGarsoHttpError(response.status); } const auth=parseGarsoAuth(response.responseText || ''); if (!auth) { recordParserIssue( 'Garso - dane sesji', 'Nie znaleziono secure_hash lub session_id.' ); throw new Error('Brak danych sesji Garso'); } return auth; }); if (immediate) return authPromise; garsoAuthPromise=authPromise; garsoAuthPromise.catch(() => {if (garsoAuthPromise === authPromise) garsoAuthPromise=null;}); return garsoAuthPromise; } function isGarsoAntifloodResponse(html) { const doc=new DOMParser().parseFromString(String(html || ''),'text/html'); const text=normalizeText(doc.body?.innerText || html); return /anty\s*-?\s*flood|anti\s*-?\s*flood|flood control/.test(text) || /(?:odczekaj|poczekaj|odczekać|zaczekaj).{0,80}(?:sekund|sek\.)/.test(text); } function normalizeGarsoResultPageUrl(value) { try { const url=new URL(value || '',GARSO_BASE_URL); if (!['garsoniera.com.pl','www.garsoniera.com.pl'].includes( url.hostname.toLowerCase() )) return null; return url.href; } catch { return null; } } function garsoResultFromCache(cached) { return {status:'ok',found:cached.count>0,count:cached.count,topics:cached.topicLinks,resultUrl:null, phoneSearchVariant:cached.phoneSearchVariant,topicLinksComplete:true,topicLinksCheckedAt:cached.topicLinksCheckedAt}; } function cacheGarsoSearchResult(mode,term,result) { const job=garsoAutoSearchJobs.get(garsoSearchCountCacheKey(mode,term)); if (job && job.result===result) return job.checkedAt; return result?.status==='ok' ? setGarsoSearchCountCache(mode,term,result.count ?? 0,{ phoneSearchVariant:result.phoneSearchVariant,topicLinks:result.topicLinksComplete ? result.topics:undefined, topicLinksCheckedAt:result.topicLinksCheckedAt}):null; } async function checkGarsoTerm( term, collectTopics=true, cancelToken=null, searchMode='', forceRefresh=false, onProgress=null ) { term=normalizeGarsoSearchTerm(term); try { cancelToken?.throwIfCancelled(); const key=garsoSearchCountCacheKey(searchMode,term),prepared=garsoAutoSearchJobs.get(key); if (prepared) { if (forceRefresh || (prepared.completedAt && Date.now()-prepared.completedAt>=GARSO_TOPIC_LINKS_CACHE_TTL_MS)) { garsoAutoSearchJobs.delete(key); prepared.cancelToken.cancel('Ponowne sprawdzenie'); } else { const unsubscribe=cancelToken?.onCancel(reason=>prepared.cancelToken.cancel(reason)); try {const result=await prepared.promise; cancelToken?.throwIfCancelled(); onProgress?.(result); return result;} finally {unsubscribe?.();} } } const cachedTopicList=collectTopics && !forceRefresh ? getGarsoSearchCountCache(searchMode,term) :null; if (cachedTopicList?.topicLinksFresh) return garsoResultFromCache(cachedTopicList); const auth=await getGarsoAuth(); cancelToken?.throwIfCancelled(); const query=getGarsoSearchQuery(term,searchMode); if (!query.term) return {status:'error',count:null,topics:[],resultUrl:null}; const targetUrl=`${GARSO_BASE_URL}index.php?app=core&module=search&do=search&fromMainBar=1&s=${encodeURIComponent(auth.sessionId)}`; const params=new URLSearchParams({search_term:query.term,andor_type:query.andorType, search_app:'forums',secure_hash:auth.secureHash,submit:'Szukaj'}); const response=await garsoRequest({ method:'POST',url:targetUrl,cancelToken,headers:{'Content-Type':'application/x-www-form-urlencoded'},data:params.toString() }); if (response.status && (response.status < 200 || response.status >= 400)) throw createGarsoHttpError(response.status); const html=response.responseText || ''; if (isGarsoAntifloodResponse(html)) return {status:'antiflood',count:null,topics:[],resultUrl:null}; const result=analyzeGarsoResults(html); result.resultUrl=normalizeGarsoResultPageUrl(response.finalUrl); if (searchMode === 'phone') result.phoneSearchVariant='both'; const publishResult=() => { result.topicLinksComplete=result.status === 'ok' && (Number(result.count) === 0 || result.topics.length >= Number(result.count)); onProgress?.({...result,searchTerm:query.term}); }; publishResult(); if (collectTopics && result.status === 'ok' && result.count > result.topics.length && result.topics.length > 0) { result.topics=await collectAllGarsoSearchTopics(html,result,cancelToken, topics => {result.topics=topics; publishResult();}); publishResult(); } return result; } catch (error) { if (isOperationCancelledError(error)) throw error; if (error?.code === 'GARSO_ANTIFLOOD') { return { status:'antiflood', count:null, topics:[], resultUrl:null }; } const httpStatus=Number(error?.status) || 0; if ([401,403].includes(httpStatus)) garsoAuthPromise=null; if (!error?.garsoLogged) { log('Błąd sprawdzania Garso:',error); try {error.garsoLogged=true;} catch {} } return {status:'error',count:null,topics:[],resultUrl:null,httpStatus, errorMessage:String(error?.message || 'Nie udało się sprawdzić Garso'),errorLogged:true}; } } function normalizeTopicUrl(href) { if (!href) return ''; try { const u=new URL(href,GARSO_BASE_URL); if (!/^https?:$/.test(u.protocol) || !/^(?:www\.)?garsoniera\.com\.pl$/i.test(u.hostname) || u.username || u.password) return ''; u.searchParams.delete('hl'); u.searchParams.delete('findpost'); u.searchParams.delete('view'); u.searchParams.delete('p'); u.hash=''; return u.href; } catch {return '';} } function getDisplayableGarsoTopicSubtitle(value) { const subtitle=normalizeEscortAdText(value); return subtitle && !subtitle.includes('?') ? subtitle:''; } function extractGarsoTopicSubtitle(doc) { const subtitle=doc.querySelector( 'h1.ipsType_pagetitle > span.desc[style*="font-size"], ' + 'h1[itemprop="name"] > span.desc[style*="font-size"]' ); return normalizeEscortAdText(subtitle?.textContent); } function extractSearchTopicSubtitle(anchor) { const heading=anchor.closest('h1,h2,h3,h4,h5,h6'); const subtitle=heading?.querySelector('span.desc[style*="font-size"]'); return normalizeEscortAdText(subtitle?.textContent); } function extractSearchTopics(doc) { const out=[]; const seen=new Set(); for (const a of doc.querySelectorAll( '#forum_table a[href*="/topic/"], ' + '#forum_table a[href*="showtopic"], ' + '#forum_table a[title="Pokaż wyniki"]' )) { const url=normalizeTopicUrl(a.getAttribute('href')); if (!url || seen.has(url)) continue; seen.add(url); out.push({ title:normalizeEscortAdText(a.textContent), subtitle:extractSearchTopicSubtitle(a), url }); } return out; } function analyzeGarsoResults(html) { if (!html) { recordParserIssue( 'Garso - wyniki wyszukiwania', 'Otrzymano pusty dokument wyników.' ); return {status:'unknown',count:null,topics:[]}; } const doc=new DOMParser().parseFromString(html,'text/html'); const topics=extractSearchTopics(doc); for (const d of doc.querySelectorAll('.ipsType_pagedesc')) { const text=(d.textContent || '').replace(/\s+/g,' ').trim(); const m=text.match(/zwróciła\s+następującą\s+liczbę\s+wyników\s*:\s*(\d+)/i); if (m) { const count=Number(m[1]); return {status:'ok',found:count > 0,count,topics}; } } const rows=[...doc.querySelectorAll('#forum_table tr._recordRow')]; if (rows.length) return {status:'ok',found:true,count:rows.length,topics}; const scripts=html.match(/ipb\.global\.searchResults\s*\[\s*\d+\s*\]\s*=/g); if (scripts?.length) return {status:'ok',found:true,count:scripts.length,topics}; const bodyText=normalizeText(doc.body ? doc.body.innerText:html); const noResultPhrases=[ 'nie znaleziono wyników','nie znaleziono żadnych wyników','brak wyników', 'brak rezultatów','wyszukiwanie nie zwróciło żadnych wyników', 'twoje wyszukiwanie nie zwróciło żadnych wyników' ]; if (noResultPhrases.some(p => bodyText.includes(p))) return {status:'ok',found:false,count:0,topics:[]}; recordParserIssue( 'Garso - wyniki wyszukiwania', `Nie rozpoznano licznika wyników; znalezione linki tematów: ${topics.length}.` ); return {status:'unknown',count:null,topics}; } async function collectAllGarsoSearchTopics( firstHtml, result, cancelToken=null, onProgress=null ) { const firstDoc=parseHtml(firstHtml); const seen=new Map(result.topics.map(t => [t.url,t])); const pageLinks=[...firstDoc.querySelectorAll('.pagination a[href], ul.pagination a[href], .pages a[href]')] .map(a => ({page:parsePageNumber(a),href:a.getAttribute('href')})) .filter(x => x.page && x.page > 1 && x.href); if (!pageLinks.length) return [...seen.values()]; const template=pageLinks[0]; const perPage=result.topics.length; const maxPage=Math.ceil(result.count / perPage); for (let p=2; p <= maxPage; p++) { cancelToken?.throwIfCancelled(); let url=null; const direct=pageLinks.find(x => x.page === p); if (direct) { try {url=new URL(direct.href,GARSO_BASE_URL).href;} catch {} } if (!url) { let templateAbs=null; try {templateAbs=new URL(template.href,GARSO_BASE_URL).href;} catch {} if (templateAbs) url=constructPageUrl(templateAbs,p,template.page); } if (!url) break; try { const html=await gmGetText( url, cancelToken, {searchRequest:true} ); for (const t of extractSearchTopics(parseHtml(html))) seen.set(t.url,t); onProgress?.([...seen.values()]); } catch (e) { if (isOperationCancelledError(e)) throw e; log(`Nie udało się pobrać strony ${p} wyników Garso`,e); } } return [...seen.values()]; } function getGarsoTopicWord(number) { return polishCountForm(number,'temat','tematy','tematów'); } function setTwoLineButton(btn,title,result) { btn._vmResponsiveValueObserver?.disconnect(); btn._vmResponsiveValueObserver=null; btn.replaceChildren(); const titleLine=makeElement('span','',title); Object.assign(titleLine.style,{ display:'block', fontWeight:'700', lineHeight:'1.2' }); const resultLine=makeElement('span','',result); Object.assign(resultLine.style,{ display:'block', marginTop:'2px', fontWeight:'600', fontSize:'0.92em', lineHeight:'1.2' }); if (btn.classList.contains('vm-research-panel-row')) { titleLine.className='vm-research-panel-label'; resultLine.className='vm-research-panel-value'; Object.assign(titleLine.style,{ minWidth:'0', marginRight:'12px', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', textAlign:'left' }); Object.assign(resultLine.style,{ flex:'0 0 auto', marginTop:'0', color:{ stale:'#9ca3af', checking:'var(--vm-ui-panel-muted, #d8c8d9)', found:'var(--vm-ui-success, #62cf7b)', empty:'#80b9ee', error:'var(--vm-ui-danger, #ff6262)' }[btn.dataset.vmResultStatus] || 'var(--vm-ui-panel-muted, #d8c8d9)', textAlign:'right', whiteSpace:'nowrap' }); const arrow=makeElement('span','','›'); arrow.setAttribute('aria-hidden','true'); Object.assign(arrow.style,{ flex:'0 0 auto', marginLeft:'9px', color:getEscortPagePinkColor(), fontSize:'20px', fontWeight:'400', lineHeight:'1' }); btn.append(titleLine,resultLine,arrow); const fullResult=String(result ?? ''); const compactResult=fullResult.replace( /^(\d+) (?:anons|anonse|anonsów|ogłoszenia) \((\d+) (?:aktywne|aktywnych)\)$/, '$1 ogł. ($2 akt.)' ); if (compactResult !== fullResult) { const renderResponsiveResult=() => { resultLine.textContent=fullResult; const labelIsClipped= titleLine.scrollWidth > titleLine.clientWidth + 1; const rowOverflows=btn.scrollWidth > btn.clientWidth + 1; if (labelIsClipped || rowOverflows) { resultLine.textContent=compactResult; } }; if (typeof ResizeObserver === 'function') { btn._vmResponsiveValueObserver=new ResizeObserver( renderResponsiveResult ); btn._vmResponsiveValueObserver.observe(btn); } requestAnimationFrame(renderResponsiveResult); } return; } btn.appendChild(titleLine); btn.appendChild(resultLine); } function renderGarsoButton(btn) { const displayTerm=btn.dataset.garsoDisplayTerm || ''; const status=btn.dataset.garsoStatus || 'checking'; const denied=[401,403].includes(Number(btn.dataset.garsoHttpStatus)); let statusText='sprawdzam...'; if (status === 'result') { const count=Number(btn.dataset.garsoCount || 0); statusText=`${count} ${getGarsoTopicWord(count)}`; setButtonColor(btn,count > 0 ? 'found':'empty'); } else if (status === 'refreshing') { const count=Number(btn.dataset.garsoCount || 0); statusText=`${count} ${getGarsoTopicWord(count)} • odświeżanie…`; setButtonColor(btn,'checking'); } else if (status === 'unknown') { statusText='?'; setButtonColor(btn,'checking'); } else if (status === 'error') { statusText=denied ? 'zaloguj się':'błąd'; setButtonColor(btn,'error'); } else if (status === 'manual') { statusText='kliknij'; setButtonColor(btn,'checking'); } else setButtonColor(btn,'checking'); if (btn.dataset.garsoSummaryState === 'ready') { const reviewCount=Math.max( 0, Number(btn.dataset.garsoReviewCount) || 0 ); if (reviewCount > 0) { statusText += btn.classList.contains('vm-research-panel-row') ? ' • są oceny' :' • oceny'; } } else if (['loading','refreshing'].includes(btn.dataset.garsoSummaryState)) { statusText += ' • sprawdzanie'; } if (btn.dataset.garsoCacheState === 'stale') { statusText += ' • nie odświeżono'; btn.title='Pokazuję wcześniejszą liczbę tematów, ponieważ odświeżenie nie powiodło się.'; } else if (btn.dataset.garsoCacheState === 'fresh') { btn.title='Liczba tematów pochodzi z aktualnego cache Garsoniery.'; } else { btn.removeAttribute('title'); } if (btn.dataset.garsoErrorMessage) { const hint=denied ? `Garso odmówiło dostępu (HTTP ${btn.dataset.garsoHttpStatus}). Otwórz Garsonierę w zwykłej karcie i zaloguj się, następnie ponów sprawdzanie.` :btn.dataset.garsoErrorMessage; btn.title=[btn.title,hint].filter(Boolean).join(' '); } const title=btn.classList.contains('vm-research-panel-row') ? `Garso - ${displayTerm}` :`Szukaj Garso: ${displayTerm}`; setTwoLineButton(btn,title,statusText); } async function updateGarsoStatus( btn, searchTerm, searchMode, forceRefresh=false, analyzeContent=true, prepareContentSummary=true, cancelToken=null ) { cancelToken?.throwIfCancelled(); await ensurePersistentCacheKeys([garsoSearchCountCacheKey(searchMode,searchTerm),garsoSummaryCacheKey(searchMode,searchTerm)]); cancelToken?.throwIfCancelled(); const cached=getGarsoSearchCountCache(searchMode,searchTerm); delete btn.dataset.garsoHttpStatus; delete btn.dataset.garsoErrorMessage; if (searchMode === 'phone' && cached) { btn.dataset.garsoPhoneSearchVariant=cached.phoneSearchVariant || 'both'; } const shouldAnalyzeContent=analyzeContent !== false; const shouldPrepareContentSummary= shouldAnalyzeContent && prepareContentSummary !== false; if (!forceRefresh && cached?.fresh && cached?.topicLinksFresh) { btn.dataset.garsoStatus='result'; btn.dataset.garsoCount=String(cached.count); btn.dataset.garsoCacheState='fresh'; btn.dataset.garsoCountCheckedAt=String(cached.checkedAt); if (!btn.dataset.garsoSummaryState) { const restored=restoreGarsoSummaryFromCache( btn, searchTerm, searchMode ); if (!restored && Number(cached.count) === 0) { setGarsoSummaryState(btn,'empty','',{ checkedAt:cached.checkedAt, cacheState:'fresh' }); } else if ( !restored && !shouldAnalyzeContent && Number(cached.count) > 0 ) { setGarsoSummaryState(btn,'count-only','',{ checkedAt:cached.checkedAt, cacheState:'fresh' }); } } renderGarsoButton(btn); emitGarsoCountState(btn,'ready',cached.count,true); const summaryState=btn.dataset.garsoSummaryState || ''; const summaryCacheState=btn.dataset.garsoSummaryCacheState || ''; const needsContentRefresh= shouldPrepareContentSummary && Number(cached.count) > 0 && ( summaryState !== 'ready' || ['stale','partial'].includes(summaryCacheState) ); if (!needsContentRefresh) return garsoResultFromCache(cached); } if (cached) { btn.dataset.garsoStatus='refreshing'; btn.dataset.garsoCount=String(cached.count); btn.dataset.garsoCacheState='refreshing'; btn.dataset.garsoCountCheckedAt=String(cached.checkedAt); } else { btn.dataset.garsoStatus='checking'; delete btn.dataset.garsoCacheState; delete btn.dataset.garsoCountCheckedAt; } renderGarsoButton(btn); const result=await checkGarsoTerm( searchTerm, shouldAnalyzeContent, cancelToken, searchMode, forceRefresh ); if (['error','unknown','antiflood'].includes(result.status)) { if (result.errorMessage) btn.dataset.garsoErrorMessage=result.errorMessage; if (result.httpStatus) btn.dataset.garsoHttpStatus=String(result.httpStatus); if (searchMode === 'phone') btn.dataset.garsoPhoneSearchVariant='both'; if (cached) { btn.dataset.garsoStatus='result'; btn.dataset.garsoCount=String(cached.count); btn.dataset.garsoCacheState='stale'; } else { btn.dataset.garsoStatus=result.status === 'antiflood' ? 'unknown' :result.status; delete btn.dataset.garsoCacheState; } } else { btn.dataset.garsoStatus='result'; btn.dataset.garsoCount=String(result.count ?? 0); delete btn.dataset.garsoCacheState; const countCheckedAt=cacheGarsoSearchResult(searchMode,searchTerm,result); if (searchMode === 'phone') { btn.dataset.garsoPhoneSearchVariant=result.phoneSearchVariant || 'both'; } if (countCheckedAt) { btn.dataset.garsoCountCheckedAt=String(countCheckedAt); } } renderGarsoButton(btn); emitGarsoCountState( btn, result.status === 'ok' ? 'ready':(cached ? 'stale':'error'), result.status === 'ok' ? (result.count ?? 0):(cached?.count ?? null), result.status !== 'ok' && !!cached ); if ( shouldPrepareContentSummary && result.status === 'ok' && result.count > 0 && result.topics?.length ) { prepareGarsoSummary( btn, searchTerm, result.topics, forceRefresh, cancelToken ); } else if ( !shouldPrepareContentSummary && result.status === 'ok' && result.count > 0 && btn.dataset.garsoSummaryState !== 'ready' ) { setGarsoSummaryState(btn,'count-only','',{ checkedAt:Number(btn.dataset.garsoCountCheckedAt) || Date.now(), cacheState:'fresh' }); } else if (result.status === 'ok' && Number(result.count) === 0) { deleteGarsoSummaryCache(searchMode,searchTerm); setGarsoSummaryState(btn,'empty',''); } return result; } async function startSearchProcess(term,btnElement) { const title=btnElement.classList.contains('vm-research-panel-row') ? `Garso - ${btnElement.dataset.garsoDisplayTerm || ''}` :`Szukaj Garso: ${btnElement.dataset.garsoDisplayTerm || ''}`; setTwoLineButton(btnElement,title,'Autoryzacja...'); setButtonColor(btnElement,'checking'); btnElement.disabled=true; try { const auth=await getGarsoAuth(); await submitRealForm(term,auth.secureHash,auth.sessionId,btnElement); } catch { setTwoLineButton(btnElement,title,'Błąd sesji'); setButtonColor(btnElement,'error'); btnElement.disabled=false; setTimeout(() => renderGarsoButton(btnElement),2000); } } async function submitGarsoSearchFormToNewTab( term, hash, sessionId, {active=true,andorType='and'}={} ) { term=normalizeGarsoSearchTerm(term); const token=`${Date.now()}-${Math.random().toString(36).slice(2)}`; const storageKey=`${GARSO_OPEN_BRIDGE_KEY_PREFIX}${token}`; const bridgeUrl=new URL( 'index.php?app=core&module=search&search_in=forums', GARSO_BASE_URL ); bridgeUrl.hash=new URLSearchParams({[GARSO_OPEN_BRIDGE_PARAM]:token}).toString(); try { await GM.setValue(storageKey,{term,andorType:andorType === 'or' ? 'or':'and',secureHash:hash,sessionId,createdAt:Date.now()}); noteGarsoNativeSearch(); GM_openInTab(bridgeUrl.href,{ active, insert:true, setParent:true }); } catch (error) { deleteHelperStorageValue(storageKey); throw error; } setTimeout(() => deleteHelperStorageValue(storageKey),5 * 60 * 1000); } async function submitRealForm(term,hash,sessionId,btnElement) { const title=btnElement.classList.contains('vm-research-panel-row') ? `Garso - ${btnElement.dataset.garsoDisplayTerm || ''}` :`Szukaj Garso: ${btnElement.dataset.garsoDisplayTerm || ''}`; setTwoLineButton(btnElement,title,'Otwieranie...'); setButtonColor(btnElement,'checking'); const query=getGarsoSearchQuery(term,btnElement.dataset.garsoSearchMode); if (query.term) await submitGarsoSearchFormToNewTab(query.term,hash,sessionId,{active:true,andorType:query.andorType}); setTimeout(() => { btnElement.disabled=false; renderGarsoButton(btnElement); },800); } function parsePageNumber(a) { const text=(a.textContent || '').trim(); if (/^\d+$/.test(text)) return Number(text); const href=a.getAttribute('href') || ''; let m=href.match(/\/page-(\d+)\/?/i); if (m) return Number(m[1]); m=href.match(/[?&]page=(\d+)/i); if (m) return Number(m[1]); return null; } function getPaginationMap(doc,baseUrl) { const map=new Map([[1,baseUrl]]); let baseTopicId=null; try {baseTopicId=getGarsoTopicPageContext(baseUrl)?.id || null;} catch {} for (const a of doc.querySelectorAll('.pagination a[href], ul.pagination a[href], .pages a[href], a[href*="page-"]')) { const n=parsePageNumber(a); if (!n || n < 1) continue; try { const url=new URL(a.getAttribute('href'),baseUrl); const candidateTopicId=getGarsoTopicPageContext(url.href)?.id || null; if (baseTopicId && candidateTopicId !== baseTopicId) continue; map.set(n,url.href); } catch {} } return map; } function getMaxPage(doc,map) { let maxPage=Math.max(...map.keys()); const text=[...doc.querySelectorAll('.pagination,.pages')].map(el => { const clone=el.cloneNode(true); clone.querySelectorAll('*').forEach(child => child.append(' ')); return clone.textContent || ''; }).join(' '); const m=text.match(/(?:strona|page)\s*\d+\s*(?:z|of)\s*(\d+)/i); if (m) maxPage=Math.max(maxPage,Number(m[1])); return Number.isFinite(maxPage) && maxPage > 0 ? maxPage:1; } function constructPageUrl(templateUrl,pageNo,knownPageNo=null) { try { const u=new URL(templateUrl); if (/\/page-\d+\/?/i.test(u.pathname)) { u.pathname=u.pathname.replace(/\/page-\d+\/?/i,`/page-${pageNo}/`); return u.href; } if (u.searchParams.has('page')) { u.searchParams.set('page',String(pageNo)); return u.href; } if (u.searchParams.has('st') && knownPageNo && knownPageNo > 1) { const current=Number(u.searchParams.get('st')); const perPage=current / (knownPageNo - 1); if (Number.isFinite(perPage) && perPage > 0) { u.searchParams.set('st',String(Math.round((pageNo - 1) * perPage))); return u.href; } } } catch {} return null; } function parseHtml(html) {return new DOMParser().parseFromString(html,'text/html');} function resolveTopicPageUrl(map,baseUrl,pageNo,maxPage) { if (pageNo === 1) return baseUrl; if (map.has(pageNo)) return map.get(pageNo); const knownPages=[...map.keys()] .filter(page => page > 1) .sort((a,b) => Math.abs(a - pageNo) - Math.abs(b - pageNo)); for (const knownPage of knownPages) { const url=constructPageUrl(map.get(knownPage),pageNo,knownPage); if (url) return url; } return constructPageUrl(map.get(maxPage) || baseUrl,pageNo,maxPage); } function extractGarsoTopicTitle(doc,fallback='') { const heading=doc?.querySelector( 'h1[itemprop="name"], h1.ipsType_pagetitle, #content h1' ); if (!heading) return normalizeEscortAdText(fallback); const clone=heading.cloneNode(true); clone.querySelectorAll('span.desc, .desc, .vm-garso-inline-tools').forEach(element => detachHelperNode(element)); return normalizeEscortAdText(clone.textContent) || normalizeEscortAdText(fallback); } function extractFirstGarsoPostLine(doc) { if (!doc) return ''; const firstBlock=doc.querySelector('.post_block, [id^="post_id_"]'); if (!firstBlock) return ''; const content=getGarsoPostContent(firstBlock); return cleanPostElement(content) .split('\n') .map(line => line.trim()) .find(Boolean) || ''; } function getGarsoTopicMatchPhones(doc,title,context) { const fullPhones=text => findGarsoPhoneTokens(text).filter(match => !/xxx$/i.test(match.value)); const titlePhones=fullPhones(title); if (titlePhones.length || context?.page !== 1) return titlePhones; return fullPhones(extractFirstGarsoPostLine(doc)); } const GARSO_REFERENCE_URL_RE=/(?<![\p{L}\p{N}_.@/-])(?:https?:\/\/|www\.|(?:[a-z]{2}\.)?escort\.club\/)[^\s<>"'“”„]+/giu; function readGarsoMatchSource(sources) { const urls=new Set(),otherUrls=new Set(),texts=[]; let hasLinks=false; const urlPattern=GARSO_REFERENCE_URL_RE; const addUrl=value => { try { const url=new URL(/^(?:www\.|(?:[a-z]{2}\.)?escort\.club\/)/i.test(value) ? `https://${value}`:value,GARSO_BASE_URL); if (!/^https?:$/.test(url.protocol)) return; hasLinks=true; if (isEscortClubHost(url.hostname)) { if (getEscortAdIdFromPath(url.pathname)) urls.add(normalizeEscortiAdUrl(url.href)); } else if (!isGarsoRequestUrl(url.href)) otherUrls.add(url.href); } catch {} }; for (const source of sources.filter(Boolean)) { const copy=cloneGarsoPostContent(source); for (const anchor of copy.querySelectorAll('a[href]')) { addUrl(anchor.getAttribute('href')); anchor.textContent=anchor.textContent.replace(urlPattern,' '); } copy.querySelectorAll('br,p,div,li').forEach(element => element.append(' ')); const body=copy.textContent || ''; for (const match of body.matchAll(urlPattern)) addUrl(match[0].replace(/[.,;:!?)\]}]+$/,'')); texts.push(body.replace(urlPattern,' ')); } return {adUrls:[...urls].filter(Boolean),otherUrls:[...otherUrls],text:texts.join(' '),hasLinks}; } let garsoCurrentTopicSourcePromise=null; function getGarsoCurrentTopicSource() { if (garsoCurrentTopicSourcePromise) return garsoCurrentTopicSourcePromise; garsoCurrentTopicSourcePromise=(async () => { const context=getGarsoTopicPageContext(); if (!context) return null; const title=extractGarsoTopicTitle(document); const titlePhones=getGarsoTopicMatchPhones(null,title,{page:2}); const key='vm_garso_topic_sources',now=Date.now(); let stored={}; try {stored=JSON.parse(sessionStorage.getItem(key) || '{}');} catch {} if (!stored || typeof stored !== 'object' || Array.isArray(stored)) stored={}; const saved=stored[context.id]; if (context.page !== 1 && saved?.at > now - GARSO_TOPIC_COUNT_CHECK_INTERVAL_MS && Array.isArray(saved.phones) && Array.isArray(saved.adUrls)) { return {...saved,phones:titlePhones.length ? titlePhones:saved.phones,eligible:titlePhones.length > 0 || saved.eligible,title,firstDoc:null}; } let firstDoc=context.page === 1 ? document:null; if (!firstDoc && !titlePhones.length) { const token=createOperationCancelToken('Garso - pierwszy post tematu'); const cancel=() => token.cancel('Opuszczono temat'); window.addEventListener('pagehide',cancel); try { const response=await garsoTopicRequest({method:'GET',url:context.baseUrl,timeout:20000,cancelToken:token}); token.throwIfCancelled(); const actual=getGarsoTopicPageContext(response.finalUrl || context.baseUrl); if (response.status < 200 || response.status >= 400 || actual?.id !== context.id || actual.page !== 1) throw new Error('Nie udało się odczytać pierwszego posta tematu.'); firstDoc=parseHtml(response.responseText || ''); if (!firstDoc.querySelector('.post_block,[id^="post_id_"]')) throw new Error('Pierwszy post jest niedostępny.'); } finally {window.removeEventListener('pagehide',cancel);} } const firstBlock=firstDoc?.querySelector('.post_block,[id^="post_id_"]'); const first=firstBlock && getGarsoPostContent(firstBlock); const result={ at:now,title, phones:titlePhones.length ? titlePhones:getGarsoTopicMatchPhones(firstDoc,title,{page:1}), adUrls:readGarsoMatchSource([first]).adUrls, eligible:firstDoc ? getGarsoSummaryTopicEligibility({url:context.baseUrl,title},firstDoc).eligible:titlePhones.length > 0 }; if (firstDoc) { stored[context.id]=result; stored=Object.fromEntries(Object.entries(stored).filter(([,v]) => v?.at > now - GARSO_TOPIC_COUNT_CHECK_INTERVAL_MS).sort((a,b) => b[1].at - a[1].at).slice(0,100)); try {sessionStorage.setItem(key,JSON.stringify(stored));} catch {} } return {...result,firstDoc}; })().catch(error => {garsoCurrentTopicSourcePromise=null; throw error;}); return garsoCurrentTopicSourcePromise; } function extractGarsoTopicForumNames(doc) { if (!doc) return []; const names=new Set(); const selectors=[ '.breadcrumb a[href*="/forum/"]', '.breadcrumb [itemprop="title"]', '#breadcrumb a[href*="/forum/"]', '#breadcrumb [itemprop="title"]', 'ol.breadcrumb a[href*="/forum/"]', 'ol.breadcrumb [itemprop="title"]', '#secondary_navigation .breadcrumb a[href*="/forum/"]', '#secondary_navigation .breadcrumb [itemprop="title"]', '.ipsBreadcrumb a[href*="/forum/"]', '.ipsBreadcrumb [itemprop="name"]' ].join(', '); for (const element of doc.querySelectorAll(selectors)) { const name=normalizeEscortAdText(element.textContent); if (name) names.add(name); } return [...names]; } function getGarsoSummaryTopicEligibility(topic,firstDoc) { const title=extractGarsoTopicTitle(firstDoc,topic?.title); return { eligible:findGarsoPhoneTokens(title).length > 0 || findGarsoPhoneTokens(extractFirstGarsoPostLine(firstDoc)).some(match => !/xxx$/i.test(match.value)) || getGarsoTopicPageContext(topic?.url)?.hasPhone === true || extractGarsoTopicForumNames(firstDoc).some(name => /(?:czatodajki|cichodajki)/i.test(name)), title }; } function getGarsoPostBlocks(doc=document) { const blocks=[...doc.querySelectorAll('.post_block')]; return blocks.length ? blocks:[...doc.querySelectorAll('[id^="post_id_"]')]; } function cloneGarsoPostContent(content) { const copy=content.cloneNode(true); copy.querySelectorAll('.post_controls,.post_footer,.post_header,.posted_info,.post_date,.author_info,.post_username,blockquote,.ipsBlockquote,.citation,.quote,.bbc_quote,.signature,.signature_content,.edit,script,style,noscript,.vm-helper-ui,.vm-garso-tracked-topics,.vm-garso-inline-tools,.vm-garso-parser-feedback,[id^="vm-"],[hidden],input,textarea,button').forEach(el => detachHelperNode(el)); return copy; } function readGarsoPost(block,topicTitle,pageNo,pageUrl) { const content=getGarsoPostContent(block),date=block.querySelector('time[datetime]') || block.querySelector('.post_date abbr[title],.posted_info abbr[title]') || block.querySelector('.post_date,.posted_info,time'); return {topicTitle,pageNo,pageUrl,postId:block.id || '', author:normalizeEscortAdText(block.querySelector('.post_username,.author_info .name,[itemprop="name"]')?.textContent), date:normalizeEscortAdText(date?.getAttribute('datetime') || date?.getAttribute('title') || date?.textContent), text:cleanPostElement(content),references:getGarsoPostReferences(content)}; } function getGarsoPostContent(block) { return block.querySelector('.post.entry-content,.post_body .post') || block.querySelector('.post_body') || block; } function cleanPostElement(postEl) { const clone=cloneGarsoPostContent(postEl); clone.querySelectorAll('br').forEach(br => br.replaceWith('\n')); clone.querySelectorAll('p,li').forEach(el => el.append('\n')); return (clone.textContent || '') .replace(/\r/g,'') .replace(/[^\S\n]+/g,' ') .replace(/\n\s*\n+/g,'\n') .trim(); } function extractPostsFromPage(html,topicTitle,pageNo,pageUrl='') { const blocks=getGarsoPostBlocks(typeof html === 'string' ? parseHtml(html):html),posts=[]; if (!blocks.length) recordParserIssue('Garso - posty tematu',`Brak postów na stronie ${pageNo || '?'}.`); for (const block of blocks) { const post=readGarsoPost(block,topicTitle,pageNo,pageUrl),{text,references}=post; if (text.length >= 15 || references.phones.length || references.adUrls.length || references.otherUrls.length || Object.keys(extractRatingsFromPost(text,post)).length) posts.push(post); } return posts; } const garsoParserRegexCache=new Map(); function garsoParserUnicodeRegex(regex) { const key=regex.flags + ':' + regex.source; if (!garsoParserRegexCache.has(key)) { // JS \b nie traktuje polskich liter jako znaków słowa. const boundary=String.raw`(?:(?<![\p{L}\p{N}_])(?=[\p{L}\p{N}_])|(?<=[\p{L}\p{N}_])(?![\p{L}\p{N}_]))`; if (garsoParserRegexCache.size >= 160) garsoParserRegexCache.clear(); garsoParserRegexCache.set(key,new RegExp(regex.source.replace(/\\b/g,boundary),regex.flags.includes('u') ? regex.flags:regex.flags + 'u')); } return garsoParserRegexCache.get(key); } function garsoParserTest(regex,text) { const rx=garsoParserUnicodeRegex(regex); rx.lastIndex=0; return rx.test(text); } // Zakres jest wyznaczany tylko z jawnych nagłówków/przejść; bez zgadywania tożsamości. function getGarsoPostScopes(text) { const aliases=getGarsoPostScopes.aliases ||= new Set(CATEGORY_DEFS.flatMap(def => [...def.patterns,...(def.shortPatterns || [])])); const memo=getGarsoPostScopes.memo ||= new Map(); if (memo.has(text)) return memo.get(text); const scopes=[{start:0,end:text.length,subject:''}]; const skip=/^(?:Witam|Cześć|Hej|Oceny|Cyfry|Podsumowanie|Rekomendacja|Polecam|Pozdrawiam|Plusy|Minusy|Ogólnie|Numer|Telefon|Zdjęcia|Fotki|Załączniki|Cytuj|Zgłoś|Edytowano|Dzięki|Dziekuje|Dziękuję)$/iu; for (const line of text.matchAll(/[^\n]+/g)) { const value=line[0].trim(); let name=value.match(/^([\p{Lu}][\p{Ll}]{2,24})\s*:?$/u)?.[1]; name ||= value.match(/^Tym\s+razem\s*[-–—:]\s*([\p{Lu}][\p{Ll}]{2,24})\s*$/u)?.[1]; name ||= value.match(/^U\s+(?:pani\s+)?([\p{Lu}][\p{Ll}]{2,24})\s+(?:również|rowniez|także|takze)(?!\p{L})/u)?.[1]; name ||= value.match(/^Po\s+(?:ostatniej|pierwszej)\s+wizycie\s+u\s+([\p{Lu}][\p{Ll}]{2,24})\b/u)?.[1]; if (!name || skip.test(name) || aliases.has(name.toLocaleLowerCase('pl-PL'))) continue; const previous=scopes.at(-1); if (previous.subject === name) continue; previous.end=line.index; scopes.push({start:line.index,end:text.length,subject:name}); } const result=scopes.filter(scope => scope.end > scope.start); if (text.length <= 30000) { if (memo.size >= 64) memo.delete(memo.keys().next().value); memo.set(text,result); } return result; } function getGarsoRatingTitleAliases(text,title) { const standard=getCategoryRatingRegexes(); // Alias imienia wymaga tabeli ocen oraz zgodności z tytułem, nie słownika imion. const categories=new Set([...text.matchAll(standard.labels)].filter(match => /^[\s:–—-]*\d+(?:[.,]\d+)?\s*\/\s*(?:5|10)(?!\d)/u.test(text.slice(match.index + match[0].length))) .map(match => standard.aliases.get(match[0].replace(/\s+/g,'').toLowerCase()))); if (categories.size < 2 || getGarsoPostScopes(text).filter(scope => scope.subject).length > 1 || /\b(?:salon|masaż[euy]|masaze|agencja|pytamy|panny|dziewczyny)\b|\s(?:i|&)\s/iu.test(title)) return []; const words=new Set((title.match(/\p{L}{3,24}/gu) || []).map(word => word.toLocaleLowerCase('pl-PL'))); const result=[]; for (const line of text.split('\n')) { const match=line.trim().match(/^([\p{Lu}][\p{Ll}]{2,24})\s*(?::|[-–—])?\s*(?=\d+(?:[.,]\d+)?\s*\/|$)/u); if (match && words.has(match[1].toLocaleLowerCase('pl-PL')) && !standard.aliases.has(match[1].toLowerCase())) result.push(match[1]); } return [...new Set(result)]; } function getGarsoMentionContext(text,span) { const left=text.slice(0,span.start),right=text.slice(span.end); const start=Math.max(left.lastIndexOf('\n'),left.lastIndexOf('.'),left.lastIndexOf('!'),left.lastIndexOf('?')) + 1; const boundary=right.search(/[.!?\n]/u),end=boundary < 0 ? text.length:span.end + boundary; return {start,end,before:text.slice(Math.max(start,span.start - 240),span.start), after:text.slice(span.end,Math.min(end,span.end + 240)),sentence:text.slice(start,Math.min(end,start + 900)), near:text.slice(Math.max(0,span.start - 240),Math.min(text.length,span.end + 240)), line:text.slice(left.lastIndexOf('\n') + 1,right.indexOf('\n') < 0 ? text.length:span.end + right.indexOf('\n'))}; } function classifyGarsoMention(text,key,span,tone) { const c=getGarsoMentionContext(text,span),matched=text.slice(span.start,span.end); const contrast=[...c.before.matchAll(/(?:,\s*|\s+)(?:ale|jednak|natomiast|a\s+okazało\s+się|a\s+okazalo\s+sie)\s+/giu)].at(-1); if (contrast) c.before=c.before.slice(contrast.index + contrast[0].length); const local=c.before + matched + c.after; if (garsoParserTest(/\b(?:gdyby|byłby|byłaby|byłoby|byłyby|bylby|bylaby|byloby|bylyby|było\s+by)\b/iu,c.before + matched)) return 'warunek niezrealizowany'; if (garsoParserTest(/\b(?:nie\s+(?:wiem|jestem\s+pewien)|(?:daj|dawaj|pyt|prosz|wiecie|wiesz|ciekaw)\p{L}*|chciałbym\s+wiedzieć)[^.!?\n]*\bczy\b/iu,c.before + matched)) return 'pytanie lub niepewność'; if (garsoParserTest(/\b(?:boję\s+się|boje\s+sie|bałem\s+się|balem\s+sie|obaw\p{L}*|ryzyko|żeby|zeby|aby)\b[^.!?\n]{0,100}$/iu,c.before) && ! garsoParserTest(/\bbez\s+obaw\b/iu,c.before)) return 'obawa lub cel, nie stwierdzenie'; if (key === 'rekomendacja') { if (garsoParserTest(/^polece(?:ni\p{L}*|ń)$/iu,matched) && !garsoParserTest(/\b(?:do|wart\p{L}*|godn\p{L}*|moja|moje|moją|daję|daje)\s*$/iu,c.before)) return 'polecenie nie jest własną rekomendacją'; if (garsoParserTest(/\b(?:prośb\p{L}*|prosb\p{L}*|prosz\p{L}*)\s+o\s*$/iu,c.before) || garsoParserTest(/\b(?:koledzy|inni|wszyscy|ktoś|ktos|kolega)\s*$/iu,c.before) || garsoParserTest(/^polec(?:ił[ao]?|il[ao]?|ali|ano|ona|ony|one)$/iu,matched)) return 'prośba, instrukcja albo cudze polecenie'; if (tone === 'positive' && garsoParserTest(/^(?:\s+\p{L}+){0,2}\s+(?:dzwonić|dzwonic|telefonować|telefonowac|parking|zaparkować|zaparkowac|lekturę|lekture)\b/iu,c.after)) return 'polecenie czynności'; } if (key === 'GFE') { if (garsoParserTest(/https?:\/\/\S+\s*>?\s*[^\n.!?]{0,60}\bgfe\b/iu,c.line) || garsoParserTest(/(?:\bu|\bo)\s+[\p{Lu}][\p{Ll}]{2,24}\s*$/u,c.before) && garsoParserTest(/\b(?:ostatnio|wcześniej|wczesniej|kiedyś|kiedys)\b/iu,c.sentence)) return 'nazwa anonsu lub inna osoba'; if (garsoParserTest(/\b(?:ceni[ea]|cenę|cene|cenie|ceną|cena)[^.!?\n]{0,65}(?:przewyższ\p{L}*|niż|niz|jak)\s*$/iu,c.before)) return 'porównanie z inną usługą'; if (garsoParserTest(/\b(?:na\s+)?gfe\s+(?:i\s+tak\s+)?nie\s+licz[eę]\b|\b(?:licz[eę]|liczyłem|liczylem|oczekuj[eę]|oczekiwałem|oczekiwalem)\s+(?:na\s+)?(?:pełne\s+)?gfe\b/iu,local)) return 'oczekiwanie autora'; } if (['wtopa','oszustwo','scam','podmianka'].includes(key)) { if (garsoParserTest(/\b(?:panny|kobiety|inne|inni)\b[^.!?\n]{0,40}\bopisywane\s+jako\b/iu,local)) return 'porównanie z innymi osobami'; if (key === 'wtopa') { if (garsoParserTest(/\bpo\s+zaliczeniu\s*$/iu,c.before) && garsoParserTest(/^\s+w\s+(?:mojej|innej|swojej)\s+okolicy/iu,c.after)) return 'wcześniejsze doświadczenia w innym miejscu'; if (garsoParserTest(/\b(?:dość|dosc|dosyć|dosyc|kilku|paru|wielu|poprzednich|wcześniejszych|wczesniejszych)\s*$/iu,c.before) || garsoParserTest(/^\s+(?:trochę\s+|troche\s+)?z\s+mojej\s+strony\b/iu,c.after) || garsoParserTest(/(?<!\p{L})[łl]atwo\s+zaliczy[ćc]\s*$/iu,c.before)) return 'inne doświadczenia albo pomyłka klienta'; if (garsoParserTest(/\b(?:po)?my[śs]la[łl]em\b[^.!?\n]{0,50}$/iu,c.before) && garsoParserTest(/\b(?:myliłem\s+się|mylilem\s+sie|udało\s+się|udalo\s+sie|jednak\s+było\s+dobrze)\b/iu,c.near)) return 'odwołane pierwsze wrażenie'; } } if (key === 'zdjęcia') { if (garsoParserTest(/\b(?:nie\s+jestem\s+pewien|nie\s+uważam|nie\s+uwazam|możliwość|mozliwosc|może\s+być|moze\s+byc|może\s+by[łl]y)\b/iu,local)) return 'niepewność dotycząca zdjęć'; if (garsoParserTest(/\bwiek\b/iu,c.sentence) && garsoParserTest(/\b(?:czy\s+prawdziwy|wywnioskować|wywnioskowac)\b/iu,local)) return 'ocena wieku, nie zdjęć'; if (tone === 'positive' && garsoParserTest(/\b(?:upewniłem|upewnilem|zapewniono|zapewnia\p{L}*|obieca\p{L}*)\b[^.!?\n]{0,90}$/iu,c.before)) return 'zapewnienie przed wizytą'; if (tone === 'negative' && garsoParserTest(/\b(?:wystylizowan\p{L}*|umalowan\p{L}*|uczesan\p{L}*)\b/iu,matched)) return 'różnica stylizacji'; if (tone === 'positive' && garsoParserTest(/\bta\s+sama\s+osoba\s*\(przynajmniej\s+na\s+zdj/iu,matched)) return 'porównanie ofert, nie wyglądu'; if (tone === 'positive' && garsoParserTest(/\b(?:te\s+same|takie\s+same)\s+zdjęcia\b[^.!?\n]{0,90}\b(?:imi|ofert|anons|profil)/iu,local)) return 'porównanie ofert, nie wyglądu'; } return ''; } const RATING_NUMBER_SOURCE='\\d+(?:[.,]\\d+)?'; let categoryRatingRegexCache=null; function escapeRatingAlias(pattern) { return pattern.trim().split(/\s+/).map(part => part.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('\\s*'); } function getCategoryRatingRegexes() { if (categoryRatingRegexCache) return categoryRatingRegexCache; const aliases=new Map(); for (const def of CATEGORY_DEFS) { for (const alias of [...def.patterns,...(def.shortPatterns || [])]) aliases.set(alias.replace(/\s+/g,'').toLowerCase(),def.key); } const patterns=CATEGORY_DEFS.flatMap(def => [...def.patterns,...(def.shortPatterns || [])]).sort((a,b) => b.length - a.length).map(escapeRatingAlias); return categoryRatingRegexCache={ aliases, labels:new RegExp(`(?<![\\p{L}\\p{N}])(?:${patterns.join('|')})(?!\\p{L})`,'giu'), numbers:new RegExp(`(?<!\\d)(?<!\\d[.,])(${RATING_NUMBER_SOURCE})(?:\\s*[-–\\u2014]\\s*(${RATING_NUMBER_SOURCE}))?\\s*[+−-]?\\s*(?:(?:/|\\bna\\b|:(?=\\s*(?:5|10)(?!\\d)))\\s*(${RATING_NUMBER_SOURCE}))?(?!\\d|[.,]\\d)`,'giu') }; } function extractRatingsFromPost(text,context={}) { const ratings={}; text=String(text || ''); const base=getCategoryRatingRegexes(),aliases=new Map(base.aliases),names=getGarsoRatingTitleAliases(text,String(context?.topicTitle || '')); for (const name of names) aliases.set(name.toLowerCase(),'girl'); const labelRegex=names.length ? new RegExp(`${base.labels.source}|(?<![\\p{L}\\p{N}])(?:${names.map(escapeRatingAlias).join('|')})(?!\\p{L})`,'giu'):base.labels; const numberRegex=base.numbers; const allowColonScale=[...text.matchAll(base.labels)].filter(match => /^[\s:–—-]*\d+(?:[.,]\d+)?\s*\/\s*(?:5|10)(?!\d)/u.test(text.slice(match.index + match[0].length))).length >= 2; let section=null; for (const raw of text.matchAll(/[^\n]+/g)) { const normalized=raw[0].replace(/[\t\r\u00a0]/g,' ').replace(/^(?:\s*\d{1,2}[.)]|\s*[IVX]+[.)])\s*/i,value => ' '.repeat(value.length)); if (!normalized.trim()) continue; const ownLabels=[...normalized.matchAll(labelRegex)]; const heading=ownLabels.length > 0 && /^[\s:.–—/+&,\-]*(?:(?:i|oraz)[\s:.–—/+&,\-]*)*$/iu.test(normalized.replace(labelRegex,'')); if (heading) {section={label:normalized.trim().replace(/[\s:.–—-]+$/u,''),start:raw.index + ownLabels[0].index}; continue;} // Nie przenosimy kontekstu przez nieznany nagłówek/osobę ani stopkę forum. if (/^\s*(?:[\p{Lu}][\p{L}]{2,24}\s*:?|[-–—]{3,}|Lubię to|Dodaj odpowiedź|Do góry|Zgłoś)\s*$/u.test(normalized)) section=null; const ownRating=ownLabels.some(match => /^[\s:=(\[–—-]*\d/.test(normalized.slice(match.index + match[0].length))); if (ownRating) section=null; const localHeading=ownLabels.find(match => !normalized.slice(0,match.index).trim()); const reverseRating=ownLabels.some(match => /^\s*\d+(?:[.,]\d+)?\s*\/\s*(?:5|10)\s*[-–—:]*\s*$/u.test(normalized.slice(0,match.index))); if (localHeading || reverseRating) section=null; const prefix=section && !ownRating ? `${section.label}: `:''; if (localHeading && /^\s*[:.,–—-]/u.test(normalized.slice(localHeading.index + localHeading[0].length))) section={label:localHeading[0],start:raw.index + localHeading.index}; const line=prefix + normalized; const labels=[...line.matchAll(labelRegex)].map(match => ({ alias:match[0].toLowerCase(),key:aliases.get(match[0].replace(/\s+/g,'').toLowerCase()),start:match.index,end:match.index + match[0].length })); if (!labels.length) continue; let lastRatingEnd=-1; for (const match of line.matchAll(numberRegex)) { const start=match.index; const end=start + match[0].trimEnd().length; const scale=match[3] == null ? 10:Number(match[3].replace(',','.')); const first=Number(match[1].replace(',','.')); const second=match[2] == null ? first:Number(match[2].replace(',','.')); const explicit=match[3] != null; if (match[0].includes(':') && (!allowColonScale || /^\s*(?:rano|wieczorem|godz|jutro)\b/iu.test(line.slice(end)))) continue; const numericContext=getGarsoMentionContext(line,{start,end}); const contrast=[...numericContext.before.matchAll(/(?:,\s*|\s+)(?:ale|natomiast|a\s+tak(?:\s+to)?)\s+/giu)].at(-1); if (contrast) numericContext.before=numericContext.before.slice(contrast.index + contrast[0].length); const rhetorical=/byłbym\s+[^.!?]{0,20}niesprawiedliwy[^.!?]{0,30}gdyby\s+nie\s+było\s*$/iu.test(numericContext.before); if (!rhetorical && /(?:\b(?:gdyby|byłby|byłaby|byłoby|bylby|bylaby|byloby)|\b(?:było|bylo)\s+by\b)/iu.test(numericContext.before) || /^\s*(?:\?|gdyby\b)/iu.test(line.slice(end))) {continue;} if (!explicit && (/^\s*(?:%|:\s*\d|w\s+\d|do\s+\d|na\s+\d)/iu.test(line.slice(end)) || /\d\s*(?:w|:|do|na)\s*$/iu.test(line.slice(0,start)))) {continue;} if (![5,10].includes(scale) || /^\s*\/\s*\d/.test(line.slice(end,end + 12)) || !explicit && /^(?:\s*(?:zł|pln|cm|kg|lat|lata|razy|osoby|osób|osob|pokoje|pokoi|telefony|telefonów|pietro|piętro|x|minut\p{L}*|min|godzin\p{L}*|godz|dni|dzień|dzien|h)\b)/iu.test(line.slice(end))) continue; const previousRatingEnd=lastRatingEnd; if (explicit && first <= scale && second <= scale) lastRatingEnd=end; const candidates=labels.filter(label => { if (label.end > start || label.start < previousRatingEnd) return false; const prefix=line.slice(0,label.start); const gap=line.slice(label.end,start); if (/^[\s:=–—-]*(?:nie\s+(?:było|bylo|oceniam)|bez\s+oceny)/iu.test(gap)) return false; const heading=label === labels[0] && label.start < 25 && !/\d/.test(prefix); const clause=/(?:^|[.!?;,–\u2014-])\s*$/.test(prefix); const direct=/^[\s:=(\[–\u2014-]*(?:(?:oceniam(?:\s+na)?|daj[eę]|mocne|solidne)\s*)?$/iu.test(gap); const previous=labels.findLast(other => other.end <= label.start); const followsLabel=previous && label.start - previous.end < 25 && !/[.!?;\d]/.test(line.slice(previous.end,label.start)); if (!(heading || clause || (direct && followsLabel))) return false; if (!explicit) { const nextLabel=labels.find(item => item.start > start); const remainder=line.slice(end,nextLabel?.start); return direct && !/\d\s*(?:[+−-]?\s*)\/\s*\d/.test(remainder); } return heading || /^\s*[:=–\u2014-]/.test(gap) ? gap.length <= 800:gap.length <= 40 && (label.key !== 'girl' || gap.length <= 24) && !/[.!?](?!\s*\(?$)/.test(gap); }); const nearest=labels.findLast(item => item.end <= start); const closerDirect=nearest && /^[\s:=(\[–\u2014-]*$/u.test(line.slice(nearest.end,start)); const inherited=nearest?.alias === 'ocena' && candidates.find(item => item.key !== 'summary'); const candidate=inherited || candidates.at(-1); const label=closerDirect && !inherited && !candidates.includes(nearest) && nearest.key !== candidate?.key ? null:candidate; let selected=label ? [label]:[]; if (!selected.length && explicit) { const reverse=labels.find(item => item.start >= end && /^[\s–\u2014:-]*$/.test(line.slice(end,item.start))); if (reverse) { selected=[reverse]; for (const item of labels.filter(item => item.start >= reverse.end)) { if (!/^\s*(?:[/+&,]|i|oraz)\s*$/iu.test(line.slice(selected.at(-1).end,item.start))) break; selected.push(item); } } } if (!selected.length) {continue;} const address=explicit && first > scale && second <= scale && selected[0].key === 'location' && /\p{L}{3,}\s+$/u.test(line.slice(selected[0].end,start)) && /\s[-–\u2014]\s/.test(match[0]); if ((!address && first > scale) || second > scale) {continue;} const value=((address ? second:(first + second) / 2) * 10) / scale; const before=labels.filter(item => item.end <= selected[0].start).reverse(); for (const item of before) { if (!/^\s*(?:[/+&,]|i|oraz|and)?\s*$/i.test(line.slice(item.end,selected[0].start))) break; selected.unshift(item); } for (const item of labels.filter(item => item.start >= selected.at(-1).end && item.end <= start)) { if (!/^\s*(?:[/+&,]|i|oraz|and)?\s*$/i.test(line.slice(selected.at(-1).end,item.start))) break; selected.push(item); } if (selected.some(item => item.key !== 'summary')) selected=selected.filter(item => item.key !== 'summary'); for (const key of new Set(selected.map(item => item.key))) { (ratings[key] ||= []).push(value); } lastRatingEnd=Math.max(end,...selected.map(item => item.end)); } } return Object.fromEntries(Object.entries(ratings).map(([key,values]) => [key,avg(values)])); } function regexSpans(text,regex) { const flags=regex.flags.includes('g') ? regex.flags:`${regex.flags}g`; const rx=garsoParserUnicodeRegex(new RegExp(regex.source,flags)); return [...String(text || '').matchAll(rx)].map(match => ({ start:match.index, end:match.index + match[0].length })); } function spansOverlap(a,b) { return a.start < b.end && b.start < a.end; } function buildGarsoMentionEvidence(post,spans,scopes=getGarsoPostScopes(String(post?.text || '')),category='') { const sourceText=String(post?.text || ''); const first=(Array.isArray(spans) ? spans:[])[0] || { start:0, end:Math.min(sourceText.length,1) }; const scope=scopes.find(item => item.start <= first.start && item.end > first.start) || {start:0,end:sourceText.length,subject:''}; const start=Math.max(scope.start,Number(first.start) - 105); const end=Math.min(scope.end,Number(first.end) + 145); const context=getGarsoMentionContext(sourceText,first); const visit=context.sentence.match(/\b(?:po|podczas|na)\s+(?:pierwszej|drugiej|ostatniej|poprzedniej)\s+wizy(?:cie|ty)/iu)?.[0] || ''; const fragment=sourceText.slice(first.start,first.end); const aspect=category !== 'zdjęcia' ? '':/nieaktualn|sprzed\s+lat/iu.test(fragment) ? 'aktualność':/chud|grub|wyższ|niższ|starsz|młodsz/iu.test(fragment) ? 'wygląd':'zgodność zdjęć'; const snippet=sourceText.slice(start,end).replace(/\s+/g,' ').trim(); return { topicTitle:normalizeEscortAdText(post?.topicTitle) || 'Temat Garso', url:post?.pageUrl ? normalizeTopicUrl(post.pageUrl):'', author:normalizeEscortAdText(post?.author), date:normalizeEscortAdText(post?.date), postId:String(post?.postId || ''), start:first.start, end:first.end, scope:scope.subject ? `Sekcja: ${scope.subject}`:'Post w temacie; osoba nieustalona', visit, aspect, snippet:`${start > 0 ? '…':''}${snippet}${end < sourceText.length ? '…':''}`, match:sourceText.slice( Math.max(0,Number(first.start)), Math.max(0,Number(first.end)) ).replace(/\s+/g,' ').trim().slice(0,120) }; } function createExplicitMentionAnalyzer() { const {rules,extraPositive,extraNegative,extraIgnored}=createExplicitMentionAnalyzer.rules ||= (() => { const agencyWord=String.raw`(?:agenc(?:j|yjn)\p{L}*|agentur\p{L}*)`; const fraudWord=String.raw`(?:oszust\p{L}*|oszuś\p{L}*)`; const scamWord=String.raw`(?:scam\p{L}*|skam\p{L}*)`; const swapWord=String.raw`(?:podmian\p{L}*|podmien\p{L}*)`; const photoWord=String.raw`(?:zdj[eę](?:ci\p{L}*|ć|c(?!\p{L}))|fot\p{L}*)`; const wordToken=source => `(?<!\\p{L})(?:${source})(?!\\p{L})`; const agencyToken=wordToken(agencyWord); const fraudToken=wordToken(fraudWord); const scamToken=wordToken(scamWord); const swapToken=wordToken(swapWord); const photoToken=wordToken(photoWord); const gfeNegative=/(?:(?<!\p{L})(?:nie\s+(?:jest|był[oa]?|byl[oa]?|b[eę]dzie|ma|czuć|czuc|do[śs]wiadcz\p{L}*)(?:\s+to)?|bez|zero|brak\p{L}*|zabrak\p{L}*|[żz]adnego)(?:\s+[\p{L}\p{N}%]+){0,6}\s+gfe\b|\bnie\s+(?:(?:serwis|klimat)\s+)?gfe\b|\bgfe\s*(?:[-–\u2014:,]\s*)?0\s*%|\bnie\s+(?:uświadczysz|uraczysz)(?:\s+\p{L}+){0,8}\s+gfe\b|\bgfe\b[\s:–\u2014-]+(?:(?:to|tu|tutaj|u\s+niej|też|tez|raczej|zdecydowanie|absolutnie)\s+){0,4}nie\s+(?:jest|był[oa]?|byl[oa]?|ma|uświadczysz|uraczysz|doświadcz\p{L}*|doswiadcz\p{L}*|tam)\b|\bgfe\b[^.!?\n]{0,80}\b(?:to\s+)?(?:absolutnie\s+)?nie\s+(?:ten|taki)\s+adres\b|\bdalek\p{L}*\s+(?:(?:temu|tutaj|tu|jej|mu|było|bylo)\s+)?(?:od|do)\s+gfe\b|\bdo\s+gfe\s+(?:to\s+)?daleko\b|\bgfe\s+(?:bym\s+)?nie\s+liczył\p{L}*|\bgfe\s+(?:to\s+)?nic\s+z\s+tego)/giu; const gfeIgnored=/(?:\bnie\s+do\s+ko[nń]ca\s+gfe\b|\bnie\s+oczek\p{L}*(?:\s+\p{L}+){0,3}\s+gfe\b|\b(?:deklarowan\p{L}*|szukam|oczekuj[eę]|preferuj[eę]|byłoby|było\s+by|byloby|bylo\s+by)(?:\s+\p{L}+){0,3}\s+gfe\b)/giu; const wtopaNegative=/(?:\b(?:bez|brak|zero)\s+(?:[żz]adnej\s+)?wtop\p{L}*|\bbez\s+obaw\s+o\s+(?:\p{L}+\s+){0,2}wtop\p{L}*|\bani[\s,;:–\u2014-]+(?:to\s+)?(?:[żz]adna\s+)?wtop\p{L}*|\bnie\s*(?:(?:był[oa]?|byl[oa]?|jest|ma|b[eę]dzie|czułem|czulem)\s+)?(?:to\s+)?(?:mowy\s+o\s+|[żz]adnej\s+)?wtop\p{L}*|\bwtop\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s*(?:był[oa]?|byl[oa]?|jest|ma|b[eę]dzie|powinno\s+by[cć])(?!\p{L})|\bnie\s+(?:traktuj\p{L}*|okre[śs]li[łl]?\p{L}*)(?:\s+\p{L}+){0,3}\s+(?:jak|jako)\s+wtop\p{L}*)/giu; const cooperativeNegative=/(?:\b(?:nie\s+jest(?:\s+to)?|to\s+nie\s+jest)\b(?:\s+\p{L}+){0,4}\s+sp[oó]łdzieln\p{L}*|\b(?:bez|brak)\s+sp[oó]łdzieln\p{L}*|\bsp[oó]łdzieln\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s+jest\b)/giu; const agencyNegative=new RegExp( String.raw`(?:\bnie\s+${agencyToken}|\b(?:nie\s+jest(?:\s+to)?|to\s+nie\s+jest)\b(?:\s+\p{L}+){0,4}\s+${agencyToken}|\b(?:bez|brak)\s+${agencyToken}|${agencyToken}(?:\s+\p{L}+){0,3}\s+nie\s+jest\b)`, 'giu' ); const privateNegative=/(?:\bnie\s+(?:jest|działa|dziala|pracuje|przyjmuje)(?:\s+to)?(?:\s+\p{L}+){0,3}\s+prywatnie\b|\bprywatnie(?:\s+\p{L}+){0,2}\s+nie\s+(?:jest|działa|dziala|pracuje|przyjmuje)\b)/giu; const makeFraudNegativeRegex=token => new RegExp( String.raw`(?:\b(?:nie\s+(?:jest|ma|są|sa|była|byla|było|bylo|były|byly)(?:\s+to)?|to\s+nie\s+(?:jest|są|sa))(?:\s+\p{L}+){0,4}\s+${token}|\b(?:to\s+)?nie\s+${token}|\b(?:bez|brak|zero)\s+(?:[żz]adn\p{L}*\s+)?${token}|(?<!\p{L})[żz]adn\p{L}*\s+${token}|\bani[\s,;:–\\u2014-]+(?:to\s+)?${token}|${token}(?:\s+\p{L}+){0,3}\s+nie\s+(?:jest|są|sa)\b|\bnigdy\s+nie\s+przejechałem\s+si[eę]\s+na\s+${token})`, 'giu' ); const fraudNegative=makeFraudNegativeRegex(fraudToken); const scamNegative=makeFraudNegativeRegex(scamToken); const swapNegative=makeFraudNegativeRegex(swapToken); const wtopaIgnored=/(?:\b(?:iloś(?:ć|ci)|dużo|wiele|mnóstwo|sporo)\s+(?:różnych\s+)?wtop\p{L}*(?:\s+w\s+\p{L}+)?|\bwtop\p{L}*\s+si[eę]\s+w\s+tłum\p{L}*|\b(?:boj[eę]\s+si[eę]|bałem\s+si[eę]|balem\s+sie|(?<!bez\s)obaw\p{L}*|ryzyko)(?:\s+\p{L}+){0,3}\s+wtop\p{L}*)/giu; const riskIgnored=new RegExp( String.raw`\b(?:o\s+ile|[żz]eby|aby|boj[eę]\s+si[eę]|obawiam\s+si[eę])(?:\s+\p{L}+){0,5}\s+(?:${fraudToken}|${scamToken}|${swapToken})`, 'giu' ); const photoConsistent=new RegExp( String.raw`(?:\bprzypomina\s+(?:t[eę]\s+)?(?:lask\p{L}*|dziewczyn\p{L}*|pann\p{L}*)\s+ze?\s+(?:\p{L}+\s+){0,4}${photoToken}|${photoToken}(?:(?!\bnie\s+(?:(?:są|sa|jest|były|byly)\s+)?(?:jej|zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b)[^.!?;\n]){0,35}\b(?:zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b|${photoToken}(?:(?!\bnie\s+(?:są|sa|jest|były|byly)\s+jej\b)[^.!?;\n]){0,18}\b(?:są|sa|to)\s+jej\b|(?<!nie\s)\b(?:są|sa|to)\s+jej\b[^.!?;\n]{0,18}${photoToken}|\b(?:zgodn\p{L}*|ta\s+sama)\b[^.!?;\n]{0,25}\b(?:na\s+)?${photoToken}|(?<!nie\s)\b(?:wygl[aą]d\p{L}*|jest)\b[^.!?;\n]{0,18}\bjak\s+na\s+${photoToken}|\b(?:dziewczyn|kobiet|pann)\p{L}*\b[^.!?;\n]{0,16}\b(?:jest\s+)?(?:ta\s+sama\s+)?(?:co|ze)\s+(?:na\s+)?${photoToken}|${photoToken}[^.!?;\n]{0,30}\bnie\s+(?:kłami\p{L}*|klami\p{L}*)\b)`, 'giu' ); const photoInconsistent=new RegExp( String.raw`(?:(?<!\p{L})[żz]adna\s+ze\s+${photoToken}|${photoToken}\s+(?:\p{L}+\s+){0,2}kradzione\s+z\s+internetu|${photoToken}\s+na\s+E\.\s+(?:\p{L}+\s+){0,2}nieaktualn\p{L}*|\b(?:dziewczyn|kobiet|pann)\p{L}*\s+ze\s+${photoToken}\s+nie\s+jest\b|\bnie\s+(?:jest|był[oa]?|byl[oa]?)\s+(?:to\s+)?(?:na\s+\d+%\s+)?(?:dziewczyn|kobiet|pann)\p{L}*\s+ze\s+${photoToken}|\bnie\s+podobn\p{L}*\s+do\s+(?:dziewczyn|kobiet|pann)\p{L}*\s+ze\s+${photoToken}|\bniezgodn\p{L}*[^.!?;\n]{0,35}${photoToken}|${photoToken}[^.!?;\n]{0,35}\b(?:niezgodn\p{L}*|fałszyw\p{L}*|falszyw\p{L}*|fake|nieaktualn\p{L}*|przerob\p{L}*)\b|\b(?:fałszyw\p{L}*|falszyw\p{L}*|fake|nieaktualn\p{L}*|przerob\p{L}*)\b[^.!?;\n]{0,25}${photoToken}|${photoToken}[^.!?;\n]{0,24}\bnie\s+(?:(?:są|sa|jest|były|byly)\s+)?(?:jej|zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b|\bnie\s+(?:są|sa|jest|były|byly)\s+jej\b[^.!?;\n]{0,18}${photoToken}|\bnie\s+jej\b[^.!?;\n]{0,18}${photoToken}|\bnie\s+wygl[aą]d\p{L}*[^.!?;\n]{0,35}(?:jak|ze)\s+(?:na\s+)?${photoToken}|\b(?:inn\p{L}*|starsz\p{L}*|młodsz\p{L}*|mlodsz\p{L}*|grubsz\p{L}*|chudsz\p{L}*|niższ\p{L}*|nizsz\p{L}*|wyższ\p{L}*|wyzsz\p{L}*|ładniejsz\p{L}*|ladniejsz\p{L}*)\s+niż\s+(?:na\s+)?${photoToken}|\bnie\s+(?:jest\s+)?(?:tak\s+)?\p{L}+(?:\s+\p{L}+){0,2}\s+jak\s+(?:na\s+)?${photoToken}|\b(?:pann|dziewczyn|kobiet)\p{L}*\b[^.!?;\n]{0,25}\bnie\s+wygl[aą]d\p{L}*[^.!?;\n]{0,20}\bjak\s+(?:na|w)\s+ogłoszen\p{L}*|\bnie\s+wygl[aą]d\p{L}*[^.!?;\n]{0,20}\bjak\s+żadn\p{L}*\s+z\s+(?:dziewczyn|kobiet|osób|osob)\p{L}*\s+przedstawion\p{L}*|\binn\p{L}*\s+(?:dziewczyn|kobiet|pann)\p{L}*\s+ni[żz]\s+(?:ta\s+)?(?:ze|z|na)\s+${photoToken}|\bnie\s+(?:ma\s+|ta\s+)?(?:dziewczyn|kobiet|pann)\p{L}*[^.!?;\n]{0,12}\b(?:ze|z)\s+${photoToken}|${photoToken}(?:(?!\bnie\s+(?:kłami\p{L}*|klami\p{L}*)\b)[^.!?;\n]){0,30}\b(?:kłami\p{L}*|klami\p{L}*)\b)`, 'giu' ); const photoAiGenerated=new RegExp( String.raw`(?:${photoToken}(?:(?!\bnie\b)[^.!?;\n]){0,55}\b(?:wygenerowan\p{L}*|generowan\p{L}*)\b[^.!?;\n]{0,40}\b(?:ai|sztuczn\p{L}*\s+inteligenc\p{L}*)\b|\b(?:wygenerowan\p{L}*|generowan\p{L}*)\b[^.!?;\n]{0,40}\b(?:ai|sztuczn\p{L}*\s+inteligenc\p{L}*)\b[^.!?;\n]{0,55}${photoToken})`, 'giu' ); const photoNegative=new RegExp( `(?:${photoInconsistent.source}|${photoAiGenerated.source})`, 'giu' ); const photoIgnored=new RegExp( String.raw`(?:\bczy\s+(?:\p{L}+\s+){0,5}${photoToken}|${photoToken}\s+(?:(?:wcale|absolutnie)\s+)?nie\s+(?:(?:są|sa|były|byly)\s+)?(?:przerob\p{L}*|retuszowan\p{L}*)|\bnie\s+(?:(?:są|sa)\s+)?(?:przerob\p{L}*|retuszowan\p{L}*)\s+${photoToken}|\b(?:do|o)\s+zgodno[śs]ci\s+${photoToken})`, 'giu' ); const recommendationWord=String.raw`polec(?:a\p{L}*|i(?:ć|ł\p{L}*|li\p{L}*)|e(?:ni\p{L}*|ń)|on\p{L}*)`; const recommendationToken=wordToken(recommendationWord); const recommendationIgnored=new RegExp( String.raw`(?:${recommendationToken}\s+(?:raczej\s+)?(?:zostaw\p{L}*|zapark\p{L}*|parkow\p{L}*|lektur\p{L}*|artykuł\p{L}*|forum|czyta\p{L}*|przeczyta\p{L}*|wej[śs]\p{L}*|zajrze\p{L}*|link\p{L}*|inn(?:e|ą|a|ego|ej|ych)(?!\p{L}))|\b(?:wydaj\p{L}*|daj\p{L}*)\s+poleceni\p{L}*|\bnie\s+odradz\p{L}*|\bnie\s+polecam\s*,?\s*(?:ani|i)\s+nie\s+odradz\p{L}*|\b(?:z|zgodnie\s+z)\s+poleceni\p{L}*|\b(?:kto[śs]|kt[oó]ry[śs]|kogo)\s+(?:mi\s+)?${recommendationToken})`, 'giu' ); const niceAtmosphere=/(?:\bmił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\batmosfer\p{L}*(?:\s+\p{L}+){0,3}\s+mił\p{L}*)/giu; const niceAtmosphereNegative=/(?:\bniemił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\bnie\s+(?:(?:ma|jest|był\p{L}*)\s+)?mił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\b(?:bez|brak)\s+(?:\p{L}+\s+){0,2}mił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\batmosfer\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s+(?:jest|był\p{L}*)\s+mił\p{L}*)/giu; const extraNegative={ rekomendacja:/\bnie\s+(?:jest\s+)?(?:do|wart\p{L}*|godn\p{L}*)\s+poleceni\p{L}*/giu, GFE:/(?:\b0\s*%?\s*gfe\b|\bnie\s+(?:mamy|miałem|mialem|miały|mialy|były|byly)\b(?:\s+\p{L}+){0,4}\s+gfe\b|\bgfe\b[\s:–—-]+(?:(?:to|raczej|też|tez|już|juz|tam|tego)\s+){0,6}nie\s+(?:u[śs]wiadcz\p{L}*|znajdz\p{L}*|jej\s+klimat\p{L}*)|\bzabrak\p{L}*\s+tej\s+magii[.!]\s*Tego\s+klimatu\s+gfe\b)/giu, wtopa:/(?:\bnie\s+(?:jest|była|byla|było|bylo)\s+(?:to\s+)?(?:jakaś|jakas|żadna|zadna)\s+wtop\p{L}*|\bnie\s+przytrafi\p{L}*\s+(?:mi\s+)?si[eę]\s+(?:jeszcze\s+)?wtop\p{L}*|\bwtop\p{L}*\s+(?:(?:raczej|też|tez|tutaj|tu)\s+){0,3}(?:brak|nie\s+zalicz\p{L}*))/giu, 'zdjęcia':/(?:\bnie\s+ma\s+(?:tam\s+|tutaj\s+)?(?:kobiet|dziewczyn|panien)\p{L}*\s+ze?\s+zdj[eę](?:ci\p{L}*|ć|c)|\b(?:dziewczyn|kobiet|pann)\p{L}*\s+ze\s+zdj[eę](?:ci\p{L}*|ć|c)\s+(?:tam\s+|tutaj\s+)?nie\s+ma\b|\bnie\s+(?:ta|taka)\s+(?:pann|dziewczyn|kobiet)\p{L}*\s+co\s+na\s+zdj[eę]ci\p{L}*|\bz\s+dziewczyn\p{L}*\s+ze\s+zdj[eę](?:ci\p{L}*|ć|c)\s+ma\s+wsp[oó]ln\p{L}*\s+tylko\s+\p{L}+|\b(?:zdj[eę](?:ci\p{L}*|ć|c)|foty)\s+(?:ze\s+strony\s+)?pokazuj\p{L}*\s+inn\p{L}*\s+osob\p{L}*|\ba\s+na\s+miejscu\s*\.{0,3}\s*panna\s+(?:kompletnie\s+)?niepodobn\p{L}*)/giu, atmosfera:/(?:\b(?:niemił\p{L}*|niemil\p{L}*|nieprzyjazn\p{L}*|nieprzyjemn\p{L}*|fataln\p{L}*|kiepsk\p{L}*|sztywn\p{L}*)\s+(?:\p{L}+\s+){0,2}atmosfer\p{L}*|\batmosfer\p{L}*\s+(?:(?:jest|była|byla|bardzo|raczej)\s+){0,3}(?:niemił\p{L}*|niemil\p{L}*|nieprzyjazn\p{L}*|nieprzyjemn\p{L}*|fataln\p{L}*|kiepsk\p{L}*|sztywn\p{L}*)|\bnie\s+(?:jest|ma|była|byla|było|bylo)\s+(?:to\s+)?(?:jak\p{L}*\s+)?(?:super\s+|świetn\p{L}*\s+|swietn\p{L}*\s+|fajn\p{L}*\s+|przyjemn\p{L}*\s+)?atmosfer\p{L}*|\bbrak\p{L}*\s+(?:(?:jakiejkolwiek|dobrej|miłej|milej)\s+)?(?:atmosfer\p{L}*|klimatu))/giu }; const extraPositive={ 'zdjęcia':/\bzdj[eę]cia\b[^.!?;\n]{0,45}\b(?:należą\s+do\s+niej|naleza\s+do\s+niej|nie\s+odbiegają\s+od\s+rzeczywistości)/giu, atmosfera:/(?:\b(?:mił\p{L}*|mil\p{L}*|świetn\p{L}*|swietn\p{L}*|super|fajn\p{L}*|przyjemn\p{L}*|przyjazn\p{L}*|wspaniał\p{L}*|wspanial\p{L}*)\s+(?:\p{L}+\s+){0,2}atmosfer\p{L}*|\batmosfer\p{L}*\s+(?:(?:jest|była|byla|bardzo|naprawdę|naprawde|wręcz|wrecz)\s+){0,3}(?:mił\p{L}*|mil\p{L}*|świetn\p{L}*|swietn\p{L}*|super|fajn\p{L}*|przyjemn\p{L}*|przyjazn\p{L}*))/giu }; const extraIgnored={ rekomendacja:/\bnie\s+(?:polec\p{L}*\s*,?\s*(?:ani|i)\s+nie\s+odradz\p{L}*|odradz\p{L}*\s*,?\s*(?:ani|i)\s+nie\s+polec\p{L}*)/giu }; const rules=[ ['GFE','normal',/\bgfe\b/giu,gfeNegative,gfeIgnored], ['wtopa','normal',/\bwtop\p{L}*/giu,wtopaNegative,wtopaIgnored], ['obsuwa','normal',/\bobsuw\p{L}*/giu,/(?:\b(?:bez|brak)\s+(?:żadnej\s+)?obsuw\p{L}*|\bnie\s+było\s+(?:żadnej\s+)?obsuw\p{L}*|\bobsuw\p{L}*\s+(?:nie\s+było|brak))/giu,null], ['spółdzielnia','normal',/\bsp[oó]łdzieln\p{L}*/giu,cooperativeNegative,null], ['agencja','normal',new RegExp(agencyToken,'giu'),agencyNegative,null], ['atmosfera','normal',niceAtmosphere,niceAtmosphereNegative,null], ['prywatnie','normal',/(?:\b(?:przyjmuje|pracuje|działa|dziala|zaprasza\p{L}*|wyje[żz]d[żz]a)(?:\s+\p{L}+){0,5}\s+prywatnie\b|\bprywatnie\s+(?:przyjmuje|pracuje|działa|dziala|na\s+masa[żz]\p{L}*)\b|^prywatnie[.!]?\s*$)/gimu,privateNegative,null], ['oszustwo','normal',new RegExp(fraudToken,'giu'),fraudNegative,riskIgnored], ['scam','normal',new RegExp(scamToken,'giu'),scamNegative,riskIgnored], ['podmianka','normal',new RegExp(swapToken,'giu'),swapNegative,riskIgnored], ['zdjęcia','photo',photoConsistent,photoNegative,photoIgnored], [ 'rekomendacja', 'normal', new RegExp(recommendationToken,'giu'), new RegExp(String.raw`(?:\bnie\s+(?:(?:mog\p{L}*|chc\p{L}*|b[eę]d\p{L}*)\s+(?:\p{L}+\s+){0,3})?${recommendationToken}|${recommendationToken}\s+nie\s+mog\p{L}*|\bodradz\p{L}*|\bbez\s+polecenia\b|\bpolecam\s+(?:to\s+)?(?:na\s+ni[aą]\s+)?uwa[żz]a\p{L}*)`,'giu'), recommendationIgnored ] ]; return {rules,extraPositive,extraNegative,extraIgnored}; })(); const stats={}; const createBucket=() => ({ positive:0, negative:0, positiveEvidence:[], negativeEvidence:[] }); for (const [key] of rules) stats[key]=createBucket(); function add(post) { const text=String(post?.text || ''); const gfePraise=regexSpans(text,/(?:\bnigdy\s+takiego\s+gfe\s+nie\s+do[śs]wiadczy[łl]em\b|\b(?:nigdy|jeszcze)\s+nie\s+(?:miałem|mialem|do[śs]wiadczy[łl]em)\s+takiego\s+gfe\b)/giu); const scopes=getGarsoPostScopes(text); const answers=regexSpans(text,/\bczy\s+polecam\s*(?:\?|[-–—:])\s*(?:tak|oczywiście|oczywiscie|jak\s+najbardziej|nie)(?!\p{L})/giu); const questions=regexSpans(text,/[^.!?\n]*\?|https?:\/\/[^\s<>]+/giu).filter(question => !answers.some(answer => spansOverlap(question,answer))); for (const [key,,positiveRegex,negativeRegex,ignoredRegex] of rules) { const bucket=stats[key]; const ignoredSpans=[...questions,...(ignoredRegex ? regexSpans(text,ignoredRegex):[]),...(extraIgnored[key] ? regexSpans(text,extraIgnored[key]):[])]; const accept=(span,tone) => { const acrossScopes=!scopes.some(scope => scope.start <= span.start && scope.end >= span.end); const reason=acrossScopes ? 'różne sekcje osób':ignoredSpans.some(ignored => spansOverlap(span,ignored)) ? 'pytanie lub wyrażenie wyłączone':classifyGarsoMention(text,key,span,tone); return !reason; }; const negativeSpans=[...regexSpans(text,negativeRegex),...(extraNegative[key] ? regexSpans(text,extraNegative[key]):[]), ...(key === 'rekomendacja' ? answers.filter(span => /\bnie\s*$/iu.test(text.slice(span.start,span.end))):[])] .filter(span => accept(span,'negative') && (key !== 'GFE' || !gfePraise.some(praise => spansOverlap(span,praise)))) .sort((a,b) => a.start - b.start || b.end - a.end); if (negativeSpans.length) { bucket.negative++; bucket.negativeEvidence.push( buildGarsoMentionEvidence(post,negativeSpans,scopes,key) ); } const positiveSpansRaw=[...regexSpans(text,positiveRegex),...(extraPositive[key] ? regexSpans(text,extraPositive[key]):[]), ...(key === 'rekomendacja' ? answers.filter(span => !/\bnie\s*$/iu.test(text.slice(span.start,span.end))):[])]; const positiveSpans=positiveSpansRaw.filter(mention => !negativeSpans.some(negated => spansOverlap(mention,negated)) && accept(mention,'positive')) .sort((a,b) => a.start - b.start || b.end - a.end); if (positiveSpans.length) { bucket.positive++; bucket.positiveEvidence.push( buildGarsoMentionEvidence(post,positiveSpans,scopes,key) ); } } } return { add, result:() => stats }; } function parseGarsoPostDate(value) { const text=String(value || '') .toLocaleLowerCase('pl-PL') .replace(/\u00a0/g,' ') .replace(/\s+/g,' ') .trim(); if (!text) return null; const relativeDate=new Date(); if (/\b(?:dzisiaj|today)\b/.test(text)) { return Date.UTC( relativeDate.getFullYear(), relativeDate.getMonth(), relativeDate.getDate() ); } if (/\b(?:wczoraj|yesterday)\b/.test(text)) { relativeDate.setDate(relativeDate.getDate() - 1); return Date.UTC( relativeDate.getFullYear(), relativeDate.getMonth(), relativeDate.getDate() ); } let match=text.match(/\b(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})\b/); if (match) { const time=Date.UTC(Number(match[1]),Number(match[2]) - 1,Number(match[3])); return Number.isFinite(time) ? time:null; } match=text.match(/\b(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{4})\b/); if (match) { const time=Date.UTC(Number(match[3]),Number(match[2]) - 1,Number(match[1])); return Number.isFinite(time) ? time:null; } const polishMonths={ styczeń:0,stycznia:0, luty:1,lutego:1, marzec:2,marca:2, kwiecień:3,kwietnia:3, maj:4,maja:4, czerwiec:5,czerwca:5, lipiec:6,lipca:6, sierpień:7,sierpnia:7, wrzesień:8,września:8,wrzesnia:8, październik:9,października:9,pazdziernika:9, listopad:10,listopada:10, grudzień:11,grudnia:11 }; match=text.match( /\b(\d{1,2})\s+(styczeń|stycznia|luty|lutego|marzec|marca|kwiecień|kwietnia|maj|maja|czerwiec|czerwca|lipiec|lipca|sierpień|sierpnia|wrzesień|września|wrzesnia|październik|października|pazdziernika|listopad|listopada|grudzień|grudnia)\s+(\d{4})\b/ ); if (!match) return null; const time=Date.UTC(Number(match[3]),polishMonths[match[2]],Number(match[1])); return Number.isFinite(time) ? time:null; } function formatIsoDateFromTimestamp(timestamp) { const time=Number(timestamp); return Number.isFinite(time) ? new Date(time).toISOString().slice(0,10):null; } function detectOralRatingVariants(text) { const value=String(text || ''); return { fbg:/(?:\b(?:fbg|fgb)\b|\bfrancuz\p{L}*.{0,30}\bbez\s+gum\p{L}*)/iu.test(value), fwg:/(?:\bfwg\b|\bfrancuz\p{L}*.{0,30}\b(?:w|z)\s+gum\p{L}*)/iu.test(value) }; } async function buildRatingStats(posts,cachedMentions=null,cancelToken=null) { const reviews=[]; const categoryValues=Object.fromEntries(CATEGORY_DEFS.map(d => [d.key,[]])); const oralRatingVariants={fbg:false,fwg:false}; const mentionAnalyzer=cachedMentions ? null:createExplicitMentionAnalyzer(); let chunkStarted=performance.now(); for (let index=0; index < posts.length; index++) { cancelToken?.throwIfCancelled(); if (performance.now() - chunkStarted >= 8) { await yieldHelperWork(); cancelToken?.throwIfCancelled(); chunkStarted=performance.now(); } const post=posts[index]; mentionAnalyzer?.add(post); const ratings=post.ratingValues || extractRatingsFromPost(post.text,post); const componentValues=CATEGORY_DEFS .filter(def => !def.overall && ratings[def.key] != null) .map(def => ratings[def.key]); const vals=componentValues.length ? componentValues :(ratings.summary == null ? []:[ratings.summary]); if (!Object.keys(ratings).length || !vals.length) continue; for (const [k,v] of Object.entries(ratings)) categoryValues[k].push(v); if (ratings.oral != null) { const variants=post.oralVariants || detectOralRatingVariants(post.text); oralRatingVariants.fbg ||= variants.fbg; oralRatingVariants.fwg ||= variants.fwg; } reviews.push({index,mean:avg(vals),ratings,post}); } cancelToken?.throwIfCancelled(); const reviewMeans=reviews.map(r => r.mean); const datedReviews=reviews .map(review => ({ time:parseGarsoPostDate(review.post?.date), url:review.post?.pageUrl ? normalizeTopicUrl(review.post.pageUrl) :null, mean:review.mean, index:review.index })) .filter(item => Number.isFinite(item.time)) .sort((a,b) => a.time - b.time || a.index - b.index); const chronologicalLastReviews=datedReviews.length === reviews.length ? datedReviews.slice(-5) :reviews.slice(-5).map(review => ({mean:review.mean})); const last5=chronologicalLastReviews.map(review => review.mean); const datedPosts=posts .map(post => ({ time:parseGarsoPostDate(post?.date), url:post?.pageUrl ? normalizeTopicUrl(post.pageUrl):null })) .filter(item => Number.isFinite(item.time)) .sort((a,b) => a.time - b.time); const reviewDateSource=reviews.length ? (datedReviews.length ? datedReviews:datedPosts) :[]; const categories={}; const oralCategoryLabel=oralRatingVariants.fbg !== oralRatingVariants.fwg ? (oralRatingVariants.fbg ? 'FBG':'FWG') :'Francuz'; for (const def of CATEGORY_DEFS) { const values=categoryValues[def.key]; categories[def.key]={ key:def.key, label:def.key === 'oral' ? oralCategoryLabel:def.label, mean:round1(avg(values)), count:values.length, min:values.length ? round1(Math.min(...values)):null, max:values.length ? round1(Math.max(...values)):null }; } return { reviews, reviewCount:reviews.length, mean:round1(avg(reviewMeans)), median:round1(median(reviewMeans)), min:reviewMeans.length ? round1(Math.min(...reviewMeans)):null, max:reviewMeans.length ? round1(Math.max(...reviewMeans)):null, last5Mean:round1(avg(last5)), last5ReviewCount:last5.length, firstReviewDate:formatIsoDateFromTimestamp(reviewDateSource[0]?.time), lastReviewDate:formatIsoDateFromTimestamp(reviewDateSource.at(-1)?.time), firstReviewUrl:reviewDateSource[0]?.url || null, lastReviewUrl:reviewDateSource.at(-1)?.url || null, categories, postDateBounds:[datedPosts[0],datedPosts.at(-1)].filter(Boolean), mentions:cachedMentions || mentionAnalyzer.result() }; } function getGarsoRatingCategoryLabel(category) { const label=String(category?.label || '').trim(); const normalizedLabel=label.toLocaleLowerCase('pl-PL'); if (normalizedLabel === 'francuz / oral') return 'Francuz'; if (normalizedLabel === 'wygląd dziewczyny') return 'Wygląd'; if (normalizedLabel === 'akcja / seks') return 'Akcja'; return label; } function getGarsoRatingBarColor(mean) { const score=Number(mean); if (mean == null || !Number.isFinite(score)) { return 'color-mix(in srgb, var(--vm-ui-panel-text, #fff) 16%, transparent)'; } if (score < 5) return 'var(--vm-ui-danger, #e04b4b)'; if (score >= 9) return 'var(--vm-ui-success, #4ac46b)'; const hue=score <= 7 ? ((score - 5) / 2) * 50 :50 + ((score - 7) / 2) * 72; return `hsl(${Math.round(hue)}, 78%, 52%)`; } function emitGarsoCountState(btn,state,count=null,fromCache=false) { if (!btn) return; btn.dispatchEvent(new CustomEvent('vm-garso-count-state',{ detail:{ state, count:count != null && Number.isFinite(Number(count)) ? Number(count):null, fromCache:!!fromCache } })); } function setGarsoSummaryState( btn, state, html, {checkedAt=null,cacheState=null}={} ) { if (!btn) return; btn.dataset.garsoSummaryState=state; btn.dataset.garsoSummaryHtml=html || ''; if (Number.isFinite(Number(checkedAt)) && Number(checkedAt) > 0) { btn.dataset.garsoSummaryCheckedAt=String(Number(checkedAt)); } else { delete btn.dataset.garsoSummaryCheckedAt; } if (cacheState) btn.dataset.garsoSummaryCacheState=cacheState; else delete btn.dataset.garsoSummaryCacheState; renderGarsoButton(btn); btn.dispatchEvent(new CustomEvent('vm-garso-summary-state',{ detail:{ state, html:btn.dataset.garsoSummaryHtml, checkedAt:Number(btn.dataset.garsoSummaryCheckedAt) || null, cacheState:btn.dataset.garsoSummaryCacheState || null, firstReviewDate:btn.dataset.garsoFirstReviewDate || null, lastReviewDate:btn.dataset.garsoLastReviewDate || null, firstReviewUrl:btn.dataset.garsoFirstReviewUrl || null, lastReviewUrl:btn.dataset.garsoLastReviewUrl || null, result:btn._vmGarsoSummaryResult || null } })); } function restoreGarsoSummaryFromCache(btn,searchTerm,searchMode) { const cached=getGarsoSummaryCache(searchMode,searchTerm); if (!cached) return null; applySummaryToButton(btn,cached.result,{ checkedAt:cached.checkedAt, cacheState:cached.fresh ? 'fresh':'stale' }); return cached; } async function prepareGarsoSummary( btn, searchTerm, topics, forceRefresh=false, operationCancelToken=null ) { const searchMode=btn.dataset.garsoSearchMode || ''; const key=getGarsoTopicsSignature(topics) + ':' + GM_getValue(GARSO_TOPIC_REVISION_KEY,''); const persistent=getGarsoSummaryCache(searchMode,searchTerm); const sameTopics=persistent && persistent.topicSignature === getGarsoTopicsSignature(topics); if (!forceRefresh && persistent?.recentCheck && sameTopics) { applySummaryToButton(btn,persistent.result,{ checkedAt:persistent.checkedAt, cacheState:persistent.fresh ? 'fresh':'stale' }); return persistent.result; } const progressOptions=sameTopics ? {checkedAt:persistent.checkedAt,cacheState:'refreshing'}:{}; const renderPending=() => setGarsoSummaryState(btn,sameTopics ? 'refreshing':'loading', sameTopics ? summaryToHtml(persistent.result):'<b>Garso - recenzje</b><br>Pobieram treść tematów…',progressOptions); renderPending(); const pending=!forceRefresh && summaryCache.get(key); const cancelToken=pending?.cancelToken || operationCancelToken || createOperationCancelToken('Analiza Garso'); btn._vmGarsoSummaryCancelToken=cancelToken; renderPending(); const renderProgress=progress => { if (!progress || !['page','topic-done','stats'].includes(progress.phase)) { return; } let line='Pobieram treść tematów…'; if (progress.phase === 'page') { line=`Temat ${progress.topicIndex + 1}/${progress.topicTotal}` + ` • ${progress.message || `strona ${progress.page}/${progress.pageTotal}`}`; } else if (progress.phase === 'topic-done') { line=`Pobrano tematy: ${progress.completedTopics}/${progress.topicTotal}`; } else if (progress.phase === 'stats') { line=`Obliczam statystyki z ${progress.postCount} postów…`; } setGarsoSummaryState( btn, sameTopics ? 'refreshing':'loading', `<b>Garso - recenzje</b><br>${escapeHtml(line)}`, progressOptions ); }; const promise=pending || buildGarsoSummary( searchTerm,topics,cancelToken,{forceRefresh,onProgress:renderProgress}).then(compactGarsoSummaryResult); promise.cancelToken=cancelToken; summaryCache.set(key,promise); try { const result=await promise; const complete=isCompleteGarsoSummary(result,topics); if (!complete && summaryCache.get(key) === promise) { summaryCache.delete(key); } const persistedAt=complete ? setGarsoSummaryCache(searchMode,searchTerm,topics,result) :null; if (!complete && sameTopics) { applySummaryToButton(btn,persistent.result,{ checkedAt:persistent.checkedAt, cacheState:'stale' }); return persistent.result; } const checkedAt=persistedAt || Date.now(); applySummaryToButton(btn,result,{ checkedAt, cacheState:result.unverifiedTopicCount ? 'stale':(persistedAt ? 'fresh':(complete ? 'session':'partial')) }); return result; } catch (e) { if (summaryCache.get(key) === promise) summaryCache.delete(key); if (isOperationCancelledError(e)) { if (sameTopics) { btn._vmGarsoSummaryResult=persistent.result; setGarsoSummaryState( btn, 'cancelled', summaryToHtml(persistent.result), { checkedAt:persistent.checkedAt, cacheState:'stale' } ); return persistent.result; } setGarsoSummaryState( btn, 'cancelled', '<b>Garso - recenzje</b><br>Analiza została przerwana.' ); return null; } log('Błąd podsumowania Garso:',e); if (sameTopics) { applySummaryToButton(btn,persistent.result,{ checkedAt:persistent.checkedAt, cacheState:'stale' }); } else { setGarsoSummaryState( btn, 'error', `<b>Garso - recenzje</b><br>Nie udało się pobrać postów: ${escapeHtml(e.message || e)}` ); } return null; } finally { if (summaryCache.get(key) === promise) summaryCache.delete(key); if (btn._vmGarsoSummaryCancelToken === cancelToken) { btn._vmGarsoSummaryCancelToken=null; } } } async function buildGarsoSummary(searchTerm,topics,cancelToken=null,{forceRefresh=false,onProgress=null}={}) { topics=mergeUniqueGarsoTopics(topics); cancelToken ||= createOperationCancelToken('Analiza Garso'); const client=createGarsoTopicCacheClient(cancelToken); let completedTopics=0; try { const results=await watchMapLimit(topics,GARSO_TOPIC_FETCH_CONCURRENT,async (topic,topicIndex) => { try { cancelToken.throwIfCancelled(); const value=await client.get(topic,{forceRefresh,onProgress:(page,pageTotal,message,fromCache) => onProgress?.({phase:'page',topicIndex,topicTotal:topics.length,page,pageTotal,message,fromCache})}); cancelToken.throwIfCancelled(); if (value?.eligible !== false && !validGarsoTopicAnalysis(value,getGarsoTopicPageContext(topic.url)?.id)) throw new Error('Niepełna analiza tematu.'); return {topic,value}; } catch (error) { if (cancelToken.cancelled || isOperationCancelledError(error)) throw error; log(`Nie udało się odczytać tematu Garso: ${topic.url}`,error); return {topic,error}; } finally { onProgress?.({phase:'topic-done',completedTopics:++completedTopics,topicTotal:topics.length,topicIndex}); } }); cancelToken.throwIfCancelled(); const ratingPosts=[],topicInfo=[],analyzedTopics=[],excludedTopics=[],mentions={}; let postCount=0,failedTopicCount=0,unverifiedTopicCount=0,sourceCheckedAt=Date.now(),sourceCheckAttemptAt=Date.now(); for (const {topic,value,error} of results) { if (error) {failedTopicCount++; continue;} if (value.eligible === false) {excludedTopics.push({...topic,title:value.title || topic.title}); continue;} const summary=value.summary; sourceCheckedAt=Math.min(sourceCheckedAt,value.checkedAt); sourceCheckAttemptAt=Math.min(sourceCheckAttemptAt,Number(value.postCountCheckedAt) || 0); if (value.postCountUnverified) unverifiedTopicCount++; topicInfo.push(...summary.topicInfo); analyzedTopics.push(...summary.topics); postCount += summary.postCount; for (const post of value.ratingPosts) ratingPosts.push({...post,pageUrl:post.pageUrl || summary.topics[0].url}); for (const bound of summary.stats.postDateBounds || []) ratingPosts.push({date:formatIsoDateFromTimestamp(bound.time),pageUrl:bound.url,ratingValues:{}}); for (const [key,bucket] of Object.entries(summary.stats.mentions || {})) { const combined=mentions[key] ||= {positive:0,negative:0,positiveEvidence:[],negativeEvidence:[]}; for (const tone of ['positive','negative']) { combined[tone] += Number(bucket[tone]) || 0; combined[tone + 'Evidence'].push(...(bucket[tone + 'Evidence'] || [])); } } } onProgress?.({phase:'stats',postCount}); const stats=await buildRatingStats(ratingPosts,mentions,cancelToken); return {searchTerm,topics:analyzedTopics,topicInfo,excludedTopics,postCount,sourceCheckedAt,sourceCheckAttemptAt,unverifiedTopicCount, screenedTopicCount:topics.length - failedTopicCount,excludedTopicCount:excludedTopics.length,failedTopicCount,stats}; } finally {client.close();} } function getGarsoMentionEntries(resultOrStats,includeZero=false) { const stats=resultOrStats?.stats || resultOrStats; const mentions=stats?.mentions; if (!mentions || typeof mentions !== 'object') return []; const entries=[['GFE','GFE','brak GFE'],['wtopa','wtopa','bez wtopy',true], ['obsuwa','obsuwa','bez obsuwy',true],['spółdzielnia','spółdzielnia','nie spółdzielnia',true],['agencja','agentura','nie agentura',true], ['atmosfera','miła atmosfera','niemiła atmosfera'],['prywatnie','prywatnie','nie prywatnie'], ['oszustwo','oszustka','nie oszustka',true],['scam','scam','brak scamu',true], ['podmianka','podmianka','brak podmianki',true],['zdjęcia','zdjęcia zgodne','zdjęcia niezgodne'], ['rekomendacja','polecam','nie polecam']].flatMap(([key,positive,negative,invert]) => ['positive','negative'].map((kind,index) => [index ? negative:positive,mentions[key]?.[kind], (index === 0) !== !!invert ? 'positive':'negative',mentions[key]?.[kind + 'Evidence']])).map(([label,count,tone,evidence]) => ({ label, count:Math.max(0,Number(count) || 0), tone, evidence:(Array.isArray(evidence) ? evidence:[]).map(item => ({ topicTitle:normalizeEscortAdText(item?.topicTitle) || 'Temat Garso', url:item?.url ? normalizeTopicUrl(item.url):'', author:normalizeEscortAdText(item?.author), date:normalizeEscortAdText(item?.date), snippet:String(item?.snippet || '').slice(0,360), match:String(item?.match || '').slice(0,120), scope:String(item?.scope || '').slice(0,100), visit:String(item?.visit || '').slice(0,100), aspect:String(item?.aspect || '').slice(0,40) })) })); return includeZero ? entries :entries.filter(entry => entry.count > 0); } function getGarsoMentionToneStyle(tone) { if (tone === 'positive') { return { color:'var(--vm-ui-success, #9aefad)', border:'color-mix(in srgb, var(--vm-ui-success, #62cf7b) 65%, transparent)', background:'color-mix(in srgb, var(--vm-ui-success, #62cf7b) 14%, transparent)' }; } if (tone === 'negative') { return { color:'var(--vm-ui-danger, #ff9a9a)', border:'color-mix(in srgb, var(--vm-ui-danger, #ff6262) 72%, transparent)', background:'color-mix(in srgb, var(--vm-ui-danger, #dc3545) 16%, transparent)' }; } return { color:'var(--vm-ui-panel-text, #fff)', border:'color-mix(in srgb, var(--vm-ui-accent, #f54da3) 55%, transparent)', background:'color-mix(in srgb, var(--vm-ui-accent, #f54da3) 10%, transparent)' }; } function getGarsoReviewWarning(result) { const popularMentions=getMostFrequentGarsoMentions(result); const negativePopularMentions=popularMentions.filter( item => item.tone === 'negative' ); const rawMean=result?.stats?.mean; const mean=Number(rawMean); const lowMean=rawMean != null && Number.isFinite(mean) && mean < 6; const reasons=[]; if (negativePopularMentions.length) { reasons.push( `Najpopularniejsze negatywne wzmianki: ${negativePopularMentions .map(item => `${item.label} (${item.count})`) .join(', ')}.` ); } if (lowMean) reasons.push(`Średnia ocena: ${rawMean}/10.`); return { show:reasons.length > 0, title:reasons.join('\n'), lowMean, negativePopularMentions }; } function ensureGarsoSummaryCardsStyle() { if (document.getElementById('vm-garso-summary-cards-style')) return; appendHelperStyle('vm-garso-summary-cards-style',` @grating-card{min-width:0;padding:7px 8px;border:1px solid color-mix(in srgb,var(--vm-ui-panel-text,#fff) 16%,transparent);border-radius:7px;background:color-mix(in srgb,var(--vm-ui-panel-text,#fff) 3.5%,transparent)} @grating-head{display:flex;justify-content:space-between;gap:8px} @grating-value{color:var(--vm-ui-panel-text,#fff);font-weight:800} @grating-bar{height:4px;margin:6px 0 4px;border-radius:999px;background:color-mix(in srgb,var(--vm-ui-panel-text,#fff) 12%,transparent);overflow:hidden} @grating-bar>div{height:100%} @grating-detail{color:var(--vm-ui-panel-muted,#cbb8ce);font-size:10px} @gmetric,@gextreme{padding:8px;border:1px solid color-mix(in srgb,var(--vm-ui-accent,#f54da3) 32%,transparent);border-radius:8px;background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 7%,transparent);text-align:center} @gmetric-value{font-size:18px;font-weight:900;line-height:1.05;color:var(--vm-ui-panel-text,#fff)} @gmetric-label{margin-top:3px;color:var(--vm-ui-panel-muted,#cbb8ce);font-size:10px;line-height:1.15} @gmetric-detail{margin-top:2px;color:var(--vm-ui-panel-muted,#9f8ca3);font-size:9px} @gtrend{margin-left:5px;font-size:17px;font-weight:900} @gmention-chip{display:inline-block;margin:3px 3px 0 0;padding:3px 7px;border:1px solid;border-radius:999px;font:inherit;cursor:pointer} @gextreme-label{font-size:10px;font-weight:900;line-height:1.15} @gextreme-name{margin-top:4px;color:var(--vm-ui-panel-text,#fff);font-size:15px;font-weight:900;line-height:1.15} @gextreme-value{margin-top:3px;font-size:10px;font-weight:900;line-height:1.1} @gsummary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} @gsummary-info{margin-top:7px;padding:7px 8px;border-radius:7px;background:color-mix(in srgb,var(--vm-ui-panel-text,#fff) 3.5%,transparent);color:var(--vm-ui-panel-muted,#d8c8d9)} @gsummary-info>b{color:var(--vm-ui-panel-text,#fff)} @gsummary-heading{margin-top:9px;font-weight:800;color:var(--vm-ui-accent,#f54da3)} @gsummary-section{margin-top:9px;padding:8px 9px;border:1px solid color-mix(in srgb,var(--vm-ui-panel-text,#fff) 14%,transparent);border-radius:7px} @gsummary-section.-topics{background:color-mix(in srgb,var(--vm-ui-panel-text,#fff) 2.5%,transparent)} @gsummary-section.-subtitles{margin-top:8px;border-color:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 34%,transparent);background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 6%,transparent)} @gsummary-section.-excluded{margin-top:8px;border-color:color-mix(in srgb,var(--vm-ui-warning,#ffb45c) 58%,transparent);background:color-mix(in srgb,var(--vm-ui-warning,#ffb45c) 7%,transparent)} @gsummary-section.-topics>b,@gsummary-section.-subtitles>b{color:var(--vm-ui-accent,#f54da3)} @gsummary-topic{display:block;margin-top:5px;color:var(--vm-ui-panel-text,#fff);text-decoration:none} @gsummary-topic>span:first-child{color:var(--vm-ui-accent,#f54da3)} @gsummary-topic>small{display:block;margin-left:12px;color:var(--vm-ui-panel-muted,#cbb8ce);font-size:10px} @gsummary-topic.-excluded,@gsummary-section.-excluded>b{color:var(--vm-ui-warning,#ffd2e8)} @gsummary-topic.-excluded>span:first-child{color:var(--vm-ui-warning,#ffb45c)} @gsummary-subtitle{margin-top:6px} @gsummary-subtitle>div{color:var(--vm-ui-panel-text,#fff);font-weight:800;line-height:1.25} @gsummary-subtitle>small{display:block;margin-top:2px;color:var(--vm-ui-panel-muted,#cbb8ce);font-size:10px;line-height:1.2} `.replaceAll('@g','.vm-garso-')); } function summaryToHtml(result,{topicView=false}={}) { if (!result.topics?.length && !result.topicInfo?.length && !result.excludedTopics?.length) return '<div class="vm-garso-summary-preview">Brak tematów.</div>'; ensureGarsoSummaryCardsStyle(); const s=result.stats; const scope=result.topicInfo?.some(topic => topic.pagesRead?.length < topic.maxPage) ? '<div class="vm-garso-summary-info">Długie tematy: pierwsze 10 i ostatnie 20 stron.</div>':''; const categoryCards=(topicView && CATEGORY_DEFS.every(d => s.categories?.[d.key]?.mean == null) ? []:CATEGORY_DEFS).map(d => { const c=s.categories[d.key],percentage=c.mean == null ? 0:Math.max(0,Math.min(100,Number(c.mean) * 10)); return `<div class="vm-garso-rating-card"><div class="vm-garso-rating-head"><b>${escapeHtml(getGarsoRatingCategoryLabel(c))}</b> <span class="vm-garso-rating-value"${c.mean == null ? ' style="color:var(--vm-ui-panel-muted,#cbb8ce)"':''}>${c.mean == null ? '-':`${c.mean}/10`}</span></div> <div class="vm-garso-rating-bar"><div style="width:${percentage}%;background:${getGarsoRatingBarColor(c.mean)}"></div></div> <div class="vm-garso-rating-detail">${c.mean == null ? 'brak ocen':`${c.count} ocen · zakres ${c.min}-${c.max}`}</div></div>`; }).join(''); const mentions=getGarsoMentionEntries(s).sort((a,b) => b.count - a.count || a.label.localeCompare(b.label,'pl-PL')); const mentionChipsHtml=mentions.map((item,index) => { const tone=getGarsoMentionToneStyle(item.tone); return `<button type="button" class="vm-garso-mention-chip" data-vm-garso-mention-index="${index}" title="Wzmianki z analizowanych stron; mogą dotyczyć różnych osób i wizyt. Pokaż źródła." style="border-color:${tone.border};background:${tone.background};color:${tone.color}">${escapeHtml(item.label)} <b>${item.count}</b></button>`; }).join(' '); const metricCard=(label,value,detail='') => `<div class="vm-garso-metric"><div class="vm-garso-metric-value">${value}</div> <div class="vm-garso-metric-label">${label}</div>${detail ? `<div class="vm-garso-metric-detail">${detail}</div>`:''}</div>`; const overall=Number(s.mean),recent=Number(s.last5Mean),comparable=s.mean != null && s.last5Mean != null && Number.isFinite(overall) && Number.isFinite(recent); const trendColor=comparable && recent !== overall ? (recent > overall ? 'var(--vm-ui-success,#67df85)':'var(--vm-ui-danger,#ff7777)'):'var(--vm-ui-panel-text,#fff)'; let overallMean=s.mean == null ? '-':`${s.mean}/10`; if (comparable && Number(s.last5ReviewCount) >= 5 && Number(s.reviewCount) > 5 && recent !== overall) { const title=escapeHtml(`Ostatnie 5 recenzji: ${s.last5Mean}/10; ${recent > overall ? 'wzrost':'spadek'} o ${round1(Math.abs(recent - overall))} względem całości.`); overallMean+=`<span class="vm-garso-trend" title="${title}" aria-label="${title}" style="color:${trendColor}">${recent > overall ? '↑':'↓'}</span>`; } if (topicView) return `<div class="vm-garso-topic-overall">${metricCard('średnia ogólna',overallMean,`${s.reviewCount} recenzji z ocenami`)}</div> ${scope}<div class="vm-garso-topic-grid">${categoryCards}</div>${mentionChipsHtml ? `<div class="vm-garso-topic-mentions">${mentionChipsHtml}</div>`:''}`; const ranked=CATEGORY_DEFS.filter(d => !d.overall).map(d => s.categories?.[d.key]).filter(c => c?.count > 0 && Number.isFinite(c.mean)).sort((a,b) => b.mean - a.mean); const getPostWord=count => polishCountForm(count,'post','posty','postów'),postCount=Math.max(0,Number(result.postCount) || 0); const excluded=Array.isArray(result.excludedTopics) ? result.excludedTopics:[]; const topicLines=(topics,skipped=false) => topics.map(t => `<a class="vm-garso-summary-topic${skipped ? ' -excluded':''}" href="${escapeHtml(normalizeTopicUrl(t.url))}" target="_blank" rel="noopener noreferrer"><span>›</span> ${escapeHtml(t.title)}${skipped ? '':`<small>${t.posts} ${getPostWord(t.posts)}</small>`}</a>`).join(''); const subtitleLines=result.topicInfo.map(t => ({...t,subtitle:getDisplayableGarsoTopicSubtitle(t.subtitle)})).filter(t => t.subtitle).map(t => `<div class="vm-garso-summary-subtitle"><div>${escapeHtml(t.subtitle)}</div><small>${escapeHtml(t.title)}</small></div>`).join(''); const extremeCard=(label,c,high) => { const color=high ? 'var(--vm-ui-success,#9aefad)':'var(--vm-ui-danger,#ffaaaa)'; const border=`color-mix(in srgb,var(--vm-ui-${high ? 'success,#62cf7b':'danger,#ff6262'}) ${high ? 62:68}%,transparent)`; const bg=`color-mix(in srgb,var(--vm-ui-${high ? 'success,#62cf7b':'danger,#dc3545'}) ${high ? 13:14}%,transparent)`; return `<div class="vm-garso-extreme" style="border-color:${border};background:${bg};color:${color}"><div class="vm-garso-extreme-label">${label}</div><div class="vm-garso-extreme-name">${c ? escapeHtml(getGarsoRatingCategoryLabel(c)):'brak ocen'}</div><div class="vm-garso-extreme-value">${c ? `${c.mean}/10`:'-'}</div></div>`; }; const section=(kind,label,content)=>`<div class="vm-garso-summary-section ${kind}"><b>${label}</b>${content}</div>`; const lastLabel=Number(s.last5ReviewCount) === 1 ? 'ostatnia ocena':`ostatnie ${Math.max(0,Number(s.last5ReviewCount) || 0)}`; return `<div class="vm-garso-summary-preview">${scope} ${result.unverifiedTopicCount ? `<div role="status" style="margin-bottom:7px;color:var(--vm-ui-warning,#b46b12)">Nie potwierdzono aktualności ${result.unverifiedTopicCount} tematów. Zachowano poprzednie wyniki; odśwież ręcznie w razie potrzeby.</div>`:''} <div class="vm-garso-summary-grid">${metricCard('recenzje z ocenami',s.reviewCount)}${metricCard('średnia ogólna',overallMean)}${extremeCard('najwyżej',ranked[0],true)}${extremeCard('najniżej',ranked.at(-1),false)}</div> <div class="vm-garso-summary-info">Analiza: <b>${result.topicInfo.length} ${getGarsoTopicWord(result.topicInfo.length)} · ${postCount} ${getPostWord(postCount)}</b> · zakres: <b>${s.reviewCount ? `${s.min}-${s.max}/10`:'-'}</b> · ${lastLabel}: <b style="color:${trendColor}">${s.last5Mean == null ? '-':`${s.last5Mean}/10`}</b></div></div> ${subtitleLines ? section('-subtitles','Dopiski z Garso',subtitleLines):''} <div class="vm-garso-summary-heading">Oceny cząstkowe</div><div class="vm-garso-summary-grid" style="margin-top:6px">${categoryCards}</div> ${mentionChipsHtml ? section('','Jawne wzmianki w analizowanych stronach',`<div style="margin-top:3px">${mentionChipsHtml}</div>`):''} ${section('-topics','Analizowane tematy',topicLines(result.topicInfo) || '<div style="margin-top:5px;color:var(--vm-ui-panel-muted,#cbb8ce)">Brak tematów spełniających kryteria analizy.</div>')} ${excluded.length ? section('-excluded',`Pominięte w statystyce (${excluded.length})`,`<div style="margin-top:3px;color:var(--vm-ui-panel-muted,#d8c8d9);font-size:10px">Tytuły mogą zawierać polecenia lub ostrzeżenia - kliknij, aby otworzyć temat.</div>${topicLines(excluded,true)}`):''}`; } function applySummaryToButton( btn, result, {checkedAt=null,cacheState=null}={} ) { btn._vmGarsoSummaryResult=result || null; const firstReviewDate=result?.stats?.firstReviewDate || ''; const lastReviewDate=result?.stats?.lastReviewDate || ''; btn.dataset.garsoReviewCount=String( Math.max(0,Number(result?.stats?.reviewCount) || 0) ); const firstReviewUrl=result?.stats?.firstReviewUrl || result?.topics?.[0]?.url || ''; const lastReviewUrl=result?.stats?.lastReviewUrl || result?.topics?.at(-1)?.url || firstReviewUrl; if (firstReviewDate) btn.dataset.garsoFirstReviewDate=firstReviewDate; else delete btn.dataset.garsoFirstReviewDate; if (lastReviewDate) btn.dataset.garsoLastReviewDate=lastReviewDate; else delete btn.dataset.garsoLastReviewDate; if (firstReviewUrl) btn.dataset.garsoFirstReviewUrl=firstReviewUrl; else delete btn.dataset.garsoFirstReviewUrl; if (lastReviewUrl) btn.dataset.garsoLastReviewUrl=lastReviewUrl; else delete btn.dataset.garsoLastReviewUrl; setGarsoSummaryState( btn, 'ready', summaryToHtml(result), {checkedAt,cacheState} ); } function normalizeEscortCity(value) { return String(value || '') .replace(/\s+/g,' ') .replace(/^[\s,;|\-–\u2014]+|[\s,;|\-–\u2014]+$/g,'') .trim(); } function normalizeEscortCityKey(value) { return normalizeEscortCity(value) .normalize('NFKD') .replace(/\p{M}/gu,'') .toLocaleLowerCase('pl-PL'); } function extractEscortClubCity(doc) { if (!doc) return null; const locationLinks=[...doc.querySelectorAll('.content-location a[href]')]; for (const link of locationLinks) { try { const url=new URL(link.getAttribute('href'),'https://pl.escort.club/'); if (!/^\/(?:anonse\/towarzyskie|personal-ads)\/[^/]+\/?$/i.test(url.pathname)) continue; if (/\/poland\/?$/i.test(url.pathname) || url.searchParams.has('district')) continue; const city=normalizeEscortCity(link.textContent); if (city) return city; } catch {} } return null; } function buildEscortClubAdDocumentResult(html,status,finalUrl,transport) { const text=String(html || ''); const numericStatus=Number(status) || 0; return { ok:!!text && (numericStatus === 0 || (numericStatus >= 200 && numericStatus < 400)), status:numericStatus, finalUrl, transport, doc:new DOMParser().parseFromString(text,'text/html') }; } function isUsableEscortClubAdDocument(result,requestedUrl) { if (!result?.ok || !result.doc) return false; try { const requestedId=parseAdIdFromUrl(requestedUrl); const finalUrl=new URL(result.finalUrl || requestedUrl,requestedUrl); if ( requestedId && isEscortClubHost(finalUrl.hostname) && finalUrl.pathname === '/' ) { return true; } } catch {} const requestedId=parseAdIdFromUrl(requestedUrl); const finalId=parseAdIdFromUrl(result.finalUrl || requestedUrl); if (requestedId && finalId && requestedId !== finalId) return false; const title=result.doc.querySelector( '.content-name h1, .content-name.-desc-name h1' ); const bodyText=normalizeEscortAdText(result.doc.body?.textContent); return !!title && !/^site unavailable\b/i.test(bodyText); } async function fetchEscortClubAdDocumentViaPage(url,cancelToken=null,requestContext=null,beforeDispatch=null) { if (typeof unsafeWindow === 'undefined' || typeof unsafeWindow.fetch !== 'function') { throw new Error('Brak dostępu do fetch strony'); } const AbortControllerClass=unsafeWindow.AbortController || AbortController; const controller=new AbortControllerClass(); let timer=null,response=null,trace=null,outcome='error'; let releaseSlot=() => {}; const unsubscribeCancel=cancelToken?.onCancel(() => controller.abort()) || (() => {}); try { cancelToken?.throwIfCancelled(); releaseSlot=await acquireEscortRequestSlot(url,cancelToken,false,'GET',requestContext); cancelToken?.throwIfCancelled(); const skipped=beforeDispatch?.(); if (skipped) { return skipped.value; } releaseSlot.beforeDispatch?.(); timer=setTimeout(() => controller.abort(),ESCORT_AD_DATA_FETCH_TIMEOUT_MS); trace=recordDiagnosticRequest(url,'GET',requestContext,'fetch'); const pendingResponse=unsafeWindow.fetch(url,{ method:'GET', credentials:'include', redirect:'follow', cache:'no-store', signal:controller.signal, headers:{ Accept:'text/html,application/xhtml+xml' } }); releaseSlot.markStarted?.(); response=await pendingResponse; trace?.(response.status,'headers',response); outcome='response'; recordDiagnosticResponse(url,response.status); let html,blockedError=null; try { const info={status:response.status,headers:response.headers,finalUrl:response.url}; if (releaseSlot.observeResponse) releaseSlot.observeResponse(info,false); else observeEscortRequestResponse(url,info); } catch (error) {blockedError=error;} try {html=await response.text();} catch (error) { if (blockedError) throw blockedError; throw error; } if (blockedError) throw refineEscortRequestBlockedError(url,{responseText:html},blockedError); const info={status:response.status,headers:response.headers,responseText:html,finalUrl:response.url}; if (releaseSlot.observeResponse) releaseSlot.observeResponse(info); else observeEscortRequestResponse(url,info); return buildEscortClubAdDocumentResult(html,response.status,response.url || url,'page-fetch'); } catch (error) { outcome=cancelToken?.cancelled ? 'cancelled':controller.signal.aborted ? 'timeout':response?.status >= 400 ? 'response':'error'; throw error; } finally { trace?.(response?.status,outcome,response); clearTimeout(timer); unsubscribeCancel(); releaseSlot(); } } async function fetchEscortClubAdDocumentViaGm(url,cancelToken=null,requestContext=null,beforeDispatch=null) { const response=await gmRequest({ method:'GET', url, cancelToken, requestContext, beforeDispatch, timeout:ESCORT_AD_DATA_FETCH_TIMEOUT_MS, responseType:'text', overrideMimeType:'text/html; charset=utf-8', anonymous:false, headers:{ Accept:'text/html,application/xhtml+xml', Referer:location.href } }); if (response.sharedActivityResult || response.sharedActivityRetry || response.sharedAdData) return response; return buildEscortClubAdDocumentResult(response.responseText || response.response || '',response.status, response.finalUrl || url,'gm-xhr'); } async function fetchEscortClubAdDocumentViaIframe(url,cancelToken=null,requestContext=null,beforeDispatch=null) { let releaseSlot=() => {}; try { cancelToken?.throwIfCancelled(); releaseSlot=await acquireEscortRequestSlot(url,cancelToken,false,'GET',requestContext); cancelToken?.throwIfCancelled(); const skipped=beforeDispatch?.(); if (skipped) { releaseSlot(); return skipped.value; } } catch (error) { releaseSlot(); throw error; } return new Promise((resolve,reject) => { const iframe=makeElement('iframe'); let finished=false,trace=null; let timer=null; let unsubscribeCancel=() => {}; const cleanup=() => { if (timer) clearTimeout(timer); unsubscribeCancel(); try {detachHelperNode(iframe);} catch {} }; const finish=(callback,value,outcome,response) => { if (finished) return; finished=true; trace?.(0,outcome || (callback === resolve ? 'response':isOperationCancelledError(value) ? 'cancelled':'error'),response); cleanup(); callback(value); }; iframe.setAttribute('sandbox','allow-same-origin'); prepareHiddenHelperFrame(iframe); iframe.addEventListener('load',() => { if (finished) return; let html='',finalUrl=url; try { const sourceDoc=iframe.contentDocument; html=sourceDoc?.documentElement?.outerHTML || ''; finalUrl=iframe.contentWindow?.location?.href || url; const status=html ? 200:0; const info={status:0,responseText:html,finalUrl}; if (releaseSlot.observeResponse) releaseSlot.observeResponse(info); else observeEscortRequestResponse(url,info); const result=buildEscortClubAdDocumentResult(html,status,finalUrl,'iframe'); recordDiagnosticResponse(url,0); finish(resolve,result,'response',{finalUrl}); } catch (error) {finish(reject,error);} },{once:true}); iframe.addEventListener('error',() => { const error=new Error('Błąd nawigacji iframe'); recordDiagnosticRequestFailure(url,error); finish(reject,error); },{once:true}); timer=setTimeout(() => finish(reject,new Error('Przekroczono czas iframe'),'timeout'),ESCORT_AD_DATA_FETCH_TIMEOUT_MS); unsubscribeCancel=cancelToken?.onCancel(reason => { finish(reject,createOperationCancelledError(reason)); }) || (() => {}); if (finished) return; try { releaseSlot.beforeDispatch?.(); trace=recordDiagnosticRequest(url,'IFRAME',requestContext,'iframe'); iframe.src=url; document.body.appendChild(iframe); releaseSlot.markStarted?.(); } catch (error) {finish(reject,error);} }).finally(() => releaseSlot()); } async function fetchEscortClubAdDocumentUnshared(url,cancelToken=null,requestContext=null,beforeDispatch=null) { requestContext ||= createRequestContext('escort-ad-document'); const errors=[]; const transports=new URL(url,location.href).origin === location.origin ? [ ['page-fetch',fetchEscortClubAdDocumentViaPage], ['gm-xhr',fetchEscortClubAdDocumentViaGm], ['iframe',fetchEscortClubAdDocumentViaIframe] ] :[ ['gm-xhr',fetchEscortClubAdDocumentViaGm], ['page-fetch',fetchEscortClubAdDocumentViaPage], ['iframe',fetchEscortClubAdDocumentViaIframe] ]; for (const [name,loader] of transports) { try { cancelToken?.throwIfCancelled(); let result; while (true) { try {result=await loader(url,cancelToken,requestContext,beforeDispatch); break;} catch (error) { if (error?.code !== 'ESCORT_QUEUE_RESCHEDULE' && (error?.status !== 503 || error?.retryable !== true)) throw error; cancelToken?.throwIfCancelled(); } } if (result.sharedActivityResult || result.sharedActivityRetry || result.sharedAdData) return result; const usable=isUsableEscortClubAdDocument(result,url); if (usable || [404,410].includes(result?.status)) return result; errors.push(`${name}: nieprawidłowa odpowiedź (HTTP ${result?.status || 0})`); } catch (error) { const cancelled=isOperationCancelledError(error) || cancelToken?.cancelled; if (cancelled) throw createOperationCancelledError(cancelToken?.reason || error?.message); if (isEscortRequestBlockedError(error)) throw error; errors.push(`${name}: ${formatHelperDiagnosticError(error)}`); } } throw new Error(errors.join(' | ')); } const ESCORT_SHARED_AD_REQUEST_PREFIX='vm_escort_ad_request_v1_'; const ESCORT_SHARED_AD_RESULT_PREFIX='vm_escort_ad_result_v1_'; const ESCORT_SHARED_AD_CONSUMER_PREFIX='vm_escort_ad_consumer_v1_'; const ESCORT_SHARED_AD_LEASE_MS=90000; const sharedEscortAdRequests=new Map(); let sharedEscortAdPruneAt=0; function isSharedEscortAdLeaseLive(value,now=Date.now()) { return typeof value?.owner === 'string' && !!value.owner && Number.isFinite(value.expiresAt) && value.expiresAt > now && value.expiresAt <= now + ESCORT_SHARED_AD_LEASE_MS; } function readSharedEscortAdResult(id,minCheckedAt=0) { try { const value=GM_getValue(ESCORT_SHARED_AD_RESULT_PREFIX + id,null),now=Date.now(); if (!value || !(value.expiresAt > now && value.expiresAt <= now + 30000) || !(value.checkedAt >= minCheckedAt && value.checkedAt <= now)) return null; return value.value?.sharedAdData || value.value?.sharedActivityResult ? value.value:null; } catch {return null;} } function pruneSharedEscortAdRequests() { if (sharedEscortAdPruneAt > Date.now()) return; sharedEscortAdPruneAt=Date.now() + 30000; try { for (const key of GM_listValues()) { if (![ESCORT_SHARED_AD_REQUEST_PREFIX,ESCORT_SHARED_AD_RESULT_PREFIX,ESCORT_SHARED_AD_CONSUMER_PREFIX] .some(prefix => key.startsWith(prefix))) continue; const value=GM_getValue(key,null); if (!(value?.expiresAt > Date.now())) deleteHelperStorageValue(key); } } catch {} } function shareFetchedEscortAdDocument(url,result) { if (result.sharedAdData || result.sharedActivityResult || result.sharedActivityRetry) return result; const id=parseAdIdFromUrl(url),checkedAt=Date.now(); let finalUrl; try {finalUrl=new URL(result.finalUrl || url,url);} catch {return result;} if (!id || !isEscortClubHost(finalUrl.hostname)) return result; if ([404,410].includes(result.status) || (result.ok && finalUrl.pathname === '/')) { const activity={url,active:false,city:null,status:result.status,checkedAt,source:'document'}; saveSharedEscortActivity(url,activity,checkedAt); return {sharedActivityResult:activity}; } if (!result.ok || parseAdIdFromUrl(finalUrl.href) !== id || !getEscortAdIdFromPath(finalUrl.pathname) || !isUsableEscortClubAdDocument(result,url)) return result; const previous=escortAdDataMemoryCache.get(id) || getStoredEscortAdData(id); const extracted=extractEscortClubAdData(result.doc,id,finalUrl.href); const data={status:'ok',...preserveEscortAdPhoneData(extracted,previous),checkedAt,fetchTransport:result.transport || null}; saveSharedEscortActivity(url,{active:true},checkedAt); return {sharedAdData:data}; } function fetchEscortClubAdDocument(url,cancelToken=null,requestContext=null,beforeDispatch=null,{minCheckedAt=Date.now()}={}) { const id=parseAdIdFromUrl(url); if (!id) return fetchEscortClubAdDocumentUnshared(url,cancelToken,requestContext,beforeDispatch); return joinEscortAdDataOperation(sharedEscortAdRequests,id, token => fetchSharedEscortAdDocument(url,token,requestContext,beforeDispatch,minCheckedAt),cancelToken,requestContext); } async function fetchSharedEscortAdDocument(url,cancelToken,requestContext,beforeDispatch,minCheckedAt=Date.now()) { const id=parseAdIdFromUrl(url),requestedAt=Math.max(0,Number(minCheckedAt) || 0),epoch=sharedEscortActivityCacheEpoch; const key=ESCORT_SHARED_AD_REQUEST_PREFIX + id,resultKey=ESCORT_SHARED_AD_RESULT_PREFIX + id; const owner=`${Date.now()}-${Math.random().toString(36).slice(2)}`,consumerPrefix=ESCORT_SHARED_AD_CONSUMER_PREFIX + id + ':'; const consumerKey=consumerPrefix + owner,token=createOperationCancelToken('Wspólne pobieranie anonsu'); let timer=null,owns=false,disposed=false; const peers=() => { try {return GM_listValues().some(name => name.startsWith(consumerPrefix) && name !== consumerKey && isSharedEscortAdLeaseLive(GM_getValue(name,null)));} catch {return false;} }; const renew=() => { if (disposed) return; try { if (!cancelToken?.cancelled) writeHelperStorageValue(consumerKey,{owner,expiresAt:Date.now() + ESCORT_SHARED_AD_LEASE_MS}); else if (!peers()) token.cancel(cancelToken.reason); const lease=GM_getValue(key,null); if (lease?.owner === owner && isSharedEscortAdLeaseLive(lease)) writeHelperStorageValue(key,{owner,expiresAt:Date.now() + ESCORT_SHARED_AD_LEASE_MS}); } catch {} clearTimeout(timer); timer=setTimeout(renew,10000); }; const off=cancelToken?.onCancel(reason => { try {deleteHelperStorageValue(consumerKey);} catch {} if (!owns || !peers()) token.cancel(reason); }) || (() => {}); const pagehide=() => token.cancel('Zamknięto stronę'); if (typeof window !== 'undefined') window.addEventListener('pagehide',pagehide,{once:true}); pruneSharedEscortAdRequests(); renew(); try { while (true) { token.throwIfCancelled(); if (epoch !== sharedEscortActivityCacheEpoch) throw createOperationCancelledError('Wyczyszczono cache'); const shared=readSharedEscortAdResult(id,requestedAt); if (shared) {recordDiagnosticCacheDecision(requestContext,'joined'); return shared;} let lease; try {lease=GM_getValue(key,null);} catch {return await fetchEscortClubAdDocumentUnshared(url,token,requestContext,beforeDispatch);} if (isSharedEscortAdLeaseLive(lease) && lease.owner !== owner) { await waitWithCancellation(250,token); continue; } writeHelperStorageValue(key,{owner,expiresAt:Date.now() + ESCORT_SHARED_AD_LEASE_MS}); await waitWithCancellation(40,token); if (GM_getValue(key,null)?.owner !== owner) continue; owns=true; const before=() => { token.throwIfCancelled(); if (epoch !== sharedEscortActivityCacheEpoch) throw createOperationCancelledError('Wyczyszczono cache'); const latest=readSharedEscortAdResult(id,requestedAt); if (latest) return {outcome:'cached',value:latest}; const current=GM_getValue(key,null); if (current?.owner !== owner || !isSharedEscortAdLeaseLive(current)) return {outcome:'superseded',value:{sharedActivityRetry:true}}; renew(); return beforeDispatch?.(); }; const fetched=await fetchEscortClubAdDocumentUnshared(url,token,requestContext,before); if (fetched.sharedActivityRetry) {owns=false; continue;} if (epoch !== sharedEscortActivityCacheEpoch || GM_getValue(key,null)?.owner !== owner) throw createOperationCancelledError('Zmieniono właściciela pobierania'); const result=shareFetchedEscortAdDocument(url,fetched); if (result.sharedAdData || result.sharedActivityResult) writeHelperStorageValue(resultKey, {checkedAt:result.sharedAdData?.checkedAt || result.sharedActivityResult?.checkedAt,expiresAt:Date.now() + 30000,value:result}); return result; } } finally { disposed=true; clearTimeout(timer); off(); if (typeof window !== 'undefined') window.removeEventListener?.('pagehide',pagehide); try { deleteHelperStorageValue(consumerKey); if (GM_getValue(key,null)?.owner === owner) deleteHelperStorageValue(key); } catch {} } } async function inspectEscortClubAd(url,cancelToken=null,forceRefresh=false,requestContext=null,beforeDispatch=null,{minCheckedAt=0}={}) { requestContext ||= createRequestContext('escort-ad-inspection'); cancelToken?.throwIfCancelled(); url=normalizeEscortiAdUrl(url) || url; const id=parseAdIdFromUrl(url); const currentId=IS_ESCORT_CLUB && new URL(url,location.href).origin === location.origin && !!getEscortAdIdFromPath(location.pathname) ? getAdIdFromUrl():null; if (!forceRefresh && !minCheckedAt && id && currentId === id && isUsableEscortClubAdDocument({ok:true,doc:document,finalUrl:url},url)) { const adData=extractEscortClubAdData(document,id,url); const navigation=typeof performance === 'object' ? performance.getEntriesByType?.('navigation')?.[0]:null; const checkedAt=typeof performance === 'object' && Number(performance.timeOrigin) > 0 && Number(navigation?.responseEnd) > 0 && (!navigation.responseStatus || (navigation.responseStatus >= 200 && navigation.responseStatus < 400)) ? performance.timeOrigin + navigation.responseEnd:0; if (checkedAt > 0) saveSharedEscortActivity(url,{active:true},checkedAt); return {url,active:true,city:adData.location?.city || extractEscortClubCity(document), imageUrl:extractWatchPageImage(document,url),adData,source:'current',checkedAt}; } const activity=readSharedEscortActivity(url); if (activity && !activity.active) return activity; try { const skipped=beforeDispatch?.(true); if (skipped?.value?.sharedActivityResult || skipped?.value?.sharedActivityRetry) return skipped.value.sharedActivityResult || skipped.value; const data=await getEscortAdData(id,url,forceRefresh,cancelToken,requestContext, {needPhone:false,maxAgeMs:ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS,minCheckedAt,beforeDispatch:beforeDispatch ? () => beforeDispatch(true):null}); cancelToken?.throwIfCancelled(); if (data?.activity) return data.activity; if (data?.sharedActivityRetry) return data; if (data?.status !== 'ok') return {url,active:false,city:null,unknown:true,detailsUnknown:true}; return {url,active:true,city:data.location?.city || null,imageUrl:data.imageUrl || '',adData:data, checkedAt:data.checkedAt,source:'document'}; } catch (error) { if (isOperationCancelledError(error) || cancelToken?.cancelled) throw createOperationCancelledError(cancelToken?.reason || error?.message); return {url,active:false,city:null,error:formatHelperDiagnosticError(error),unknown:true}; } } async function inspectEscortClubAdActivityOnly(url,cancelToken=null,requestContext=null,{strict=false,forceRefresh=false,useSharedCache=true,beforeDispatch=null}={}) { requestContext ||= createRequestContext('escort-ad-activity'); cancelToken?.throwIfCancelled(); if (useSharedCache) { return inspectSharedEscortActivity(url,cancelToken,requestContext,{strict}); } if (GARSO_MENU_ONLY_MODE && !strict) { return inspectEscortClubAd(url,cancelToken,forceRefresh,requestContext,beforeDispatch); } const requestedId=parseAdIdFromUrl(url); const currentId=IS_ESCORT_CLUB && new URL(url,location.href).origin === location.origin && !!getEscortAdIdFromPath(location.pathname) ? getAdIdFromUrl():null; if (!forceRefresh && requestedId && currentId && requestedId === currentId && isUsableEscortClubAdDocument({ok:true,doc:document,finalUrl:url},url)) { if (strict) return inspectEscortClubAd(url,cancelToken,false,requestContext,beforeDispatch); return { url, active:true, city:null, imageUrl:'', source:'current-head' }; } try { const response=await gmRequest({ method:'HEAD', url, cancelToken, requestContext, beforeDispatch, timeout:ESCORT_AD_DATA_FETCH_TIMEOUT_MS, anonymous:false, headers:{ Accept:'text/html,application/xhtml+xml', Referer:location.href } }); if (response.sharedActivityResult || response.sharedActivityRetry) return response.sharedActivityResult || response; const status=Number(response?.status) || 0; const finalUrl=new URL(response?.finalUrl || url,url); const finalId=parseAdIdFromUrl(finalUrl.href); const exactAdPath=!!getEscortAdIdFromPath(finalUrl.pathname); if (isEscortClubHost(finalUrl.hostname) && (status === 404 || status === 410)) { return {url,active:false,city:null,status,source:'head'}; } if (status === 405 || status === 501) { return inspectEscortClubAd(url,cancelToken,forceRefresh,requestContext,beforeDispatch); } if (status < 200 || status >= 400) { return {url,active:false,city:null,status,unknown:true,source:'head'}; } if ( isEscortClubHost(finalUrl.hostname) && exactAdPath && requestedId && finalId === requestedId ) { return { url, active:true, city:null, imageUrl:'', status, source:'head' }; } if ( isEscortClubHost(finalUrl.hostname) && finalUrl.pathname === '/' ) { return { url, active:false, city:null, status, source:'head' }; } if (strict) return {url,active:false,city:null,status,unknown:true,source:'head'}; } catch (error) { if (isOperationCancelledError(error) || cancelToken?.cancelled) throw createOperationCancelledError(cancelToken?.reason || error?.message); if (strict || isEscortRequestBlockedError(error)) { return {url,active:false,city:null,status:Number(error.status) || 0,unknown:true,source:'head', error:formatHelperDiagnosticError(error)}; } } return inspectEscortClubAd(url,cancelToken,forceRefresh,requestContext,beforeDispatch); } const ESCORT_SHARED_ACTIVITY_CACHE_PREFIX='vm_escort_activity_v1_'; const ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS=30 * 60 * 1000; const sharedEscortActivityRequests=new Map(); const sharedEscortActivityResultTimes=new WeakMap(); let sharedEscortActivityPruneAt=0,sharedEscortActivityCacheEpoch=0; const ESCORT_SHARED_ACTIVITY_PENDING_PREFIX='vm_escort_activity_pending_v1_'; const ESCORT_SHARED_ACTIVITY_LEASE_MS=90000; const sharedEscortActivityOwners=new Map(); let sharedEscortActivityPageHooked=false,sharedEscortActivityLeasePruneAt=0; function isSharedEscortActivityLeaseLive(value,now=Date.now()) { return typeof value?.owner === 'string' && !!value.owner && Number.isFinite(value.expiresAt) && value.expiresAt > now && value.expiresAt <= now + ESCORT_SHARED_ACTIVITY_LEASE_MS; } function releaseSharedEscortActivityOwner(job) { clearTimeout(job.timer); sharedEscortActivityOwners.delete(job.owner); try { if (GM_getValue(job.key,null)?.owner === job.owner) deleteHelperStorageValue(job.key); } catch {} } function waitForSharedEscortActivity(key,delayMs,cancelToken) { cancelToken.throwIfCancelled(); return new Promise((resolve,reject) => { let settled=false,timer=null,unsubscribe=() => {}; const listeners=[]; const finish=error => { if (settled) return; settled=true; clearTimeout(timer); unsubscribe(); for (const id of listeners) {try {GM_removeValueChangeListener(id);} catch {}} if (error) reject(error); else resolve(); }; try { if (typeof GM_addValueChangeListener === 'function' && typeof GM_removeValueChangeListener === 'function') { listeners.push(GM_addValueChangeListener(key,() => finish())); listeners.push(GM_addValueChangeListener(ESCORT_SHARED_ACTIVITY_CACHE_PREFIX + key.slice(ESCORT_SHARED_ACTIVITY_PENDING_PREFIX.length),() => finish())); } } catch {} timer=setTimeout(() => finish(),Math.max(1,Math.min(2000,delayMs))); unsubscribe=cancelToken.onCancel(reason => finish(createOperationCancelledError(reason))); }); } async function inspectSharedEscortActivity(url,cancelToken,requestContext,options) { const inspect=(token,beforeDispatch=null) => inspectEscortClubAdActivityOnly(url,token,requestContext,{...options,useSharedCache:false,beforeDispatch}); const id=parseAdIdFromUrl(url); if (!id) return inspect(cancelToken); const token=createOperationCancelToken('Wspólne sprawdzanie dostępności'); const unsubscribe=cancelToken?.onCancel(reason => token.cancel(reason)) || (() => {}); const job={key:ESCORT_SHARED_ACTIVITY_PENDING_PREFIX + id,owner:`${Date.now()}-${Math.random().toString(36).slice(2)}`, token,timer:null}; const epoch=sharedEscortActivityCacheEpoch; sharedEscortActivityOwners.set(job.owner,job); if (!sharedEscortActivityPageHooked && typeof window !== 'undefined') { sharedEscortActivityPageHooked=true; window.addEventListener('pagehide',() => { for (const pending of [...sharedEscortActivityOwners.values()]) { pending.token.cancel('Zamknięto stronę'); releaseSharedEscortActivityOwner(pending); } }); } const remember=result => { if (result && typeof result === 'object') sharedEscortActivityResultTimes.set(result,{ at:result.checkedAt || Date.now(),epoch:-1,managed:true}); return result; }; const skipped=(result,outcome='cached') => ({value:{sharedActivityResult:result},outcome}); const cleared=() => remember({url,active:false,unknown:true,source:'activity-cache-cleared'}); const renew=() => { try { const current=GM_getValue(job.key,null); if (current?.owner !== job.owner || !isSharedEscortActivityLeaseLive(current) || epoch !== sharedEscortActivityCacheEpoch) return; writeHelperStorageValue(job.key,{owner:job.owner,expiresAt:Date.now() + ESCORT_SHARED_ACTIVITY_LEASE_MS}); job.timer=setTimeout(renew,30000); } catch {} }; try { try { if (sharedEscortActivityLeasePruneAt <= Date.now()) { sharedEscortActivityLeasePruneAt=Date.now() + 300000; for (const key of GM_listValues()) if (key.startsWith(ESCORT_SHARED_ACTIVITY_PENDING_PREFIX) && !isSharedEscortActivityLeaseLive(GM_getValue(key,null))) deleteHelperStorageValue(key); } } catch {} while (true) { token.throwIfCancelled(); if (epoch !== sharedEscortActivityCacheEpoch) return cleared(); const previous=readSharedEscortActivity(url,true),cached=previous && !previous.stale ? previous:null; requestContext={...requestContext,cacheKind:'activity',cacheReason:previous ? 'expired':'missing'}; if (!cached && typeof ESCORT_SHARED_AD_REQUEST_PREFIX !== 'undefined') { let detailLease=null; try {detailLease=GM_getValue(ESCORT_SHARED_AD_REQUEST_PREFIX + id,null);} catch {} if (isSharedEscortAdLeaseLive(detailLease)) { await waitWithCancellation(250,token); continue; } } if (cached) { return remember(cached);} let current; try { current=GM_getValue(job.key,null); if (!isSharedEscortActivityLeaseLive(current) || current.owner === job.owner) { writeHelperStorageValue(job.key,{owner:job.owner,expiresAt:Date.now() + ESCORT_SHARED_ACTIVITY_LEASE_MS}); } else { await waitForSharedEscortActivity(job.key,current.expiresAt - Date.now(),token); continue; } } catch (error) { if (isOperationCancelledError(error)) throw error; return await inspect(token); } await waitWithCancellation(40,token); try {current=GM_getValue(job.key,null);} catch {return await inspect(token);} if (!current) return cleared(); if (current.owner !== job.owner) continue; clearTimeout(job.timer); renew(); const beforeDispatch=(details=false) => { token.throwIfCancelled(); if (epoch !== sharedEscortActivityCacheEpoch) return skipped(cleared(),'superseded'); const hit=readSharedEscortActivity(url); if (!details && !hit && typeof ESCORT_SHARED_AD_REQUEST_PREFIX !== 'undefined' && isSharedEscortAdLeaseLive(GM_getValue(ESCORT_SHARED_AD_REQUEST_PREFIX + id,null))) return {value:{sharedActivityRetry:true},outcome:'superseded'}; if (hit) { return skipped(remember(hit));} let lease; try {lease=GM_getValue(job.key,null);} catch {return;} if (!lease) return skipped(cleared(),'superseded'); if (lease.owner !== job.owner || !isSharedEscortActivityLeaseLive(lease)) { return {value:{sharedActivityRetry:true},outcome:'superseded'}; } clearTimeout(job.timer); renew(); }; const result=await inspect(token,beforeDispatch); clearTimeout(job.timer); if (result.sharedActivityRetry) continue; let owns=false; try {owns=GM_getValue(job.key,null)?.owner === job.owner && epoch === sharedEscortActivityCacheEpoch;} catch {} remember(result); if (owns && result.source !== 'activity-cache' && (!/^current/.test(result.source || '') || result.checkedAt > 0)) saveSharedEscortActivity(url,result,result.checkedAt || Date.now()); return result; } } finally { unsubscribe(); releaseSharedEscortActivityOwner(job); } } function isSharedEscortActivityEntryValid(value,now=Date.now()) { return Array.isArray(value) && value.length === 2 && Number.isFinite(value[0]) && value[0] > 0 && value[0] <= now && (value[1] === 0 || value[1] === 1); } function isSharedEscortActivityEntryFresh(value,now=Date.now()) { return isSharedEscortActivityEntryValid(value,now) && now - value[0] < ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS; } function pruneSharedEscortActivityCache(force=false) { const now=Date.now(); if ((!force && sharedEscortActivityPruneAt > now)) return; sharedEscortActivityPruneAt=now + 5 * 60 * 1000; try { for (const key of GM_listValues()) if (key.startsWith(ESCORT_SHARED_ACTIVITY_CACHE_PREFIX) && !isSharedEscortActivityEntryValid(GM_getValue(key,null),now)) deleteHelperStorageValue(key); } catch {} } function clearSharedEscortActivityCache() { sharedEscortActivityCacheEpoch++; sharedEscortActivityRequests.clear(); sharedEscortActivityPruneAt=0; if (typeof sharedEscortAdRequests !== 'undefined') { for (const job of sharedEscortAdRequests.values()) job.token.cancel('Wyczyszczono cache'); sharedEscortAdRequests.clear(); sharedEscortAdPruneAt=0; } let removed=0; try { for (const key of GM_listValues()) { if (key.startsWith(ESCORT_SHARED_ACTIVITY_CACHE_PREFIX)) {deleteHelperStorageValue(key); removed++;} else if (['vm_escort_activity_pending_v1_','vm_escort_ad_request_v1_','vm_escort_ad_result_v1_', 'vm_escort_ad_consumer_v1_'].some(prefix => key.startsWith(prefix))) deleteHelperStorageValue(key); } } catch {} return removed; } function readSharedEscortActivity(url,allowStale=false) { const id=parseAdIdFromUrl(url); if (!id) return null; pruneSharedEscortActivityCache(); try { const key=ESCORT_SHARED_ACTIVITY_CACHE_PREFIX + id,value=GM_getValue(key,null); if (!isSharedEscortActivityEntryValid(value)) { if (value !== null) deleteHelperStorageValue(key); return null; } const stale=!isSharedEscortActivityEntryFresh(value); if (stale && !allowStale) return null; return {url:normalizeEscortiAdUrl(url),active:value[1] === 1,city:null,imageUrl:'',source:'activity-cache',checkedAt:value[0],stale}; } catch {return null;} } function saveSharedEscortActivity(url,result,checkedAt=Date.now()) { if (!result || result.stale || result.unknown || result.error || typeof result.active !== 'boolean') return; const id=parseAdIdFromUrl(url),value=[checkedAt,result.active ? 1:0]; if (!id || !isSharedEscortActivityEntryValid(value)) return; pruneSharedEscortActivityCache(); try { const key=ESCORT_SHARED_ACTIVITY_CACHE_PREFIX + id,previous=GM_getValue(key,null); if (isSharedEscortActivityEntryValid(previous) && (previous[0] > checkedAt || (previous[0] === checkedAt && previous[1] === value[1]))) return; writeHelperStorageValue(key,value); } catch {} } function shareEscortActivityResult(url,result) { const check=result && typeof result === 'object' ? sharedEscortActivityResultTimes.get(result):null; if (check?.epoch === sharedEscortActivityCacheEpoch) saveSharedEscortActivity(url,result,check.at); } function activeAdsCacheKey(adUrls) { return [...new Set((adUrls || []).map(url => normalizeEscortiAdUrl(url) || url).filter(Boolean))] .sort() .join('|'); } function escortiActivityAdIdsKey(adUrls) { return [...new Set( (adUrls || []) .map(parseAdIdFromUrl) .filter(Boolean) )] .sort((a,b) => Number(a) - Number(b)) .join(','); } function summarizeActiveAdResults(results) { const active=results.filter(item => item.active); const unknown=results.filter(item => item.unknown).length; const knownCities=active.map(item => normalizeEscortCity(item.city)).filter(Boolean); const uniqueCities=[...new Set( knownCities.map(city => normalizeEscortCityKey(city)) )].map(key => knownCities.find(city => normalizeEscortCityKey(city) === key)); const allActiveCitiesKnown=active.length > 0 && knownCities.length === active.length; let cityText=''; if (active.length > 0) { if (allActiveCitiesKnown && uniqueCities.length === 1) cityText=uniqueCities[0]; else if (uniqueCities.length > 1) cityText='różne miasta'; else cityText='miasto ?'; } return { totalChecked:results.length, activeCount:active.length, unknownCount:unknown, staleCount:results.filter(item => item.stale).length, cities:uniqueCities, allActiveCitiesKnown, cityText, results }; } function compactEscortiActivitySummary(adUrls,summary) { return { adIdsKey:escortiActivityAdIdsKey(adUrls), totalChecked:Number(summary?.totalChecked) || 0, activeCount:Number(summary?.activeCount) || 0, unknownCount:Number(summary?.unknownCount) || 0, cities:Array.isArray(summary?.cities) ? [...summary.cities]:[], allActiveCitiesKnown:!!summary?.allActiveCitiesKnown, cityText:String(summary?.cityText || ''), checkedAt:Date.now() }; } function getCachedEscortiActivitySummary(adId,searchMode,adUrls=null) { if (!adId || !searchMode) return null; const stored=getStoredEscortAdData(adId); const summary=stored?.escortiActivity?.[searchMode]; if (!summary || typeof summary !== 'object') return null; if ( Array.isArray(adUrls) && summary.adIdsKey !== escortiActivityAdIdsKey(adUrls) ) { return null; } if ( !Number.isFinite(Number(summary.totalChecked)) || !Number.isFinite(Number(summary.activeCount)) || !Number.isFinite(Number(summary.unknownCount)) ) { return null; } return { ...summary, totalChecked:Number(summary.totalChecked), activeCount:Number(summary.activeCount), unknownCount:Number(summary.unknownCount), cities:Array.isArray(summary.cities) ? summary.cities:[], allActiveCitiesKnown:!!summary.allActiveCitiesKnown, cityText:String(summary.cityText || '') }; } function getLatestCachedEscortiActivitySummary(adId,searchModes) { return (Array.isArray(searchModes) ? searchModes:[searchModes]) .map(mode => getCachedEscortiActivitySummary(adId,mode)) .filter(Boolean) .sort((a,b) => Number(b.checkedAt || 0) - Number(a.checkedAt || 0))[0] || null; } function getEscortiAdUrlsFromActivitySummary(summary) { return [...new Set( String(summary?.adIdsKey || '') .split(',') .map(value => value.trim()) .filter(value => /^\d+$/.test(value)) .map(id => `https://pl.escort.club/anons/${id}.html`) )]; } function recoverEscortiResultAdUrls(result,adId,searchMode) { if (result?.status !== 'ok' || Number(result?.profiles) === 0) return []; const normalizeUrls=values => [...new Set( (Array.isArray(values) ? values:[]) .map(url => normalizeEscortiAdUrl(url)) .filter(Boolean) )]; const directUrls=normalizeUrls([ ...(Array.isArray(result.adUrls) ? result.adUrls:[]), ...(Array.isArray(result.adIds) ? result.adIds:[]) .map(id => /^\d+$/.test(String(id || '').trim()) ? `https://pl.escort.club/anons/${String(id).trim()}.html` :null) ]); const activitySummary=getCachedEscortiActivitySummary( adId, searchMode ); const activityUrls=normalizeUrls( getEscortiAdUrlsFromActivitySummary(activitySummary) ); const profileCacheUrls=normalizeUrls( getCachedEscortiAdUrlsForProfiles(normalizeProfileUrls(result)) ); const combinedUrls=normalizeUrls([ ...directUrls, ...activityUrls, ...profileCacheUrls ]); const reportedCount=Number(result.adLinks); const profileSummaries=Array.isArray(result.profileSummaries) ? result.profileSummaries :[]; const singleProfileCount=profileSummaries.length === 1 ? Number(profileSummaries[0]?.adLinks) :NaN; const activityCount=Number(activitySummary?.totalChecked); const expectedCount=Number.isFinite(reportedCount) && reportedCount > 0 ? reportedCount :(Number.isFinite(singleProfileCount) && singleProfileCount > 0 ? singleProfileCount :(Number.isFinite(activityCount) && activityCount > 0 ? activityCount :reportedCount)); if (Number.isFinite(expectedCount) && expectedCount >= 0) { const exactMatch=[ directUrls, activityUrls, profileCacheUrls, combinedUrls ].find(urls => urls.length === expectedCount); if (exactMatch) return exactMatch; return directUrls; } return directUrls.length ? directUrls :(activityUrls.length ? activityUrls:profileCacheUrls); } function hydrateEscortiResultAdUrls(result,adId,searchMode) { if (result?.status !== 'ok') return result; const adUrls=recoverEscortiResultAdUrls(result,adId,searchMode); if (!adUrls.length) return result; const currentUrls=(Array.isArray(result.adUrls) ? result.adUrls:[]) .map(url => normalizeEscortiAdUrl(url)) .filter(Boolean); const unchanged=currentUrls.length === adUrls.length && currentUrls.every((url,index) => url === adUrls[index]); if (unchanged) return result; return { ...result, adIds:adUrls.map(parseAdIdFromUrl).filter(Boolean), adUrls, adLinks:adUrls.length }; } function saveEscortiActivitySummary(adId,searchModes,adUrls,summary) { if (!adId || !summary) return; if (!persistentCacheKeyIsLoaded(escortAdDataCacheKey(adId))) { ensurePersistentCacheKeys([escortAdDataCacheKey(adId)]).then(() => saveEscortiActivitySummary(adId,searchModes,adUrls,summary)).catch(error => log('Nie udało się zapisać aktywności w cache',error)); return; } const modes=[...new Set( (Array.isArray(searchModes) ? searchModes:[searchModes]).filter(Boolean) )]; if (!modes.length) return; const entry=compactEscortiActivitySummary(adUrls,summary); const activityPatch=Object.fromEntries( modes.map(mode => [mode,{...entry,cities:[...entry.cities]}]) ); const cacheId=String(adId); const stored=getStoredEscortAdData(adId); if (!stored) { pendingEscortiActivityCacheWrites.set(cacheId,{ ...(pendingEscortiActivityCacheWrites.get(cacheId) || {}), ...activityPatch }); return; } try { const escortiActivity={ ...(stored.escortiActivity || {}), ...activityPatch }; const value={...stored,escortiActivity}; writePersistentCacheValue(escortAdDataCacheKey(adId),value); const memoryValue=escortAdDataMemoryCache.get(cacheId); if (memoryValue) { escortAdDataMemoryCache.set(cacheId,{ ...memoryValue, escortiActivity }); } } catch (error) { log(`Nie udało się zapisać aktywności Escorti dla anonsu ${adId}`,error); } } async function scanEscortAdsHeadFirst(adUrls,onProgress,forceRefresh,options) { const requestContext=options.requestContext || createRequestContext('active-ad-scan'); const urls=[...new Set((adUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(Boolean))]; const results=new Array(urls.length),previews=new Map(); let next=0,checked=0; const notify=(index=null,result=null) => { const displayed=urls.map((_,i) => results[i] || previews.get(i)).filter(Boolean); const summary=summarizeActiveAdResults(displayed); try { onProgress?.({phase:'details',checked,total:urls.length,activeCount:summary.activeCount, summary,url:index == null ? null:urls[index],result}); } catch {} }; for (let index=0;index < urls.length;index++) { const cached=readSharedEscortActivity(urls[index],true); if (cached) { if (!cached.stale && !cached.active) {results[index]=cached; checked++;} else previews.set(index,cached); } } notify(); results.forEach((result,index) => {if (result) notify(index,result);}); const worker=async () => { while (next < urls.length) { const index=next++,url=urls[index]; if (results[index]) continue; let result=options.verifiedResults?.get(url); if (!result?.adData) result=await inspectEscortClubAd(url,null,options.forceDetails ?? forceRefresh, requestContext,null,{minCheckedAt:options.detailMinCheckedAt || 0}); if ((result.unknown || result.error) && previews.has(index)) result={...previews.get(index),unknown:true,refreshFailed:true,detailsUnknown:true}; results[index]=result; checked++; notify(index,result); } }; await Promise.all(Array(Math.min(getEscortActivityDetailConcurrency(),urls.length)).fill().map(worker)); const summary=summarizeActiveAdResults(results.filter(Boolean)); const detailsUnknownCount=results.filter(result => result?.detailsUnknown || (result?.active && !result.adData)).length; return {...summary,detailsUnknownCount}; } function scanActiveEscortAds( adUrls, onProgress=null, forceRefresh=false, options={} ) { if (options.headFirst) return scanEscortAdsHeadFirst(adUrls,onProgress,forceRefresh,options); const activityOnly=options?.activityOnly === true,useActivityCache=activityOnly && options.cacheActivity !== false; const requestContext=options.requestContext || createRequestContext('active-ad-scan'); const urls=[...new Set( (adUrls || []) .map(url => normalizeEscortiAdUrl(url) || url) .filter(Boolean) )]; const baseKey=activeAdsCacheKey(urls); const key=baseKey && !useActivityCache ? `${activityOnly ? 'activity:':'details:'}${baseKey}`:''; if (!forceRefresh && key && escortActiveAdsSummaryCache.has(key)) { const cached=escortActiveAdsSummaryCache.get(key); return options.cacheActivity ? cached.then(summary => { for (const result of summary.results || []) shareEscortActivityResult(result?.url,result); return summary; }):cached; } if (forceRefresh && key) escortActiveAdsSummaryCache.delete(key); const promise=(async () => { const results=new Array(urls.length),previews=new Map(); let nextIndex=0; let completed=0; const finish=(index,result,preview=false) => { const url=urls[index]; if (!preview) { if ((result?.unknown || result?.error) && previews.has(index)) result={...previews.get(index),unknown:true,refreshFailed:true}; results[index]=result; if (options.cacheActivity || useActivityCache) shareEscortActivityResult(url,result); completed++; } const displayed=urls.map((_,i) => results[i] || previews.get(i)).filter(Boolean); try { onProgress?.({checked:completed,total:urls.length, activeCount:displayed.filter(item => item.active).length,url,result,preview, ...(options.headFirstActivity ? {summary:summarizeActiveAdResults(displayed)}:{})}); } catch {} }; // Cache przed uruchomieniem kolejki. if (useActivityCache) urls.forEach((url,index) => { const cached=!options.verifiedResults?.get(url) && readSharedEscortActivity(url,true); if (!cached) return; if (cached.stale) previews.set(index,cached); finish(index,cached,cached.stale); }); async function worker() { while (true) { const index=nextIndex++; if (index >= urls.length) return; if (results[index]) continue; const url=urls[index]; const checkCache=activityOnly ? escortActiveAdProbeCache :escortActiveAdCheckCache; const cached=useActivityCache ? readSharedEscortActivity(url):null; let checkPromise=options.verifiedResults?.get(url) || cached || (useActivityCache ? sharedEscortActivityRequests.get(url):forceRefresh ? null:checkCache.get(url)); if (!checkPromise) { const epoch=sharedEscortActivityCacheEpoch; checkPromise=(activityOnly ? inspectEscortClubAdActivityOnly(url,null,requestContext, {strict:!!options.headFirstActivity,forceRefresh,useSharedCache:useActivityCache}) :inspectEscortClubAd(url,null,false,requestContext)).then(result => { if (result && typeof result === 'object' && !sharedEscortActivityResultTimes.get(result)?.managed) { sharedEscortActivityResultTimes.set(result,{at:result.checkedAt || Date.now(),epoch}); } return result; }); if (useActivityCache) { sharedEscortActivityRequests.set(url,checkPromise); const forget=() => {if (sharedEscortActivityRequests.get(url) === checkPromise) sharedEscortActivityRequests.delete(url);}; checkPromise.then(forget,forget); } else checkCache.set(url,checkPromise); } const result=await checkPromise; finish(index,result); } } const workerCount=Math.min( activityOnly && !GARSO_MENU_ONLY_MODE ? ESCORT_ACTIVE_PROBE_CONCURRENT :getEscortActivityDetailConcurrency(), Math.max(1,urls.length) ); const workers=[]; for (let workerIndex=0; workerIndex < workerCount; workerIndex++) { workers.push(worker()); } await Promise.all(workers); return summarizeActiveAdResults(results.filter(Boolean)); })(); if (key) escortActiveAdsSummaryCache.set(key,promise); promise.catch(() => { if (key) escortActiveAdsSummaryCache.delete(key); }); return promise; } function buildActiveAdsTitle(baseTitle,detailResult,summary) { const lines=[baseTitle]; if (typeof detailResult?.adLinks === 'number') { lines.push(`Anonse znalezione w profilu Escorti: ${detailResult.adLinks}.`); } lines.push(`Aktywne na Escort.club: ${summary.activeCount} z ${summary.totalChecked}.`); if (summary.activeCount > 0) { if (summary.allActiveCitiesKnown && summary.cities.length === 1) { lines.push(`Wszystkie aktywne anonse są z miasta: ${summary.cities[0]}.`); } else if (summary.cities.length > 1) { lines.push(`Aktywne anonse są z różnych miast: ${summary.cities.join(', ')}.`); } else { lines.push('Nie udało się jednoznacznie ustalić miasta wszystkich aktywnych anonsów.'); } } return lines.join('\n'); } async function getEscortiDetailForSingleAd( adId, searchValue, searchMode, forceRefresh=false ) { await ensureEscortPageCache([adId]); const rawCached=getListCache(adId,searchMode); const cached=hydrateEscortiResultAdUrls( rawCached, adId, searchMode ); if (cached && cached !== rawCached) { setListCache(adId,cached,searchMode); } const cacheHasProfileSummary=cached?.profiles === 0 || ( Array.isArray(cached?.profileSummaries) && cached.profileSummaries.length > 0 ); if ( !forceRefresh && isListCacheFresh(cached) && cacheHasProfileSummary ) { return {result:cached,source:'cache'}; } const refreshed=await checkEscortiBackground(searchValue,{ timeoutMs:60000, mergeProfiles:true, preferDirect:true }); if (refreshed?.status === 'ok') { const hydratedRefreshed=hydrateEscortiResultAdUrls( refreshed, adId, searchMode ); setListCache(adId,hydratedRefreshed,searchMode); return { result:getListCache(adId,searchMode) || hydratedRefreshed, source:'network' }; } if (cached?.status === 'ok') { return {result:cached,source:'stale-cache'}; } return {result:refreshed,source:'network'}; } function createGarsoButton(displayTerm,searchTerm,searchMode) { const btn=makeElement('button','vm-research-panel-row'); btn.dataset.garsoDisplayTerm=displayTerm; btn.dataset.garsoSearchMode=searchMode; const cached=getGarsoSearchCountCache(searchMode,searchTerm); if (cached) btn.dataset.garsoCountCheckedAt=String(cached.checkedAt); if (searchMode === 'phone' && cached) { btn.dataset.garsoPhoneSearchVariant=cached.phoneSearchVariant || 'both'; } if (cached?.fresh) { btn.dataset.garsoStatus='result'; btn.dataset.garsoCount=String(cached.count); btn.dataset.garsoCacheState='fresh'; } else { btn.dataset.garsoStatus=SETTINGS.autoGarsoCheck ? 'checking':'manual'; } renderGarsoButton(btn); btn.addEventListener('click',async e => { e.preventDefault(); e.stopPropagation(); if (btn.dataset.garsoStatus === 'manual') { btn.disabled=true; try {await updateGarsoStatus(btn,searchTerm,searchMode,false,false);} finally {btn.disabled=false;} } startSearchProcess(searchTerm,btn); }); return btn; } function formatResearchPanelList(values,maxItems=5) { const clean=[...new Set((values || []).filter(Boolean))]; if (clean.length <= maxItems) return clean.join(', '); return `${clean.slice(0,maxItems).join(', ')} (+${clean.length - maxItems})`; } function formatResearchPriceDuration(durationKey) { if (durationKey === 'night') return 'cała noc'; const minutes=Number(durationKey); if (!Number.isFinite(minutes) || minutes <= 0) return String(durationKey); if (minutes < 60) return `${minutes} min`; const hours=minutes / 60; return `${String(hours).replace('.',',')} h`; } function normalizeEscortDescriptionComparisonValue(value) { return String(value || '').replace(/\s+/gu,''); } function getEscortActiveAdsDescriptionComparison(summary) { const active=(summary?.results || []) .filter(item => item?.active); if (active.length < 2) { return { activeCount:active.length, comparableCount:0, missingCount:active.length, same:null }; } const descriptions=active.map(item => ({ item, raw:String(item.adData?.description || ''), normalized:normalizeEscortDescriptionComparisonValue( item.adData?.description ) })); const comparable=descriptions.filter(entry => entry.normalized); const missingCount=descriptions.length - comparable.length; const uniqueDescriptions=[...new Set( comparable.map(entry => entry.normalized) )]; return { activeCount:active.length, comparableCount:comparable.length, missingCount, same:uniqueDescriptions.length > 1 ? false: missingCount === 0 && comparable.length >= 2 && !(summary?.unknownCount > 0) ? true:null }; } function getEscortActiveAdsConsistencyWarnings(summary,statFields=[]) { const active=(summary?.results || []) .filter(item => item?.active && item.adData) .map(item => ({item,data:item.adData})); if (active.length < 2) return []; const warnings=[]; const missing='__brak__'; const displayValue=value => value === missing ? 'brak':value; const uniqueComparedValues=values => [...new Set(values)]; const cities=uniqueComparedValues(active.map(({item,data}) => { const city=normalizeEscortCity(data.location?.city || item.city); return city || missing; })); if (cities.length > 1) { warnings.push(`Różne miasta: ${formatResearchPanelList(cities.map(displayValue))}`); } const ages=uniqueComparedValues(active.map(({data}) => { const entry=getEscortObjectEntryByNormalizedKey(data.stats,'Wiek'); const match=normalizeEscortAdText(entry?.[1]).match(/\d{1,3}/); return match ? match[0]:missing; })); if (ages.length > 1) { warnings.push(`Różny wiek: ${formatResearchPanelList(ages.map(displayValue))}`); } const statLabels=new Map(); for (const {data} of active) { for (const rawLabel of Object.keys(data.stats || {})) { const key=normalizeEscortChangeComparisonValue(rawLabel).replace(/:\s*$/,''); if (!key || key === 'wiek' || normalizeEscortClubPriceDurationKey(key)) continue; if (!statLabels.has(key)) statLabels.set(key,normalizeEscortAdText(rawLabel)); } } const differentStats=[]; for (const [key,label] of statLabels) { const rawValues=active.map(({data}) => { const entry=Object.entries(data.stats || {}).find(([candidate]) => normalizeEscortChangeComparisonValue(candidate).replace(/:\s*$/,'') === key ); return entry ? (normalizeEscortAdText(entry[1]) || 'brak'):'brak'; }); const comparedValues=rawValues.map(value => normalizeEscortChangeComparisonValue(value) || missing ); if (uniqueComparedValues(comparedValues).length > 1) { statFields.push(key); differentStats.push( `${label} (${formatResearchPanelList(rawValues)})` ); } } if (differentStats.length) { warnings.push(`Różne parametry: ${formatResearchPanelList(differentStats)}`); } const priceDurationKeys=new Set( active.flatMap(({data}) => Object.keys(data.prices || {})) ); const differentPrices=[]; for (const durationKey of priceDurationKeys) { const comparedValues=[]; const displayedValues=[]; for (const {data} of active) { const prices=data.prices || {}; if (!Object.prototype.hasOwnProperty.call(prices,durationKey)) { comparedValues.push('__brak_pozycji__'); displayedValues.push('brak pozycji'); continue; } const price=prices[durationKey]; const amount=price?.amount != null && Number.isFinite(Number(price.amount)) ? String(Number(price.amount)) :'__brak_ceny__'; const currency=normalizeEscortAdText(price?.currency).toUpperCase(); comparedValues.push(`${amount}|${currency}`); displayedValues.push(formatEscortTilePrice(price) || 'brak ceny'); } if (uniqueComparedValues(comparedValues).length > 1) { differentPrices.push( `${formatResearchPriceDuration(durationKey)} ` + `(${formatResearchPanelList(displayedValues)})` ); } } if (differentPrices.length) { warnings.push(`Różne ceny: ${formatResearchPanelList(differentPrices)}`); } const availabilityDays=new Map(); for (const {data} of active) { for (const day of Object.keys(data.availability || {})) { const key=normalizeEscortChangeComparisonValue(day).replace(/:\s*$/,''); if (key && !availabilityDays.has(key)) { availabilityDays.set(key,normalizeEscortAdText(day).replace(/:\s*$/,'')); } } } const differentAvailability=[]; for (const [dayKey,dayLabel] of availabilityDays) { const values=active.map(({data}) => { const entry=Object.entries(data.availability || {}).find(([day]) => normalizeEscortChangeComparisonValue(day).replace(/:\s*$/,'') === dayKey ); return entry ? (normalizeEscortChangeComparisonValue(entry[1]) || missing) :missing; }); if (uniqueComparedValues(values).length > 1) { differentAvailability.push(dayLabel); } } if (differentAvailability.length) { warnings.push( `Różne godziny przyjmowania: ${formatResearchPanelList(differentAvailability)}` ); } const tagSets=active.map(({data}) => new Map( (Array.isArray(data.tags) ? data.tags:[]).map(tag => [ normalizeEscortChangeComparisonValue(tag), normalizeEscortAdText(tag) ]) )); const tagSignatures=tagSets.map(tags => [...tags.keys()].sort().join('|')); if (uniqueComparedValues(tagSignatures).length > 1) { const tagCounts=new Map(); const tagLabels=new Map(); for (const tags of tagSets) { for (const [key,label] of tags) { tagCounts.set(key,(tagCounts.get(key) || 0) + 1); if (!tagLabels.has(key)) tagLabels.set(key,label); } } const differingTags=[...tagCounts] .filter(([,count]) => count !== tagSets.length) .map(([key]) => tagLabels.get(key)); warnings.push( `Różne tagi${differingTags.length ? `: ${formatResearchPanelList(differingTags)}`:''}` ); } const descriptionComparison=getEscortActiveAdsDescriptionComparison(summary); if (descriptionComparison.same === false) { warnings.push('Różny opis'); } return warnings; } function buildEscortClubPhoneSearchUrl(phone) { if (!normalizeEscortCachedPhone(phone)) return ''; const query=normalizeEscortAdText(phone) .replace(/^\+/,'') .replace(/[^\d]+/g,' ') .trim(); const params=new URLSearchParams({province:'',q:query}); return `https://pl.escort.club/anonse/towarzyskie/poland/?${params}`; } function buildEscortiSearchUrl(value) { const params=new URLSearchParams({search:String(value || ''),vm_mode:'open'}); return `${ESCORTI_BASE_URL}search?${params}`; } function buildEscortiPhoneSearchUrl(phone) { const digits=normalizeEscortCachedPhone(phone); return digits ? buildEscortiSearchUrl(digits) :''; } function normalizeGlobalQuickSearchPhrase(value) { let phrase=String(value || '').trim(); if (!phrase) return ''; phrase=phrase .replace(/^[\s"“”„]+|[\s"“”„]+$/g,'') .replace(/["“”„]+/g,' ') .replace(/\s+/g,' ') .trim(); return phrase; } function quoteGlobalQuickSearchPhrase(value) { const phrase=normalizeGlobalQuickSearchPhrase(value); return phrase ? `"${phrase}"`:''; } function parseGlobalQuickSearchValue(value) { const raw=String(value || '').trim(); if (!raw) return null; const adMatch=raw.match(/(?<![\w.@/-])(?:https?:\/\/)?(?:(?:[a-z]{2}|www)\.)?escort\.club\/(?:anons|personal-ads)\/(\d+)\.html\b/i) || raw.match(/\b(?:anons|personal-ads)\s*\/\s*(\d+)\.html\b/i); if (adMatch) { const adId=adMatch[1]; return { type:'ad', adId, adUrl:`https://pl.escort.club/anons/${adId}.html` }; } const phone=normalizeEscortCachedPhone(raw); if (phone) return {type:'phone',phone}; const phrase=normalizeGlobalQuickSearchPhrase(raw); return phrase ? {type:'phrase',phrase}:null; } function buildGlobalQuickSearchUrl(target,parsed) { if (!parsed) return ''; if (target === 'escort-club') { if (parsed.type === 'ad') return parsed.adUrl; if (parsed.type === 'phone') return buildEscortClubPhoneSearchUrl(parsed.phone); const phrase=quoteGlobalQuickSearchPhrase(parsed.phrase); if (!phrase) return ''; const params=new URLSearchParams({province:'',q:phrase}); return `https://pl.escort.club/anonse/towarzyskie/poland/?${params}`; } if (target === 'escorti') { if (parsed.type === 'ad') { return buildEscortiSearchUrl(parsed.adUrl); } if (parsed.type === 'phone') return buildEscortiPhoneSearchUrl(parsed.phone); const phrase=normalizeGlobalQuickSearchPhrase(parsed.phrase); return phrase ? buildEscortiSearchUrl(phrase) :''; } return ''; } async function openGlobalQuickSearch(target,parsed,openMode='new-tab') { if (!parsed) return; if (target === 'garso') { const garsoPhone=parsed.type === 'phone' ? (formatEscortPhoneForClipboard(parsed.phone,'local-hyphens') || parsed.phone) :''; const searchTerm=parsed.type === 'ad' ? buildGarsoAdLinkSearchTerm(parsed.adId) :parsed.type === 'phone' ? `"${garsoPhone}"` :quoteGlobalQuickSearchPhrase(parsed.phrase); if (!searchTerm) throw new Error('Nie udało się przygotować wyszukiwania Garsoniery.'); const auth=await getGarsoAuth(null,true); await submitGarsoSearchFormToNewTab( searchTerm, auth.secureHash, auth.sessionId, {active:true} ); return; } const url=buildGlobalQuickSearchUrl(target,parsed); if (!url) throw new Error('Nie udało się przygotować adresu wyszukiwania.'); if (openMode === 'same-tab') location.assign(url); else GM_openInTab(url,{active:true,insert:true}); } function initGlobalQuickSearch(forceShow=false) { if (window.top !== window.self) { detachHelperNode(document.getElementById(GLOBAL_QUICK_SEARCH_ID)); return; } const hostName=location.hostname.toLowerCase(); const supportedHosts=new Set([ 'escorti.pl', 'www.escorti.pl', 'garsoniera.com.pl', 'www.garsoniera.com.pl' ]); if (!IS_ESCORT_CLUB && !supportedHosts.has(hostName)) return; const mount=() => { if (!document.body) return false; detachHelperNode(document.getElementById(GLOBAL_QUICK_SEARCH_ID)); if (!SETTINGS.showTopPhoneSearch && !forceShow) return true; const root=prepareHelperStyleApi( makeElement('div','vm-global-quick-search') ); root.id=GLOBAL_QUICK_SEARCH_ID; Object.assign(root.style,{ position:'fixed', right:'16px', bottom:'16px', zIndex:'2147483000', display:'flex', flexDirection:'row-reverse', alignItems:'flex-end', gap:'0', fontFamily:'var(--vm-ui-font-family, Lato, Arial, sans-serif)', transition:'right .22s ease', pointerEvents:'none' }); const trigger=makeElement('button','vm-global-quick-search-trigger',null,{type:'button',title:'Szukaj numeru telefonu, linku Escort.club lub nazwy / frazy'}); trigger.setAttribute('aria-label',trigger.title); trigger.innerHTML='<svg viewBox="0 0 24 24" width="22" height="22" focusable="false" aria-hidden="true"><path fill="currentColor" d="M9.5 3a6.5 6.5 0 1 0 3.96 11.65L19.81 21 21 19.81l-6.35-6.35A6.5 6.5 0 0 0 9.5 3Zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"/></svg>'; Object.assign(trigger.style,{ width:'46px', height:'46px', padding:'0', border:'1px solid color-mix(in srgb, var(--vm-ui-accent-contrast, #fff) 32%, var(--vm-ui-border, #bbb))', borderRadius:'50%', display:'inline-flex', alignItems:'center', justifyContent:'center', background:'var(--vm-ui-accent, #f54da3)', color:'var(--vm-ui-accent-contrast, #fff)', boxShadow:'var(--vm-ui-shadow, 0 4px 16px rgba(0,0,0,.28))', cursor:'pointer', boxSizing:'border-box', pointerEvents:'auto' }); const panel=makeElement('div','vm-global-quick-search-panel'); Object.assign(panel.style,{ width:'350px', maxWidth:'calc(100vw - 78px)', marginRight:'8px', padding:'8px', border:'1px solid color-mix(in srgb, var(--vm-ui-accent, #f54da3) 42%, var(--vm-ui-border, #bbb))', borderRadius:'8px', background:'var(--vm-ui-panel-bg, #1f1821)', boxShadow:'var(--vm-ui-shadow, 0 5px 22px rgba(0,0,0,.32))', boxSizing:'border-box', opacity:'0', visibility:'hidden', pointerEvents:'none', transform:'translateX(7px)', transition:'opacity .14s ease, transform .14s ease, visibility .14s ease' }); const form=makeElement('form','vm-global-quick-search-form'); Object.assign(form.style,{ display:'flex', flexDirection:'column', gap:'6px', margin:'0' }); const input=makeElement('input','vm-global-quick-search-input',null,{type:'text',inputMode:'search',autocomplete:'off',placeholder:'Numer telefonu, link Escort.club lub nazwa / fraza'}); input.title=input.placeholder; input.classList.add('vm-ui-field'); Object.assign(input.style,{width:'100%'}); input.addEventListener('input',() => input.setCustomValidity('')); const defaultTarget=normalizeTopPhoneSearchEnterTarget( SETTINGS.topPhoneSearchEnterTarget ); const openMode=normalizeTopPhoneSearchOpenMode( SETTINGS.topPhoneSearchOpenMode ); const buttons=makeElement('div','vm-global-quick-search-buttons'); Object.assign(buttons.style,{ display:'grid', gridTemplateColumns:'repeat(3, minmax(0, 1fr))', gap:'5px' }); const setBusy=busy => { input.disabled=busy; buttons.querySelectorAll('button').forEach(button => { button.disabled=busy; button.style.opacity=busy ? '.6':'1'; }); }; const getParsed=() => { const parsed=parseGlobalQuickSearchValue(input.value); if (parsed) { input.setCustomValidity(''); return parsed; } input.setCustomValidity('Wpisz numer telefonu, link do anonsu Escort.club albo nazwę / dowolną frazę.'); input.reportValidity(); input.focus(); return null; }; const runSearch=async target => { if (input.disabled) return; const parsed=getParsed(); if (!parsed) return; setBusy(true); try { await openGlobalQuickSearch(target,parsed,openMode); } catch (error) { log('Błąd podręcznej wyszukiwarki',error); input.setCustomValidity(error?.message || 'Nie udało się otworzyć wyszukiwania.'); input.reportValidity(); } finally { setBusy(false); } }; const buttonDefs=[ ['garso','Garso'], ['escort-club','Escort.club'], ['escorti','Escorti.pl'] ]; for (const [target,label] of buttonDefs) { const button=makeElement('button','vm-global-quick-search-button',label); button.type='button'; button.dataset.target=target; button.title=target === 'garso' ? 'Szukaj na Garsonierze' :`Szukaj na ${label}`; const active=target === defaultTarget; button.classList.add('vm-ui-button'); button.classList.toggle('-primary',active); button.addEventListener('click',() => runSearch(target)); buttons.appendChild(button); } form.addEventListener('submit',event => { event.preventDefault(); runSearch(defaultTarget); }); form.append(input,buttons); panel.appendChild(form); root.append(trigger,panel); let forcedOpen=false; const setOpen=open => { panel.style.transitionProperty=open ? 'opacity, transform':'opacity, transform, visibility'; root.style.pointerEvents=open ? 'auto':'none'; panel.style.opacity=open ? '1':'0'; panel.style.visibility=open ? 'visible':'hidden'; panel.style.pointerEvents=open ? 'auto':'none'; panel.style.transform=open ? 'translateX(0)':'translateX(7px)'; }; const syncHoverState=() => setOpen( forcedOpen || root.matches(':hover') || root.contains(document.activeElement) ); const focusQuickSearch=() => { setOpen(true); input.focus(); input.setSelectionRange(input.value.length,input.value.length); }; root._vmPrefill=value => { if (input.disabled) return; input.value=String(value || ''); input.setCustomValidity(''); forcedOpen=true; focusQuickSearch(); }; const isEditableTarget=target => target?.matches?.('input, textarea, select, [contenteditable="true"]'); document.addEventListener('keydown',event => { if (input.disabled || document.activeElement === input || !root.matches(':hover')) return; if (isEditableTarget(document.activeElement)) return; const copyShortcut= (event.ctrlKey || event.metaKey) && String(event.key).toLowerCase() === 'c'; const typedCharacter= event.key?.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey; if (!copyShortcut && !typedCharacter) return; if (copyShortcut) { setTimeout(focusQuickSearch,0); return; } event.preventDefault(); focusQuickSearch(); input.value += event.key; input.dispatchEvent(new Event('input',{bubbles:true})); },true); trigger.addEventListener('mouseenter',() => { focusQuickSearch(); }); root.addEventListener('mouseleave',() => setTimeout(syncHoverState,40)); root.addEventListener('focusin',event => { if (event.target === trigger || panel.contains(event.target)) setOpen(true); }); root.addEventListener('focusout',() => setTimeout(syncHoverState,40)); trigger.addEventListener('click',() => { forcedOpen=!forcedOpen; setOpen(forcedOpen); if (forcedOpen) setTimeout(() => input.focus(),0); }); document.addEventListener('pointerdown',event => { if (!forcedOpen || root.contains(event.target)) return; forcedOpen=false; setOpen(false); },true); document.body.appendChild(root); return true; }; if (document.body) mount(); else document.addEventListener('DOMContentLoaded',mount,{once:true}); } function findGarsoPhoneTokens(text) { const source=String(text || ''); const tokens=[]; for (const match of source.matchAll(/(?:(?:\+48|0048)[ \t\u00a0-]*)?\d{3}[ \t\u00a0-]*\d{3}[ \t\u00a0-]*(?:\d{3}|xxx)/giu)) { const start=match.index; const end=start + match[0].length; const beforeChar=start > 0 ? source[start - 1]:''; const afterChar=end < source.length ? source[end]:''; const beforeText=source.slice(Math.max(0,start - 12),start); const hasNormalBoundary=!beforeChar || !/[\p{L}\p{N}_+]/u.test(beforeChar); const hasPhonePrefix=/(?:^|[^\p{L}\p{N}_])tel(?:efon)?[.:\s-]?$/iu.test(beforeText); if ((!hasNormalBoundary && !hasPhonePrefix) || (afterChar && /[\p{L}\p{N}_]/u.test(afterChar))) continue; tokens.push({start,end,value:match[0].toLowerCase()}); } return tokens; } function getGarsoTopicPageContext(value=location.href) { try { const url=new URL(value,GARSO_BASE_URL); if (!/^https?:$/.test(url.protocol) || !/^(?:www\.)?garsoniera\.com\.pl$/i.test(url.hostname) || url.username || url.password) return null; const match=url.pathname.match(/^\/forum\/topic\/(\d+)(?:-([^/]+))?(?:\/page-(\d+))?\/?$/i); const queryId=/^\/forum\/index\.php$/i.test(url.pathname) && url.searchParams.get('showtopic'); if (!match && !/^\d+$/.test(queryId || '')) return null; const offset=Number(url.searchParams.get('st') || 0); const page=match?.[3] || url.searchParams.get('page') || (offset > 0 ? null:1); if (page != null && (!Number.isSafeInteger(Number(page)) || Number(page) < 1)) return null; const id=match?.[1] || queryId; const baseUrl=match ? `${url.origin}/forum/topic/${id}${match[2] ? '-' + match[2]:''}/`:`${url.origin}/forum/index.php?showtopic=${id}`; return {id,page:page == null ? null:Number(page),baseUrl,url:url.href, hasPhone:findGarsoPhoneTokens(decodeURIComponent(match?.[2] || '')).length > 0}; } catch {return null;} } function getGarsoTopicPostCount(doc) { for (const element of doc.querySelectorAll('h2,h3,.maintitle,.ipsType_sectionTitle')) { if (element.closest('.post_block,[id^="post_id_"],.author_info,.vm-helper-ui,[id^="vm-"]')) continue; const match=element.textContent.trim().match(/^(\d+(?:[\s.,]\d{3})*)\s+(odpowiedź|odpowiedzi|repl(?:y|ies)|post(?:y|ów|s)?)\s+(?:w\s+tym\s+temacie|to\s+this\s+topic)\b/iu); if (!match) continue; const count=Number(match[1].replace(/[^\d]/g,'')) + (/^(?:odpowied|repl)/i.test(match[2]) ? 1:0); if (Number.isSafeInteger(count) && count > 0) return count; } return null; } async function readGarsoTopicSummaryPage(doc,context) { const blocks=getGarsoPostBlocks(doc); if (!blocks.length) throw new Error('Nie znaleziono postów. Sprawdź, czy temat jest dostępny i czy jesteś zalogowany.'); const title=extractGarsoTopicTitle(doc,'Temat Garso'); const active=doc.querySelector('.pagination .active, .pages .active, .pagination [aria-current="page"]'); const currentNumber=active?.textContent.trim(); if (context.page == null && /^\d+$/.test(currentNumber || '')) context.page=Number(currentNumber); if (!context.page || (/^\d+$/.test(currentNumber || '') && Number(currentNumber) !== context.page)) throw new Error('Forum zwróciło inną stronę tematu niż żądana.'); const url=context.url || (context.page === 1 ? context.baseUrl:`${context.baseUrl}page-${context.page}/`); const posts=extractPostsFromPage(doc,title,context.page,url); const pageMap=getPaginationMap(doc,context.baseUrl); const maxPage=Math.max(context.page,getMaxPage(doc,pageMap)); const ids=blocks.map(block => block.id).filter(Boolean); const bytes=new TextEncoder().encode(JSON.stringify([ids,posts.map(post => [post.author,post.date,post.text,post.references])])); const hash=await crypto.subtle.digest('SHA-256',bytes); const stamp=[...new Uint8Array(hash)].map(value => value.toString(16).padStart(2,'0')).join(''); return {page:context.page,posts,ids,stamp,pageMap,maxPage,title,topicPostCount:getGarsoTopicPostCount(doc),subtitle:extractGarsoTopicSubtitle(doc), eligible:getGarsoSummaryTopicEligibility({url:context.baseUrl,title},doc).eligible}; } async function collectGarsoFullTopic(context,seed,cancelToken,onProgress,includeReferences=false) { const pages=new Map(),pageMap=new Map(seed.pageMap),seenIds=new Set(),seenPosts=new Set(); let maxPage=seed.maxPage,selected=getGarsoAnalysisPages(maxPage); for (let index=0; index < selected.length; index++) { const page=selected[index]; cancelToken.throwIfCancelled(); onProgress(index,selected.length,`Czytanie strony ${page}/${maxPage}…`); let data; if (page === context.page) data=seed; else { const url=resolveTopicPageUrl(pageMap,context.baseUrl,page,maxPage) || (page === 1 ? context.baseUrl:`${context.baseUrl}page-${page}/`); const requested=getGarsoTopicPageContext(url); if (!requested || requested.id !== context.id || (requested.page != null && requested.page !== page)) throw new Error(`Nieprawidłowy adres strony ${page}.`); data=await loadGarsoSummaryPage(url,page,context.id,cancelToken); } cancelToken.throwIfCancelled(); if (page === 1 && !data.eligible && !includeReferences) return {eligible:false,title:data.title,subtitle:data.subtitle}; if (data.ids.length && data.ids.every(id => seenIds.has(id))) throw new Error(`Strona ${page} powtarza już odczytane posty. Analiza jest niepełna.`); for (const id of data.ids) seenIds.add(id); const uniquePosts=data.posts.filter(post => { const key=post.postId || `${page}:${post.author}:${post.date}:${post.text}`; if (seenPosts.has(key)) return false; seenPosts.add(key); return true; }); pages.set(page,{...data,posts:uniquePosts}); for (const [number,url] of data.pageMap) pageMap.set(number,url); if (page === 1) {maxPage=data.maxPage; selected=getGarsoAnalysisPages(maxPage);} onProgress(index + 1,selected.length,`Przeczytano ${index + 1}/${selected.length} wybranych stron`); await waitWithCancellation(0,cancelToken); } cancelToken.throwIfCancelled(); onProgress(pages.size,pages.size,'Obliczanie ocen…'); const first=pages.get(1),posts=[...pages.values()].flatMap(value => value.posts); const topic={url:context.baseUrl,title:first.title,subtitle:first.subtitle}; const stats=await buildRatingStats(posts,null,cancelToken); const ratingPosts=stats.reviews.map(({post,index,ratings}) => ({postId:post.postId || `${context.id}:${index}`, date:formatIsoDateFromTimestamp(parseGarsoPostDate(post.date) ?? undefined) || '',pageUrl:post.pageUrl, ratingValues:ratings,oralVariants:ratings.oral != null ? detectOralRatingVariants(post.text):null})); const summary=compactGarsoSummaryResult({topics:[topic],allPosts:posts, topicInfo:[{...topic,maxPage,pagesRead:[...pages.keys()],posts:posts.length}], screenedTopicCount:1,excludedTopicCount:0,failedTopicCount:0,stats}); return {schema:2,topicId:context.id,parserVersion:GARSO_SUMMARY_PARSER_VERSION,checkedAt:Date.now(),maxPage,ratingPosts, eligible:first.eligible,references:mergeGarsoTopicReferences(posts.map(post => post.references)), topicPostCount:first.topicPostCount ?? (pages.size === maxPage ? seenIds.size || null:null),postCountCheckedAt:Date.now(),postCountUnverified:first.topicPostCount == null, pageStamps:Object.fromEntries([...pages].map(([page,data]) => [page,data.stamp])),summary}; } function getGarsoTopicCacheOrigin() { const key='vm_garso_topic_cache_origin',saved=GM_getValue(key,''); if (/^https:\/\/(?:www\.)?garsoniera\.com\.pl$/.test(saved)) return saved; const origin=GARSO_MENU_ONLY_MODE ? location.origin:new URL(GARSO_BASE_URL).origin; writeHelperStorageValue(key,origin); return origin; } function validGarsoTopicAnalysis(value,id) { const topic=value?.summary?.topicInfo?.[0],pages=topic?.pagesRead; return value?.schema === 2 && value.topicId === String(id) && value.parserVersion === GARSO_SUMMARY_PARSER_VERSION && Number.isFinite(value.checkedAt) && value.checkedAt > 0 && topic?.maxPage === value.maxPage && hasGarsoAnalysisPages(topic) && Array.isArray(value.ratingPosts) && value.ratingPosts.length === value.summary?.stats?.reviewCount && value.summary.failedTopicCount === 0 && value.pageStamps && Object.keys(value.pageStamps).length === pages.length && pages.every(page => typeof value.pageStamps[page] === 'string'); } async function accessGarsoTopicAnalysis(id,value=null,metadataOnly=false) { if (!GARSO_MENU_ONLY_MODE || window.top !== window.self) throw new Error('Cache tematów wymaga karty Garsoniery.'); const prefix='vm_garso_summary_topic_',key=prefix + id,db=await openPersistentCacheDb(); try { if (!value) return await new Promise((resolve,reject) => { const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readonly'); const request=transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).get(key); request.onsuccess=() => resolve(request.result?.value || null); request.onerror=() => reject(request.error); transaction.onabort=() => reject(transaction.error); }); if (!validGarsoTopicAnalysis(value,id)) throw new Error('Niepełna analiza - zapis pominięty.'); await putPersistentCacheRecords(db,[{key,value,updatedAt:value.checkedAt,sizeBytes:getPersistentCacheEntrySizeBytes(key,value)}]); if (metadataOnly) return value; writeHelperStorageValue(GARSO_TOPIC_REVISION_KEY,`${Date.now()}-${Math.random()}`); const rows=await new Promise((resolve,reject) => { const request=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readonly').objectStore(PERSISTENT_CACHE_STORE_NAME) .getAll(IDBKeyRange.bound(prefix,prefix + '\uffff')); request.onsuccess=() => resolve(request.result); request.onerror=() => reject(request.error); }); const expired=rows.sort((a,b) => b.updatedAt - a.updatedAt).slice(200); if (expired.length) { const transaction=db.transaction(PERSISTENT_CACHE_STORE_NAME,'readwrite'); for (const row of expired) transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).delete(row.key); await waitForIndexedDbTransaction(transaction); } return value; } finally {db.close();} } async function loadGarsoSummaryPage(url,page,id,cancelToken) { const response=await garsoTopicRequest({method:'GET',url,timeout:20000,cancelToken}); cancelToken.throwIfCancelled(); const context=getGarsoTopicPageContext(response.finalUrl || url); if (response.status < 200 || response.status >= 400 || !context || context.id !== id || (context.page != null && context.page !== page)) { throw new Error(`Nie udało się odczytać strony ${page} (HTTP ${response.status || '?'} lub przekierowanie).`); } context.page=page; return readGarsoTopicSummaryPage(parseHtml(response.responseText || ''),context); } async function getLocalGarsoTopicAnalysis(topic,cancelToken,{forceRefresh=false,onProgress=() => {},seed=null,observed=null,includeReferences=false}={}) { const context=getGarsoTopicPageContext(topic.url); if (!context) throw new Error('Nieprawidłowy adres tematu Garso.'); const abort=new AbortController(),unsubscribe=cancelToken.onCancel(() => abort.abort()); const work=async () => { cancelToken.throwIfCancelled(); let saved=null,storageError=''; try {saved=await accessGarsoTopicAnalysis(context.id);} catch {storageError=GARSO_MENU_ONLY_MODE ? 'IndexedDB niedostępne - bez zapisu':'Brak otwartej karty Garsoniery - bez zapisu w cache tematów';} let check=observed || (seed && {page:seed.page || context.page,postCount:seed.topicPostCount,stamp:seed.stamp}); const hasSaved=validGarsoTopicAnalysis(saved,context.id); const reuse=async (unverified,error='',updateCheck=true) => { if (updateCheck) { saved={...saved,postCountCheckedAt:Date.now(),postCountUnverified:unverified,postCountCheckError:error}; try {await accessGarsoTopicAnalysis(context.id,saved,true);} catch {storageError='Nie zapisano czasu sprawdzenia licznika w IndexedDB';} } onProgress(saved.summary.topicInfo[0].pagesRead.length,saved.summary.topicInfo[0].pagesRead.length,unverified ? 'Cache - nie potwierdzono aktualności licznika postów':'Cache - bez zmian w sprawdzonych danych tematu',true); return {...saved,fromCache:true,persisted:true,storageError,postCountUnverified:unverified}; }; if (!forceRefresh && hasSaved && (!includeReferences || Array.isArray(saved.references?.otherUrls)) && Date.now() - saved.checkedAt < getGarsoCacheTtlMs()) { if (!check) { if (isGarsoTopicCountCheckRecent(saved.postCountCheckedAt)) return reuse(!!saved.postCountUnverified,'',false); context.page=1; onProgress(0,1,'Sprawdzanie liczby postów - pierwsza strona tematu…'); try {seed=await loadGarsoSummaryPage(context.baseUrl,1,context.id,cancelToken);} catch (error) { if (cancelToken.cancelled || isOperationCancelledError(error)) throw error; return reuse(true,String(error?.message || error)); } check={page:1,postCount:seed.topicPostCount,stamp:seed.stamp}; } const hasCount=Number.isSafeInteger(check.postCount) && check.postCount > 0; const hasSavedCount=Number.isSafeInteger(saved.topicPostCount) && saved.topicPostCount > 0; const changedPage=[[check.page,check.stamp],[seed?.page || check.page,seed?.stamp]] .some(([page,stamp]) => stamp && saved.pageStamps[page] && saved.pageStamps[page] !== stamp); const changedPageCount=seed && seed.maxPage !== saved.maxPage; if (!changedPage && !changedPageCount) { if (!hasCount) return reuse(true,'Nie odczytano licznika postów'); if (hasSavedCount && check.postCount === saved.topicPostCount) return reuse(false); } } if (seed) context.page=seed.page || check?.page || context.page; if (!seed || forceRefresh) { context.page=1; onProgress(0,1,'Czytanie pierwszej strony tematu…'); seed=await loadGarsoSummaryPage(context.baseUrl,1,context.id,cancelToken); } const value=await collectGarsoFullTopic(context,seed,cancelToken,onProgress,includeReferences); cancelToken.throwIfCancelled(); if (value.eligible === false && !value.summary) return value; let persisted=false; if (!storageError) { try {await accessGarsoTopicAnalysis(context.id,value); persisted=true;} catch {storageError='Nie udało się zapisać analizy w IndexedDB';} } return {...value,fromCache:false,persisted,storageError}; }; try { if (navigator.locks?.request) return await navigator.locks.request(`vm-garso-topic-summary:${context.id}`,{signal:abort.signal},work); return await work(); } finally {unsubscribe();} } function pruneGarsoTopicBridge() { const now=Date.now(),prefix=GARSO_TOPIC_BRIDGE_PREFIX; for (const key of GM_listValues().filter(key => key.startsWith(prefix))) { const value=GM_getValue(key,null); if (key.startsWith(prefix + 'session:')) {if (!(value?.expiresAt > now)) deleteHelperStorageValue(key);} else if (key.startsWith(prefix + 'request:') || key.startsWith(prefix + 'reply:')) { if (!(GM_getValue(prefix + 'session:' + value?.session,null)?.expiresAt > now)) deleteHelperStorageValue(key); } } } function initGarsoTopicCacheBridge() { pruneGarsoTopicBridge(); if (typeof GM_addValueChangeListener === 'function') GM_addValueChangeListener(GARSO_TOPIC_REVISION_KEY,() => summaryCache.clear()); if (!GARSO_MENU_ONLY_MODE || window.top !== window.self) return false; const prefix=GARSO_TOPIC_BRIDGE_PREFIX,worker=GARSO_TOPIC_WORKER_ID,processing=new Set(); if (worker) { const session=GM_getValue(prefix + 'session:' + worker,null); if (session?.expiresAt > Date.now() && session.origin !== location.origin) { session.origin=location.origin; writeHelperStorageValue(prefix + 'session:' + worker,session); writeHelperStorageValue('vm_garso_topic_cache_origin',location.origin); } } else getGarsoTopicCacheOrigin(); const serve=async id => { if (!id || processing.has(id)) return; const requestKey=prefix + 'request:' + id,replyKey=prefix + 'reply:' + id; const request=GM_getValue(requestKey,null),sessionKey=prefix + 'session:' + request?.session; const session=GM_getValue(sessionKey,null); if (!request || request.protocol !== 1 || !(session?.expiresAt > Date.now()) || session.origin !== location.origin || (worker && request.session !== worker) || !getGarsoTopicPageContext(request.topic?.url)) return; processing.add(id); const work=async () => { if (!GM_getValue(requestKey,null) || GM_getValue(replyKey,null)?.done) return; const token=createOperationCancelToken('Garso - wspólna analiza tematu'); const alive=() => GM_getValue(requestKey,null) && GM_getValue(sessionKey,null)?.expiresAt > Date.now(); const publish=data => {if (alive()) writeHelperStorageValue(replyKey,{session:request.session,at:Date.now(),...data});}; const pulse=setInterval(() => {if (!alive()) token.cancel('Zakończono odczyt w karcie źródłowej'); else publish({working:true});},15000); let listener=null; if (typeof GM_addValueChangeListener === 'function') listener=GM_addValueChangeListener(requestKey,(_n,_o,value) => {if (!value) token.cancel();}); const unload=() => token.cancel('Zamknięto kartę obsługującą cache'); window.addEventListener('pagehide',unload); try { publish({working:true}); const url=new URL(request.topic.url); url.host=location.host; url.protocol=location.protocol; const value=await getLocalGarsoTopicAnalysis({...request.topic,url:url.href},token,{ forceRefresh:request.forceRefresh === true,observed:request.observed || null,includeReferences:request.includeReferences === true, onProgress:(done,total,message,fromCache) => publish({working:true,progress:{done,total,message,fromCache:!!fromCache}}) }); token.throwIfCancelled(); publish({done:true,value}); } catch (error) {publish({done:true,error:String(error?.message || error),retryable:isOperationCancelledError(error)});} finally { clearInterval(pulse); window.removeEventListener('pagehide',unload); if (listener != null) GM_removeValueChangeListener(listener); setTimeout(() => {if (!GM_getValue(requestKey,null)) deleteHelperStorageValue(replyKey);},1000); } }; try { if (navigator.locks?.request) await navigator.locks.request(`vm-garso-topic-bridge:${id}`,{ifAvailable:true},lock => lock ? work():undefined); else { const claim=GM_getValue(replyKey,null); if (!claim) { writeHelperStorageValue(replyKey,{session:request.session,at:Date.now(),owner:GARSO_SEARCH_INSTANCE_ID}); await waitWithCancellation(80); if (GM_getValue(replyKey,null)?.owner === GARSO_SEARCH_INSTANCE_ID) await work(); } } } finally {processing.delete(id);} }; const dispatch=id => {void serve(id).catch(error => log('Błąd komunikacji cache tematów Garso',error));}; if (typeof GM_addValueChangeListener === 'function') GM_addValueChangeListener(prefix + 'signal',(_n,_o,id) => dispatch(id)); for (const key of GM_listValues().filter(key => key.startsWith(prefix + 'request:'))) dispatch(key.slice((prefix + 'request:').length)); if (worker) { const timer=setInterval(() => { if (!(GM_getValue(prefix + 'session:' + worker,null)?.expiresAt > Date.now())) {clearInterval(timer); pruneGarsoTopicBridge(); window.close();} },15000); } return !!worker; } function createGarsoTopicCacheClient(cancelToken) { pruneGarsoTopicBridge(); const prefix=GARSO_TOPIC_BRIDGE_PREFIX,sessionId=`${Date.now()}-${Math.random().toString(36).slice(2)}`; const sessionKey=prefix + 'session:' + sessionId,pending=new Map(); let sequence=0,closed=false,pulse=null,unsubscribe=() => {}; const touch=() => { if (closed) return; const origin=GM_getValue(sessionKey,null)?.origin || getGarsoTopicCacheOrigin(); writeHelperStorageValue(sessionKey,{origin,expiresAt:Date.now() + 120000}); }; const close=() => { if (closed) return; closed=true; clearInterval(pulse); unsubscribe(); window.removeEventListener('pagehide',close); for (const stop of [...pending.values()]) stop(createOperationCancelledError('Zakończono odczyt tematów')); deleteHelperStorageValue(sessionKey); }; window.addEventListener('pagehide',close); unsubscribe=cancelToken.onCancel(close); return { close, async get(topic,{forceRefresh=false,onProgress=() => {},seed=null,observed=null,includeReferences=false}={}) { cancelToken.throwIfCancelled(); if (!getGarsoTopicPageContext(topic?.url)) throw new Error('Nieprawidłowy adres tematu Garso.'); if (closed) throw createOperationCancelledError('Zamknięto odczyt tematów'); if (GARSO_MENU_ONLY_MODE && location.origin === getGarsoTopicCacheOrigin()) { return getLocalGarsoTopicAnalysis(topic,cancelToken,{forceRefresh,onProgress,seed,observed,includeReferences}); } if (!pulse) {touch(); pulse=setInterval(touch,15000);} const id=sessionId + ':' + (++sequence),requestKey=prefix + 'request:' + id,replyKey=prefix + 'reply:' + id; return new Promise((resolve,reject) => { let listener=null,timer=null,settled=false,lastAt=Date.now(),lastReply=0,localToken=null; const startedAt=Date.now(); const finish=(error,value) => { if (settled) return; settled=true; clearInterval(timer); pending.delete(id); localToken?.cancel('Zakończono odczyt tematów'); if (listener != null) GM_removeValueChangeListener(listener); deleteHelperStorageValue(requestKey); deleteHelperStorageValue(replyKey); if (error) reject(error); else resolve(value); }; const check=() => { if (settled || localToken) return; try { const reply=GM_getValue(replyKey,null); if (reply?.done && !reply.retryable) {finish(reply.error ? new Error(reply.error):null,reply.value); return;} if (reply?.at > lastReply) { lastReply=reply.at; lastAt=Date.now(); if (reply.progress) {const p=reply.progress; onProgress(p.done,p.total,p.message,p.fromCache);} } if (reply?.retryable || (!lastReply && Date.now() - startedAt >= 1500) || (lastReply && Date.now() - lastAt > 120000)) { localToken=createOperationCancelToken('Garso - odczyt bez karty forum'); clearInterval(timer); if (listener != null) {GM_removeValueChangeListener(listener); listener=null;} deleteHelperStorageValue(requestKey); deleteHelperStorageValue(replyKey); getLocalGarsoTopicAnalysis(topic,localToken,{forceRefresh,onProgress,seed,observed,includeReferences}) .then(value => finish(null,value),error => finish(error)); } } catch (error) {finish(error);} }; pending.set(id,finish); try { if (typeof GM_addValueChangeListener === 'function') listener=GM_addValueChangeListener(replyKey,check); writeHelperStorageValue(requestKey,{protocol:1,session:sessionId,topic,forceRefresh,observed,includeReferences}); writeHelperStorageValue(prefix + 'signal',id); timer=setInterval(check,250); check(); } catch (error) {finish(error);} }); } }; } function getGarsoPostReferences(content) { const source=readGarsoMatchSource([content]); const phones=[...new Set(findGarsoPhoneTokens(source.text).map(({value}) => value.replace(/^(?:\+48|0048)/,'').replace(/[\s-]/g,'')))]; return {phones,adUrls:source.adUrls,otherUrls:source.otherUrls,hasLinks:source.hasLinks}; } function mergeGarsoTopicReferences(groups) { return Object.fromEntries(['phones','adUrls','otherUrls'].map(key => [key,[...new Set(groups.flatMap(group => group?.[key] || []))]])); } function createGarsoTopicTools(context) { const first=document.querySelector('.post_block,[id^="post_id_"]'); if (!first) return null; const panel=prepareHelperStyleApi(makeElement('details','vm-ui-panel')); panel.id='vm-garso-topic-tools'; panel.hidden=true; const title=makeElement('summary'); const history=makeElement('div','vm-topic-history'); const row=prepareHelperStyleApi(makeElement('label','',' Posty:')); row.id='vm-garso-topic-filter'; let bar=[...document.querySelectorAll('h2,h3,.maintitle,.ipsType_sectionTitle')].find(element => !element.closest('.post_block,[id^="post_id_"],.vm-helper-ui') && /^(?:\d[\d\s.,]*\s+(?:odpowied|repl|post)|brak odpowiedzi|no replies)/i.test(element.textContent.trim())); if (!bar) {bar=makeElement('div','maintitle'); first.before(bar);} bar.classList.add('vm-topic-filter-bar'); bar.appendChild(row); const style=makeElement('style'); style.textContent=` .vm-topic-filter-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:6px 16px; } #vm-garso-topic-filter { display:flex; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:7px; margin-left:auto; color:inherit; font:12px/1.35 var(--vm-ui-font-family,Arial,sans-serif); } #vm-garso-topic-filter select { width:auto; max-width:100%; min-height:28px; padding:3px 7px; border:1px solid var(--vm-ui-input-border,#8b8b9b); border-radius:5px; background:var(--vm-ui-input-bg,#fff); color:var(--vm-ui-input-text,#222); font:inherit; cursor:pointer; } #vm-garso-topic-filter .vm-topic-filter-count { white-space:nowrap; opacity:.85; font-size:11px; } @t { margin:8px 0 12px; padding:9px 12px; font:12px/1.4 var(--vm-ui-font-family,Arial,sans-serif); } @t[hidden], .vm-garso-post-filtered { display:none !important; } @t > summary { cursor:pointer; font-weight:600; } @t > summary:focus-visible { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:3px; } @t .vm-topic-history { display:grid; grid-template-columns:minmax(130px,.6fr) repeat(2,minmax(0,1fr)); gap:8px 20px; margin-top:8px; max-height:min(50vh,420px); overflow:auto; overscroll-behavior:contain; overflow-wrap:anywhere; } @t section, @t li > a { min-width:0; max-width:100%; } @media (max-width:700px) { @t .vm-topic-history { grid-template-columns:1fr; } } @t a { color:var(--vm-ui-accent,#b52d77); text-underline-offset:2px; } @t a:focus-visible { outline:2px solid currentColor; outline-offset:2px; } @t section > button { margin:3px 0 3px 8px; } @t ul { list-style:none; margin:5px 0 0; padding:0; } @t li { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin:0; padding:3px 0; } @t .vm-topic-link-status { display:inline-flex; align-items:center; gap:4px; font-size:15px; font-weight:700; } `.replaceAll('@t','#vm-garso-topic-tools'); const select=makeElement('select'); select.setAttribute('aria-label','Filtr postów w temacie'); appendSelectOptions(select,[['all','Wszystkie'],['ratings','Tylko posty z ocenami'],['contacts','Tylko posty z telefonami lub linkami']]); const key=`vm_garso_post_filter_${context.id}`; let mode='all'; try {mode=sessionStorage.getItem(key) || 'all';} catch {} select.value=['ratings','contacts'].includes(mode) ? mode:'all'; const status=makeElement('span','vm-topic-filter-count'); status.setAttribute('role','status'); row.append(select,status); panel.append(title,history); document.head.appendChild(style); first.before(panel); let fullHistory=null,current=[],timer=null,historyKey='',openAllButton=null; const showStatuses=SETTINGS.garsoCheckEscortLinks !== false; const statusByUrl=new Map(); const refreshOpenAllButton=() => updateOpenAvailableEscortAdsButton(openAllButton,statusByUrl, [...history.querySelectorAll('.vm-topic-link-status')].some(marker => !marker.dataset.vmStatusReady)); const checkLinks=async () => { if (!showStatuses || !panel.open || panel.hidden) return; const pending=new Map(); for (const marker of history.querySelectorAll('.vm-topic-link-status')) if (!marker.dataset.vmRequested) { marker.dataset.vmRequested='1'; pending.set(marker.dataset.adUrl,marker); } if (!pending.size) return; const show=(url,result) => { const marker=pending.get(normalizeEscortiAdUrl(url)); if (marker?.isConnected) { statusByUrl.set(marker.dataset.adUrl,result || null); renderEscortAdLinkStatusMarker(marker,result); refreshOpenAllButton(); } }; try { const summary=await scanActiveEscortAds([...pending.keys()],progress => show(progress.url,progress.result),false,{activityOnly:true,useActivityCache:true,cacheActivity:true}); const results=new Map((summary?.results || []).map(result => [normalizeEscortiAdUrl(result.url),result])); for (const url of pending.keys()) show(url,results.get(url)); } catch { for (const [url,marker] of pending) if (!marker.dataset.vmStatusReady) show(url,null); } }; const renderHistory=references => { references=mergeGarsoTopicReferences([references]); title.textContent='Zobacz listę telefonów i linków z tematu';title.title=fullHistory?'Cały temat':'Bieżąca strona'; panel.hidden=!Object.values(references).some(values => values.length); const signature=JSON.stringify(references); if (signature === historyKey) return; historyKey=signature; history.replaceChildren(); statusByUrl.clear(); openAllButton=null; for (const [key,label] of [['phones','Numery telefonów'],['adUrls','Linki Escort.club'],['otherUrls','Linki do innych serwisów']]) { const values=references[key],column=makeElement('section'),list=makeElement('ul'); column.append(makeElement('strong','',`${label} (${values.length})`),list); if (key === 'adUrls' && values.length && showStatuses) { openAllButton=makeButton('vm-ui-button vm-ui-compact'); openAllButton.addEventListener('click',event => openAvailableEscortAds(statusByUrl,event)); column.insertBefore(openAllButton,list); } if (!values.length) list.appendChild(makeElement('li','','Brak')); for (const value of values) { const item=makeElement('li'); if (key === 'phones') item.textContent=value.replace(/^(\d{3})(\d{3})(\d{3}|xxx)$/,'$1-$2-$3'); else { const link=makeElement('a','',value); link.style.overflowWrap='anywhere'; link.style.minWidth='0'; Object.assign(link,{href:value,target:'_blank',rel:'noopener noreferrer',title:value}); item.appendChild(link); if (key === 'adUrls' && showStatuses) { const marker=makeElement('span','vm-topic-link-status','…'); marker.dataset.adUrl=value; marker.title='Sprawdzanie aktywności anonsu'; item.appendChild(marker); } } if (SETTINGS.showTopPhoneSearch !== false && SETTINGS.garsoShowSearchIcons !== false) item.appendChild(makeGarsoInlineSearchButton(value)); list.appendChild(item); } history.appendChild(column); } refreshOpenAllButton(); void checkLinks(); }; panel.addEventListener('toggle',() => {void checkLinks();}); const applyFilter=() => { let visible=0; for (const {block,references,rated} of current) { const hide=select.value === 'ratings' ? !rated:select.value === 'contacts' && !references.phones.length && !references.hasLinks; block.classList.toggle('vm-garso-post-filtered',hide); if (!hide) visible++; } status.textContent=`${visible} / ${current.length}`; setControlTitle(status,`Widoczne ${visible} z ${current.length} postów na tej stronie.`); }; const parsedPosts=new WeakMap(),dirtyPosts=new WeakSet(); const scan=() => { current=getGarsoPostBlocks().map(block => { let value=parsedPosts.get(block); if (!value || dirtyPosts.has(block)) { const content=getGarsoPostContent(block),text=cleanPostElement(content); value={block,text,references:getGarsoPostReferences(content), rated:value?.text === text ? value.rated:Object.keys(extractRatingsFromPost(text,{topicTitle:extractGarsoTopicTitle(document)})).length > 0}; parsedPosts.set(block,value); dirtyPosts.delete(block); } return value; }); applyFilter(); renderHistory(mergeGarsoTopicReferences([fullHistory,...current.map(post => post.references)])); }; select.addEventListener('change',() => { try {sessionStorage.setItem(key,select.value);} catch {} applyFilter(); }); const own=node => (node.nodeType === 1 ? node:node.parentElement)?.closest('.vm-helper-ui,.vm-garso-inline-tools,.vm-garso-parser-feedback'); const observer=new MutationObserver(mutations => { let changed=false; for (const m of mutations) { if (own(m.target)) continue; const node=m.target.nodeType === 1 ? m.target:m.target.parentElement; const post=node?.closest('.post_block,[id^="post_id_"]'); const relevant=m.type !== 'childList' ? !!post:[...m.addedNodes,...m.removedNodes].some(n => !own(n) && (post || (n.nodeType === 1 && (n.matches('.post_block,[id^="post_id_"]') || n.querySelector('.post_block,[id^="post_id_"]'))))); if (relevant) {changed=true; if (post) dirtyPosts.add(post);} } if (changed && timer === null) timer=setTimeout(() => {timer=null; scan();},120); }); observer.observe(document.getElementById('content') || document.body,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['href']}); window.addEventListener('pagehide',event => {if (!event.persisted) {observer.disconnect(); clearTimeout(timer);}}); scan(); return {render(value) {fullHistory=value.references; renderHistory(mergeGarsoTopicReferences([fullHistory,...current.map(post => post.references)]));}}; } async function initGarsoTopicSummary() { if (!GARSO_MENU_ONLY_MODE || window.top !== window.self || (SETTINGS.garsoShowTopicSummary === false && SETTINGS.garsoShowTopicTools === false)) return; const context=getGarsoTopicPageContext(); if (!context || document.getElementById('vm-garso-topic-summary')) return; const tools=SETTINGS.garsoShowTopicTools !== false ? createGarsoTopicTools(context):null; let cached=null; try {cached=await accessGarsoTopicAnalysis(context.id);} catch {} if (!validGarsoTopicAnalysis(cached,context.id)) cached=null; const source=cached?.eligible && context.page !== 1 ? {eligible:true,firstDoc:null}:await getGarsoCurrentTopicSource(); if (!source?.eligible && !tools) return; const showRatings=source?.eligible && SETTINGS.garsoShowTopicSummary !== false; const heading=document.querySelector('h1[itemprop="name"], h1.ipsType_pagetitle, #content h1'); if (!heading || !document.querySelector('.post_block, [id^="post_id_"]')) return; const panel=prepareHelperStyleApi(makeElement('section')); panel.id='vm-garso-topic-summary'; panel.setAttribute('aria-label','Analiza tematu: do 10 pierwszych i 20 ostatnich stron'); const style=makeElement('style'); style.textContent=` @s { --vm-ui-panel-text:var(--vm-ui-text,#262226); --vm-ui-panel-muted:var(--vm-ui-text-muted,#666); box-sizing:border-box; margin:12px 0; padding:10px; border:1px solid var(--vm-ui-border,#cec4c6); border-radius:7px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-panel-text); font:12px/1.35 var(--vm-ui-font-family,Arial,sans-serif); } @s[hidden], @s [hidden], @s .vm-garso-topic-status:empty { display:none !important; } @s .vm-garso-topic-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:6px; } @s .vm-garso-topic-grid > div { min-width:0; } @s .vm-garso-topic-header { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:9px; } @s .vm-garso-topic-overall { min-width:160px; max-width:100%; } @s .vm-garso-topic-header > .vm-garso-topic-status { min-width:160px; } @s .vm-garso-topic-header > button { margin-left:auto; } @s .vm-garso-topic-mentions { margin-top:7px; } @s .vm-garso-mention-chip { color:var(--vm-ui-panel-text) !important; } @s .vm-garso-topic-status { flex:1 1 230px; color:var(--vm-ui-text-muted,#666); font-size:11px; } @s progress { display:block; width:100%; height:5px; margin:8px 0 0; accent-color:var(--vm-ui-accent,#b52d77); } `.replaceAll('@s','#vm-garso-topic-summary'); const content=makeElement('div'),overview=makeElement('div'),headerRow=makeElement('div','vm-garso-topic-header'); const status=makeElement('div','vm-garso-topic-status','Przygotowanie analizy…'); status.setAttribute('role','status'); const refresh=makeButton('vm-ui-button vm-ui-compact','Odśwież',{title:'Ponownie przeczytaj do 10 pierwszych i 20 ostatnich stron'}); const progress=makeElement('progress'); progress.setAttribute('aria-label','Postęp czytania wybranych stron'); progress.hidden=true; overview.hidden=content.hidden=!showRatings; headerRow.append(overview,status,refresh); panel.append(style,headerRow,progress,content); let header=heading.closest('.ipsPageHeader, .ipsBox_withphoto, .topic_header, .topic-header') || heading.parentElement; if (!header || header === document.body || header.id === 'content' || header.querySelector('.post_block, [id^="post_id_"]')) header=heading; header.after(panel); let token=null,latest=null; const render=value => { latest=value; tools?.render(value); if (showRatings) { content.innerHTML=summaryToHtml(value.summary,{topicView:true}); const metric=content.querySelector('.vm-garso-topic-overall'); if (metric) overview.replaceChildren(metric); } panel._vmSummary=value.summary; panel._vmReferences=value.references; window.dispatchEvent(new Event('vm-garso-topic-summary-updated')); }; const update=(done,total,message) => { progress.max=Math.max(1,total); progress.value=done; status.textContent=message; }; const run=async (forceRefresh=false) => { if (token) return; token=createOperationCancelToken('Garso - analiza bieżącego tematu'); const cancelToken=token,client=createGarsoTopicCacheClient(cancelToken); refresh.textContent='Przerwij'; refresh.title='Przerwij czytanie tematu'; progress.hidden=false; panel.hidden=false; panel.setAttribute('aria-busy','true'); try { status.textContent='Przygotowanie analizy / oczekiwanie na analizę w innej karcie…'; const observedSeed=await readGarsoTopicSummaryPage(document,context); const seed=!forceRefresh && context.page !== 1 && source.firstDoc ? await readGarsoTopicSummaryPage(source.firstDoc,{...context,page:1,url:context.baseUrl}):observedSeed; const value=await client.get({url:context.url},{forceRefresh,seed,onProgress:update,includeReferences:!!tools, observed:{page:context.page,postCount:observedSeed.topicPostCount,stamp:observedSeed.stamp}}); cancelToken.throwIfCancelled(); if (!validGarsoTopicAnalysis(value,context.id) || (tools && !value.references)) throw new Error('Brak kompletnej analizy tematu.'); render(value); status.textContent=[value.storageError,value.postCountUnverified?'Aktualność niepotwierdzona':''].filter(Boolean).join(' • '); status.title=`Analiza: ${formatWatchDateTime(value.checkedAt)}`; } catch (error) { status.textContent=(cancelToken.cancelled || isOperationCancelledError(error) ? 'Przerwano analizę.':`Analiza niepełna: ${error?.message || error}.`) + ` Nie zapisano nowego wyniku.${latest ? ' Widoczne są poprzednie wyniki.':''}`; } finally { client.close(); token=null; progress.hidden=true; panel.setAttribute('aria-busy','false'); panel.hidden=!showRatings && !status.textContent.trim(); refresh.disabled=false; refresh.textContent='Odśwież'; refresh.title='Ponownie przeczytaj do 10 pierwszych i 20 ostatnich stron'; } }; refresh.addEventListener('click',event => { stopUiEvent(event); if (token) {token.cancel(); refresh.disabled=true; refresh.textContent='Przerywam…';} else void run(true); }); content.addEventListener('click',event => { const chip=event.target.closest('.vm-garso-mention-chip'); if (!chip || !content.contains(chip)) return; const entries=getGarsoMentionEntries(latest?.summary).sort((a,b) => b.count - a.count || a.label.localeCompare(b.label,'pl-PL')); const entry=entries[Number(chip.dataset.vmGarsoMentionIndex)]; if (entry) {stopUiEvent(event); openGarsoMentionEvidenceDialog(entry);} }); window.addEventListener('pagehide',() => token?.cancel('Opuszczono temat')); if (cached) {render(cached); status.textContent='Sprawdzanie aktualności…'; await yieldHelperWork();} await run(); } function readEscortAdCacheBundle(adId) { const stored=getStoredEscortAdData(adId); if (!stored) return null; const adData=makeWatchedAdDataMirrorValue(stored); adData.adId=String(adId); adData.imageUrl=getEscortCachedArchiveImageUrl(adId,stored); const phone=getEscortAdPhoneDigits(stored),url=stored.adUrl || `https://pl.escort.club/anons/${adId}.html`; const facts=getEscortArchiveResearchFactsFromCache(adId,url,phone); return {adData,facts}; } function getGarsoAdTileProfileStats(value) { const age=getFavoriteTileStat(value,'wiek','lat'),height=getFavoriteTileStat(value,'wzrost','cm'),weight=getFavoriteTileStat(value,'waga','kg'),breasts=getFavoriteTileStat(value,'biust'); return {age,height,weight,breasts,text:[age,height,weight,breasts ? `biust ${breasts}`:''].filter(Boolean).join(' • ')}; } function createGarsoProfileStatsElement(profileStats,className='vm-garso-visited-stats') { return profileStats?.text ? makeElement('span',className,profileStats.text):null; } function makeGarsoAdTileData(adId,value) { const labels=[]; for (const [duration] of ESCORT_PRICE_DURATION_OPTIONS) { const price=formatEscortTilePrice(value.prices?.[duration]); if (price) labels.push(`${getEscortPriceDurationLabel(duration)}: ${price}`); if (labels.length >= 4) break; } const profileStats=getGarsoAdTileProfileStats(value); return { adId:String(adId),url:normalizeEscortiAdUrl(value.adUrl) || `https://pl.escort.club/anons/${adId}.html`, title:normalizeEscortAdText(value.title),imageUrl:String(value.imageUrl || '').trim(), phoneDigits:normalizePhoneReputationPhone(getEscortAdPhoneDigits(value))?.local || '', locationText:normalizeEscortCity(value.location?.city) || '',priceText:labels.join(' • '), profileStats,profileStatsText:profileStats.text,visitedAt:Number(value.lastVisitedAt) || 0 }; } async function findGarsoEscortiFallback(phone,cancelToken) { const search=await searchEscortiProfilesDirect(phone,cancelToken),candidates=new Map(); for (const profileUrl of search.profileUrls || []) { cancelToken.throwIfCancelled(); const profile=await readEscortiProfileDirect(profileUrl,cancelToken); for (const url of profile.adUrls || []) { const id=parseAdIdFromUrl(url); if (id) candidates.set(String(id),{url,profile}); } } let unknown=false; for (const id of [...candidates.keys()].sort((a,b) => Number(b) - Number(a))) { cancelToken.throwIfCancelled(); const {url,profile}=candidates.get(id),cacheHost=getEscortCachedAdHost(id); const activity=cacheHost ? null:await inspectEscortClubAdActivityOnly(url,cancelToken); if (activity) escortActiveAdProbeCache.set(normalizeEscortiAdUrl(url),Promise.resolve(activity)); unknown ||= !!activity?.unknown; if (!cacheHost && !activity?.active) continue; const tile=makeGarsoAdTileData(id,{ title:profile.profileName,imageUrl:profile.imageUrl, ...getWatchedAdDataMirror(id),...activity?.adData,adUrl:url,phoneDigits:phone }); if (cacheHost) { tile.url=addEscortCachedArchiveMarkerToUrl(`https://${cacheHost}${getEscortAdPath(id,cacheHost)}`); tile.cached=true; } return tile; } if (unknown) throw new Error('Nie udało się potwierdzić aktywności anonsów z Escorti.pl.'); return null; } async function findGarsoPhoneAdTiles(phones,cancelToken) { const found=new Map(); for (const phone of phones) { let matched=false; const search={requests:[{url:buildEscortClubPhoneSearchUrl(phone),method:'GET'}]}; for await (const {context,finalUrl} of iterateFavoriteSearchPages(search,cancelToken)) { for (const anchor of context?.anchors || []) { const adUrl=normalizeEscortiAdUrl(anchor.getAttribute('href'),finalUrl),id=parseAdIdFromUrl(adUrl); if (!id) continue; matched=true; if (found.has(String(id))) continue; escortActiveAdProbeCache.set(adUrl,Promise.resolve({url:adUrl,active:true})); const card=anchor.closest('.item-col.col') || anchor,img=card.querySelector('img'); const tile=makeGarsoAdTileData(id,{...getWatchedAdDataMirror(id),adUrl,phoneDigits:phone}); const title=normalizeEscortAdText(card.querySelector('.item-name')?.textContent); if (title) tile.title=title; const stats=normalizeEscortAdText(card.querySelector('.item-stats')?.textContent); if (stats) tile.locationText=stats.replace(/,\s*\d{1,3}\s*(?:y|lat|lata|rok|years?)\b.*$/i,''); const src=img?.getAttribute('data-src') || img?.getAttribute('data-lazy-src') || img?.getAttribute('data-original') || img?.getAttribute('src'); if (src) {try {tile.imageUrl=new URL(src,finalUrl).href;} catch {}} found.set(String(id),tile); } } if (!matched) { const tile=await findGarsoEscortiFallback(phone,cancelToken); if (tile) found.set(tile.adId,tile); } } cancelToken.throwIfCancelled(); return [...found.values()]; } async function findGarsoLinkedAdTiles(urls,cancelToken,onTile) { const seen=new Set(); for (const value of urls) { cancelToken.throwIfCancelled(); const url=normalizeEscortiAdUrl(value),id=parseAdIdFromUrl(url); if (!id || seen.has(id)) continue; seen.add(id); const cacheHost=getEscortCachedAdHost(id); let activity=null; if (!cacheHost) { let request=escortActiveAdProbeCache.get(url); if (!request) { request=inspectEscortClubAdActivityOnly(url,cancelToken); escortActiveAdProbeCache.set(url,request); } try {activity=await request;} catch { if (escortActiveAdProbeCache.get(url) === request) escortActiveAdProbeCache.delete(url); cancelToken.throwIfCancelled(); continue; } cancelToken.throwIfCancelled(); if (!activity?.active) continue; } const tile=makeGarsoAdTileData(id,{...mergeEscortDefinedObject(getWatchedAdDataMirror(id),activity?.adData),adUrl:url}); if (cacheHost) { tile.url=addEscortCachedArchiveMarkerToUrl(`https://${cacheHost}${getEscortAdPath(id,cacheHost)}`); tile.cached=true; } onTile(tile); } } function renderGarsoVisitedAds(host,ads,label='Odwiedzone anonse') { host.hidden=!ads.length; const signature=JSON.stringify([label,ads]); if (host._vmSignature === signature) return; host._vmSignature=signature; const restoreMoreFocus=document.activeElement === host.querySelector('.vm-garso-visited-more'); host.replaceChildren(makeElement('span','vm-garso-visited-label',`${label} (${ads.length}):`)); const grid=makeElement('div','vm-garso-visited-grid'); for (const ad of ads) { const card=makeElement('div','vm-garso-visited-card'); const placeholder=makeElement('span','vm-garso-visited-thumb','—',{title:'Brak zdjęcia'}),thumb=ad.imageUrl ? makeElement('img','vm-garso-visited-thumb',null,{alt:'',loading:'lazy',decoding:'async',referrerPolicy:'no-referrer'}):placeholder; if (ad.imageUrl) { thumb.src=ad.imageUrl; thumb.title='Powiększ zdjęcie'; thumb.addEventListener('error',() => {hideWatchImagePreview(); thumb.replaceWith(placeholder);},{once:true}); thumb.addEventListener('mouseenter',() => showWatchImagePreview(thumb,thumb.currentSrc || thumb.src)); thumb.addEventListener('mouseleave',hideWatchImagePreview); } const body=makeElement('div','vm-garso-visited-body'); const row=makeElement('div','vm-garso-visited-title-row'); const link=makeElement('a','',null,{title:ad.title ? `${ad.title} [${ad.adId}]`:`Anons ${ad.adId}`}); link.append(makeElement('span','vm-garso-visited-title-text',ad.title || 'Anons'), makeElement('span','vm-garso-visited-title-id',` [${ad.adId}]`)); Object.assign(link,{href:ad.url,target:'_blank',rel:'noopener noreferrer'}); row.appendChild(link); body.appendChild(row); if (ad.locationText) body.appendChild(makeElement('span','vm-garso-visited-location',ad.locationText)); if (ad.profileStatsText) body.appendChild(makeElement('span','vm-garso-visited-stats',ad.profileStatsText)); if (ad.priceText) body.appendChild(makeElement('span','vm-garso-visited-price',ad.priceText)); const visitedWhen=makeElement('span','vm-garso-visited-when',ad.visitedAt ? `Ostatnio obejrzany: ${formatWatchDateTime(ad.visitedAt)} (${formatEscortLastVisit(ad.visitedAt)})`:'Jeszcze nieobejrzany'); card.append(thumb,body,visitedWhen); grid.appendChild(card); } host.appendChild(grid); if (ads.length > 3) { const more=makeButton('vm-tracked-profile-picker vm-garso-visited-more'); const update=() => { const expanded=host.dataset.vmExpanded === '1'; for (let index=0; index < grid.children.length; index++) grid.children[index].hidden=!expanded && index >= 3; more.textContent=expanded ? 'Pokaż mniej':`Pokaż pozostałe (${ads.length - 3})`; more.setAttribute('aria-expanded',String(expanded)); }; more.addEventListener('click',event => {stopUiEvent(event); host.dataset.vmExpanded=host.dataset.vmExpanded === '1' ? '0':'1'; update();}); host.appendChild(more); update(); if (restoreMoreFocus) more.focus({preventScroll:true}); } } function createGarsoTrackedBox(topicView) { const box=makeElement('div','vm-garso-tracked-topics' + (topicView ? ' vm-garso-topic-helper-card':'')); box.setAttribute('aria-label','Zapisane dane Escort Helper'); const notice=makeElement('div','vm-ui-help'); notice.hidden=true; notice.style.gridColumn='1 / -1'; notice.setAttribute('role','status'); box.appendChild(notice); const visited=makeElement('div','vm-garso-visited-ads'); visited.hidden=true; const {root:profile,panel,render}=createTrackedEntriesBox(changeTrackedEntry,{accent:'#b52d77'}); const footer=makeElement('div','vm-ui-row'); const add=makeButton('vm-tracked-profile-picker','+ Dodaj profil'),status=makeElement('div','vm-ui-help'); status.setAttribute('role','status'); add.title='Dodaj profil na podstawie numeru telefonu powiązanego z tematem.'; footer.append(add,status); panel.appendChild(footer); const remaining=makeElement('div','vm-garso-unvisited-ads'); const otherTiles=makeElement('div','vm-garso-visited-ads'); const details=makeElement('div','vm-garso-profile-column'); const compact=topicView && SETTINGS.showCompactSummaryAboveDescription !== false ? createEscortCompactSummaryBox(false):null; const caption=makeElement('a','vm-garso-visited-label vm-helper-ui'); if (compact) { Object.assign(caption,{target:'_blank',rel:'noopener noreferrer'}); details.append(caption,compact.box); } details.appendChild(profile); remaining.append(otherTiles); box.append(visited,remaining,details); let fetched=[],searchToken=null,requestedKey='',completedKey='',compactSignature=''; let topicAds=[],linksToken=null,linksKey='',linksFrame=0; const cache=new Map(),cacheToken=createOperationCancelToken('Garso - dane zapisanych anonsów'); const updateSummary=() => {if (box.isConnected && box._vmState) box._vmUpdate(box._vmState);}; if (topicView) window.addEventListener('vm-garso-topic-summary-updated',updateSummary); box._vmCancel=() => { searchToken?.cancel('Opuszczono temat'); linksToken?.cancel('Opuszczono temat'); cacheToken.cancel('Opuszczono temat'); cancelAnimationFrame(linksFrame); compact?.popularMentionsResizeObserver?.disconnect(); window.removeEventListener('vm-garso-topic-summary-updated',updateSummary); }; const loadCachedAds=ads => { const byHost=new Map(); for (const ad of ads) { const id=String(ad.adId),host=getEscortCachedAdHost(id); if (!host || cache.has(id)) continue; cache.set(id,null); if (!byHost.has(host)) byHost.set(host,[]); byHost.get(host).push(id); } for (const [host,adIds] of byHost) { void requestPersistentCacheBridgeAction('read-ad-data',{host,adIds,cancelToken:cacheToken}) .then(response => { cacheToken.throwIfCancelled(); for (const value of response.ads || []) { const id=String(value.adData?.adId); if (adIds.includes(id)) cache.set(id,value); } updateSummary(); }).catch(error => { for (const id of adIds) if (cache.get(id) === null) cache.delete(id); if (!cacheToken.cancelled) log('Nie udało się odczytać danych anonsów z cache',error); }); } }; const enrich=ad => { const data=cache.get(String(ad.adId))?.adData; if (!data) return ad; const saved=makeGarsoAdTileData(ad.adId,data); const result={...ad,visitedAt:Math.max(ad.visitedAt || 0,saved.visitedAt)}; for (const key of ['title','imageUrl','locationText','priceText','profileStats','profileStatsText']) { result[key]=(ad.cached && saved[key]) || ad[key] || saved[key]; } if (!result.profileStats?.text && saved.profileStats?.text) result.profileStats=saved.profileStats; return result; }; const searchInBackground=async (phones,key) => { const token=createOperationCancelToken('Garso - nieodwiedzone anonse'); searchToken=token; requestedKey=key; try { const results=await findGarsoPhoneAdTiles(phones,token); token.throwIfCancelled(); if (!box.isConnected || JSON.stringify([...box._vmState.phones].sort()) !== key) return; fetched=results; completedKey=key; box._vmUpdate(box._vmState); } catch (error) { if (!token.cancelled) log('Nie udało się wyszukać anonsów tematu Garso',error); } finally { if (searchToken === token) searchToken=null; } }; add.addEventListener('click',async event => { stopUiEvent(event); if (add.disabled || !box._vmState) return; add.disabled=true; try { const state=box._vmState; const phoneDigits=state.phones.length === 1 ? state.phones[0]:await chooseWatchOptionDialog({ id:'vm-garso-profile-phone',title:'Wybierz numer profilu',info:'Temat zawiera kilka numerów telefonu.', choices:state.phones.map(phone => [phone,formatEscortPhoneForClipboard(phone,'local-hyphens')]) }); if (!phoneDigits) return; const shownModes=state.items.filter(item => item.sourceType !== 'escort-ad' && normalizePhoneReputationPhone(item.phoneDigits)?.local === phoneDigits).map(item => item.resolutionMode); const mode=await chooseFavoriteProfileResolutionMode(shownModes,['phone-escorti','phone-escort-search']); if (!mode) return; status.textContent='Wyszukiwanie profilu…'; const ads=state.ads.filter(ad => ad.phoneDigits === phoneDigits),seed=ads[0]; let context={ id:'garso-profile-preview',sourceType:'phone',profileUrls:[],resolvedAdUrls:ads.map(ad => ad.url), adUrl:seed?.url || null,phoneDigits,displayName:extractGarsoTopicTitle(document,`Profil numeru ${phoneDigits}`), imageUrl:seed?.imageUrl || '',watchKey:`phone:${phoneDigits}`,resolutionMode:mode, favorite:false,hidden:false,note:'',categories:[],modes:{},keepProfile:true }; const resolved=await resolveWatchedProfileData(normalizeWatchedItem(context)); if (!box.isConnected || !box._vmState.phones.includes(phoneDigits) || !getWatchSettings().enabled) return; const resolvedUrls=[...new Set([...ads.map(ad => ad.url),...(resolved.adUrls || [])])]; const profileIds=(resolved.profileUrls || []).map(getEscortiProfileId).filter(Boolean).sort((a,b) => Number(a) - Number(b)); context={...context, sourceType:mode === 'phone-escorti' && profileIds.length ? 'escorti-profile':'phone', profileUrls:resolved.profileUrls || [],resolvedAdUrls:resolvedUrls,adUrl:seed?.url || resolvedUrls[0] || null, displayName:resolved.displayName || context.displayName,imageUrl:resolved.imageUrl || context.imageUrl, watchKey:profileIds.length ? `profiles:${profileIds.join(',')}`:`phone:${phoneDigits}` }; delete context.id; const existing=getWatchedItems().find(item => item.sourceType !== 'escort-ad' && watchedItemsOverlap(item,context)); saveFavoriteItemFromContext(context,{favorite:existing?.favorite === true},'person'); status.textContent=''; } catch (error) {status.textContent=error?.message || 'Nie udało się dodać profilu.';} finally {add.disabled=false;} }); box._vmUpdate=state => { box._vmState=state; profile.hidden=!state.showProfiles || (!state.items.length && !state.canAdd); footer.hidden=!state.canAdd; if (!profile.hidden) render(state.items); const key=JSON.stringify([...state.phones].sort()); if (topicView && state.phones.length && requestedKey !== key) { searchToken?.cancel('Zmieniono numer tematu'); fetched=[]; completedKey=''; void searchInBackground([...state.phones],key); } const candidates=completedKey === key ? fetched:(state.otherAds || []); const visits=new Map(state.ads.map(ad => [String(ad.adId),enrich(ad)])),others=new Map(); const addTile=(candidate,fromTopic=false) => { const ad=enrich(candidate),id=String(ad.adId); if (visits.has(id) || (!fromTopic && !state.phones.includes(ad.phoneDigits))) return; const visitedAt=Math.max(ad.visitedAt || 0,Number(getWatchedAdDataMirror(id)?.lastVisitedAt) || 0); if (visitedAt) visits.set(id,{...ad,visitedAt}); else others.set(id,ad); }; for (const candidate of candidates) addTile(candidate); const fromTopic=topicView && !visits.size && !others.size && (!state.phones.length || completedKey === key); const urls=fromTopic && document.getElementById('vm-garso-topic-summary')?._vmReferences?.adUrls; const nextLinksKey=Array.isArray(urls) ? JSON.stringify([key,urls]):''; if (nextLinksKey !== linksKey) { linksToken?.cancel('Zmieniono źródło kafelków'); linksToken=null; topicAds=[]; linksKey=nextLinksKey; if (urls?.length) { const token=linksToken=createOperationCancelToken('Garso - anonse z linków w temacie'); void findGarsoLinkedAdTiles(urls,token,ad => { if (!token.cancelled && box.isConnected && linksToken === token) { topicAds.push(ad); if (!linksFrame) linksFrame=requestAnimationFrame(() => {linksFrame=0; updateSummary();}); } }).catch(error => {if (!token.cancelled) log('Nie udało się sprawdzić linków tematu Garso',error);}); } } if (fromTopic) for (const ad of topicAds) addTile(ad,true); if (topicView) loadCachedAds([...visits.values(),...others.values()]); notice.hidden=!fromTopic || (!visits.size && !others.size); if (!notice.hidden) notice.replaceChildren( state.phones.length ? 'Nie znaleziono ogłoszeń online z tym numerem telefonu. ' : 'Nie udało się ustalić numeru telefonu. ', makeElement(state.phones.length ? 'span':'strong','','Wyświetlane są anonse podane w postach'), ' - dostępne online lub z cache.'); const visitAds=[...visits.values()].sort((a,b) => b.visitedAt - a.visitedAt || Number(b.adId) - Number(a.adId)); renderGarsoVisitedAds(visited,visitAds); remaining.hidden=!topicView || !others.size; if (!remaining.hidden) renderGarsoVisitedAds(otherTiles,[...others.values()],fromTopic ? 'Nieodwiedzone anonse z tematu':'Nieodwiedzone anonse z tym numerem telefonu'); if (compact) { const seed=visitAds[0] || [...others.values()].sort((a,b) => Number(b.adId) - Number(a.adId))[0]; const saved=seed && cache.get(String(seed.adId)); const topicSummary=document.getElementById('vm-garso-topic-summary')?._vmSummary; caption.hidden=compact.box.hidden=!seed && !topicSummary; const facts={...saved?.facts}; if (topicSummary) facts.garsoPhoneSummaryResult=facts.garsoAddressSummaryResult=topicSummary; const data=saved?.adData || (seed && getWatchedAdDataMirror(seed.adId)); if (!facts.photoRange && data?.oldestPhotoDate) facts.photoRange={oldest:data.oldestPhotoDate}; const signature=JSON.stringify([seed?.adId,seed?.url,facts,data?.tags,data?.checkedAt]); if (!compact.box.hidden && signature !== compactSignature) { compactSignature=signature; caption.textContent=`Skrócone podsumowanie${seed ? ` [${seed.adId}]`:''}`; if (seed) caption.href=seed.url; else caption.removeAttribute('href'); caption.title=data?.checkedAt ? `Dane anonsu z cache: ${formatWatchDateTime(data.checkedAt)}`:''; renderEscortCompactSummary(compact,facts,data?.tags?.length || 0,'Brak zapisanych danych do podsumowania'); } } details.hidden=profile.hidden && (!compact || compact.box.hidden); box.hidden=visited.hidden && remaining.hidden && details.hidden; }; return box; } let garsoTrackedIndex=null; function getGarsoTrackedIndex() { const mirror=readWatchedAdDataMirror(); if (watchedItemsReadCache && garsoTrackedIndex?.itemsCache === watchedItemsReadCache && garsoTrackedIndex.mirror === mirror) return garsoTrackedIndex; const byPhone=new Map(),byAd=new Map(); const add=(map,key,item) => {if (key) {if (!map.has(key)) map.set(key,new Set()); map.get(key).add(item);}}; const phoneKey=value => normalizePhoneReputationPhone(value)?.local || ''; for (const item of getWatchedItems()) { add(byPhone,phoneKey(item.phoneDigits),item); for (const url of getWatchedItemAdUrls(item)) { const id=parseAdIdFromUrl(url); add(byAd,id,item); if (id) add(byPhone,phoneKey(getEscortAdPhoneDigits(mirror[id])),item); } } return garsoTrackedIndex={itemsCache:watchedItemsReadCache,mirror,byPhone,byAd}; } async function initGarsoTrackedTopics() { if (!GARSO_MENU_ONLY_MODE || window.top !== window.self || getSettings().garsoShowTopicHelperData === false || document.getElementById('vm-garso-tracked-topics-style')) return; ensureTrackedNoteCategoryStyles(); const root=document.getElementById('content') || document.body; if (!root) return; const className='vm-garso-tracked-topics',context=getGarsoTopicPageContext(); const ignored='.vm-helper-ui,[id^="vm-"],.vm-garso-inline-tools,.' + className; const postContent='.post_block,[id^="post_id_"],.post_wrap,.post_body,.post,.ipsComment,[data-role="commentContent"]'; appendHelperStyle('vm-garso-tracked-topics-style',` .${className} { --vm-ui-panel-text:var(--vm-ui-text,#222); --vm-ui-panel-muted:var(--vm-ui-text-muted,#666); display:block; max-width:100%; margin:5px 0; font:11px/1.3 var(--vm-ui-font-family,Arial,sans-serif); text-align:left; } .${className} [hidden], .${className}[hidden] { display:none !important; } .${className}.vm-garso-topic-helper-card { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr)); align-items:start; width:100%; max-width:100%; gap:12px; box-sizing:border-box; margin:12px 0; padding:10px; border:1px solid var(--vm-ui-border,#cec4c6); border-radius:7px; background:var(--vm-ui-bg,#fff); color:var(--vm-ui-panel-text); } .vm-garso-profile-column { min-width:0; } .vm-garso-profile-column > #vm-escort-compact-summary { margin:0 0 8px; } .vm-garso-profile-column > @vlabel { display:block; margin-bottom:6px; } .vm-garso-unvisited-ads { display:grid; align-content:start; gap:8px; min-width:0; } @vads { display:flex; flex-direction:column; align-items:flex-start; gap:6px; min-width:0; color:var(--vm-ui-panel-muted); font:11px/1.3 var(--vm-ui-font-family,Arial,sans-serif); text-align:left; } @vlabel { font-weight:700; color:var(--vm-ui-panel-text); } @vgrid { display:flex; flex-wrap:wrap; align-items:flex-start; gap:7px; width:100%; } @vcard { flex:0 1 100%; width:100%; max-width:100%; display:grid; grid-template-columns:54px minmax(0,1fr); gap:7px; min-height:68px; padding:5px; box-sizing:border-box; border:1px solid var(--vm-ui-border,#d8d8dc); border-radius:6px; background:var(--vm-ui-bg-alt,#fafafa); } @vthumb { display:grid; place-items:center; width:54px; height:68px; border-radius:4px; object-fit:cover; background:var(--vm-ui-bg-hover,#eee); } @vbody { min-width:0; display:flex; flex-direction:column; align-items:flex-start; gap:4px; } @vtitle-row { display:flex; align-items:center; flex-wrap:nowrap; gap:2px; min-width:0; width:100%; overflow:hidden; } @vtitle-row > a { display:flex; align-items:center; gap:3px; flex:0 1 auto; min-width:0; overflow:hidden; white-space:nowrap; overflow-wrap:normal; } @vtitle-text { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } @vtitle-id { flex:0 0 auto; white-space:nowrap; } @vtitle-row > .vm-garso-inline-tools { flex:0 0 auto; margin-left:1px; } @vcard a { color:var(--vm-ui-accent,#b52d77); font-weight:700; text-decoration:none; overflow-wrap:anywhere; } @vcard a:hover { text-decoration:underline; } @vlocation, @vstats, @vprice, @vwhen { color:var(--vm-ui-panel-muted); font-size:10px; } @vwhen { grid-column:1 / -1; grid-row:2; justify-self:start; min-width:0; text-align:left; } `.replaceAll('@v','.vm-garso-visited-')); let topicSource=null; if (context) { try {topicSource=await getGarsoCurrentTopicSource();} catch (error) { log('Nie udało się odczytać danych pierwszego posta Garso',error); topicSource={phones:getGarsoTopicMatchPhones(document,extractGarsoTopicTitle(document),context),adUrls:[]}; } } const phoneKey=value => normalizePhoneReputationPhone(value)?.local || ''; const boxes=new Map(); let sourceCache=new WeakMap(); const render=() => { const used=new Set(),settings=getWatchSettings(),show=getSettings().garsoShowTopicHelperData !== false; const showProfiles=show && settings.enabled,showVisited=show && !!context; const visibleItem=item => settings.enabled && (hasFavoriteStar(item) || item.hidden || item.categories.length || watchedItemHasActiveAlerts(item) || item.note); if (showProfiles || showVisited) { const {mirror,byPhone,byAd}=getGarsoTrackedIndex(); const decorate=(target,sources,topicView=false) => { if (used.has(target)) return; let data=topicView ? topicSource:sourceCache.get(target); if (!data) {data=readGarsoMatchSource(sources); sourceCache.set(target,data);} const adUrls=data?.adUrls || [],matches=new Set(); const tokens=topicView ? data?.phones || []:findGarsoPhoneTokens(data?.text || ''); const phones=[...new Set(tokens.filter(t => !/xxx$/i.test(t.value)).map(t => phoneKey(t.value)).filter(Boolean))]; const addMatches=(map,key) => {for (const item of map.get(key) || []) matches.add(item);}; for (const adUrl of adUrls) addMatches(byAd,parseAdIdFromUrl(adUrl)); for (const phone of phones) addMatches(byPhone,phone); if (!phones.length && topicView) { for (const item of matches) {const p=phoneKey(item.phoneDigits); if (p && !phones.includes(p)) phones.push(p);} for (const adUrl of adUrls) {const p=phoneKey(getEscortAdPhoneDigits(mirror[parseAdIdFromUrl(adUrl)])); if (p && !phones.includes(p)) phones.push(p);} } const visited=new Map(),otherAds=new Map(); const remember=(adId,value) => { if (!adId) return; const tile=makeGarsoAdTileData(adId,value); if (tile.visitedAt) visited.set(String(adId),tile); else if (phones.includes(tile.phoneDigits)) otherAds.set(String(adId),tile); }; if (showVisited && topicView) { const linked=new Set(adUrls.map(parseAdIdFromUrl).filter(Boolean)),events=getWatchEvents(); for (const item of matches) if (item.sourceType !== 'escort-search') { for (const adUrl of getWatchedItemVisitedAdUrls(item,mirror,events)) linked.add(parseAdIdFromUrl(adUrl)); } for (const [id,value] of Object.entries(mirror)) { if (linked.has(id) || (value?.status === 'ok' && phones.includes(phoneKey(getEscortAdPhoneDigits(value))))) remember(id,value); } } const items=[...matches].filter(item => (topicView && item.keepProfile && item.sourceType !== 'escort-ad') || visibleItem(item)); const canAdd=showProfiles && topicView && phones.length > 0; if ((!showProfiles || (!items.length && !canAdd)) && !visited.size && !otherAds.size && !(showVisited && topicView)) return; used.add(target); let box=boxes.get(target); if (!box?.isConnected) { box=createGarsoTrackedBox(topicView); if (topicView) target.after(box); else target.appendChild(box); boxes.set(target,box); } box._vmUpdate({items,phones,canAdd,showProfiles,otherAds:[...otherAds.values()],ads:[...visited.values()].sort((a,b) => b.visitedAt - a.visitedAt || Number(b.adId) - Number(a.adId))}); }; if (context) { const heading=root.querySelector('h1[itemprop="name"],h1.ipsType_pagetitle,h1'); if (heading && root.querySelector('.post_block,[id^="post_id_"]')) { let header=heading.closest('.ipsPageHeader,.ipsBox_withphoto,.topic_header,.topic-header') || heading; if (header.querySelector(postContent)) header=heading; decorate(header,[],true); } } else for (const anchor of root.querySelectorAll('#forum_table a[href],.topic_list a[href],#search_results h3 a[href],#search_results h4 a[href],#search_results .ipsDataItem_title a[href]')) { if (anchor.closest(ignored) || !getGarsoTopicPageContext(anchor.href)) continue; if (!anchor.matches('.topic_title,[id^="tid_"]') && !anchor.closest('h3,h4,.ipsDataItem_title')) continue; const row=anchor.closest('tr,.ipsDataItem,li.search_topic,.search_topic'); if (!row || row.closest(postContent) || row.querySelector(postContent)) continue; const target=anchor.closest('td') || row; decorate(target,[target]); } } for (const [target,box] of boxes) if (!used.has(target)) { const picker=document.querySelector('.vm-favorites-category-picker'); if (picker?._vmAnchor && box.contains(picker._vmAnchor)) closeHelperDialog(picker); box._vmCancel?.(); detachHelperNode(box); boxes.delete(target); } }; let timer=null; const schedule=() => {if (timer === null) timer=setTimeout(() => {timer=null; try {render();} catch (error) {log('Błąd oznaczeń tematów Garso',error);}},120);}; const own=node => !!(node.nodeType === 1 ? node:node.parentElement)?.closest(ignored); const listSelector='#forum_table,.topic_list,#search_results'; const observer=new MutationObserver(mutations => { let changed=false; for (const m of mutations) { if (own(m.target) || (m.type === 'childList' && ![...m.addedNodes,...m.removedNodes].some(node => !own(node)))) continue; const node=m.target.nodeType === 1 ? m.target:m.target.parentElement; if (context) { if (node?.closest('h1') || (m.type === 'childList' && [...m.addedNodes,...m.removedNodes].some(n => n.nodeType === 1 && (n.matches('h1,.post_block,[id^="post_id_"]') || n.querySelector('h1,.post_block,[id^="post_id_"]'))))) changed=true; } else if (node?.closest(listSelector)) { const row=node.closest('tr,.ipsDataItem,li.search_topic,.search_topic'); if (row) {sourceCache.delete(row); for (const cell of row.querySelectorAll('td')) sourceCache.delete(cell);} else sourceCache=new WeakMap(); changed=true; } else if (m.type === 'childList' && [...m.addedNodes,...m.removedNodes].some(n => n.nodeType === 1 && (n.matches(listSelector) || n.querySelector(listSelector)))) {sourceCache=new WeakMap(); changed=true;} } if (changed) schedule(); }); observer.observe(root,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['href']}); hiddenUiRefreshers.add(schedule); let listener=null; if (typeof GM_addValueChangeListener === 'function') listener=GM_addValueChangeListener(WATCH_AD_DATA_MIRROR_STORAGE_KEY,schedule); window.addEventListener('pagehide',event => {for (const box of boxes.values()) box._vmCancel?.(); if (event.persisted) return; observer.disconnect(); clearTimeout(timer); hiddenUiRefreshers.delete(schedule); if (listener != null) GM_removeValueChangeListener(listener);}); render(); } function ensureInlineQuickSearchButtonStyle() { if (document.getElementById('vm-inline-quick-search-style')) return; appendHelperStyle('vm-inline-quick-search-style',` .vm-garso-inline-search { display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; box-sizing:border-box; width:19px; height:19px; min-width:19px; min-height:19px; margin:0; padding:1px; border:1px solid transparent; border-radius:3px; background:transparent; color:var(--vm-ui-text-muted,#777); cursor:pointer; line-height:1; box-shadow:none; flex:0 0 19px; } .vm-garso-inline-search:hover, .vm-garso-inline-search:focus-visible { border-color:currentColor; background:color-mix(in srgb,currentColor 9%,transparent); } .vm-garso-inline-search svg { width:14px; height:14px; pointer-events:none; } `); } function makeGarsoInlineSearchButton(value) { ensureInlineQuickSearchButtonStyle(); const button=makeButton('vm-garso-inline-search',null,{title:`Wstaw ${value} do podręcznej wyszukiwarki`}); button.setAttribute('aria-label',button.title); button.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg>'; button.addEventListener('click',event => { stopUiEvent(event); try { if (!document.getElementById(GLOBAL_QUICK_SEARCH_ID)) initGlobalQuickSearch(true); document.getElementById(GLOBAL_QUICK_SEARCH_ID)?._vmPrefill(value); } catch (error) { log('Błąd otwierania podręcznej wyszukiwarki z treści Garsoniery',error); showWatchToast('Nie udało się otworzyć podręcznej wyszukiwarki. Spróbuj ponownie.',true); } }); return button; } function initGarsoInlineTools() { if (!GARSO_MENU_ONLY_MODE || window.top !== window.self || !/^\/forum(?:\/|$)/i.test(location.pathname)) return; if (!document.body || document.getElementById('vm-garso-inline-tools-style')) return; const showStatuses=SETTINGS.garsoCheckEscortLinks !== false,showSearch=SETTINGS.showTopPhoneSearch !== false && SETTINGS.garsoShowSearchIcons !== false; const toolsClass='vm-garso-inline-tools'; const ignoredSelector='script,style,noscript,template,textarea,input,select,button,pre,code,svg,math,iframe,' + '[contenteditable]:not([contenteditable="false"]),[role="textbox"],[hidden],[aria-hidden="true"],' + '.cke,.cke_editable,.ipsEditor,.vm-helper-ui,[id^="vm-"],.' + toolsClass; const blockTags=new Set('BODY DIV P H1 H2 H3 H4 H5 H6 LI UL OL DL DT DD TD TH TR TABLE SECTION ARTICLE HEADER FOOTER MAIN NAV ASIDE BLOCKQUOTE FIGURE FIGCAPTION FORM FIELDSET BR HR'.split(' ')); const statusByUrl=new Map(); const queuedUrls=new Set(); const pendingRoots=new Set(); let scanTimer=null,checking=false; const style=makeElement('style'); style.id='vm-garso-inline-tools-style'; style.textContent=` :is(.post_block,[id^="post_id_"]) > :is(h3,.post_header), :is(.post_block,[id^="post_id_"]) > .post_wrap > :is(h3,.post_header) { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .${toolsClass} { display:inline-flex; align-items:center; gap:3px; margin:0 3px; vertical-align:baseline; white-space:nowrap; font:700 13px/1 var(--vm-ui-font-family,Arial,sans-serif); } .${toolsClass} .vm-garso-ad-online-status { display:inline-flex; align-items:center; gap:3px; font-size:16px; font-weight:900; } `; document.head.appendChild(style); if (!showStatuses && !showSearch) return; const isIgnored=node => !!(node?.nodeType === 1 ? node:node?.parentElement)?.closest(ignoredSelector); const nextTools=node => { let next=node.nextSibling; while (next?.nodeType === 3 && !next.textContent.trim()) next=next.nextSibling; return next?.classList?.contains(toolsClass) ? next:null; }; const adUrlFromLink=value => { try { const url=new URL(value,location.href); if (!/^https?:$/.test(url.protocol) || !isEscortClubHost(url.hostname) || !getEscortAdIdFromPath(url.pathname)) return null; return normalizeEscortiAdUrl(url.href); } catch {return null;} }; const findTokens=text => { const tokens=[],urlRanges=[]; for (const match of text.matchAll(GARSO_REFERENCE_URL_RE)) { urlRanges.push([match.index,match.index + match[0].length]); const value=match[0].replace(/[.,;:!?\)\]}]+$/,''); const adUrl=adUrlFromLink(/^https?:\/\//i.test(value) ? value:`https://${value}`); if (adUrl) tokens.push({start:match.index,end:match.index + value.length,value:/^https?:\/\//i.test(value) ? value:`https://${value}`,adUrl}); } if (!showSearch) return tokens; for (const match of findGarsoPhoneTokens(text)) { if (urlRanges.some(([from,to]) => match.start < to && match.end > from)) continue; tokens.push({...match,adUrl:null}); } return tokens.sort((a,b) => a.start - b.start); }; const showStatus=(url,result) => { const entry=statusByUrl.get(url); if (!entry) return; entry.result=result || null; for (const marker of entry.markers) { if (!marker.isConnected) {entry.markers.delete(marker); continue;} renderEscortAdLinkStatusMarker(marker,entry.result); } }; const checkStatuses=async () => { if (checking) return; checking=true; try { while (queuedUrls.size) { const urls=[...queuedUrls].filter(url => statusByUrl.get(url).result === undefined && [...statusByUrl.get(url).markers].some(marker => marker.isConnected)); queuedUrls.clear(); if (!urls.length) break; try { const summary=await scanActiveEscortAds(urls,progress => { const url=normalizeEscortiAdUrl(progress?.result?.url); if (url) showStatus(url,progress.result); },false,{activityOnly:true,useActivityCache:true,cacheActivity:true}); const results=new Map((summary?.results || []).map(result => [normalizeEscortiAdUrl(result.url),result])); for (const url of urls) if (statusByUrl.get(url).result === undefined) showStatus(url,results.get(url)); } catch { for (const url of urls) if (statusByUrl.get(url).result === undefined) showStatus(url,null); } } } finally {checking=false;} }; const createTools=token => { const tools=makeElement('span',toolsClass); tools.dataset.vmQuery=token.value; if (showStatuses && token.adUrl) { const marker=makeElement('span','vm-garso-ad-online-status','…'); marker.dataset.adUrl=token.adUrl; setControlTitle(marker,'Sprawdzanie aktywności anonsu'); marker.style.color='#9ca3af'; tools.appendChild(marker); let entry=statusByUrl.get(token.adUrl); if (!entry) {entry={markers:new Set(),result:undefined}; statusByUrl.set(token.adUrl,entry);} entry.markers.add(marker); if (entry.result !== undefined) renderEscortAdLinkStatusMarker(marker,entry.result); else queuedUrls.add(token.adUrl); } if (!showSearch) return tools; tools.appendChild(makeGarsoInlineSearchButton(token.value)); return tools; }; const scanRoot=root => { const oldTools=new Set(root.querySelectorAll('.' + toolsClass)); const used=new Set(); let parts=[],text=''; const flushText=() => { for (const token of findTokens(text).reverse()) { const part=parts.find(item => token.end > item.start && token.end <= item.end); if (!part) continue; const offset=token.end - part.start; let tools=offset === part.node.length ? nextTools(part.node):null; if (!tools || tools.dataset.vmQuery !== token.value) { tools=createTools(token); const range=document.createRange(); range.setStart(part.node,offset); range.collapse(true); range.insertNode(tools); } used.add(tools); } parts=[]; text=''; }; const visit=node => { if (node.nodeType === 3) { parts.push({node,start:text.length,end:text.length + node.length}); text += node.data; return; } if (node.nodeType !== 1) return; if (node.matches(ignoredSelector)) {flushText(); return;} if (node.tagName === 'A') { flushText(); const adUrl=adUrlFromLink(node.getAttribute('href')); const tokens=adUrl ? [{value:new URL(node.getAttribute('href'),location.href).href,adUrl}]:findTokens(node.textContent).filter(token => !token.adUrl); const seen=new Set(); let previous=node; for (const token of tokens) { if (seen.has(token.value)) continue; seen.add(token.value); let tools=nextTools(previous); if (!tools || tools.dataset.vmQuery !== token.value) { tools=createTools(token); previous.after(tools); const marker=tools.querySelector('.vm-garso-ad-online-status'),result=statusByUrl.get(token.adUrl)?.result; if (marker && result !== undefined) renderEscortAdLinkStatusMarker(marker,result); } used.add(tools); previous=tools; } return; } const block=blockTags.has(node.tagName); const inlineBoundary=node.matches?.('h1.ipsType_pagetitle > span.desc, h1[itemprop="name"] > span.desc'); if (block || inlineBoundary) flushText(); for (const child of [...node.childNodes]) visit(child); if (block || inlineBoundary) flushText(); }; visit(root); flushText(); for (const tools of oldTools) if (!used.has(tools)) detachHelperNode(tools); }; const queueRoot=node => { let root=node?.nodeType === 1 ? node:node?.parentElement; if (!root || isIgnored(root)) return; while (root !== document.body && root.parentElement && !blockTags.has(root.tagName)) root=root.parentElement; pendingRoots.add(root); if (scanTimer === null) scanTimer=setTimeout(flushRoots,120); }; const observer=new MutationObserver(mutations => { for (const mutation of mutations) { if (isIgnored(mutation.target)) continue; if (mutation.type === 'childList' && ![...mutation.addedNodes,...mutation.removedNodes].some(node => !isIgnored(node))) continue; queueRoot(mutation.target); } }); const observe=() => observer.observe(document.body,{ childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['href','hidden','aria-hidden'] }); function flushRoots() { scanTimer=null; const roots=[...pendingRoots].filter(root => root.isConnected && ![...pendingRoots].some(other => other !== root && other.contains(root))); pendingRoots.clear(); observer.disconnect(); try {for (const root of roots) scanRoot(root);} catch (error) {log('Błąd oznaczania telefonów i linków na Garsonierze',error);} finally {observe();} for (const entry of statusByUrl.values()) for (const marker of entry.markers) if (!marker.isConnected) entry.markers.delete(marker); void checkStatuses(); } pendingRoots.add(document.body); flushRoots(); } function getEscortClubSearchAdUrls(doc,pageUrl) { const context=getEscortClubListContext(doc,pageUrl); const anchors=context?.anchors || []; return [...new Set( anchors .map(anchor => normalizeEscortiAdUrl(anchor.getAttribute('href'),pageUrl)) .filter(Boolean) )]; } async function fetchEscortClubPhoneSearchAds(phone,cancelToken=null,requestContext=null) { requestContext ||= createRequestContext('escort.phone-search'); const firstUrl=buildEscortClubPhoneSearchUrl(phone); const found=new Set(); const seenPages=new Set(); let pageUrl=firstUrl; while (pageUrl && !seenPages.has(pageUrl) && seenPages.size < 100) { cancelToken?.throwIfCancelled(); seenPages.add(pageUrl); const {doc,finalUrl}=await fetchWatchDocumentDirect(pageUrl,cancelToken,{requestContext}); for (const url of getEscortClubSearchAdUrls(doc,finalUrl)) found.add(url); pageUrl=findNextEscortListPageUrl(doc,finalUrl); } return { searchUrl:firstUrl, adUrls:[...found] }; } function createResearchPanelResultRow(label,searchUrl) { const row=makeButton('vm-research-panel-row'); row.dataset.searchUrl=searchUrl; setButtonColor(row,'checking'); setTwoLineButton(row,label,'sprawdzam…'); row.addEventListener('click',event => { stopUiEvent(event); if (!row.dataset.searchUrl) return; GM_openInTab(row.dataset.searchUrl,{active:true,insert:true}); }); return row; } function getGarsoExtendedResultSignature(result) { if (result?.signature) return String(result.signature); const topicUrls=[...new Set( (result?.topics || []) .map(topic => normalizeTopicUrl(topic?.url)) .filter(Boolean) )].sort(); if (topicUrls.length && result?.topicLinksComplete !== false) return `topics:${topicUrls.join('|')}`; return `fallback:${String(result?.searchTerm || '')}|count:${ Number(result?.count) || 0 }|url:${normalizeGarsoResultPageUrl(result?.resultUrl) || ''}`; } function getGarsoExtendedPhoneTarget(phoneDigits,trailDepth=0) { const query=getGarsoSearchQuery(phoneDigits,'phone'); return {adUrl:null,searchTerm:query.term,andorType:query.andorType,phoneDigits,phoneSearchKind:'standard',trailDepth}; } function getCachedGarsoExtendedResults(targets,previous=[]) { const results=new Map(previous.map(result => [result.searchTerm,result])),counts=new Map(); for (const target of targets) { if (results.has(target.searchTerm)) continue; const mode=target.adUrl ? 'address':'phone',term=target.phoneDigits || target.searchTerm; const key=`${mode}:${term}`; if (!counts.has(key)) counts.set(key,getGarsoSearchCountCache(mode,term)); const cached=counts.get(key); if (!(cached?.count > 0)) continue; if (mode === 'phone' && cached.phoneSearchVariant !== 'both') continue; results.set(target.searchTerm,{ ...target,status:'ok',count:cached.count, topics:cached.topicLinks }); } return [...results.values()]; } function getUniqueGarsoResultPages(results) { const seenResults=new Set(); const uniqueResults=[]; for (const item of results || []) { if (item?.status !== 'ok' || Number(item.count) <= 0) continue; const signature=getGarsoExtendedResultSignature(item); if (seenResults.has(signature)) continue; seenResults.add(signature); uniqueResults.push(item); } return uniqueResults; } function formatGarsoUniquePageCount(count) { return formatPolishCount(count,'unikalna strona','unikalne strony','unikalnych stron'); } async function openUniqueGarsoResultPages(results) { const uniqueResults=getUniqueGarsoResultPages(results); if (!uniqueResults.length) return 0; const auth=await getGarsoAuth(); for (const result of uniqueResults) { await submitGarsoSearchFormToNewTab(result.searchTerm,auth.secureHash,auth.sessionId,{active:false,andorType:result.andorType}); } return uniqueResults.length; } function summarizeActiveAdsSubset(summary,adUrls) { const wanted=new Set( (adUrls || []).map(url => normalizeEscortiAdUrl(url) || url) ); return summarizeActiveAdResults( (summary?.results || []).filter(item => wanted.has(normalizeEscortiAdUrl(item.url) || item.url) ) ); } function normalizedEscortiResultSet(result,type) { const values=type === 'profiles' ? normalizeProfileUrls(result) :(Array.isArray(result?.adUrls) ? result.adUrls:[]); return [...new Set(values.map(value => type === 'profiles' ? (getEscortiProfileId(value) || value) :(parseAdIdFromUrl(value) || value) ).filter(Boolean))].sort(); } function areSameEscortiResults(first,second) { if (first?.status !== 'ok' || second?.status !== 'ok') return false; return ['profiles','ads'].every(type => { const a=normalizedEscortiResultSet(first,type); const b=normalizedEscortiResultSet(second,type); return a.length === b.length && a.every((value,index) => value === b[index]); }); } function activeEscortAdsWord(count) { return polishCountForm(count,'aktywny','aktywne','aktywnych'); } function getCompactSummaryAggregationSource(facts) { const mode=normalizeEscortCompactSummaryMode( SETTINGS.compactSummaryComparisonMode ); const sources={ 'address-escorti':{ mode, label:'adres anonsu → Escorti.pl', description: 'Biorę aktywne anonse Escort.club przypisane do profili znalezionych w Escorti.pl po adresie bieżącego anonsu.', result:facts?.escortiAddressResult, summary:facts?.escortiAddressSummary, loading:!!facts?.escortiLoading }, 'phone-escorti':{ mode, label:'nr tel. → Escorti.pl', description: 'Biorę aktywne anonse Escort.club przypisane do profili znalezionych w Escorti.pl po numerze telefonu bieżącego anonsu.', result:facts?.escortiPhoneResult, summary:facts?.escortiPhoneSummary, loading:!!facts?.escortiLoading }, 'exact-phone':{ mode, label:'nr tel. → wyszukiwarka Escort.club', description: 'Biorę aktualnie dostępne anonse zwrócone przez wyszukiwarkę Escort.club po numerze telefonu bieżącego anonsu.', result:null, summary:facts?.escortPhoneSummary, loading:!!facts?.escortPhoneLoading } }; return sources[mode] || sources['address-escorti']; } function buildCompactSummaryAggregationTooltip(source,maxAds=10) { const summary=source?.summary; const lines=[ `Sposób pozyskania anonsów: ${source?.label || 'brak danych'}.`, source?.description || '' ].filter(Boolean); const activeResults=(summary?.results || []).filter(item => item?.active); const activeCount=Number(summary?.activeCount) || activeResults.length; lines.push(`Do porównania używam tylko aktywnych anonsów (${activeCount}).`); if (!activeResults.length) { if (activeCount > 0) { lines.push( '', 'Nazwy profili/anonsów nie są dostępne w skróconych danych zapisanych w cache.' ); } return lines.join('\n'); } const descriptions=activeResults.map(item => { const adId=parseAdIdFromUrl(item.url); const name=normalizeEscortAdText(item.adData?.title) || (adId ? `Anons nr ${adId}`:'Anons bez nazwy'); const city=normalizeEscortCity(item.adData?.location?.city || item.city); return `${name}${city ? ` - ${city}`:''}${adId ? ` (nr ${adId})`:''}`; }); lines.push( '', 'Nazwy aktywnych profili/anonsów uwzględnionych w porównaniu:' ); lines.push(...descriptions.slice(0,maxAds).map(value => `• ${value}`)); if (descriptions.length > maxAds) { lines.push(`• …oraz ${descriptions.length - maxAds} kolejnych`); } return lines.join('\n'); } function formatEscortiAdsAndActivity(result,summary) { const total=Number.isFinite(Number(result?.adLinks)) ? Number(result.adLinks) :Number(summary?.totalChecked) || 0; const active=Number(summary?.activeCount) || 0; return `${total} ${polishAdWord(total)} (${active} ${activeEscortAdsWord(active)})`; } function formatResearchDurationDays(days) { const safeDays=Math.max(0,Math.round(Number(days) || 0)); if (safeDays < 31) return `${safeDays} dni`; const months=Math.floor(safeDays / 30.4375); if (months < 12) return `${months} mies.`; const years=Math.floor(months / 12); const restMonths=months % 12; return restMonths ? `${years} r. ${restMonths} mies.`:`${years} r.`; } function formatResearchDateAge(value) { const normalizedValue=String(value || '').match(/^\d{4}-\d{2}-\d{2}/)?.[0] || value; const timestamp=profileDateToTime(normalizedValue); const date=formatEscortiDate(normalizedValue); if (!date) return 'brak'; if (timestamp == null) return date; const days=Math.max(0,(Date.now() - timestamp) / 86400000); return `${date} (${formatResearchDurationDays(days)})`; } function getEscortiProfileSummariesForResearch(result) { if (Array.isArray(result?.profileSummaries) && result.profileSummaries.length) { return result.profileSummaries; } if (result?.status !== 'ok' || !result.profiles) return []; return [{ profileUrl:result.profileUrl || result.profileUrls?.[0] || null, profileName:result.profileName || null, creationDate:result.creationDate || null, currentCity:result.currentCity || null, adLinks:result.adLinks, cityHistory:Array.isArray(result.cityHistory) ? result.cityHistory:[] }]; } function getEscortGalleryPhotoUrls(sourceDocument=document) { const urls=new Set(); for (const anchor of sourceDocument.querySelectorAll( '.content-gallery-col .galleryContainer a.simple-zoom-image:not(.video-layer)' )) { const candidates=[ anchor.getAttribute('href'), anchor.querySelector('img')?.getAttribute('data-src'), anchor.querySelector('img')?.getAttribute('src') ]; const found=candidates.find(isEscortGalleryPhotoUrl); if (found) urls.add(new URL(found,location.href).href); } return [...urls]; } async function getEscortGalleryPhotoDateRange(sourceDocument=document) { const urls=getEscortGalleryPhotoUrls(sourceDocument); if (!urls.length) { return { total:0, dated:0, oldest:null, newest:null, dates:[], items:[] }; } const items=(await watchMapLimit(urls,4,async url => ({ url, date:await getMediaServerDate(url) }))) .filter(item => item.date instanceof Date && Number.isFinite(item.date.getTime()) ) .sort((a,b) => a.date.getTime() - b.date.getTime()); const dates=items.map(item => item.date); return { total:urls.length, dated:dates.length, oldest:dates[0] || null, newest:dates.at(-1) || null, dates, items }; } const ESCORT_RESEARCH_TIMELINE_CATEGORIES={ profile:{label:'profile Escorti',color:'#55c7ff'}, ad:{label:'anonse',color:'#ffb45c'}, garso:{label:'recenzje Garso',color:'#75db91'}, photo:{label:'zdjęcia',color:'#f54da3'}, location:{label:'lokalizacje Escorti',color:'#b99cff'} }; function getEscortTimelineReadableTextColor(baseColor) { return `color-mix(in srgb, ${baseColor} 65%, var(--vm-ui-panel-text, #fff))`; } function parseEscortResearchTimelineDate(value) { if (value instanceof Date && Number.isFinite(value.getTime())) { return new Date(value.getTime()); } if (Number.isFinite(Number(value)) && Number(value) > 0) { const date=new Date(Number(value)); return Number.isFinite(date.getTime()) ? date:null; } const normalizedValue=String(value || '').match(/^\d{4}-\d{2}-\d{2}/)?.[0] || formatEscortiDate(value); const timestamp=profileDateToTime(normalizedValue); if (timestamp != null) return new Date(timestamp); const fallback=Date.parse(String(value || '')); return Number.isFinite(fallback) ? new Date(fallback):null; } function getGarsoSummaryReviewDates(state) { const data=state?.dataset; return {first:data?.garsoFirstReviewDate || null,last:data?.garsoLastReviewDate || null, firstUrl:data?.garsoFirstReviewUrl || null,lastUrl:data?.garsoLastReviewUrl || null}; } function getEscortArchiveActivitySummary(adId,mode,result) { const urls=Array.isArray(result?.adUrls) ? result.adUrls:[]; const previous=getCachedEscortiActivitySummary(adId,mode,result ? urls:null); const results=urls.map(url => { const data=getStoredEscortAdData(parseAdIdFromUrl(url)),activity=readSharedEscortActivity(url,true); const pageCheckedAt=Number(data?.pageCheckedAt) || 0,newerPage=pageCheckedAt > Number(activity?.checkedAt || 0); const checkedAt=newerPage ? pageCheckedAt:activity?.checkedAt || Number(data?.checkedAt) || 0; return {url,active:newerPage || (activity ? activity.active:!!checkedAt),unknown:!checkedAt, checkedAt,stale:!checkedAt || Date.now() - checkedAt >= ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS, city:data?.location?.city || null,adData:data || null}; }); const summary=summarizeActiveAdResults(results),times=results.map(item => item.checkedAt).filter(Boolean); if (previous && (!times.length || (previous.checkedAt >= Math.max(...times) && (previous.activeCount !== summary.activeCount || previous.unknownCount !== summary.unknownCount)))) return {...previous,results:[], stale:!previous.checkedAt || Date.now() - previous.checkedAt >= ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS}; if (!result && !results.length) return null; return {...summary,checkedAt:times.length ? Math.min(...times):0,stale:summary.staleCount > 0}; } function renderEscortArchiveResearchRow(row,label,result,summary,availability=true) { const total=result?.adLinks != null ? Number(result.adLinks):Number(summary?.totalChecked); const known=Number.isFinite(total) && total >= 0; const active=Number(summary?.activeCount) || 0,unknown=Number(summary?.unknownCount) || 0; const complete=summary && Number(summary.totalChecked) === total && !unknown; const checkedAt=Number(availability ? summary?.checkedAt || result?.checkedAt:result?.checkedAt) || 0; const stale=!checkedAt || Date.now() - checkedAt >= ESCORT_SHARED_ACTIVITY_CACHE_TTL_MS || summary?.stale; let value=known ? `${total} ${polishAdWord(total)}`:'brak zapisanego wyniku'; if (known && availability && total > 0) value=complete ? formatEscortiAdsAndActivity(result,summary) :value + (active ? ` • online: ≥${active}`:' • online: b/d'); setButtonColor(row,!known || stale || (availability && total > 0 && !complete) ? 'stale':total > 0 ? 'found':'empty'); setTwoLineButton(row,label,value); row.title=`Wynik zapisany w cache${checkedAt ? `: ${formatEscortCachedArchiveDate(checkedAt)}`:''}.` + (availability && known && !complete && total > 0 ? ' Brak pełnego zapisanego wyniku dostępności.':''); } function getEscortArchiveResearchFactsFromCache( adId, adUrl, phone, garsoCombinedSummaryState, cachedGarsoPhone, cachedGarsoAddress, archiveData=null ) { const stored=archiveData || getStoredEscortAdData(adId) || {}; const phoneResult=getListCache(adId,'phone-escorti'); const addressResult=getListCache(adId,'address-escorti'); const escortPhoneResult=getListCache(adId,'phone-escort-search'); const normalizedPhone=normalizePhoneReputationPhone(phone); const garsoSummary= (!['phone','address'].includes(garsoCombinedSummaryState?.dataset?.garsoSearchMode) ? garsoCombinedSummaryState?._vmGarsoSummaryResult:null) || getGarsoSummaryCache('combined',buildCombinedGarsoSummaryTerm(phone,adUrl))?.result || null; const reviewDates=getGarsoSummaryReviewDates(garsoCombinedSummaryState); return { garsoPhoneCount:cachedGarsoPhone?.count ?? null, garsoAddressCount:cachedGarsoAddress?.count ?? null, escortiPhoneResult:phoneResult || null, escortiAddressResult:addressResult || null, escortiPhoneSummary:getEscortArchiveActivitySummary(adId,'phone-escorti',phoneResult), escortiAddressSummary:getEscortArchiveActivitySummary(adId,'address-escorti',addressResult), escortPhoneResult,escortPhoneSummary:getEscortArchiveActivitySummary(adId,'phone-escort-search',escortPhoneResult), sameEscortiResult: phoneResult && addressResult ? areSameEscortiResults(phoneResult,addressResult) :null, escortiWarnings:[], escortiLoading:false, escortPhoneLoading:false, photoRange:stored.oldestPhotoDate ? {oldest:stored.oldestPhotoDate}:null, photoDatesLoading:false, garsoPhoneSummaryResult:garsoSummary || getGarsoSummaryCache('phone',phone)?.result || null, garsoAddressSummaryResult:garsoSummary || getGarsoSummaryCache('address',adUrl)?.result || null, datePosted:stored.datePosted || null, currentAdUrl:adUrl, phoneReputationResult: SETTINGS.showPhoneReputationLinks && normalizedPhone ? getCachedPhoneReputation(normalizedPhone) :null, garsoPhoneReviewDates:{...reviewDates}, garsoAddressReviewDates:{...reviewDates}, savedAt:null }; } function collectEscortResearchTimelineEvents(facts) { const events=[]; const seen=new Set(); const add=( value, label, category, identity='', url='', source='' ) => { const date=parseEscortResearchTimelineDate(value); if (!date || !ESCORT_RESEARCH_TIMELINE_CATEGORIES[category]) return; const key=`${category}|${identity || label}|${date.toISOString().slice(0,10)}`; if (seen.has(key)) return; seen.add(key); events.push({ date, label, category, url:String(url || ''), source:String(source || '') }); }; add( facts?.datePosted, isEscortCachedArchiveRuntime() ? 'Publikacja bieżącego anonsu Escort.club' :'Włączenie bieżącego anonsu Escort.club', 'ad', `current:${facts?.currentAdUrl || location.pathname}`, facts?.currentAdUrl || location.href ); const resultPairs=[ ['adres',facts?.escortiAddressResult], ['telefon',facts?.escortiPhoneResult] ]; for (const [,result] of resultPairs) { for (const profile of getEscortiProfileSummariesForResearch(result)) { const profileIdentity=profile.profileUrl || profile.profileName || 'profil'; add( profile.creationDate, `Założenie profilu Escorti${profile.profileName ? `: ${profile.profileName}`:''}`, 'profile', profileIdentity, profile.profileUrl ); for (const entry of Array.isArray(profile.cityHistory) ? profile.cityHistory:[]) { add( entry?.date, `Lokalizacja Escorti${entry?.city ? `: ${entry.city}`:''}`, 'location', `${profileIdentity}:${entry?.city || ''}`, profile.profileUrl ); } } } const activeResultSources=[ { source:'escorti', label:'Dodanie aktywnego anonsu znalezionego przez Escorti.pl', results:[ ...(facts?.escortiAddressSummary?.results || []), ...(facts?.escortiPhoneSummary?.results || []) ] }, { source:'escort-phone', label:'Dodanie aktywnego anonsu Escort.club z tym samym nr tel.', results:facts?.escortPhoneSummary?.results || [] } ]; for (const activeSource of activeResultSources) { for (const item of activeSource.results) { if (!item?.active || !item.adData?.datePosted) continue; const adId=parseAdIdFromUrl(item.url); add( item.adData.datePosted, `${activeSource.label}${adId ? ` nr ${adId}`:''}`, 'ad', `${activeSource.source}:${item.url || adId || item.adData.datePosted}`, item.url, activeSource.source ); } } const reviewSources=[ ['nr tel. + adres anonsu', facts?.garsoPhoneReviewDates || facts?.garsoAddressReviewDates] ]; for (const [source,dates] of reviewSources) { add( dates?.first, `Pierwsza recenzja Garso (${source})`, 'garso', `first:${source}`, dates?.firstUrl ); add( dates?.last, `Ostatnia recenzja Garso (${source})`, 'garso', `last:${source}`, dates?.lastUrl ); } const photoItems=Array.isArray(facts?.photoRange?.items) ? facts.photoRange.items :(facts?.photoRange?.dates || []).map(date => ({date,url:''})); for (let index=0;index<photoItems.length;index++) { const item=photoItems[index]; add( item?.date, 'Zdjęcie – data pliku na serwerze', 'photo', `photo:${index}`, item?.url ); } return events.sort((a,b) => a.date.getTime() - b.date.getTime()); } function makeEscortSummarySectionCollapsible( section, header, content, storageSuffix, defaultExpanded=true, persistState=true ) { if (!section || !header || header.dataset.vmCollapsibleReady === '1') return null; const contentNodes=(Array.isArray(content) ? content:[content]).filter(Boolean); if (!contentNodes.length) return null; header.dataset.vmCollapsibleReady='1'; const storageKey=`vm_escort_summary_section_expanded_${storageSuffix}`; let expanded=persistState ? !!GM_getValue(storageKey,defaultExpanded) :!!defaultExpanded; const originalMarginBottom=header.style.marginBottom || ''; const toggleHint=makeElement('span','vm-escort-summary-collapse-hint'); toggleHint.setAttribute('aria-hidden','true'); const hintText=makeElement('span'); const arrow=makeElement('span'); arrow.setAttribute('aria-hidden','true'); toggleHint.append(hintText,arrow); header.appendChild(toggleHint); header.classList.add('vm-escort-summary-collapsible-header'); header.setAttribute('role','button'); header.setAttribute('tabindex','0'); const apply=() => { for (const node of contentNodes) node.hidden=!expanded; hintText.textContent=expanded ? 'Zwiń':'Rozwiń'; arrow.textContent=expanded ? '▾':'▸'; section.classList.toggle( 'vm-escort-summary-section-collapsed', !expanded ); header.style.marginBottom=expanded ? originalMarginBottom:'0'; header.setAttribute('aria-expanded',String(expanded)); header.title=expanded ? 'Kliknij, aby zwinąć sekcję':'Kliknij, aby rozwinąć sekcję'; }; const setExpanded=nextExpanded => { expanded=!!nextExpanded; if (persistState) writeHelperStorageValue(storageKey,expanded); apply(); }; const toggle=() => setExpanded(!expanded); header.addEventListener('click',event => { if (event.target.closest('a, button, input, select, textarea')) return; toggle(); }); header.addEventListener('keydown',event => { if (event.target !== header || (event.key !== 'Enter' && event.key !== ' ')) return; event.preventDefault(); toggle(); }); apply(); return { setExpanded, isExpanded:() => expanded }; } function makeEscortTimelineElementClickable(element,url,titleText) { let targetUrl=null; try { const parsed=new URL(String(url || ''),location.href); if (['http:','https:'].includes(parsed.protocol)) targetUrl=parsed.href; } catch {} if (!element || !targetUrl) return false; element.setAttribute('role','link'); element.setAttribute('tabindex','0'); element.title=titleText || 'Otwórz stronę źródłową'; element.classList.add('vm-summary-source-link'); element.addEventListener('mouseenter',() => { element.style.filter='brightness(1.12)'; }); element.addEventListener('mouseleave',() => { element.style.filter=''; }); const open=event => { stopUiEvent(event); GM_openInTab(targetUrl,{ active:true, insert:true, setParent:true }); }; element.addEventListener('click',open); element.addEventListener('keydown',event => { if (event.key !== 'Enter' && event.key !== ' ') return; open(event); }); return true; } function formatEscortResearchTimelineAge(date) { const days=Math.max(0,(Date.now() - date.getTime()) / 86400000); return `${formatResearchDurationDays(days)} temu`; } function createEscortResearchDateTimeline(facts) { const box=makeElement('section','vm-escort-summary-integrated-section'); const title=makeElement('div','vm-summary-timeline-title'); const titleText=makeElement('span','','Oś czasu i zgodność dat'); const titleWarning=makeElement('span','vm-summary-timeline-title-warning','⚠︎',{hidden:true}); titleWarning.setAttribute('aria-label','Ostrzeżenia na osi czasu'); title.append(titleText,titleWarning); box.appendChild(title); const timelineHeaderWarnings=[]; const addTimelineHeaderWarning=(message,color,priority) => { timelineHeaderWarnings.push({message,color,priority}); const strongest=[...timelineHeaderWarnings] .sort((a,b) => b.priority - a.priority)[0]; titleWarning.hidden=false; titleWarning.style.color=strongest.color; titleWarning.title=timelineHeaderWarnings .map(item => item.message) .join('\n'); }; const events=collectEscortResearchTimelineEvents(facts); const photos=events.filter(event => event.category === 'photo'); const profiles=events.filter(event => event.category === 'profile'); const currentAds=events.filter(event => event.label === 'Włączenie bieżącego anonsu Escort.club' || event.label === 'Publikacja bieżącego anonsu Escort.club' ); const escortiActiveAds=events.filter(event => event.source === 'escorti'); const escortPhoneActiveAds=events.filter(event => event.source === 'escort-phone' ); const garsoReviews=events.filter(event => event.category === 'garso'); const locations=events.filter(event => event.category === 'location'); const oldestPhoto=photos[0] || null; const newestPhoto=photos.at(-1) || null; const oldestProfile=profiles[0] || null; const newestProfile=profiles.at(-1) || null; const currentAd=currentAds[0] || null; const keyDates=[ { label:'Najstarsze zdjęcie', event:oldestPhoto, color:ESCORT_RESEARCH_TIMELINE_CATEGORIES.photo.color }, { label:'Najstarszy profil', event:oldestProfile, color:ESCORT_RESEARCH_TIMELINE_CATEGORIES.profile.color }, { label:'Ten anons', event:currentAd, color:ESCORT_RESEARCH_TIMELINE_CATEGORIES.ad.color } ]; const keyGrid=makeElement('div','vm-summary-key-dates'); for (const item of keyDates) { const card=makeElement('div','vm-summary-key-date-card'); card.style.border= `1px solid ${item.event ? item.color:'rgba(255,255,255,.14)'}`; const label=makeElement('div','vm-summary-key-date-label',item.label); label.style.color=item.event ? getEscortTimelineReadableTextColor(item.color) :'var(--vm-ui-panel-muted, #a997ae)'; const date=makeElement('div','vm-summary-key-date-value'); date.textContent=item.event ? formatServerMediaDate(item.event.date) :(facts?.escortiLoading || facts?.photoDatesLoading ? '…':'brak'); const age=makeElement('div','vm-summary-key-date-age',item.event ? formatEscortResearchTimelineAge(item.event.date):''); card.append(label,date,age); makeEscortTimelineElementClickable( card, item.event?.url, `Otwórz źródło: ${item.label}` ); keyGrid.appendChild(card); } box.appendChild(keyGrid); const threeYearsAgo=new Date(); threeYearsAgo.setFullYear(threeYearsAgo.getFullYear() - 3); if (newestPhoto && newestPhoto.date.getTime() < threeYearsAgo.getTime()) { addTimelineHeaderWarning( 'Wszystkie zdjęcia mają daty plików starsze niż 3 lata.', '#ff8b8b', 2 ); const oldPhotosWarning=makeElement('div','vm-summary-old-photos-warning',`⚠ Wszystkie zdjęcia mają daty plików starsze niż 3 lata. ` + `Najnowsze: ${formatServerMediaDate(newestPhoto.date)} ` + `(${formatEscortResearchTimelineAge(newestPhoto.date)}).`); makeEscortTimelineElementClickable( oldPhotosWarning, newestPhoto.url, 'Otwórz najnowsze zdjęcie' ); box.appendChild(oldPhotosWarning); } if (oldestPhoto && oldestProfile) { const differenceDays= (oldestProfile.date.getTime() - oldestPhoto.date.getTime()) / 86400000; const comparison=makeElement('div'); const mismatch=differenceDays > 30; comparison.className= `vm-summary-date-comparison ${mismatch ? '-mismatch':'-ok'}`; if (mismatch) { addTimelineHeaderWarning( 'Data najstarszego zdjęcia jest wcześniejsza niż założenie najstarszego profilu Escorti.', '#ffb08e', 1 ); } comparison.textContent=differenceDays > 0 ? `${mismatch ? '⚠ ':''}Najstarsze zdjęcie ma datę pliku o ${formatResearchDurationDays(differenceDays)} wcześniejszą niż założenie najstarszego profilu Escorti.` :'Daty plików zdjęć nie są wcześniejsze niż data założenia najstarszego profilu Escorti.'; makeEscortTimelineElementClickable( comparison, oldestPhoto.url, 'Otwórz najstarsze zdjęcie użyte w porównaniu' ); box.appendChild(comparison); } const timelineItems=[]; const timelineSeen=new Set(); const addTimelineItem=(event,label,detail='') => { if (!event) return; const key=`${label}|${event.date.toISOString().slice(0,10)}`; if (timelineSeen.has(key)) return; timelineSeen.add(key); timelineItems.push({...event,label,detail}); }; if (oldestPhoto) { addTimelineItem( oldestPhoto, photos.length === 1 ? 'Zdjęcie – data pliku':'Najstarsze zdjęcie – data pliku', `${photos.length} ${photos.length === 1 ? 'zdjęcie z datą':'zdjęć z datami'}` ); } if (newestPhoto && newestPhoto.date.getTime() !== oldestPhoto?.date.getTime()) { addTimelineItem(newestPhoto,'Najnowsze zdjęcie – data pliku'); } if (oldestProfile) { addTimelineItem( oldestProfile, profiles.length > 1 ? 'Założenie najstarszego profilu Escorti':'Założenie profilu Escorti', profiles.length > 1 ? `${profiles.length} znalezione profile`:'' ); } if (newestProfile && newestProfile.date.getTime() !== oldestProfile?.date.getTime()) { addTimelineItem(newestProfile,'Założenie najnowszego profilu Escorti'); } addTimelineItem( currentAd, isEscortCachedArchiveRuntime() ? 'Publikacja bieżącego anonsu Escort.club' :'Włączenie bieżącego anonsu Escort.club' ); addTimelineItem( escortiActiveAds[0], 'Najstarszy aktywny anons z Escorti.pl' ); addTimelineItem( escortPhoneActiveAds[0], 'Najstarszy aktywny anons Escort.club z tym samym nr tel.' ); addTimelineItem(garsoReviews[0],'Pierwsza recenzja Garso'); if (garsoReviews.at(-1)?.date.getTime() !== garsoReviews[0]?.date.getTime()) { addTimelineItem(garsoReviews.at(-1),'Ostatnia recenzja Garso'); } addTimelineItem(locations[0],'Pierwsza zapisana lokalizacja Escorti'); if (locations.at(-1)?.date.getTime() !== locations[0]?.date.getTime()) { addTimelineItem(locations.at(-1),'Ostatnia zapisana lokalizacja Escorti'); } timelineItems.sort((a,b) => a.date.getTime() - b.date.getTime()); const timelineDays=[]; const timelineDaysByDate=new Map(); for (const item of timelineItems) { const dayKey=[ item.date.getFullYear(), String(item.date.getMonth() + 1).padStart(2,'0'), String(item.date.getDate()).padStart(2,'0') ].join('-'); let day=timelineDaysByDate.get(dayKey); if (!day) { day={date:item.date,items:[]}; timelineDaysByDate.set(dayKey,day); timelineDays.push(day); } day.items.push(item); } if (timelineDays.length) { const timeline=makeElement('div','vm-summary-timeline'); const line=makeElement('div','vm-summary-timeline-line'); timeline.appendChild(line); for (const day of timelineDays) { const row=makeElement('div','vm-summary-timeline-row'); const markerColors=[...new Set(day.items.map(item => ESCORT_RESEARCH_TIMELINE_CATEGORIES[item.category]?.color || '#fff' ))]; const markerBackground=markerColors.length === 1 ? markerColors[0] :`conic-gradient(${markerColors.map((color,index) => { const start=Math.round(index / markerColors.length * 100); const end=Math.round((index + 1) / markerColors.length * 100); return `${color} ${start}% ${end}%`; }).join(', ')})`; const marker=makeElement('span','vm-summary-timeline-marker'); marker.style.background=markerBackground; const dateLine=makeElement('div'); const date=makeElement('strong','vm-summary-timeline-date',formatServerMediaDate(day.date)); const age=makeElement('span','vm-summary-timeline-age',` • ${formatEscortResearchTimelineAge(day.date)}`); dateLine.append(date,age); const events=makeElement('div','vm-summary-timeline-events'); for (const item of day.items) { const event=makeElement('div','vm-summary-timeline-event'); const label=makeElement('div','vm-summary-timeline-label',`• ${item.label}`); const categoryColor= ESCORT_RESEARCH_TIMELINE_CATEGORIES[item.category]?.color || '#fff'; label.style.color= getEscortTimelineReadableTextColor(categoryColor); event.appendChild(label); if (item.detail) { const detail=makeElement('div','vm-summary-timeline-detail',item.detail); event.appendChild(detail); } makeEscortTimelineElementClickable( event, item.url, `Otwórz źródło: ${item.label}` ); events.appendChild(event); } row.append(marker,dateLine,events); timeline.appendChild(row); } box.appendChild(timeline); } const note=makeElement('div','vm-summary-timeline-note'); const loadingParts=[]; if (facts?.escortiLoading) loadingParts.push('Escorti i aktywne anonse'); if (facts?.escortPhoneLoading) { loadingParts.push('Escort.club po nr tel.'); } if (facts?.photoDatesLoading) loadingParts.push('zdjęcia'); note.textContent= (loadingParts.length ? `Trwa uzupełnianie: ${loadingParts.join(', ')}. `:'') + 'Daty zdjęć pochodzą z nagłówka Last-Modified. Nie są pewną datą wykonania zdjęcia, ale pokazują, od kiedy dany plik istniał na serwerze.'; box.appendChild(note); makeEscortSummarySectionCollapsible( box, title, [...box.children].filter(child => child !== title && !child.matches('.vm-summary-key-dates, .vm-summary-old-photos-warning, .vm-summary-date-comparison')), 'timeline', true ); return box; } const ESCORT_SUMMARY_SIDE_PANEL_ID='vm-escort-summary-side-panel'; const ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID='vm-escort-summary-side-panel-style'; const ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS='vm-escort-summary-side-panel-open'; const ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS='vm-escort-summary-side-panel-resizing'; const ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY='vm_escort_summary_side_panel_width'; const ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY='vm_escort_summary_side_panel_pinned'; const ESCORT_SUMMARY_SIDE_PANEL_PREVIOUS_DEFAULT_WIDTH=400; const ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH=340; const ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH=300; function getEscortSummarySidePanelMaxWidth( view=window ) { const viewportWidth= Number(view?.innerWidth) || window.innerWidth; return Math.max( ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH, Math.min( 800, viewportWidth - 260 ) ); } function normalizeEscortSummarySidePanelWidth( value, view=window ) { const width=Number(value); const fallback= ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH; return Math.round( Math.min( getEscortSummarySidePanelMaxWidth( view ), Math.max( ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH, Number.isFinite(width) ? width :fallback ) ) ); } const escortSummarySidePanelBindings= new WeakMap(); function bindEscortSummarySidePanelControls( doc, panel, toggle, { onOpen=null }={} ) { if (!doc || !panel || !toggle) { return null; } const previous= escortSummarySidePanelBindings.get( panel ); previous?.dispose?.(); const cleanup=[]; const listen=( target, type, handler, options=false ) => { if (!target) return; target.addEventListener( type, handler, options ); cleanup.push(() => { try { target.removeEventListener( type, handler, options ); } catch {} }); }; const root=doc.documentElement; const body=doc.body; const view= doc.defaultView || window; const pin= panel.querySelector( '.vm-summary-panel-pin' ); const pinIcon= panel.querySelector( '.vm-summary-panel-pin-icon' ); const resizeHandle= panel.querySelector( '.vm-escort-summary-side-resize-handle' ); const storedPanelWidth=Number( GM_getValue( ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH ) ); const initialPanelWidth= storedPanelWidth === ESCORT_SUMMARY_SIDE_PANEL_PREVIOUS_DEFAULT_WIDTH ? ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH :storedPanelWidth; let panelWidth= normalizeEscortSummarySidePanelWidth( initialPanelWidth, view ); if ( storedPanelWidth === ESCORT_SUMMARY_SIDE_PANEL_PREVIOUS_DEFAULT_WIDTH ) { writeHelperStorageValue( ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, panelWidth ); } let pinned= !!GM_getValue( ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY, true ); let opened=pinned; let resizing=false; const applyWidth=value => { panelWidth= normalizeEscortSummarySidePanelWidth( value, view ); root.style.setProperty( '--vm-escort-summary-panel-width', `${panelWidth}px` ); return panelWidth; }; const renderPin=() => { if (!pin) return; pin.setAttribute( 'aria-pressed', String(pinned) ); pin.dataset.vmPinned= pinned ? '1':'0'; setControlTitle(pin,pinned ? 'Panel przypięty - będzie otwierany automatycznie' :'Przypnij panel, aby otwierał się automatycznie'); if (pinIcon) { pinIcon.style.transform= pinned ? 'rotate(0deg)' :'rotate(-45deg)'; } }; const setOpen=value => { opened=!!value; body.classList.toggle( ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS, opened ); toggle.textContent= opened ? 'Zwiń ›' :'‹ Podsumowanie'; toggle.title= opened ? 'Zwiń podsumowanie' :'Pokaż podsumowanie'; if ( opened && typeof onOpen === 'function' ) { onOpen(); } }; listen( toggle, 'click', event => { event.preventDefault(); setOpen(!opened); } ); listen( pin, 'click', event => { event.preventDefault(); pinned=!pinned; writeHelperStorageValue( ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY, pinned ); renderPin(); if (pinned) { setOpen(true); } } ); listen( resizeHandle, 'pointerdown', event => { if (event.button !== 0) return; event.preventDefault(); resizing=true; body.classList.add( ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS ); try { resizeHandle.setPointerCapture( event.pointerId ); } catch {} } ); listen( resizeHandle, 'pointermove', event => { if (!resizing) return; applyWidth( ( Number(view.innerWidth) || window.innerWidth ) - event.clientX ); } ); const finishResize=event => { if (!resizing) return; resizing=false; body.classList.remove( ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS ); try { if ( event?.pointerId != null && resizeHandle ?.hasPointerCapture?.( event.pointerId ) ) { resizeHandle .releasePointerCapture( event.pointerId ); } } catch {} writeHelperStorageValue( ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, panelWidth ); }; listen( resizeHandle, 'pointerup', finishResize ); listen( resizeHandle, 'pointercancel', finishResize ); listen( resizeHandle, 'dblclick', event => { event.preventDefault(); applyWidth( ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH ); writeHelperStorageValue( ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, panelWidth ); } ); applyWidth(panelWidth); renderPin(); setOpen(pinned); const binding={ dispose() { while (cleanup.length) { cleanup.pop()?.(); } body.classList.remove( ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS ); }, setOpen, applyWidth, isOpen:() => opened }; escortSummarySidePanelBindings.set( panel, binding ); return binding; } function ensureEscortSummarySidePanelStyle() { if (document.getElementById(ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID)) return; const sidePanel=`#${ESCORT_SUMMARY_SIDE_PANEL_ID}`; const accent=`var(--vm-ui-accent, ${getEscortPagePinkColor()})`; const panelText=`var(--vm-ui-panel-text, #fff)`; const panelMuted=`var(--vm-ui-panel-muted, #cbb8ce)`; const panelBackground=`var(--vm-ui-panel-bg, #2a0833)`; const danger=`var(--vm-ui-danger, #ff7777)`; const success=`var(--vm-ui-success, #62cf7b)`; const mixAccent=`var(--vm-ui-accent, #f54da3)`; const mix=(color,percent) => `color-mix(in srgb, ${color} ${percent}%, transparent)`; appendHelperStyle(ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID,` :root { --vm-escort-summary-panel-width: ${ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH}px; --vm-escort-summary-panel-top-offset: 0px; --vm-escort-summary-panel-effective-width: min( var(--vm-escort-summary-panel-width), 92vw ); } body { transition: width .22s ease; } body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} { width: calc(100% - var(--vm-escort-summary-panel-effective-width)) !important; max-width: calc(100% - var(--vm-escort-summary-panel-effective-width)) !important; } body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS}, body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} ${sidePanel}, body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} #vm-escort-summary-side-toggle { transition: none !important; user-select: none !important; } ${sidePanel} { position: fixed; z-index: 2147483000; top: var(--vm-escort-summary-panel-top-offset); right: 0; width: var(--vm-escort-summary-panel-effective-width); height: calc(100vh - var(--vm-escort-summary-panel-top-offset)); height: calc(100dvh - var(--vm-escort-summary-panel-top-offset)); box-sizing: border-box; display: flex; flex-direction: column; transform: translateX(100%); transition: transform .22s ease; background: ${panelBackground}; color: ${panelText}; border-left: 2px solid ${accent}; box-shadow: -10px 0 30px rgba(0,0,0,.35); font-family: var(--vm-ui-font-family, Arial, sans-serif); } body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} ${sidePanel} { transform: translateX(0); } #vm-escort-summary-side-toggle { position: fixed; z-index: 2147483001; top: 50%; right: 0; transform: translateY(-50%); padding: 11px 7px; border: 1px solid ${accent}; border-right: 0; border-radius: 8px 0 0 8px; background: ${accent}; color: var(--vm-ui-accent-contrast,#fff); font-size: 12px; font-weight: 800; line-height: 1.15; cursor: pointer; box-shadow: -3px 2px 12px rgba(0,0,0,.28); transition: right .22s ease; writing-mode: vertical-rl; } body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} #vm-escort-summary-side-toggle { right: var(--vm-escort-summary-panel-effective-width); } body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} #${GLOBAL_QUICK_SEARCH_ID} { right: calc(var(--vm-escort-summary-panel-effective-width) + 16px) !important; } .vm-escort-summary-side-resize-handle { position: absolute; z-index: 3; top: 0; bottom: 0; left: -6px; width: 12px; cursor: ew-resize; touch-action: none; } .vm-escort-summary-side-resize-handle::after { content: ''; position: absolute; top: 0; bottom: 0; left: 5px; width: 2px; background: transparent; transition: background .15s ease; } .vm-escort-summary-side-resize-handle:hover::after, body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} .vm-escort-summary-side-resize-handle::after { background: ${accent}; } ${sidePanel} .vm-summary-panel-header { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 10px 11px; border-bottom: 1px solid ${accent}; background: color-mix(in srgb, ${panelBackground} 84%, #000); } ${sidePanel} .vm-summary-panel-title { flex: 1 1 auto; min-width: 0; color: ${accent}; font-size: 16px; font-weight: 800; } ${sidePanel} .vm-summary-panel-pin { width: 30px; height: 30px; padding: 0; border: 1px solid ${mix(panelText,25)}; border-radius: 50%; background: ${mix(panelText,7)}; color: ${panelText}; cursor: pointer; transition: background .16s ease, border-color .16s ease, color .16s ease, box-shadow .16s ease; } ${sidePanel} .vm-summary-panel-pin[aria-pressed="true"] { border-color: ${panelText}; background: ${panelText}; color: ${panelBackground}; box-shadow: 0 0 0 2px color-mix( in srgb, ${panelText} 22%, transparent ); } ${sidePanel} .vm-summary-panel-pin-icon { display: block; width: 17px; height: 17px; margin: auto; fill: currentColor; pointer-events: none; transition: transform .16s ease; } .vm-escort-summary-side-content { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 10px; box-sizing: border-box; scrollbar-width: thin; } .vm-escort-summary-side-card { margin-bottom: 10px; border: 2px solid ${mix(mixAccent,58)}; border-radius: 7px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,3.5)}); box-shadow: 0 3px 12px rgba(0,0,0,.2); overflow: hidden; } ${sidePanel} .vm-escort-summary-integrated-section { border-width: 2px !important; border-color: ${mix(mixAccent,58)} !important; box-shadow: 0 3px 12px rgba(0,0,0,.2); } ${sidePanel} .vm-escort-summary-side-card > .vm-escort-summary-collapsible-header { border: 0 !important; border-bottom: 1px solid ${mix(mixAccent,48)} !important; border-radius: 0 !important; } ${sidePanel} .vm-escort-summary-side-card.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header { border-bottom: 0 !important; } .vm-escort-summary-side-card-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 2px 8px; padding: 8px 10px 7px; border-bottom: 1px solid var(--vm-ui-panel-border, ${mix(panelText,13)}); background: var(--vm-ui-panel-bg-alt, ${mix(panelText,3.5)}); } ${sidePanel} [hidden], #vm-escort-research-panel [hidden] { display: none !important; } .vm-summary-section-preview { padding: 8px 10px; font-size: 12px; line-height: 1.4; color: ${panelText}; overflow-wrap: anywhere; } .vm-summary-section-preview.-warning { color: var(--vm-ui-warning, #ffcf5a); } .vm-escort-summary-side-card-body { padding: 9px 10px 10px; font-size: 12px; line-height: 1.38; } ${sidePanel} .vm-escort-summary-integrated-section { box-sizing: border-box; margin: 0 0 10px; padding: 9px; border: 1px solid var(--vm-ui-panel-border, ${mix(panelText,18)}); border-radius: 6px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,2.5)}); color: ${panelText}; font-size: 11px; line-height: 1.3; } ${sidePanel} .vm-summary-timeline-title { margin-bottom: 8px; font-size: 12px; font-weight: 800; } ${sidePanel} .vm-summary-timeline-title-warning { margin-left: 5px; font-size: 13px; font-weight: 900; line-height: 1; } ${sidePanel} .vm-summary-key-dates { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; margin-bottom: 8px; } ${sidePanel} .vm-summary-key-date-card { min-width: 0; padding: 6px 5px; border-radius: 5px; background: ${panelBackground}; text-align: center; } ${sidePanel} .vm-summary-key-date-label { min-height: 23px; font-size: 9px; font-weight: 700; line-height: 1.2; } ${sidePanel} .vm-summary-key-date-value { margin-top: 2px; font-size: 11px; font-weight: 800; white-space: nowrap; } ${sidePanel} .vm-summary-key-date-age { margin-top: 2px; color: ${panelMuted}; font-size: 8px; white-space: nowrap; } ${sidePanel} .vm-summary-old-photos-warning, ${sidePanel} .vm-summary-date-comparison { padding: 7px 8px; border-radius: 5px; font-size: 10px; line-height: 1.3; } ${sidePanel} .vm-summary-old-photos-warning { margin-bottom: 8px; border: 1px solid ${mix(danger,78)}; background: ${mix(danger,14)}; color: ${danger}; font-weight: 800; } ${sidePanel} .vm-summary-date-comparison { margin-bottom: 9px; } ${sidePanel} .vm-summary-date-comparison.-mismatch { border: 1px solid color-mix(in srgb, var(--vm-ui-warning, #ffcf5a) 80%, transparent); background: color-mix(in srgb, var(--vm-ui-warning, #ffcf5a) 13%, transparent); color: var(--vm-ui-warning, #ffcf5a); font-weight: 700; } ${sidePanel} .vm-summary-date-comparison.-ok { border: 1px solid ${mix(success,55)}; background: ${mix(success,9)}; color: ${success}; font-weight: 600; } ${sidePanel} .vm-summary-source-link { cursor: pointer; transition: background-color .15s ease, filter .15s ease; } ${sidePanel} .vm-summary-timeline { position: relative; margin: 2px 0 0 5px; padding-left: 17px; } ${sidePanel} .vm-summary-timeline-line { position: absolute; top: 7px; bottom: 9px; left: 5px; width: 2px; background: ${mix(panelText,20)}; } ${sidePanel} .vm-summary-timeline-row { position: relative; padding-bottom: 10px; } ${sidePanel} .vm-summary-timeline-marker { position: absolute; top: 4px; left: -16px; width: 9px; height: 9px; box-sizing: border-box; border: 2px solid ${panelBackground}; border-radius: 50%; } ${sidePanel} .vm-summary-timeline-date { font-size: 10px; } ${sidePanel} .vm-summary-timeline-age { color: var(--vm-ui-panel-muted, #bca9c1); font-size: 9px; } ${sidePanel} .vm-summary-timeline-events { display: grid; gap: 2px; margin-top: 2px; } ${sidePanel} .vm-summary-timeline-event { min-width: 0; padding: 1px 3px; border-radius: 3px; } ${sidePanel} .vm-summary-timeline-label { font-size: 10px; font-weight: 700; } ${sidePanel} .vm-summary-timeline-detail { margin: 1px 0 0 9px; color: var(--vm-ui-panel-muted, #a997ae); font-size: 9px; } ${sidePanel} .vm-summary-timeline-note { margin-top: 3px; padding-top: 6px; border-top: 1px solid ${mix(panelText,12)}; color: var(--vm-ui-panel-muted, #a997ae); font-size: 9px; line-height: 1.3; } ${sidePanel} .vm-summary-other-cities-framed { box-sizing: border-box; margin-top: 0; padding: 8px 9px; border: 1px solid ${mix(panelText,14)}; border-radius: 7px; background: ${mix(panelText,2.5)}; color: ${panelText}; } ${sidePanel} .vm-escort-summary-other-label { margin-bottom: 6px; color: ${panelText}; font-size: 11px; font-weight: 700; line-height: 1.25; } ${sidePanel} .vm-escort-summary-other-city { display: inline-flex; align-items: center; padding: 2px 6px; border: 1px solid ${mix(panelText,18)}; border-radius: 999px; background: ${mix(panelText,4)}; color: ${panelText}; font-size: 10px; font-weight: 600; line-height: 1.2; } ${sidePanel} .vm-garso-side-card > .vm-escort-summary-collapsible-header { padding-right: 9px !important; } ${sidePanel} .vm-garso-card-status-full { flex: 0 0 100%; width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; margin-top: 2px; padding-right: 0; white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.25; } .vm-garso-card-title { display: flex; flex: 0 0 100%; align-items: center; gap: 6px; min-width: 0; box-sizing: border-box; padding-right: 94px; color: ${accent}; font-size: 13px; font-weight: 800; } .vm-garso-card-title-warning { flex: 0 0 auto; color: ${danger}; font-size: 14px; font-weight: 900; line-height: 1; } .vm-garso-card-meta { margin-top: 3px; color: ${panelMuted}; font-size: 10px; line-height: 1.25; } ${sidePanel} .vm-escort-summary-collapsible-header, #vm-escort-research-panel .vm-escort-summary-collapsible-header { position: relative; min-height: 34px; box-sizing: border-box; padding: 8px 82px 8px 9px !important; border: 1px solid ${mix(mixAccent,36)}; border-radius: 6px; background: ${mix(mixAccent,9)} !important; color: ${accent}; cursor: pointer; user-select: none; transition: background .15s ease, border-color .15s ease; } ${sidePanel} .vm-escort-summary-collapsible-header:hover, #vm-escort-research-panel .vm-escort-summary-collapsible-header:hover { border-color: ${mix(mixAccent,72)}; background: ${mix(mixAccent,17)} !important; } ${sidePanel} .vm-escort-summary-collapsible-header:focus-visible, #vm-escort-research-panel .vm-escort-summary-collapsible-header:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; } ${sidePanel} .vm-escort-summary-collapse-hint, #vm-escort-research-panel .vm-escort-summary-collapse-hint { position: absolute; top: 50%; right: 7px; transform: translateY(-50%); display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px; border: 1px solid ${mix(mixAccent,55)}; border-radius: 999px; background: color-mix(in srgb, ${panelBackground} 80%, #000); color: color-mix(in srgb, ${mixAccent} 42%, ${panelText}); font-size: 9px; font-weight: 800; line-height: 1; pointer-events: none; } ${sidePanel} .vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header, #vm-escort-research-panel .vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header { background: ${mix(mixAccent,14)} !important; } ${sidePanel} .vm-escort-summary-integrated-section { overflow: hidden; } ${sidePanel} .vm-escort-summary-integrated-section > .vm-escort-summary-collapsible-header { margin: -9px -9px 8px !important; border: 0 !important; border-bottom: 1px solid ${mix(mixAccent,36)} !important; border-radius: 0 !important; } ${sidePanel} .vm-escort-summary-integrated-section.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header { margin: -9px -9px 8px !important; border-bottom: 0 !important; } ${sidePanel} .vm-escort-summary-inconsistencies { border: 2px solid ${mix(mixAccent,58)}; border-radius: 7px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,3.5)}); box-shadow: 0 3px 12px rgba(0,0,0,.2); overflow: hidden; } ${sidePanel} .vm-escort-summary-inconsistencies > .vm-escort-summary-collapsible-header { margin: 0 !important; border: 0 !important; border-bottom: 1px solid ${mix(mixAccent,48)} !important; border-radius: 0 !important; color: ${accent} !important; } ${sidePanel} .vm-escort-summary-inconsistencies.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header { border-bottom: 0 !important; } ${sidePanel} .vm-escort-summary-inconsistencies > .vm-escort-summary-inconsistencies-content { border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; } #vm-escort-research-panel { width: 100%; box-sizing: border-box; margin-top: 12px; overflow: hidden; border: 1px solid ${accent}; border-radius: 8px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,2.5)}); color: ${panelText}; } #vm-escort-research-panel > .vm-research-panel-main-header { display: flex; align-items: center; gap: 8px; padding: 10px 11px 8px; color: ${accent}; font-size: 14px; font-weight: 700; line-height: 1.2; } #vm-escort-research-panel > .vm-research-panel-row, #vm-phone-reputation-links .vm-research-panel-row { display: flex; align-items: center; width: 100%; min-height: 42px; margin: 0; padding: 9px 11px; border: 0; border-bottom: 1px solid var(--vm-ui-panel-border, rgba(255,255,255,.1)); border-radius: 0; background: transparent; color: ${panelText}; cursor: pointer; font-size: 13px; font-weight: 600; box-sizing: border-box; transition: background-color .15s ease; } #vm-escort-research-panel > .vm-research-panel-row:hover, #vm-phone-reputation-links .vm-research-panel-row:hover { background: var(--vm-ui-panel-bg-alt, rgba(255,255,255,.055)); } #vm-escort-research-panel .vm-research-panel-main-title { min-width: 0; flex: 1 1 auto; } #vm-escort-research-panel .vm-research-panel-refresh { flex: 0 0 auto; width: 28px; height: 28px; padding: 0; border: 1px solid ${accent}; border-radius: 50%; background: transparent; color: ${accent}; font-size: 20px; font-weight: 700; line-height: 24px; cursor: pointer; } ${sidePanel} .vm-escort-summary-inconsistencies-title { display: flex; align-items: center; gap: 0; margin-bottom: 6px; font-weight: 800; } ${sidePanel} .vm-escort-summary-inconsistencies { margin: 0 0 10px; color: ${panelText}; font-size: 12px; font-weight: 400; line-height: 1.35; text-align: left; } ${sidePanel} .vm-inconsistencies-status-badge { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; font-weight: 800; line-height: 1; } ${sidePanel} .vm-escort-summary-status-alert .vm-inconsistencies-status-badge { display: inline; margin-left: 5px; padding: 0; border: 0; border-radius: 0; background: transparent; color: ${danger}; box-shadow: none; font-size: 13px; font-weight: 900; } ${sidePanel} .vm-escort-summary-status-ok .vm-inconsistencies-status-badge { margin-left: 6px; padding: 3px 7px; border: 1px solid ${mix(success,72)}; border-radius: 999px; background: ${mix(success,12)}; color: ${success}; box-shadow: none; font-size: 10px; } ${sidePanel} .vm-inconsistencies-empty { padding: 8px 9px; border: 1px solid ${mix(success,42)}; border-radius: 6px; background: ${mix(success,8)}; color: ${success}; font-size: 11px; font-weight: 800; } ${sidePanel} .vm-inconsistencies-source-card { overflow: hidden; border: 1px solid ${mix(danger,35)}; border-radius: 7px; background: ${mix(danger,4.5)}; } ${sidePanel} .vm-inconsistencies-source-head { display: flex; align-items: center; gap: 7px; padding: 7px 8px; border-bottom: 1px solid ${mix(danger,22)}; background: ${mix(danger,6)}; } ${sidePanel} .vm-inconsistencies-source-marker { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 1px solid ${mix(danger,55)}; border-radius: 50%; background: ${mix(danger,14)}; color: ${danger}; font-size: 13px; font-weight: 900; line-height: 1; } ${sidePanel} .vm-inconsistencies-source-title { display: block; color: ${danger}; font-weight: 900; line-height: 1.2; } ${sidePanel} .vm-inconsistencies-source-subtitle { display: block; margin-top: 2px; color: ${panelMuted}; font-size: 9px; font-weight: 600; line-height: 1.2; } ${sidePanel} .vm-inconsistencies-source-count { margin-left: auto; padding: 2px 6px; border: 1px solid ${mix(danger,45)}; border-radius: 999px; color: ${danger}; font-size: 9px; font-weight: 800; white-space: nowrap; } ${sidePanel} .vm-inconsistencies-warning-list { display: grid; gap: 6px; padding: 7px; } ${sidePanel} .vm-inconsistencies-warning-row { display: grid; grid-template-columns: 24px minmax(0,1fr); align-items: start; gap: 7px; padding: 6px 7px; border-left: 3px solid ${mix(danger,78)}; border-radius: 5px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,3.5)}); } ${sidePanel} .vm-inconsistencies-warning-icon { display: inline-flex; align-items: center; justify-content: center; min-width: 22px; height: 22px; border-radius: 5px; background: ${mix(danger,13)}; color: ${danger}; font-size: 10px; font-weight: 900; line-height: 1; } ${sidePanel} .vm-inconsistencies-warning-label { display: block; color: ${danger}; font-weight: 900; line-height: 1.2; } ${sidePanel} .vm-inconsistencies-warning-detail { display: block; margin-top: 2px; color: ${panelText}; font-size: 10px; font-weight: 400; line-height: 1.25; } ${sidePanel} .vm-garso-main-head { padding-right: 108px !important; } ${sidePanel} .vm-garso-main-title { box-sizing: border-box; overflow: hidden; padding-right: 0; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } ${sidePanel} .vm-garso-main-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #vm-escort-research-panel .vm-research-panel-footer { padding: 8px 10px 10px; } ${sidePanel} .vm-escort-summary-inconsistencies-content { display: grid; gap: 7px; padding: 8px 9px; border: 0; border-radius: 0; background: transparent; color: ${panelText}; } ${sidePanel} .vm-garso-download-button { position: absolute; z-index: 1; top: 16px; right: 72px; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; width: 21px; height: 21px; margin: 0; padding: 0; border: 1px solid ${accent}; border-radius: 50%; background: color-mix(in srgb, ${panelBackground} 80%, #000); color: color-mix(in srgb, ${mixAccent} 42%, ${panelText}); font-size: 14px; font-weight: 800; line-height: 1; cursor: pointer; } #vm-phone-reputation-links { overflow: hidden; margin-bottom: 7px; border: 1px solid var(--vm-ui-panel-border, ${mix(panelText,16)}); border-radius: 7px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,2.5)}); } #vm-phone-reputation-links > .vm-phone-reputation-head { display: flex; align-items: center; gap: 7px; min-height: 34px; padding: 8px 82px 8px 9px !important; color: ${accent}; font-size: 11px; font-weight: 800; } #vm-escort-research-panel .vm-research-section-title { color:var(--vm-ui-accent,#f54da3); font-size:12px; font-weight:800; } #vm-phone-reputation-links .vm-phone-reputation-title { min-width: 0; flex: 0 1 auto; } #vm-phone-reputation-links .vm-phone-reputation-badge { flex: 0 0 auto; padding: 2px 6px; border-radius: 999px; font-size: 9px; font-weight: 900; line-height: 1.1; white-space: nowrap; } #vm-phone-reputation-links .vm-phone-reputation-badge.-negative { border: 1px solid ${mix(danger,70)}; background: ${mix(danger,14)}; color: ${danger}; } #vm-phone-reputation-links .vm-phone-reputation-badge.-positive { border: 1px solid ${mix(success,70)}; background: ${mix(success,13)}; color: ${success}; } #vm-phone-reputation-links .vm-phone-reputation-body { overflow: hidden; border-top: 1px solid ${mix(panelText,10)}; } #vm-phone-reputation-links .vm-phone-reputation-rows > .vm-research-panel-row:last-child { border-bottom: 0 !important; } .vm-escort-summary-other-city-list { display: flex; flex-wrap: wrap; gap: 4px; } #vm-escort-research-panel .vm-escort-summary-collapse-hint { padding:3px 6px !important; font:700 9px/1.15 var(--vm-ui-font-family,Lato,Arial,sans-serif) !important; } #vm-garso-extended-panel { overflow: hidden; border: 1px solid var(--vm-ui-panel-border, ${mix(panelText,16)}); border-radius: 7px; background: var(--vm-ui-panel-bg-alt, ${mix(panelText,2.5)}); } #vm-garso-extended-panel > .vm-garso-extended-head { display: flex; align-items: center; gap: 7px; color: ${accent}; font-size: 12px; font-weight: 800; } #vm-garso-extended-panel .vm-garso-extended-status-badge { flex: 0 0 auto; padding: 2px 5px; border: 1px solid ${mix(success,70)}; border-radius: 999px; background: ${mix(success,14)}; color: ${success}; font-size: 10px; line-height: 1; } #vm-garso-extended-panel .vm-garso-extended-body { display:grid; gap:8px; padding:10px; font:400 12px/1.4 var(--vm-ui-font-family,Lato,Arial,sans-serif); } #vm-garso-extended-panel .vm-garso-extended-message { margin:0; color:var(--vm-ui-panel-muted,var(--vm-ui-text-muted,#d8c8d9)); text-align:left; overflow-wrap:anywhere; } #vm-garso-extended-panel .vm-garso-extended-recommendation { display:block; width:calc(100% - 14px); margin:7px 7px 0; padding:7px 9px; box-sizing:border-box; border:1px solid color-mix(in srgb,var(--vm-ui-warning,#e2b84b) 75%,transparent); border-radius:6px; background:color-mix(in srgb,var(--vm-ui-warning,#e2b84b) 9%,var(--vm-ui-panel-bg-alt,transparent)); color:var(--vm-ui-panel-text,var(--vm-ui-text,#f2f2f2)); font:inherit; text-align:left; cursor:pointer; } #vm-garso-extended-panel .vm-garso-extended-recommendation strong { display:block; color:var(--vm-ui-warning,#e2b84b); font-size:10px; } #vm-garso-extended-panel .vm-garso-extended-recommendation span { display:block; margin-top:2px; color:var(--vm-ui-panel-muted,var(--vm-ui-text-muted,#d8c8d9)); font-size:9px; line-height:1.3; } #vm-garso-extended-panel .vm-garso-extended-controls { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; } #vm-garso-extended-panel .vm-garso-extended-progress { display:block; width:100%; height:5px; margin:0; accent-color:var(--vm-ui-accent,#f54da3); } #vm-garso-extended-panel [hidden] { display:none !important; } #vm-garso-extended-panel .vm-garso-extended-action { display:flex; align-items:center; justify-content:center; gap:6px; box-sizing:border-box; width:100%; min-width:0; min-height:34px; margin:0; padding:6px 8px; border:1px solid var(--vm-ui-panel-border,var(--vm-ui-border,#806383)); border-radius:5px; background:transparent; color:var(--vm-ui-panel-text,var(--vm-ui-text,#f2f2f2)); font:700 12px/1.25 var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor:pointer; } #vm-garso-extended-panel .vm-garso-extended-action.-search { border-color:var(--vm-ui-accent,#f54da3); background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 12%,transparent); } #vm-garso-extended-panel .vm-garso-extended-action.-cancel { border-color:var(--vm-ui-danger,#dc2626); color:var(--vm-ui-danger,#dc2626); } #vm-garso-extended-panel .vm-garso-extended-action:hover:enabled { background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 20%,transparent); } #vm-garso-extended-panel .vm-garso-extended-action:focus-visible { outline:2px solid var(--vm-ui-accent,#f54da3); outline-offset:2px; } #vm-garso-extended-panel .vm-garso-extended-action svg { flex:0 0 14px; width:14px; height:14px; } #vm-garso-extended-panel .vm-garso-extended-count { padding:1px 5px; border-radius:4px; background:color-mix(in srgb,var(--vm-ui-accent,#f54da3) 15%,transparent); font-size:10px; font-variant-numeric:tabular-nums; } ${sidePanel} .vm-garso-mention-chip:hover, ${sidePanel} .vm-garso-mention-chip:focus-visible { filter: brightness(1.18); outline: 1px solid ${mix(panelText,72)}; outline-offset: 1px; } .vm-helper-ui .vm-garso-evidence-dialog { width:min(760px,calc(100vw - 32px)); display:flex; flex-direction:column; overflow:hidden; } .vm-garso-evidence-content { min-height:0; overflow-y:auto; } .vm-garso-evidence-title { min-width:0; flex:1; } .vm-garso-evidence-intro { margin:0 0 8px; } .vm-garso-evidence-topic { margin-top:8px; } .vm-garso-evidence-topic-title { display:block; padding-bottom:8px; color:var(--vm-ui-accent,#f54da3); font-size:12px; font-weight:700; text-decoration:none; } .vm-garso-evidence-snippet { padding:8px 0; border-top:1px solid var(--vm-ui-border,#ddd); } .vm-garso-evidence-meta { margin-bottom:4px; color:var(--vm-ui-text-muted,#666); font-size:var(--vm-ui-small-font-size,11px); } .vm-garso-evidence-text { color:var(--vm-ui-text,#222); font-size:12px; line-height:1.4; } .vm-garso-evidence-text mark { padding: 0 2px; border-radius: 2px; background: var(--vm-ui-warning, #ffcf5a); color: #351523; font-weight: 800; } .vm-garso-cancel-button { position: absolute; right: 7px; bottom: 3px; display: none; align-items: center; margin: 0; padding: 3px 7px; border: 1px solid ${danger}; border-radius: 999px; background: color-mix(in srgb, ${danger} 12%, ${panelBackground}); color: ${danger}; font:700 9px/1 var(--vm-ui-font-family,Lato,Arial,sans-serif); cursor: pointer; } .vm-garso-cancel-button.-visible { display: inline-flex; } @media (max-width: 1100px) { body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} { width: 100% !important; max-width: 100% !important; } } `); } function appendGarsoEvidenceText(container,snippet,matchText) { const text=String(snippet || ''); const match=String(matchText || ''); const index=match ? text.toLocaleLowerCase('pl-PL').indexOf( match.toLocaleLowerCase('pl-PL') ) :-1; if (index < 0) { container.textContent=text; return; } container.append( document.createTextNode(text.slice(0,index)), Object.assign(makeElement('mark'),{ textContent:text.slice(index,index + match.length) }), document.createTextNode(text.slice(index + match.length)) ); } function openGarsoMentionEvidenceDialog(entry) { closeHelperDialog(document.querySelector('.vm-garso-evidence-overlay')); ensureEscortSummarySidePanelStyle(); const overlay=prepareHelperStyleApi( makeElement('div',"vm-garso-evidence-overlay vm-ui-overlay") ); const dialog=makeElement('section',"vm-garso-evidence-dialog vm-ui-dialog"); dialog.setAttribute('role','dialog'); dialog.setAttribute('aria-modal','true'); const header=makeElement('div',"vm-garso-evidence-header vm-ui-header"); const title=makeElement('div',"vm-garso-evidence-title vm-ui-title",`Dlaczego zliczono: ${entry?.label || 'wzmiankę'} (${entry?.count || 0})`); const close=makeButton("vm-garso-evidence-close vm-ui-button vm-ui-icon",'×',{title:'Zamknij'}); header.append(title,close); const content=makeElement('div','vm-garso-evidence-content'); const intro=makeElement('p',"vm-garso-evidence-intro vm-ui-help",'Wzmianki dotyczą całych tematów i mogą opisywać różne osoby lub wizyty. Każdy fragment odpowiada jednemu postowi w liczniku. Zgodność zdjęć obejmuje tożsamość, aktualność i wygląd; szczegół wynika z cytatu. Kliknij tytuł, aby otworzyć temat Garso.'); content.appendChild(intro); const evidence=Array.isArray(entry?.evidence) ? entry.evidence:[]; if (!evidence.length) { const empty=makeElement('div',"vm-garso-evidence-intro vm-ui-help",'Brak zapisanych fragmentów. Odśwież analizę Garso, aby utworzyć uzasadnienia.'); content.appendChild(empty); } else { const groups=new Map(); for (const item of evidence) { const key=item.url || item.topicTitle || 'temat'; if (!groups.has(key)) groups.set(key,[]); groups.get(key).push(item); } for (const items of groups.values()) { const first=items[0]; const topic=makeElement('article',"vm-garso-evidence-topic vm-ui-panel"); const topicTitle=first.url ? makeElement('a') :makeElement('div'); topicTitle.className='vm-garso-evidence-topic-title'; topicTitle.textContent=first.topicTitle || 'Temat Garso'; if (first.url) { topicTitle.href=first.url; topicTitle.target='_blank'; topicTitle.rel='noopener noreferrer'; } topic.appendChild(topicTitle); for (const item of items) { const row=makeElement('div','vm-garso-evidence-snippet'); const meta=makeElement('div','vm-garso-evidence-meta',[item.author,item.date,item.scope,item.visit,item.aspect] .filter(Boolean) .join(' • ') || 'post Garso'); const fragment=makeElement('div','vm-garso-evidence-text'); appendGarsoEvidenceText(fragment,item.snippet,item.match); row.append(meta,fragment); topic.appendChild(row); } content.appendChild(topic); } } const closeController=createDialogCloseController(overlay); closeController.bindButton(close); dialog.append(header,content); overlay.appendChild(dialog); document.body.appendChild(overlay); close.focus(); } function formatGarsoCacheTimestamp(timestamp) { const time=Number(timestamp); if (!Number.isFinite(time) || time <= 0) return 'brak zapisanego cache'; const date=new Date(time).toLocaleString('pl-PL',{ day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }); const age=formatEscortLastVisit(time); return age ? `${date} • ${age}`:date; } function removeEscortSummarySidePanel() { detachHelperNode(document.getElementById(ESCORT_SUMMARY_SIDE_PANEL_ID)); detachHelperNode(document.getElementById('vm-escort-summary-side-toggle')); document.body.classList.remove(ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS); } function createEscortSummarySidePanel() { removeEscortSummarySidePanel(); ensureEscortSummarySidePanelStyle(); if (!SETTINGS.showSummarySidePanel) { return { panel:null, content:makeElement('div'), onOpen() {} }; } const panel= prepareHelperStyleApi( makeElement('aside') ); panel.id= ESCORT_SUMMARY_SIDE_PANEL_ID; panel.setAttribute( 'aria-label', 'Podsumowanie anonsu' ); const resizeHandle= makeElement('div','vm-escort-summary-side-resize-handle',null,{title:'Przeciągnij, aby zmienić szerokość panelu. Kliknij dwukrotnie, aby przywrócić domyślną.'}); resizeHandle.setAttribute( 'role', 'separator' ); resizeHandle.setAttribute( 'aria-orientation', 'vertical' ); const header= makeElement( 'div', 'vm-summary-panel-header' ); const title= makeElement( 'div', 'vm-summary-panel-title', 'Podsumowanie' ); const pin= makeElement('button','vm-summary-panel-pin',null,{type:'button'}); const pinIcon= document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); pinIcon.classList.add( 'vm-summary-panel-pin-icon' ); pinIcon.setAttribute( 'viewBox', '0 0 24 24' ); pinIcon.setAttribute( 'aria-hidden', 'true' ); const pinPath= document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); pinPath.setAttribute( 'd', 'M9 2.5h6l-.8 5.3 3.3 3.3v1.7H13l-1 8.7-1-8.7H6.5v-1.7l3.3-3.3L9 2.5z' ); pinIcon.appendChild(pinPath); pin.appendChild(pinIcon); header.append(title,pin); const content= makeElement( 'div', 'vm-escort-summary-side-content' ); panel.append( resizeHandle, header, content ); const toggle= makeElement('button'); toggle.id= 'vm-escort-summary-side-toggle'; toggle.type='button'; toggle.textContent= 'Podsumowanie'; toggle.title= 'Pokaż lub ukryj podsumowanie'; const openCallbacks=new Set(); document.body.append( panel, toggle ); bindEscortSummarySidePanelControls( document, panel, toggle, { onOpen() { openCallbacks.forEach( callback => callback() ); } } ); return { panel, content, onOpen(callback) { if ( typeof callback === 'function' ) { openCallbacks.add(callback); } } }; } function createEscortSummarySideCard( sidePanel, titleText, storageSuffix, defaultExpanded=true, sectionClass='' ) { const section=makeElement( 'section', `vm-escort-summary-side-card${sectionClass ? ` ${sectionClass}`:''}` ); const head=makeElement('div','vm-escort-summary-side-card-head'); const title=makeElement('div','vm-garso-card-title',titleText); const body=makeElement('div','vm-escort-summary-side-card-body'); head.appendChild(title); section.append(head,body); sidePanel.content.appendChild(section); const collapsible=makeEscortSummarySectionCollapsible( section,head,body,storageSuffix,defaultExpanded ); return {section,head,title,body,collapsible}; } function createGarsoSideSummaryCard(sidePanel,btn,titleText) { const card=makeElement('section','vm-escort-summary-side-card vm-garso-side-card'); const head=makeElement('div','vm-escort-summary-side-card-head'); const title=makeElement('div','vm-garso-card-title'); const titleLabel=makeElement('span','',titleText); const titleWarning=makeElement('span','vm-garso-card-title-warning','⚠︎',{hidden:true}); title.append(titleLabel,titleWarning); const meta=makeElement('div','vm-garso-card-meta vm-garso-card-status-full'); head.append(title,meta); const body=makeElement('div','vm-escort-summary-side-card-body'); const bodyContent=makeElement('div'); const cancelButton=makeButton('vm-garso-cancel-button','Przerwij'); cancelButton.addEventListener('click',event => { stopUiEvent(event); const token=btn._vmGarsoSummaryCancelToken; if (!token || token.cancelled) return; cancelButton.disabled=true; cancelButton.textContent='Przerywam…'; token.cancel('Pełna analiza Garso przerwana przez użytkownika'); }); bodyContent.addEventListener('click',event => { const chip=event.target.closest('.vm-garso-mention-chip'); if (!chip || !bodyContent.contains(chip)) return; stopUiEvent(event); const entries=getGarsoMentionEntries(btn._vmGarsoSummaryResult); entries.sort((a,b) => b.count - a.count || a.label.localeCompare(b.label,'pl-PL') ); const index=Number(chip.dataset.vmGarsoMentionIndex); if (Number.isInteger(index) && entries[index]) { openGarsoMentionEvidenceDialog(entries[index]); } }); const preview=makeElement('div','vm-escort-summary-side-card-body vm-summary-preview',null,{hidden:true}); body.append(bodyContent); head.appendChild(cancelButton); card.append(head,preview,body); sidePanel.content.appendChild(card); const collapsible=makeEscortSummarySectionCollapsible( card, head, body, `garso-${btn.dataset.garsoSearchMode || normalizeText(titleText).replace(/\s+/g,'-')}`, true ); const render=detail => { const state=detail?.state || btn.dataset.garsoSummaryState || ''; const html=detail?.html ?? btn.dataset.garsoSummaryHtml ?? ''; const result=detail?.result || btn._vmGarsoSummaryResult || null; const summaryCheckedAt=Number( detail?.checkedAt ?? btn.dataset.garsoSummaryCheckedAt ) || null; const countCheckedAt=Number(btn.dataset.garsoCountCheckedAt) || null; const checkedAt=summaryCheckedAt || countCheckedAt; const cacheState=detail?.cacheState || btn.dataset.garsoSummaryCacheState || ''; preview.replaceChildren(); if (state === 'empty') bodyContent.innerHTML=summaryToHtml({}); else if (html || result?.stats) bodyContent.innerHTML=html || summaryToHtml(result); else if (state === 'count-only') { bodyContent.textContent= 'Sprawdzono tylko, czy są tematy. Aby przeanalizować tematy, kliknij przycisk odświeżania w nagłówku sekcji.'; } else if (state === 'loading') bodyContent.textContent='Obliczanie statystyk…'; else if (state === 'cancelled') bodyContent.textContent='Analiza została przerwana.'; else if (state === 'error') bodyContent.textContent='Nie udało się przygotować analizy.'; else bodyContent.textContent='Analiza pojawi się po sprawdzeniu Garsoniery.'; const summaryPreview=bodyContent.querySelector('.vm-garso-summary-preview'); if (summaryPreview) preview.appendChild(summaryPreview); preview.hidden=!summaryPreview; const cancellationAvailable= ['loading','refreshing'].includes(state) && btn._vmGarsoSummaryCancelToken && !btn._vmGarsoSummaryCancelToken.cancelled; cancelButton.classList.toggle('-visible',!!cancellationAvailable); cancelButton.disabled=false; cancelButton.textContent='Przerwij'; const reviewWarning=getGarsoReviewWarning(result); titleWarning.hidden=!reviewWarning.show; titleWarning.setAttribute( 'aria-hidden', String(!reviewWarning.show) ); titleWarning.title=reviewWarning.title; if (checkedAt) { const prefix=state === 'count-only' ? 'Sprawdzenie obecności tematów' :(!summaryCheckedAt ? 'Liczba tematów z cache' :(['session','partial'].includes(cacheState) ? 'Analiza bieżąca' :'Cache analizy')); let suffix=''; if (cacheState === 'refreshing') suffix=' • trwa odświeżanie'; else if (cacheState === 'stale') suffix=result?.unverifiedTopicCount ? ' • nie potwierdzono aktualności':' • wymaga sprawdzenia aktualności'; else if (cacheState === 'partial') suffix=' • analiza niepełna, nie zapisano'; else if (!summaryCheckedAt) suffix=' • brak cache analizy'; if (summaryCheckedAt && result?.sourceCheckAttemptAt && !result.unverifiedTopicCount) suffix += ` • licznik postów sprawdzono: ${formatGarsoCacheTimestamp(result.sourceCheckAttemptAt)}`; meta.textContent=`${prefix}: ${formatGarsoCacheTimestamp(checkedAt)}${suffix}`; } else if (state === 'loading') { meta.textContent='Brak cache • trwa analiza'; } else { meta.textContent='Brak zapisanego cache'; } meta.title=meta.textContent; }; btn.addEventListener('vm-garso-summary-state',event => render(event.detail)); btn.addEventListener('vm-garso-count-state',() => render()); sidePanel.onOpen(() => render()); render(); return { card, head, title, titleLabel, titleWarning, meta, body, bodyContent, collapsible }; } const ESCORT_COMPACT_SUMMARY_ID='vm-escort-compact-summary'; function findEscortCompactSummaryPlacement() { const map=document.querySelector( '.content-info-col.-desc .content-location > #map' ); const locationBox=map?.parentElement; if (!locationBox?.classList.contains('content-location')) return null; return locationBox; } function createEscortCompactSummaryBox(mountOnAd=true) { const styleId=ESCORT_COMPACT_SUMMARY_ID + '-style'; if (!document.getElementById(styleId)) { appendHelperStyle(styleId,` #${ESCORT_COMPACT_SUMMARY_ID} { display: block; clear: both; flex: 0 0 100%; align-self: stretch; width: 100%; max-width: 100%; box-sizing: border-box; margin: 8px 0 12px; } #${ESCORT_COMPACT_SUMMARY_ID} > .vm-escort-compact-summary-content { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 7px; } #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip { display: inline-block; max-width: 100%; box-sizing: border-box; overflow: hidden; padding: 3px 6px; border: 1px solid color-mix(in srgb, var(--vm-ui-panel-text, #fff) 20%, transparent); border-radius: 999px; background: color-mix(in srgb, var(--vm-ui-panel-text, #fff) 5.5%, transparent); color: var(--vm-ui-panel-text, #fff); text-overflow: ellipsis; white-space: nowrap; } #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip.-ok { border-color: color-mix(in srgb, var(--vm-ui-success, #62cf7b) 65%, transparent); background: color-mix(in srgb, var(--vm-ui-success, #62cf7b) 14%, transparent); color: var(--vm-ui-success, #62cf7b); } #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip.-alert { border-color: color-mix(in srgb, var(--vm-ui-danger, #ff7777) 72%, transparent); background: color-mix(in srgb, var(--vm-ui-danger, #ff7777) 16%, transparent); color: var(--vm-ui-danger, #ff7777); } `); } detachHelperNode(document.getElementById(ESCORT_COMPACT_SUMMARY_ID)); const box=makeElement('section'); box.id=ESCORT_COMPACT_SUMMARY_ID; prepareHelperStyleApi(box).classList.add('vm-ad-summary-box'); box.setAttribute('aria-label','Skrócone podsumowanie anonsu'); const content=makeElement('div','vm-escort-compact-summary-content'); box.appendChild(content); if (!mountOnAd) return {box,content}; const placeBox=() => { const parent=findEscortCompactSummaryPlacement(); if (!parent) return false; parent.style.setProperty('flex-wrap','wrap','important'); if (box.parentNode !== parent || box.nextSibling) parent.appendChild(box); return true; }; if (!placeBox()) { let attempts=0; const timer=setInterval(() => { attempts++; if (placeBox() || attempts >= 40) clearInterval(timer); },250); } return {box,content}; } function renderEscortCompactSummary(compactSummaryUi,researchFacts,currentTagCount,emptyText='Tylko jedno ogłoszenie') { if (!compactSummaryUi) return; const {content}=compactSummaryUi; content.replaceChildren(); const addChip=(text,status='normal',title='') => { const chip=makeElement('span','vm-compact-summary-chip'); if (status === 'ok' || status === 'alert') { chip.classList.add(`-${status}`); } chip.textContent=text; if (title) chip.title=title; content.appendChild(chip); return chip; }; const escortiResults=[ researchFacts.escortiPhoneResult, researchFacts.escortiAddressResult ].filter(result => result?.status === 'ok'); const profileDates=escortiResults .flatMap(getEscortiProfileSummariesForResearch) .map(profile => formatEscortiDate(profile?.creationDate)) .filter(Boolean) .sort((a,b) => (profileDateToTime(a) || Infinity) - (profileDateToTime(b) || Infinity) ); if (profileDates.length) { addChip( `Pierwszy profil: ${formatResearchDateAge(profileDates[0])}`, 'normal', 'Najstarsza znaleziona data utworzenia profilu na Escorti.pl powiązanego z tym anonsem lub numerem telefonu. Wiek w nawiasie jest liczony do dzisiaj.' ); } const oldestPhoto=parseEscortResearchTimelineDate(researchFacts.photoRange?.oldest); if (oldestPhoto) addChip( `Najstarsze zdjęcie: ${formatResearchDateAge(formatServerMediaDate(oldestPhoto))}`, 'normal','Najstarsza odczytana data pliku zdjęcia z galerii (Last-Modified). Nie jest to potwierdzona data wykonania zdjęcia.' ); const overlaps=getEscortiCityHistoryOverlaps([ researchFacts.escortiAddressResult,researchFacts.escortiPhoneResult ]); if (overlaps.length) addChip( 'W tym samym czasie w więcej niż jednej lokalizacji','alert', 'Historia Escorti.pl zawiera różne miasta przy tej samej dacie:\n' + [...new Set(overlaps.map(day => `${day.date}: ${[...day.cities.values()].join(', ')}`))].join('\n') ); const alarmingTagCountLimit=normalizeCustomFilterInteger(SETTINGS.alarmingTagCountLimit,69,0,10000); if (currentTagCount > alarmingTagCountLimit) { addChip( `Uwaga: dużo usług / tagów (${currentTagCount})`, 'alert', `Liczba oferowanych usług / tagów (${currentTagCount}) przekracza ustawiony limit ${alarmingTagCountLimit}.` ); } const phoneReputationReport=getPhoneReputationReport( researchFacts.phoneReputationResult ); if (phoneReputationReport.sentiment === 'negative') { addChip( 'Nr tel.: opinia negatywna', 'alert', 'W sprawdzaniu numeru telefonu znaleziono negatywną opinię.' ); } else if (phoneReputationReport.sentiment === 'positive') { addChip( 'Nr tel.: opinia pozytywna', 'ok', 'W sprawdzaniu numeru telefonu znaleziono pozytywną opinię.' ); } const aggregationSource=getCompactSummaryAggregationSource(researchFacts); const comparisonSummary=aggregationSource.summary || null; const aggregationTooltip=buildCompactSummaryAggregationTooltip( aggregationSource ); const relatedAdsCount=Math.max( 0, Number(aggregationSource.result?.adLinks) || 0, Number(comparisonSummary?.totalChecked) || 0 ); if (relatedAdsCount > 1) { if (aggregationSource.loading) { addChip( 'Rozbieżności: sprawdzanie…', 'normal', `Trwa sprawdzanie rozbieżności.\n\n${aggregationTooltip}` ); } else { const warnings=[...new Set( getEscortActiveAdsConsistencyWarnings(comparisonSummary) )].filter(warning => warning !== 'Różny opis'); const comparableAds=(comparisonSummary?.results || []) .filter(item => item?.active && item.adData) .length; const activeAds=Math.max(Number(comparisonSummary?.activeCount) || 0, (comparisonSummary?.results || []).filter(item => item?.active).length); if (warnings.length) { addChip( `Rozbieżności (${comparableAds} ogł.): tak`, 'alert', 'Wykryte rozbieżności:\n' + warnings.map(warning => `• ${warning}`).join('\n') + `\n\n${aggregationTooltip}` ); } else if (comparableAds < activeAds || comparisonSummary?.unknownCount > 0) { addChip('Rozbieżności: niepełne dane','normal', `Dane porównawcze: ${comparableAds} z ${activeAds} potwierdzonych aktywnych anonsów.` + (comparisonSummary?.unknownCount > 0 ? ` Anonse o nieustalonej dostępności: ${comparisonSummary.unknownCount}.`:'') + `\n\n${aggregationTooltip}`); } else if (comparableAds >= 2) { addChip( `Rozbieżności (${comparableAds} ogł.): nie`, 'ok', `Wykryte rozbieżności: brak.\n\n${aggregationTooltip}` ); } else if (comparableAds === 1) { addChip( 'Tylko jeden aktywny anons', 'normal', 'Znaleziono tylko jeden aktywny anons, więc nie ma drugiego aktywnego anonsu do porównania.' + `\n\n${aggregationTooltip}` ); } else { addChip( `Rozbieżności (${comparableAds} ogł.): brak danych porównawczych`, 'normal', 'Nie wykryto rozbieżności, ponieważ nie znaleziono aktywnych anonsów z pełnymi danymi możliwymi do porównania.' + `\n\n${aggregationTooltip}` ); } } } if (!aggregationSource.loading) { const descriptionComparison= getEscortActiveAdsDescriptionComparison(comparisonSummary); if (descriptionComparison.same === true) { addChip( `Opis (${descriptionComparison.comparableCount} ogł.): taki sam`, 'ok', 'Porównano pełną treść opisu aktywnych anonsów. Przy porównaniu pomijane są wszystkie białe znaki: spacje, tabulatory i przejścia do nowej linii.' + `\n\n${aggregationTooltip}` ); } } const simultaneousDifferentCitiesSummary= Number(comparisonSummary?.activeCount) >= 2 && Array.isArray(comparisonSummary?.cities) && comparisonSummary.cities.length >= 2 ? comparisonSummary :null; if ( simultaneousDifferentCitiesSummary && !aggregationSource.loading ) { const activeCount=Number( simultaneousDifferentCitiesSummary.activeCount ) || 0; const cities=simultaneousDifferentCitiesSummary.cities .map(normalizeEscortCity) .filter(Boolean); addChip( `Różne lokalizacje jednocześnie: ` + `${formatResearchPanelList(cities,4)} ` + `(${activeCount} ${activeEscortAdsWord(activeCount)} ` + `${polishAdWord(activeCount)})`, 'alert', `${aggregationTooltip}\n\nRównocześnie aktywne anonse z wybranego źródła wskazują różne lokalizacje.` ); } const garsoResults=[...new Set([ researchFacts.garsoPhoneSummaryResult, researchFacts.garsoAddressSummaryResult ].filter(result => result?.stats))]; const bestGarsoResult=garsoResults.sort((a,b) => (Number(b.stats?.reviewCount) || 0) - (Number(a.stats?.reviewCount) || 0) || (Number(b.stats?.mean != null) - Number(a.stats?.mean != null)) || (Number(b.topicInfo?.length) || 0) - (Number(a.topicInfo?.length) || 0) )[0] || null; if (bestGarsoResult?.stats?.mean != null) { const reviewCount=Math.max( 0, Number(bestGarsoResult.stats.reviewCount) || 0 ); addChip( `Średnia Garso: ${bestGarsoResult.stats.mean}/10`, 'normal', reviewCount ? `Średnia z ${reviewCount} ocenionych recenzji.` :'' ); } const garsoMentionCandidates=garsoResults .flatMap(getMostFrequentGarsoMentions); const maximumMentionCount=garsoMentionCandidates.reduce( (maximum,item) => Math.max(maximum,item.count), 0 ); const popularMentions=[...new Map( garsoMentionCandidates .filter(item => item.count === maximumMentionCount) .map(item => [item.label,item]) ).values()]; if (popularMentions.length && maximumMentionCount > 0) { const allLabels=popularMentions.map(item => item.label); const popularMentionChip=addChip( '', 'normal', `Najwyższa liczba wystąpień (${maximumMentionCount}):\n` + allLabels.map(label => `• ${label}`).join('\n') ); popularMentionChip.dataset.vmGarsoPopularMentions='1'; const renderPopularMentions=visibleCount => { const visibleMentions=popularMentions.slice(0,visibleCount); const nodes=[document.createTextNode('Najczęściej: ')]; visibleMentions.forEach((item,index) => { if (index) nodes.push(document.createTextNode(', ')); const label=makeElement('span','',item.label); label.style.color=getGarsoMentionToneStyle( item.tone ).color; label.style.fontWeight='800'; nodes.push(label); }); nodes.push(document.createTextNode(` (${maximumMentionCount})`)); popularMentionChip.replaceChildren(...nodes); }; const fitPopularMentions=() => { if (!popularMentionChip.isConnected) return; const availableWidth=Math.floor( content.getBoundingClientRect().width ); if (!availableWidth) return; for ( let visibleCount=allLabels.length; visibleCount >= 1; visibleCount-- ) { renderPopularMentions(visibleCount); if ( visibleCount === 1 || popularMentionChip.scrollWidth <= availableWidth + 1 ) { break; } } }; compactSummaryUi.fitPopularGarsoMentions=fitPopularMentions; requestAnimationFrame(fitPopularMentions); if ( !compactSummaryUi.popularMentionsResizeObserver && typeof ResizeObserver === 'function' ) { compactSummaryUi.popularMentionsResizeObserver= new ResizeObserver(() => { compactSummaryUi.fitPopularGarsoMentions?.(); }); compactSummaryUi.popularMentionsResizeObserver.observe(content); } } const subtitles=[...new Set( garsoResults.flatMap(result => (result.topicInfo || []) .map(topic => getDisplayableGarsoTopicSubtitle(topic?.subtitle)) .filter(Boolean) ) )]; if (subtitles.length) { const firstSubtitle=subtitles[0].length > 100 ? `${subtitles[0].slice(0,97).trim()}…` :subtitles[0]; addChip( `Dopisek z Garso: ${firstSubtitle}${subtitles.length > 1 ? ` (+${subtitles.length - 1})` :''}`, 'normal', subtitles.join('\n') ); } if (!content.childElementCount) { addChip( researchFacts.escortiLoading ? 'Pobieranie skróconego podsumowania…' :emptyText ); } } function getMostFrequentGarsoMentions(result) { const candidates=getGarsoMentionEntries(result,true); const maximumCount=candidates.reduce( (maximum,item) => Math.max(maximum,item.count), 0 ); return maximumCount > 0 ? candidates.filter(item => item.count === maximumCount) :[]; } const PHONE_REPUTATION_CACHE_PREFIX='vm_phone_reputation_'; const PHONE_REPUTATION_PARSER_VERSION=2; const phoneReputationInFlight=new Map(); function getPhoneReputationServices() { return normalizePhoneReputationServices(SETTINGS.phoneReputationServices) .filter(service => service.enabled !== false); } function getAutoPhoneReputationSources() { return getPhoneReputationServices().filter(service => PHONE_REPUTATION_AUTO_SOURCE_IDS.has(service.id) ); } function buildPhoneReputationQuery(phone) { if (!phone?.digits) return ''; const local=phone.local || phone.digits; const variants=phone.supported ? [ `"${local}"`, `"${local.replace(/(\d{3})(\d{3})(\d{3})/,'$1 $2 $3')}"`, `"${local.replace(/(\d{3})(\d{3})(\d{3})/,'$1-$2-$3')}"`, `"+48 ${local.replace(/(\d{3})(\d{3})(\d{3})/,'$1 $2 $3')}"`, `"+48${local}"` ] :[`"+${phone.digits}"`,`"${phone.digits}"`]; return variants.join(' OR '); } function buildPhoneReputationServiceUrl(service,phone) { if (!service?.urlTemplate || !phone?.digits) return ''; const local=phone.local || phone.digits; const template=String(service.urlTemplate); const hasToken=/\{(?:phone|phone48|query)\}/.test(template); const replaced=template .replaceAll('{phone}',encodeURIComponent(local)) .replaceAll('{phone48}',encodeURIComponent(phone.e164Digits || phone.digits)) .replaceAll('{query}',encodeURIComponent(buildPhoneReputationQuery(phone))); return hasToken ? replaced:`${replaced}${encodeURIComponent(local)}`; } function getPhoneReputationSourceSignature() { return `${PHONE_REPUTATION_PARSER_VERSION}|` + getAutoPhoneReputationSources() .map(source => `${source.id}:${source.urlTemplate}`) .sort() .join('|'); } function normalizePhoneReputationPhone(value) { let digits=digitsOnly(value); if (!digits) return null; if (digits.startsWith('0048') && digits.length === 13) digits=digits.slice(2); if (digits.length === 9) digits=`48${digits}`; if (!/^48\d{9}$/.test(digits)) { return { supported:false, digits, local:null, e164Digits:digits, display:String(value || '').trim() || `+${digits}` }; } const local=digits.slice(2); return { supported:true, digits, local, e164Digits:digits, display:`+48 ${local.replace(/(\d{3})(\d{3})(\d{3})/,'$1 $2 $3')}` }; } function phoneReputationCacheKey(phone) { return `${PHONE_REPUTATION_CACHE_PREFIX}${phone?.digits || ''}`; } function getCachedPhoneReputation(phone) { if (!phone?.digits) return null; const value=readPersistentCacheValue(phoneReputationCacheKey(phone),null); if (!value || typeof value !== 'object' || !value.checkedAt) return null; if (String(value.sourceSignature || '') !== getPhoneReputationSourceSignature()) return null; return { ...value, fresh:Date.now() - Number(value.checkedAt) < getListCacheTtlMs() }; } function setCachedPhoneReputation(phone,result) { if (!phone?.digits || !result) return; writePersistentCacheValue(phoneReputationCacheKey(phone),result); } function normalizePhoneReputationPageText(doc) { return String(doc?.body?.textContent || doc?.documentElement?.textContent || '') .replace(/\u00a0/g,' ') .replace(/\s+/g,' ') .trim(); } function firstPhoneReputationInteger(text,patterns) { for (const pattern of patterns) { const match=text.match(pattern); if (!match) continue; const value=Number(String(match[1] || '').replace(/[^\d]/g,'')); if (Number.isFinite(value)) return value; } return null; } function firstPhoneReputationText(text,patterns) { for (const pattern of patterns) { const match=text.match(pattern); const value=normalizeEscortAdText(match?.[1]); if (value) return value; } return null; } function parsePhoneReputationSource(sourceId,html,phone,finalUrl='') { const doc=parseHtml(html); const text=normalizePhoneReputationPageText(doc); const compactDigits=digitsOnly(text); const containsPhone=!!( phone?.local && ( text.includes(phone.local) || compactDigits.includes(phone.local) || compactDigits.includes(phone.e164Digits) ) ); const base={ status:containsPhone ? 'ok':'unknown', comments:null, ratings:null, searches:null, reputation:null, risk:null, category:null, finalUrl:finalUrl || null }; if (sourceId === 'ktoto') { base.comments=firstPhoneReputationInteger(text,[ /Liczba komentarzy:\s*([\d.,]+(?:\s\d{3})*)/i, /komentarz(?:y|e|ów)?\s*:\s*([\d.,]+(?:\s\d{3})*)/i ]); base.ratings=firstPhoneReputationInteger(text,[ /Opinia większości:[^(]{0,80}\(\s*(\d+)\s*\)/i, /Liczba ocen:\s*(\d+)/i ]); base.reputation=firstPhoneReputationText(text,[ /Opinia większości:\s*([A-Za-zĄĆĘŁŃÓŚŹŻąćęłńóśźż -]+?)(?:\s*\(\d+\)|\s+Liczba ocen:)/i ]); } else if (sourceId === 'numerytelefonu') { base.comments=firstPhoneReputationInteger(text,[ /Numer telefonu\s+\d+\s+skomentowano\s+(\d+(?:[ .,]\d{3})*)\s+raz(?:y)?\b/i ]); base.ratings=firstPhoneReputationInteger(text,[ /Głosowa(?:ł[oa]?|li)\s+(\d+(?:[ .,]\d{3})*)\s+użytkownik/i ]); base.reputation=firstPhoneReputationText(text,[ /Ten numer jest\s+([^.!?]{1,60})[.!?]/i ]); } else if (sourceId === 'sprawdzenienumeru') { base.comments=firstPhoneReputationInteger(text,[ /(?:Opinie użytkowników|Opublikowane wpisy)\s*\(([\d\s.,]+)\)/i, /\bOpinie\s+([\d\s.,]+)\b/i, /na podstawie\s+([\d\s.,]+)\s+(?:opinii|opublikowanych wpisów)/i ]); base.searches=firstPhoneReputationInteger(text,[ /\bWyszukania\s+([\d\s.,]+)\b/i, /był sprawdzany na naszej stronie\s+([\d\s.,]+)\s+raz/i ]); base.risk=firstPhoneReputationInteger(text,[ /Wskaźnik ryzyka(?: tego numeru)? wynosi\s*(\d{1,3})\s*\/\s*100/i, /\b(\d{1,3})\s*\/\s*100\b/i ]); base.category=firstPhoneReputationText(text,[ /Najczęściej przypisywana kategoria to:\s*([^.!?]{1,80})[.!?]/i, /Użytkownicy klasyfikują połączenia z tego numeru jako:\s*([^.!?]{1,80})[.!?]/i ]); } if (containsPhone && base.comments == null) base.comments=0; return base; } async function fetchPhoneReputationSource(source,phone) { const url=buildPhoneReputationServiceUrl(source,phone); try { const response=await gmRequest({ method:'GET', url, headers:{ Accept:'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language':'pl-PL,pl;q=0.9,en;q=0.6' } }); const statusCode=Number(response?.status) || 0; if (statusCode < 200 || statusCode >= 400) { throw new Error(`HTTP ${statusCode || '?'}`); } const parsed=parsePhoneReputationSource( source.id, String(response?.responseText || ''), phone, response?.finalUrl || url ); return { id:source.id, name:source.name, url, ...parsed }; } catch (error) { recordDiagnosticError( `Sprawdzanie numeru - ${source.name}`, error ); return { id:source.id, name:source.name, url, status:'error', comments:null, ratings:null, searches:null, reputation:null, risk:null, category:null, error:formatHelperDiagnosticError(error) }; } } async function getPhoneReputationResult(phoneValue,forceRefresh=false) { const phone=normalizePhoneReputationPhone(phoneValue); const sourcesToCheck=getAutoPhoneReputationSources(); if (!phone) { return { status:'unsupported',checkedAt:Date.now(),phone:null, sourceSignature:getPhoneReputationSourceSignature(),sources:[] }; } if (!phone.supported) { return { status:'unsupported',checkedAt:Date.now(),phone, sourceSignature:getPhoneReputationSourceSignature(), sources:sourcesToCheck.map(source => ({ id:source.id, name:source.name, url:buildPhoneReputationServiceUrl(source,phone), status:'unsupported' })) }; } await ensurePersistentCacheKeys([phoneReputationCacheKey(phone)]); if (!forceRefresh) { const cached=getCachedPhoneReputation(phone); if (cached?.fresh) return {...cached,fromCache:true}; } if (!sourcesToCheck.length) { return { sourceSignature:getPhoneReputationSourceSignature(), status:'empty',checkedAt:Date.now(),phone,sources:[] }; } const inFlightKey=`${phone.digits}|${getPhoneReputationSourceSignature()}`; if (!forceRefresh && phoneReputationInFlight.has(inFlightKey)) { return phoneReputationInFlight.get(inFlightKey); } const promise=(async () => { const sources=await Promise.all( sourcesToCheck.map(source => fetchPhoneReputationSource(source,phone)) ); const result={ sourceSignature:getPhoneReputationSourceSignature(), status:sources.some(source => source.status === 'ok') ? 'ok' :(sources.some(source => source.status === 'unknown') ? 'partial':'error'), checkedAt:Date.now(), phone, sources }; setCachedPhoneReputation(phone,result); return result; })(); phoneReputationInFlight.set(inFlightKey,promise); promise.finally(() => { if (phoneReputationInFlight.get(inFlightKey) === promise) { phoneReputationInFlight.delete(inFlightKey); } }); return promise; } function formatPhoneReputationRowSummary(source) { if (source?.status === 'unsupported') return 'numer spoza PL'; if (source?.status === 'error') return 'błąd pobierania'; if (source?.status === 'unknown') return 'nie udało się odczytać wyniku'; const comments=Math.max(0,Number(source?.comments) || 0); const ratings=Math.max(0,Number(source?.ratings) || 0); if (!comments && !ratings) return 'brak opinii'; const parts=[]; const sentiment=getPhoneReputationSourceSentiment(source); const label=sentiment === 'negative' ? 'Negatywny':(sentiment === 'positive' ? 'Pozytywny':normalizeEscortAdText(source?.reputation || source?.category)); if (label) parts.push(label); if (comments) parts.push(formatPolishCount(comments,'komentarz','komentarze','komentarzy')); if (ratings) parts.push(formatPolishCount(ratings,'ocena','oceny','ocen')); return parts.join(' • '); } function getPhoneReputationSourceSentiment(source) { if (source?.status !== 'ok') return null; const text=`${source.reputation || ''} ${source.category || ''}`; const negativePattern=/negatyw|niebezpie|oszust|spam|wyłudz|naciąg|podejrz|phishing|scam|fraud/i; const positivePattern=/pozytyw|dobr(?:y|a|e)|super|bezpiecz|zaufan|wiarygod|polecan/i; if ((Number(source.risk) || 0) >= 50 || negativePattern.test(text)) return 'negative'; if (positivePattern.test(text)) return 'positive'; return null; } function getPhoneReputationReport(result) { const sources=Array.isArray(result?.sources) ? result.sources:[]; const details=sources.map(source => `${source.name || source.id}: ${formatPhoneReputationRowSummary(source)}` ); const negative=sources.filter(source => getPhoneReputationSourceSentiment(source) === 'negative'); const positive=sources.filter(source => getPhoneReputationSourceSentiment(source) === 'positive'); const sentiment=negative.length ? 'negative':(positive.length ? 'positive':null); return { sentiment, title:details.join('\n'), badgeText:sentiment === 'negative' ? 'ocena negatywna' :(sentiment === 'positive' ? 'ocena pozytywna':'') }; } function createPhoneReputationLinksSection( phoneValue, onResult=null, cacheOnly=false ) { const phone=normalizePhoneReputationPhone(phoneValue); const section=makeElement('section','vm-phone-reputation-links',null,{id:'vm-phone-reputation-links'}); const head=makeElement('div','vm-phone-reputation-head'); const title=makeElement('span','vm-phone-reputation-title vm-research-section-title','Sprawdzenie nr. tel.'); const badge=makeElement('span','vm-phone-reputation-badge',null,{hidden:true}); head.append(title,badge); const body=makeElement('div','vm-phone-reputation-body'); const rows=makeElement('div','vm-phone-reputation-rows'); body.appendChild(rows); section.append(head,body); makeEscortSummarySectionCollapsible( section,head,body,'phone-reputation-links',false,true ); const configuredServices=getPhoneReputationServices(); const rowById=new Map(); for (const service of configuredServices) { const row=createResearchPanelResultRow( service.name, buildPhoneReputationServiceUrl(service,phone) ); if (PHONE_REPUTATION_AUTO_SOURCE_IDS.has(service.id)) { setTwoLineButton( row, service.name, cacheOnly ? 'brak wyniku w cache':SETTINGS.autoPhoneReputationSearch ? 'sprawdzanie / cache…' :'kliknij, aby sprawdzić' ); setButtonColor( row, cacheOnly ? 'stale':SETTINGS.autoPhoneReputationSearch ? 'checking':'empty' ); } else { setTwoLineButton( row, service.name, service.id === 'google' ? 'wyszukaj ręcznie' :'kliknij, aby sprawdzić' ); setButtonColor(row,'empty'); } rowById.set(service.id,row); rows.appendChild(row); } const applyResult=result => { const report=getPhoneReputationReport(result); badge.hidden=!report.badgeText; badge.textContent=report.badgeText; badge.className=`vm-phone-reputation-badge${report.sentiment ? ` -${report.sentiment}`:''}`; head.title=report.title || 'Kliknij, aby rozwinąć sekcję'; for (const source of result?.sources || []) { const row=rowById.get(source.id); if (!row) continue; const configuredName=configuredServices.find( service => service.id === source.id )?.name; setTwoLineButton( row, configuredName || source.name || source.id, formatPhoneReputationRowSummary(source) ); const sentiment=getPhoneReputationSourceSentiment(source); setButtonColor( row, source.status === 'error' ? 'error':((Number(source.comments) || 0) > 0 ? 'found':'empty') ); const value=row.querySelector('.vm-research-panel-value'); if (value && sentiment === 'negative') value.style.color='var(--vm-ui-danger,#ff8b8b)'; if (value && sentiment === 'positive') value.style.color='var(--vm-ui-success,#7ee497)'; if (source.error) row.title=source.error; } onResult?.(result); }; const cached=phone ? getCachedPhoneReputation(phone):null; if (cached) applyResult({...cached,fromCache:cached.fresh}); const refresh=async (forceRefresh=false) => { if (!SETTINGS.autoPhoneReputationSearch) { if (!cached) onResult?.(null); return cached; } for (const service of getAutoPhoneReputationSources()) { const row=rowById.get(service.id); if (!row) continue; setTwoLineButton(row,service.name,forceRefresh ? 'odświeżanie…':'sprawdzanie…'); setButtonColor(row,'checking'); } const result=await getPhoneReputationResult(phoneValue,forceRefresh); applyResult(result); return result; }; if ( !cacheOnly && SETTINGS.autoPhoneReputationSearch && !cached?.fresh ) { refresh(false).catch(error => { log('Błąd sprawdzania numeru telefonu',error); }); } else if (cached) { onResult?.(cached); } return {section,refresh,applyResult}; } function createEscortResearchPanel(phone,adUrl,{archiveData=null,archiveDocument=null}={}) { const archiveRuntime=!!archiveDocument || isEscortCachedArchiveRuntime(); const setResearchControlDisabled=(control,disabled,opacity='.55') => { if ('disabled' in control) control.disabled=disabled; control.style.opacity=disabled ? opacity:'1'; control.style.cursor=disabled ? 'default':'pointer'; }; const setResearchRefreshState=(button,busy,title) => { button.disabled=busy; button.textContent=busy ? '⟳':'↻'; setControlTitle(button,title); button.style.opacity=busy ? '.72':'1'; }; const sidePanel=createEscortSummarySidePanel(); const panel=prepareHelperStyleApi(makeElement('section')); panel.id='vm-escort-research-panel'; panel.setAttribute('aria-label','Linki do powiązanych wyników'); const header=makeElement('div','vm-research-panel-main-header'); const headerTitle=makeElement('div','vm-research-panel-main-title','Linki'); const refreshButton=makeButton('vm-research-panel-refresh','↻',{title:'Pobierz ponownie dane z sekcji „Linki” (Garsoniera: tylko sprawdzenie, czy są tematy; sprawdzanie numeru: jeśli włączone automatyczne wyszukiwanie)'}); refreshButton.setAttribute('aria-label',refreshButton.title); header.append(headerTitle,refreshButton); panel.appendChild(header); const normalizedPhone=normalizeEscortCachedPhone(phone) || ''; phone=normalizedPhone ? phone:''; const phoneSearch=normalizedPhone; const adId=parseAdIdFromUrl(adUrl); const garsoPhoneTerm=getGarsoSearchTerms(phone,'phone')[0] || ''; const garsoAddressTerm=buildGarsoAdLinkSearchTerm(adId); const garsoCombinedSummaryTerm=buildCombinedGarsoSummaryTerm( garsoPhoneTerm, garsoAddressTerm ); const garsoPhoneRow=createGarsoButton('nr tel.',garsoPhoneTerm,'phone'); const garsoAddressRow=createGarsoButton('adres anonsu',garsoAddressTerm,'address'); const garsoCombinedSummaryState=makeElement('button'); garsoCombinedSummaryState.dataset.garsoDisplayTerm='nr tel. + adres anonsu'; garsoCombinedSummaryState.dataset.garsoSearchMode='combined'; garsoCombinedSummaryState.dataset.garsoStatus='manual'; const cachedCombinedGarsoSummary=restoreGarsoSummaryFromCache( garsoCombinedSummaryState, garsoCombinedSummaryTerm, 'combined' ); if (archiveRuntime && !cachedCombinedGarsoSummary) { const saved=[['phone',garsoPhoneTerm,'nr tel.'],['address',garsoAddressTerm,'adres anonsu']] .map(([mode,term,label]) => ({mode,term,label,cached:getGarsoSummaryCache(mode,term)})) .filter(item => item.cached).sort((a,b) => b.cached.checkedAt - a.cached.checkedAt)[0]; if (saved) { garsoCombinedSummaryState.dataset.garsoSearchMode=saved.mode; garsoCombinedSummaryState.dataset.garsoDisplayTerm=saved.label; restoreGarsoSummaryFromCache(garsoCombinedSummaryState,saved.term,saved.mode); } } panel.append(garsoPhoneRow,garsoAddressRow); const cachedGarsoPhone=getGarsoSearchCountCache('phone',garsoPhoneTerm); const cachedGarsoAddress=getGarsoSearchCountCache('address',garsoAddressTerm); const researchFacts=archiveRuntime ? getEscortArchiveResearchFactsFromCache( adId, adUrl, phone, garsoCombinedSummaryState, cachedGarsoPhone, cachedGarsoAddress, archiveData ) :{ garsoPhoneCount:cachedGarsoPhone?.fresh ? cachedGarsoPhone.count:null, garsoAddressCount:cachedGarsoAddress?.fresh ? cachedGarsoAddress.count:null, escortiPhoneResult:null, escortiAddressResult:null, escortiPhoneSummary:null, escortiAddressSummary:null, escortPhoneSummary:null, sameEscortiResult:null, escortiWarnings:[], escortiLoading:true, escortPhoneLoading:true, photoRange:null, photoDatesLoading:true, garsoPhoneSummaryResult: garsoCombinedSummaryState._vmGarsoSummaryResult || null, garsoAddressSummaryResult: garsoCombinedSummaryState._vmGarsoSummaryResult || null, datePosted: extractEscortClubStructuredAdData(document).datePosted || null, currentAdUrl:adUrl, phoneReputationResult:(() => { if (!SETTINGS.showPhoneReputationLinks) return null; const normalizedPhone=normalizePhoneReputationPhone(phone); return normalizedPhone ? getCachedPhoneReputation(normalizedPhone) :null; })(), garsoPhoneReviewDates:getGarsoSummaryReviewDates(garsoCombinedSummaryState), garsoAddressReviewDates:getGarsoSummaryReviewDates(garsoCombinedSummaryState) }; const compactSummaryUi=SETTINGS.showCompactSummaryAboveDescription ? createEscortCompactSummaryBox(!archiveDocument) :null; if (compactSummaryUi && archiveDocument) archiveDocument.querySelector('.content-info-col.-desc .content-location')?.appendChild(compactSummaryUi.box); const photoHistogramHost=makeElement('div'); sidePanel.content.appendChild(photoHistogramHost); const renderCompactResearchSummary=() => { if (compactSummaryUi) renderEscortCompactSummary(compactSummaryUi,researchFacts, archiveData ? (archiveData.tags || []).length:extractEscortClubTags(document).length, archiveRuntime ? 'Brak zapisanych danych do podsumowania':'Tylko jedno ogłoszenie'); }; let updateGarsoExtendedRecommendation=() => {},updateGarsoExtendedButton=() => {}; let renderOtherCities=() => {}; const renderResearchFacts=() => { try { photoHistogramHost.replaceChildren( createEscortResearchDateTimeline( researchFacts ) ); } catch (error) { log( 'Nie udało się wyrenderować osi czasu w panelu bocznym', error ); photoHistogramHost.replaceChildren(); } try { renderCompactResearchSummary(); } catch (error) { log( 'Nie udało się wyrenderować skróconego podsumowania', error ); } try { renderOtherCities(); } catch (error) { log( 'Nie udało się wyrenderować sekcji Inne', error ); } try { updateGarsoExtendedButton(); } catch (error) { log('Nie udało się zaktualizować podpowiedzi Garso',error); } }; const bindGarsoCountForFacts=(row,key) => { row.addEventListener('vm-garso-count-state',event => { researchFacts[key]=Number.isFinite(event.detail?.count) ? Number(event.detail.count) :null; renderResearchFacts(); }); }; bindGarsoCountForFacts(garsoPhoneRow,'garsoPhoneCount'); bindGarsoCountForFacts(garsoAddressRow,'garsoAddressCount'); if (archiveRuntime) { for (const [row,cached] of [ [garsoPhoneRow,cachedGarsoPhone], [garsoAddressRow,cachedGarsoAddress] ]) { if (!cached) continue; row.dataset.garsoStatus='result'; row.dataset.garsoCount=String(cached.count); row.dataset.garsoCountCheckedAt=String(cached.checkedAt); row.dataset.garsoCacheState=cached.fresh ? 'fresh':'stale'; renderGarsoButton(row); } } garsoCombinedSummaryState.addEventListener( 'vm-garso-summary-state', event => { const reviewDates={ first:event.detail?.firstReviewDate || null, last:event.detail?.lastReviewDate || null, firstUrl:event.detail?.firstReviewUrl || null, lastUrl:event.detail?.lastReviewUrl || null }; researchFacts.garsoPhoneReviewDates={...reviewDates}; researchFacts.garsoAddressReviewDates={...reviewDates}; if (event.detail?.result) { researchFacts.garsoPhoneSummaryResult=event.detail.result; researchFacts.garsoAddressSummaryResult=event.detail.result; } renderResearchFacts(); } ); renderResearchFacts(); const escortiPhoneUrl= buildEscortiSearchUrl(phoneSearch); const escortiAddressUrl= buildEscortiSearchUrl(adUrl); const escortClubPhoneUrl=buildEscortClubPhoneSearchUrl(phone); const escortiAddressRow=createResearchPanelResultRow( 'Escorti.pl', escortiAddressUrl ); const escortiPhoneRow=createResearchPanelResultRow( 'Escorti.pl - nr tel.', escortiPhoneUrl ); escortiPhoneRow.style.display='none'; const escortPhoneRow=createResearchPanelResultRow( 'Escort - nr tel.', escortClubPhoneUrl ); escortPhoneRow.title= 'Aktualnie znalezione na Escort.club anonse z tym samym numerem telefonu.'; panel.append(escortiAddressRow,escortiPhoneRow,escortPhoneRow); const warningBox=makeElement('section','vm-escort-summary-inconsistencies vm-escort-summary-status-ok'); const warningTitle=makeElement('div','vm-escort-summary-inconsistencies-title'); const warningTitleText=makeElement('span','','Niezgodności'); const warningStatusBadge=makeElement('span','vm-inconsistencies-status-badge'); warningTitle.append(warningTitleText,warningStatusBadge); const warningContent=makeElement('div','vm-escort-summary-inconsistencies-content'); const warningPreview=makeElement('div','vm-summary-section-preview'); warningBox.append(warningTitle,warningPreview,warningContent); makeEscortSummarySectionCollapsible( warningBox, warningTitle, warningContent, 'inconsistencies', true ); const garsoSummaryCard=createGarsoSideSummaryCard( sidePanel, garsoCombinedSummaryState, 'Garso - recenzje' ); const { section:otherSection, body:otherBody }=createEscortSummarySideCard( sidePanel, 'Inne', 'other', false, 'vm-escort-summary-other' ); const otherPreview=makeElement('div','vm-summary-section-preview'); otherSection.insertBefore(otherPreview,otherBody); const otherCities=makeElement( 'div', 'vm-escort-summary-other-cities vm-summary-other-cities-framed' ); otherBody.appendChild(otherCities); renderOtherCities=() => { const cities= getEscortiHistoricalCityStats([ researchFacts.escortiPhoneResult, researchFacts.escortiAddressResult ]); const summaries=[researchFacts.escortiAddressSummary, researchFacts.escortiPhoneSummary,researchFacts.escortPhoneSummary].filter(Boolean); const activeCities=[...new Map(summaries.flatMap(summary => summary.cities || []) .map(normalizeEscortCity).filter(Boolean) .map(city => [normalizeEscortCityKey(city),city])).values()]; const loading=researchFacts.escortiLoading || researchFacts.escortPhoneLoading; const overlaps=getEscortiCityHistoryOverlaps([ researchFacts.escortiAddressResult,researchFacts.escortiPhoneResult ]); const activeCitiesText=`W aktywnych anonsach: ${activeCities.join(', ') || (loading ? 'sprawdzam…':summaries.length ? 'brak ustalonych miast':'brak danych')}`; otherPreview.classList.toggle('-warning',overlaps.length > 0); otherPreview.textContent=overlaps.length ? `⚠ W tym samym czasie w więcej niż jednej lokalizacji · ${activeCitiesText}` :activeCitiesText; otherCities.replaceChildren(); for (const day of overlaps) { const line=makeElement('div','vm-escort-summary-other-label', `⚠ Ta sama data przy różnych miastach: ${day.date} - ${[...day.cities.values()].join(', ')}`); makeEscortTimelineElementClickable(line,day.url,'Otwórz historię lokalizacji w profilu Escorti'); otherCities.appendChild(line); } const heading=makeElement( 'div', 'vm-escort-summary-other-label', `Miasta w historii Escorti.pl (${ cities.length }):` ); otherCities.appendChild(heading); if (!cities.length) { otherCities.appendChild( makeElement( 'div', '', researchFacts.escortiLoading ? 'Pobieranie danych Escorti.pl…' :'Brak znalezionych miast w danych Escorti.pl.' ) ); return; } const list=makeElement('div','vm-escort-summary-other-city-list'); for (const item of cities) { const chip=makeElement('span','vm-escort-summary-other-city',`${item.city} (${item.count})`,{title:item.count > 1 ? `Znalezione wystąpienia tego miasta w historii Escorti.pl: ${item.count}` :'Miasto znalezione w historii Escorti.pl'}); list.appendChild(chip); } otherCities.appendChild(list); }; renderOtherCities(); garsoSummaryCard.head.classList.add('vm-garso-main-head'); garsoSummaryCard.title.classList.add('vm-garso-main-title'); garsoSummaryCard.meta.classList.add('vm-garso-main-meta'); const garsoCollapseHint=garsoSummaryCard.head.querySelector( '.vm-escort-summary-collapse-hint' ); if (garsoCollapseHint) garsoCollapseHint.style.top='16px'; const garsoDownloadButton=makeButton('vm-garso-download-button','↻',{title:'Odśwież i przeanalizuj treść tematów Garsoniery dla numeru telefonu i adresu anonsu.'}); garsoDownloadButton.setAttribute('aria-label',garsoDownloadButton.title); garsoSummaryCard.head.appendChild(garsoDownloadButton); sidePanel.content.prepend(warningBox); const footer=makeElement('div','vm-research-panel-footer'); const phoneReputationLinksUi=SETTINGS.showPhoneReputationLinks ? createPhoneReputationLinksSection( phone, result => { researchFacts.phoneReputationResult=result; renderCompactResearchSummary(); }, archiveRuntime ) :null; if (phoneReputationLinksUi) footer.appendChild(phoneReputationLinksUi.section); const garsoExtendedPanel=makeElement('section','',null,{id:'vm-garso-extended-panel'}); const garsoExtendedHead=makeElement('div','vm-garso-extended-head'); const garsoExtendedTitle=makeElement('span','vm-research-section-title','Garso - szukanie rozszerzone'); const garsoExtendedTitleHelp='Od razu pokazuje zapisane wyniki, także ze zwykłych wyszukiwań powiązanych numerów i adresów. „Szukaj” sprawdza Garsonierę na żywo. Wyszukuje na Garso linki anonsów z Escorti.pl, pełny numer 123-456-789 i zapis 123-456-xxx (dosłowne iksy), a także historyczne numery zapisane w cache powiązanych anonsów Escort.club. Jeśli standardowe warianty danego numeru nic nie znajdą, próbuje jeszcze zapisu 123 456 789. W aktywnych anonsach odczytuje aktualny telefon i sprawdza go tak samo. Dodatkowo w pierwszym poście tematów znalezionych w tym kroku szuka innego pełnego numeru lub linku Escort.club i sprawdza go jeden poziom dalej. Nie analizuje już pierwszych postów tematów znalezionych tym dodatkowym krokiem. Nie powtarza zapytań. Przycisk „Otwórz wyniki” otwiera unikalne strony wyników w nowych kartach.'; garsoExtendedTitle.title=garsoExtendedTitleHelp; garsoExtendedTitle.style.flex='1 1 auto'; garsoExtendedTitle.style.minWidth='0'; const garsoExtendedStatusBadge=makeElement('span','vm-garso-extended-status-badge','✓',{hidden:true}); garsoExtendedHead.append( garsoExtendedTitle, garsoExtendedStatusBadge ); const garsoExtendedRecommendation=makeButton('vm-garso-extended-recommendation',null,{hidden:true}); const garsoExtendedBody=makeElement('div','vm-garso-extended-body'); const garsoExtendedControls=makeElement('div','vm-garso-extended-controls'); const garsoExtendedButton=makeButton('vm-garso-extended-action -search'); garsoExtendedButton.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg>'; const garsoExtendedButtonLabel=makeElement('span','','Szukaj'); garsoExtendedButton.appendChild(garsoExtendedButtonLabel); const garsoExtendedOpenButton=makeButton('vm-garso-extended-action'); garsoExtendedOpenButton.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 3h7v7M21 3 10 14M10 3H3v18h18v-7"/></svg>'; const garsoExtendedOpenLabel=makeElement('span','','Otwórz wyniki'); const garsoExtendedOpenCount=makeElement('span','vm-garso-extended-count'); garsoExtendedOpenButton.append(garsoExtendedOpenLabel,garsoExtendedOpenCount); const garsoExtendedProgress=makeElement('progress','vm-garso-extended-progress'); garsoExtendedProgress.setAttribute('aria-label','Postęp szukania rozszerzonego'); const garsoExtendedMessage=makeElement('div','vm-garso-extended-message'); garsoExtendedMessage.setAttribute('role','status'); garsoExtendedControls.append(garsoExtendedButton,garsoExtendedOpenButton); garsoExtendedBody.append(garsoExtendedControls,garsoExtendedProgress,garsoExtendedMessage); garsoExtendedPanel.append(garsoExtendedHead,garsoExtendedRecommendation,garsoExtendedBody); footer.appendChild(garsoExtendedPanel); panel.appendChild(footer); const garsoExtendedCollapsible=makeEscortSummarySectionCollapsible( garsoExtendedPanel, garsoExtendedHead, garsoExtendedBody, 'garso-extended', false, false ); garsoExtendedRecommendation.addEventListener('click',event => {stopUiEvent(event); garsoExtendedCollapsible?.setExpanded(true);}); let escortiRefreshRun=0; let escortPhoneRefreshRun=0; let escortPhoneWarningSummary=archiveRuntime ? researchFacts.escortPhoneSummary:null; let escortiAllAdUrls=[]; let escortiPhoneAdUrls=getListCache(adId,'phone-escorti')?.adUrls || []; let escortiAddressAdUrls=getListCache(adId,'address-escorti')?.adUrls || []; let escortiAdUrlSourcesReady=archiveRuntime; let garsoExtendedResults=null; let garsoExtendedCheckRunning=false; let garsoExtendedCancelToken=null; let garsoExtendedOpenRunning=false; let garsoExtendedCheckCompleted=false; const getGarsoExtendedAdUrls=() => { return [...new Set( [adUrl,...escortiPhoneAdUrls,...escortiAddressAdUrls].map(url => { const normalized=normalizeEscortiAdUrl(url); if (normalized) return normalized; const linkedAdId=parseAdIdFromUrl(url); return linkedAdId ? `https://pl.escort.club/anons/${linkedAdId}.html` :null; }).filter(Boolean) )]; }; const getGarsoExtendedPhoneDigits=() => { const phones=new Set([normalizeEscortCachedPhone(phone)].filter(Boolean)); for (const adUrl of escortiAllAdUrls) { const linkedAdId=parseAdIdFromUrl(adUrl); if (!linkedAdId) continue; const data=getStoredEscortAdData(linkedAdId) || getWatchedAdDataMirror(linkedAdId); for (const source of [data,data?.visitSnapshot,data?.watchSnapshot]) { const digits=getEscortAdPhoneDigits(source); if (digits) phones.add(digits); } } return [...phones]; }; const getGarsoExtendedPhoneTargets=() => getGarsoExtendedPhoneDigits().map(digits => getGarsoExtendedPhoneTarget(digits)); const getGarsoExtendedPhoneTermsKey=() => getGarsoExtendedPhoneTargets().map(target => target.searchTerm).sort().join('|'); const getGarsoExtendedSearchTargets=() => [ ...getGarsoExtendedPhoneTargets(), ...escortiAllAdUrls.map(adUrl => ({adUrl,searchTerm:buildGarsoAdLinkSearchTerm(parseAdIdFromUrl(adUrl)) || adUrl})) ]; const readGarsoFirstPostTrails=async (topicUrl,cancelToken) => { const context=getGarsoTopicPageContext(topicUrl); if (!context) return {phones:[],adUrls:[]}; const response=await garsoTopicRequest({method:'GET',url:context.baseUrl,timeout:20000,cancelToken}); cancelToken?.throwIfCancelled(); const finalContext=getGarsoTopicPageContext(response.finalUrl || context.baseUrl); if (response.status < 200 || response.status >= 400 || finalContext?.id !== context.id) throw new Error(`Garso: nie udało się odczytać pierwszego posta tematu ${context.id}`); const doc=parseHtml(response.responseText || ''); const block=doc.querySelector('.post_block, [id^="post_id_"]'); if (!block) return {phones:[],adUrls:[]}; const content=getGarsoPostContent(block); const text=cleanPostElement(content); const phones=[...new Set(findGarsoPhoneTokens(text).map(token => normalizePhoneReputationPhone(token.value)?.local).filter(Boolean))]; const adUrls=new Set(); const addAdUrl=value => { try { const url=new URL(value,context.baseUrl); const normalized=normalizeEscortiAdUrl(url.href); if (normalized && parseAdIdFromUrl(normalized)) adUrls.add(normalized); } catch {} }; for (const link of content.querySelectorAll('a[href]')) { if (link.closest('blockquote,.ipsBlockquote,.citation,.quote,.bbc_quote,.signature')) continue; addAdUrl(link.getAttribute('href')); } for (const match of text.matchAll(/(?:https?:\/\/|www\.|(?:[a-z]{2}\.)?escort\.club\/)[^\s<>"'“”„]+/giu)) { addAdUrl(/^https?:\/\//i.test(match[0]) ? match[0]:`https://${match[0]}`); } return {phones,adUrls:[...adUrls]}; }; let garsoExtendedStatusText=''; let garsoExtendedPhoneScanChecked=false; let garsoExtendedCheckedAdUrls=[]; let hasCachedGarsoExtendedInfo=false; let garsoExtendedCachedPhoneTermsKey=''; let garsoExtendedCachedResults=[]; const applyGarsoExtendedCache=cached => { const matchesMode=cached?.searchMode === 'combined'; hasCachedGarsoExtendedInfo=!!(matchesMode && cached?.checkedAt); garsoExtendedCachedPhoneTermsKey=String(cached?.phoneTermsKey || ''); garsoExtendedPhoneScanChecked=false; garsoExtendedCheckedAdUrls=cached?.adUrls || []; garsoExtendedCachedResults=matchesMode ? (cached?.results || []) :[]; return matchesMode; }; const syncGarsoExtendedAdUrls=() => { if (!escortiAdUrlSourcesReady) return; escortiAllAdUrls=getGarsoExtendedAdUrls(); garsoExtendedPhoneScanChecked=hasCachedGarsoExtendedInfo && garsoExtendedCachedPhoneTermsKey === getGarsoExtendedPhoneTermsKey(); }; const cachedGarsoExtended=getGarsoExtendedCache(adId); applyGarsoExtendedCache(cachedGarsoExtended); escortiAllAdUrls=[...new Set([ ...getGarsoExtendedAdUrls(),...(cachedGarsoExtended?.adUrls || []) ].filter(Boolean))]; garsoExtendedPhoneScanChecked=hasCachedGarsoExtendedInfo && garsoExtendedCachedPhoneTermsKey === getGarsoExtendedPhoneTermsKey(); const getGarsoExtendedResults=() => garsoExtendedResults ?? getCachedGarsoExtendedResults(getGarsoExtendedSearchTargets(),garsoExtendedCachedResults); updateGarsoExtendedButton=(statusText=null) => { if (statusText !== null) garsoExtendedStatusText=statusText; const count=escortiAllAdUrls.length; const pageCount=getUniqueGarsoResultPages(getGarsoExtendedResults()).length; const canSearch=escortiAdUrlSourcesReady && (count > 0 || getGarsoExtendedPhoneTargets().length > 0); const completed=(garsoExtendedResults === null ? hasCachedGarsoExtendedInfo && garsoExtendedPhoneScanChecked:garsoExtendedCheckCompleted) && activeAdsCacheKey(garsoExtendedCheckedAdUrls) === activeAdsCacheKey(escortiAllAdUrls); garsoExtendedStatusBadge.hidden=!completed || garsoExtendedCheckRunning || (garsoExtendedResults === null && !cachedGarsoExtended?.fresh); garsoExtendedStatusBadge.title=`Szukanie zakończone • ${formatGarsoUniquePageCount(pageCount)}`; setResearchControlDisabled(garsoExtendedButton,!garsoExtendedCheckRunning && (!canSearch || garsoExtendedOpenRunning)); garsoExtendedButton.classList.toggle('-cancel',garsoExtendedCheckRunning); garsoExtendedButtonLabel.textContent=garsoExtendedCheckRunning ? 'Przerwij':'Szukaj'; garsoExtendedButton.title=garsoExtendedCheckRunning ? 'Przerwij szukanie' :!escortiAdUrlSourcesReady ? 'Czekam na listę Escorti.pl' :!canSearch ? 'Brak dodatkowych anonsów i numeru telefonu':'Wyszukaj na żywo na Garso, ponownie sprawdzając numery i adresy'; setResearchControlDisabled(garsoExtendedOpenButton,!pageCount || garsoExtendedCheckRunning || garsoExtendedOpenRunning); garsoExtendedOpenLabel.textContent=garsoExtendedOpenRunning ? 'Otwieranie…':'Otwórz wyniki'; garsoExtendedOpenCount.textContent=String(pageCount); garsoExtendedOpenButton.title=pageCount ? `Otwórz ${formatPolishCount(pageCount,'kartę','karty','kart')} z wynikami Garso (bez powtarzających się stron)` :'Brak znalezionych stron do otwarcia'; garsoExtendedProgress.hidden=!garsoExtendedCheckRunning; garsoExtendedMessage.textContent=garsoExtendedResults === null && pageCount ? `Zapisane wyniki: ${formatGarsoUniquePageCount(pageCount)}. „Szukaj” sprawdzi Garsonierę na żywo.` :garsoExtendedStatusText || (!escortiAdUrlSourcesReady ? 'Czekam na listę Escorti.pl…' :canSearch && garsoExtendedResults === null ? 'Brak wyników w cache. Kliknij „Szukaj”, aby sprawdzić Garsonierę.' :canSearch ? '':'Brak dodatkowych anonsów i numeru telefonu do sprawdzenia.'); updateGarsoExtendedRecommendation(); }; updateGarsoExtendedRecommendation=() => { const escortiAdCount=escortiAdUrlSourcesReady ? escortiAllAdUrls.length:0; const currentScopeCompleted=(garsoExtendedResults === null ? hasCachedGarsoExtendedInfo && garsoExtendedPhoneScanChecked :garsoExtendedCheckCompleted) && activeAdsCacheKey(garsoExtendedCheckedAdUrls) === activeAdsCacheKey(escortiAllAdUrls); const show=!archiveRuntime && researchFacts.garsoPhoneCount === 0 && researchFacts.garsoAddressCount === 0 && escortiAdCount > 1 && !currentScopeCompleted && !garsoExtendedCheckRunning && !getGarsoExtendedResults().length; garsoExtendedTitle.textContent='Garso - szukanie rozszerzone'; garsoExtendedTitle.title=garsoExtendedTitleHelp; garsoExtendedRecommendation.hidden=!show; if (show) garsoExtendedRecommendation.innerHTML=`<strong>💡 Warto sprawdzić szerzej</strong><span>Zwykłe wyszukiwanie po numerze i adresie nic nie znalazło, ale Escorti.pl wskazuje ${escortiAdCount} ${polishAdWord(escortiAdCount)}. Mogę sprawdzić te linki, odszukać inne numery telefonów oraz sprawdzić alternatywne formatowanie numerów.</span>`; }; updateGarsoExtendedRecommendation(); updateGarsoExtendedButton(); const runGarsoExtendedAction=async () => { if (garsoExtendedCheckRunning) { garsoExtendedCancelToken?.cancel('Przerwano szukanie rozszerzone Garso'); updateGarsoExtendedButton('Przerywanie…'); return; } if (garsoExtendedButton.disabled || garsoExtendedOpenRunning) return; const targets=getGarsoExtendedSearchTargets().map(target => ({...target,trailDepth:0})); const adUrls=[...escortiAllAdUrls]; const searchedPhones=new Set(getGarsoExtendedPhoneDigits()); const phoneDepth=new Map([...searchedPhones].map(digits => [digits,0])); const searchedTerms=new Set(targets.map(target => target.searchTerm)); const phoneStandardChecks=new Map(); const fallbackPhonesQueued=new Set(); const firstLevelTopicUrls=new Set(); let trailQueued=false; let total=targets.length; garsoExtendedCheckRunning=true; garsoExtendedCancelToken=createOperationCancelToken('Garso - szukanie rozszerzone'); refreshButton.disabled=true; refreshButton.style.opacity='.55'; garsoExtendedResults=[...getGarsoExtendedResults()]; const freshResults=new Map(); const keepResult=(result,target) => { if (result?.status !== 'ok') return; garsoExtendedResults=garsoExtendedResults.filter(item => item.searchTerm !== target.searchTerm); if (Number(result.count) > 0) { const entry={...result,...target}; garsoExtendedResults.push(entry); freshResults.set(target.searchTerm,entry); } else { freshResults.delete(target.searchTerm); } }; garsoExtendedCheckCompleted=false; garsoExtendedProgress.max=Math.max(1,total); garsoExtendedProgress.value=0; let failed=0,phoneErrors=0,trailErrors=0,newPhones=0,newLinks=0,checked=0; let finalStatus=''; const addPhoneTargets=(digits,depth=0,insertAt=null) => { digits=normalizeEscortCachedPhone(digits); if (!/^\d{9}$/.test(digits || '')) return 0; const wasNew=!searchedPhones.has(digits); if (wasNew) {searchedPhones.add(digits); newPhones++;} if (!phoneDepth.has(digits) || depth < phoneDepth.get(digits)) phoneDepth.set(digits,depth); const next=getGarsoExtendedPhoneTarget(digits,depth); if (searchedTerms.has(next.searchTerm)) return 0; searchedTerms.add(next.searchTerm); if (insertAt == null) targets.push(next); else targets.splice(insertAt,0,next); return 1; }; const addAdTarget=(adUrl,depth=1) => { const normalized=normalizeEscortiAdUrl(adUrl); const linkedAdId=parseAdIdFromUrl(normalized); const searchTerm=linkedAdId ? buildGarsoAdLinkSearchTerm(linkedAdId):''; if (!normalized || !searchTerm || searchedTerms.has(searchTerm)) return false; searchedTerms.add(searchTerm); targets.push({adUrl:normalized,searchTerm,trailDepth:depth}); return true; }; const queuePhoneFallbacks=() => { let added=0; for (const digits of searchedPhones) { if (fallbackPhonesQueued.has(digits)) continue; const state=phoneStandardChecks.get(digits); if (!state || !state.checked) continue; fallbackPhonesQueued.add(digits); if (state.found || state.error) continue; const spaced=`"${digits.replace(/(\d{3})(\d{3})(\d{3})/,'$1 $2 $3')}"`; if (searchedTerms.has(spaced)) continue; searchedTerms.add(spaced); targets.push({adUrl:null,searchTerm:spaced,phoneDigits:digits,phoneSearchKind:'spaces-fallback',trailDepth:phoneDepth.get(digits) || 0}); added++; } return added; }; const queueFirstPostTrails=async () => { if (trailQueued) return 0; trailQueued=true; let added=0,scanned=0; const topicUrls=[...firstLevelTopicUrls]; for (const topicUrl of topicUrls) { garsoExtendedCancelToken.throwIfCancelled(); scanned++; updateGarsoExtendedButton(`Sprawdzam pierwszy post ${scanned}/${topicUrls.length} • szukam innych numerów i linków`); try { const trails=await readGarsoFirstPostTrails(topicUrl,garsoExtendedCancelToken); for (const digits of trails.phones) added += addPhoneTargets(digits,1); for (const trailAdUrl of trails.adUrls) { if (addAdTarget(trailAdUrl,1)) {added++; newLinks++;} const linkedAdId=parseAdIdFromUrl(trailAdUrl); const data=linkedAdId ? (getStoredEscortAdData(linkedAdId) || getWatchedAdDataMirror(linkedAdId)):null; for (const source of [data,data?.visitSnapshot,data?.watchSnapshot]) added += addPhoneTargets(getEscortAdPhoneDigits(source),1); } } catch (error) { if (isOperationCancelledError(error) || garsoExtendedCancelToken.cancelled) throw error; trailErrors++; log(`Garso rozszerzone - pierwszy post ${topicUrl}`,error); } } total=targets.length; garsoExtendedProgress.max=Math.max(1,total); return added; }; try { for (let index=0; index < targets.length; index++) { const target=targets[index]; garsoExtendedCancelToken.throwIfCancelled(); const label=target.adUrl ? `anons ${parseAdIdFromUrl(target.adUrl)}`:`telefon ${target.searchTerm.replaceAll('"','')}`; updateGarsoExtendedButton(`Sprawdzanie ${checked + 1}/${total} • ${label}`); const phoneSearch=target.phoneSearchKind === 'standard'; const result=await checkGarsoTerm(phoneSearch ? target.phoneDigits:target.searchTerm,true,garsoExtendedCancelToken,phoneSearch ? 'phone':'',true,partial => { keepResult(partial,target); updateGarsoExtendedButton(); }); keepResult(result,target); garsoExtendedCancelToken.throwIfCancelled(); if (result?.status === 'ok' && Number(result.count) > 0) { if (!target.trailDepth) for (const topic of result.topics || []) { const topicContext=getGarsoTopicPageContext(topic?.url); if (topicContext?.baseUrl) firstLevelTopicUrls.add(topicContext.baseUrl); } } if (result?.status !== 'ok' || result.topicLinksComplete === false) failed++; if (target.phoneDigits && target.phoneSearchKind === 'standard') { const state=phoneStandardChecks.get(target.phoneDigits) || {checked:0,found:false,error:false}; state.checked++; state.found ||= result?.status === 'ok' && Number(result.count) > 0; state.error ||= result?.status !== 'ok'; phoneStandardChecks.set(target.phoneDigits,state); } if (target.adUrl) { try { const linkedAdId=parseAdIdFromUrl(target.adUrl); updateGarsoExtendedButton(`Sprawdzanie ${checked + 1}/${total} • odczyt telefonu anonsu ${linkedAdId}`); const activity=await inspectEscortClubAdActivityOnly(target.adUrl,garsoExtendedCancelToken); garsoExtendedCancelToken.throwIfCancelled(); if (activity.unknown) throw new Error(activity.error || 'Nie udało się sprawdzić aktywności'); if (activity.active) { const data=await getEscortAdData(linkedAdId,target.adUrl,true,garsoExtendedCancelToken); garsoExtendedCancelToken.throwIfCancelled(); const digits=data?.status === 'ok' ? getEscortAdPhoneDigits(data):null; if (!digits) throw new Error('Nie udało się odczytać aktualnego numeru'); addPhoneTargets(digits,target.trailDepth || 0,index + 1); total=targets.length; garsoExtendedProgress.max=Math.max(1,total); } } catch (error) { if (isOperationCancelledError(error) || garsoExtendedCancelToken.cancelled) throw error; phoneErrors++; log(`Garso rozszerzone - odczyt telefonu anonsu ${parseAdIdFromUrl(target.adUrl)}`,error); } } checked++; garsoExtendedProgress.value=checked; if (index === targets.length - 1) { if (queuePhoneFallbacks()) { total=targets.length; garsoExtendedProgress.max=Math.max(1,total); } else if (!trailQueued) { await queueFirstPostTrails(); } } } garsoExtendedCheckCompleted=failed === 0 && phoneErrors === 0 && trailErrors === 0; if (garsoExtendedCheckCompleted) { garsoExtendedResults=[...freshResults.values()]; setGarsoExtendedCache(adId,adUrls,garsoExtendedResults,'combined',getGarsoExtendedPhoneTermsKey()); garsoExtendedCachedResults=garsoExtendedResults.map(result => ({...result,signature:getGarsoExtendedResultSignature(result)})); hasCachedGarsoExtendedInfo=true; garsoExtendedPhoneScanChecked=true; garsoExtendedCheckedAdUrls=adUrls; } finalStatus=(failed || phoneErrors || trailErrors ? `Sprawdzono ${total - failed}/${total}`:`Zakończono ${total}/${total}`) + ` • Znalezione nowe numery telefonów: ${newPhones}` + (newLinks ? ` • nowe linki: ${newLinks}`:'') + (failed ? ` • błędy Garso: ${failed}`:'') + (phoneErrors ? ` • nieodczytane telefony: ${phoneErrors}`:'') + (trailErrors ? ` • nieodczytane pierwsze posty: ${trailErrors}`:''); } catch (error) { if (isOperationCancelledError(error) || garsoExtendedCancelToken.cancelled) { finalStatus=`Przerwano • sprawdzono ${checked}/${total}`; } else { log('Błąd szukania rozszerzonego Garso',error); finalStatus=`Błąd • sprawdzono ${checked}/${total}`; } } finally { garsoExtendedCheckRunning=false; garsoExtendedCancelToken=null; refreshButton.disabled=false; refreshButton.style.opacity='1'; updateGarsoExtendedButton(`${finalStatus} • ${formatGarsoUniquePageCount(getUniqueGarsoResultPages(garsoExtendedResults).length)}`); } }; garsoExtendedButton.addEventListener('click',event => { stopUiEvent(event); void runGarsoExtendedAction(); }); garsoExtendedOpenButton.addEventListener('click',async event => { stopUiEvent(event); if (garsoExtendedOpenButton.disabled) return; garsoExtendedOpenRunning=true; updateGarsoExtendedButton('Otwieram strony wyników…'); let status=''; try { const opened=await openUniqueGarsoResultPages(getGarsoExtendedResults()); status=`Otwarto ${formatPolishCount(opened,'kartę','karty','kart')} z wynikami.`; } catch (error) { log('Nie udało się otworzyć wyników rozszerzonego Garso',error); status='Nie udało się otworzyć wyników. Spróbuj ponownie.'; } finally { garsoExtendedOpenRunning=false; updateGarsoExtendedButton(status); } }); const renderWarnings=() => { const sections=getResearchConsistencySections(researchFacts); const hasWarnings=sections.length > 0; warningBox.classList.toggle( 'vm-escort-summary-status-alert', hasWarnings ); warningBox.classList.toggle( 'vm-escort-summary-status-ok', !hasWarnings ); const labels=[...new Set(sections.flatMap(section => section.warnings) .filter(warning => !warning.startsWith('Różne parametry:')) .map(warning => warning.split(':')[0].replace(/^Różn[ey] /,'').replace(/^tagi$/,'usługi/tagi')))]; labels.push(...getEscortStatDifferenceLabels([...new Set(sections.flatMap(section => section.statFields))])); const comparable=[researchFacts.escortiAddressSummary, researchFacts.escortiPhoneSummary,escortPhoneWarningSummary] .some(summary => (summary?.results || []).filter(item => item.active && item.adData).length >= 2); warningPreview.textContent=hasWarnings ? `⚠ Znaleziono różnice w powiązanych anonsach: ${labels.join(', ')}.` :comparable ? 'Nie znaleziono różnic w porównanych anonsach.' :archiveRuntime ? 'Brak wystarczających danych w cache do porównania.':'Tylko jedno ogłoszenie'; warningPreview.classList.toggle('-warning',hasWarnings); warningStatusBadge.textContent=hasWarnings ? '⚠︎':comparable ? '✓ brak':''; warningStatusBadge.hidden=!warningStatusBadge.textContent; warningStatusBadge.title=warningPreview.textContent; warningContent.replaceChildren(); if (!sections.length) { const emptyLine=makeElement('div','vm-inconsistencies-empty',warningPreview.textContent); warningContent.appendChild(emptyLine); return; } const getWarningIcon=label => { if (/miast/i.test(label)) return '📍'; if (/wiek/i.test(label)) return 'W'; if (/parametr/i.test(label)) return '≠'; if (/cen/i.test(label)) return 'zł'; if (/godzin/i.test(label)) return '◷'; if (/tag/i.test(label)) return '#'; return '!'; }; const getDifferenceWord=count => count === 1 ? 'różnica' :(count >= 2 && count <= 4 ? 'różnice':'różnic'); sections.forEach(section => { const sourceCard=makeElement('section','vm-inconsistencies-source-card'); const sourceHead=makeElement('div','vm-inconsistencies-source-head'); const sourceMarker=makeElement('span','vm-inconsistencies-source-marker','!'); const sourceCopy=makeElement('span'); sourceCopy.style.minWidth='0'; const sourceTitle=makeElement('span','vm-inconsistencies-source-title',section.title); const sourceSubtitle=makeElement('span','vm-inconsistencies-source-subtitle',section.subtitle); sourceCopy.append(sourceTitle,sourceSubtitle); const sourceCount=makeElement('span','vm-inconsistencies-source-count',`${section.warnings.length} ${getDifferenceWord(section.warnings.length)}`); sourceHead.append( sourceMarker, sourceCopy, sourceCount ); sourceCard.appendChild(sourceHead); const warningList=makeElement('div','vm-inconsistencies-warning-list'); for (const warning of section.warnings) { const line=makeElement('div','vm-inconsistencies-warning-row'); const separator=warning.indexOf(':'); const label=separator >= 0 ? warning.slice(0,separator).trim() :warning.trim(); const detail=separator >= 0 ? warning.slice(separator + 1).trim() :''; const icon=makeElement('span','vm-inconsistencies-warning-icon',getWarningIcon(label)); const copy=makeElement('span'); const labelLine=makeElement('span','vm-inconsistencies-warning-label',label); copy.appendChild(labelLine); if (detail) { const detailLine=makeElement('span','vm-inconsistencies-warning-detail',detail); copy.appendChild(detailLine); } line.append(icon,copy); warningList.appendChild(line); } sourceCard.appendChild(warningList); warningContent.appendChild(sourceCard); }); }; renderWarnings(); const renderArchiveCachedResearchRows=() => { if (!archiveRuntime) return; const phoneResult=researchFacts.escortiPhoneResult; const addressResult=researchFacts.escortiAddressResult; const sameResult=researchFacts.sameEscortiResult === true; const phoneSummary=researchFacts.escortiPhoneSummary; const addressSummary=researchFacts.escortiAddressSummary; escortiAddressRow.style.display='flex'; escortiPhoneRow.style.display=(phoneResult || phoneSummary) && !sameResult ? 'flex':'none'; renderEscortArchiveResearchRow(escortiAddressRow,sameResult ? 'Escorti.pl':'Escorti.pl - adres anonsu',addressResult,addressSummary); renderEscortArchiveResearchRow(escortiPhoneRow,'Escorti.pl - nr tel.',phoneResult,phoneSummary); renderEscortArchiveResearchRow(escortPhoneRow,'Escort - nr tel.',researchFacts.escortPhoneResult,researchFacts.escortPhoneSummary,false); }; const refreshEscorti=async (forceRefresh=false) => { const run=++escortiRefreshRun; const requestContext=createRequestContext('research-escorti'); researchFacts.escortiLoading=true; renderResearchFacts(); updateGarsoExtendedButton( garsoExtendedResults === null ? 'Pobieram listę Escorti.pl…':null ); const cachedActivity=getLatestCachedEscortiActivitySummary( adId, ['phone-escorti','address-escorti'] ); escortiPhoneRow.style.display='none'; escortiAddressRow.style.display='flex'; setButtonColor(escortiAddressRow,cachedActivity ? 'stale':'checking'); setTwoLineButton( escortiAddressRow, 'Escorti.pl', cachedActivity ? `${cachedActivity.activeCount} aktywnych • odświeżanie…` :'szukam powiązań…' ); try { const [phoneData,addressData]=await Promise.all([ getEscortiDetailForSingleAd( adId, phoneSearch, 'phone-escorti', forceRefresh ), getEscortiDetailForSingleAd( adId, adUrl, 'address-escorti', forceRefresh ) ]); if (run !== escortiRefreshRun) return; const phoneResult=phoneData.result; const addressResult=addressData.result; const sameResult=areSameEscortiResults(phoneResult,addressResult); const phoneAdUrls=phoneResult?.status === 'ok' ? [...new Set(phoneResult.adUrls || [])] :[]; const addressAdUrls=addressResult?.status === 'ok' ? [...new Set(addressResult.adUrls || [])] :[]; escortiPhoneAdUrls=phoneAdUrls; escortiAddressAdUrls=addressAdUrls; escortiAdUrlSourcesReady=true; syncGarsoExtendedAdUrls(); const adUrls=[...new Set( [...phoneAdUrls,...addressAdUrls] )]; updateGarsoExtendedButton(garsoExtendedResults === null ? '':null); researchFacts.escortiPhoneResult=phoneResult; researchFacts.escortiAddressResult=addressResult; researchFacts.sameEscortiResult=sameResult; researchFacts.escortiPhoneSummary= getCachedEscortiActivitySummary( adId, 'phone-escorti', phoneAdUrls ); researchFacts.escortiAddressSummary= getCachedEscortiActivitySummary( adId, 'address-escorti', addressAdUrls ); renderResearchFacts(); const activityRows=[{row:escortiAddressRow,title:sameResult ? 'Escorti.pl':'Escorti.pl - adres anonsu', result:addressResult,urls:addressAdUrls,summary:researchFacts.escortiAddressSummary}]; if (!sameResult) activityRows.push({row:escortiPhoneRow,title:'Escorti.pl - nr tel.', result:phoneResult,urls:phoneAdUrls,summary:researchFacts.escortiPhoneSummary}); escortiAddressRow.style.display='flex'; escortiPhoneRow.style.display=sameResult ? 'none':'flex'; for (const entry of activityRows) { setTwoLineButton(entry.row,entry.title,entry.summary ? formatEscortiAdsAndActivity(entry.result,entry.summary):'sprawdzam dostępność…'); setButtonColor(entry.row,entry.summary ? 'stale':'checking'); } if (!adUrls.length) { const emptySummary=summarizeActiveAdResults([]); researchFacts.escortiPhoneSummary=emptySummary; researchFacts.escortiAddressSummary=emptySummary; setButtonColor(escortiAddressRow,'empty'); setTwoLineButton( escortiAddressRow, sameResult ? 'Escorti.pl':'Escorti.pl - adres anonsu', formatEscortiAdsAndActivity(addressResult,emptySummary) ); if (!sameResult) { setButtonColor(escortiPhoneRow,'empty'); setTwoLineButton( escortiPhoneRow, 'Escorti.pl - nr tel.', formatEscortiAdsAndActivity(phoneResult,emptySummary) ); } researchFacts.escortiWarnings=[]; renderWarnings(); renderResearchFacts(); return; } const detailedUrls=new Set(); const summary=await scanActiveEscortAds( adUrls, progress => { if (run !== escortiRefreshRun) return; if (progress.summary) { researchFacts.escortiPhoneSummary=summarizeActiveAdsSubset(progress.summary,phoneAdUrls); researchFacts.escortiAddressSummary=summarizeActiveAdsSubset(progress.summary,addressAdUrls); activityRows[0].summary=researchFacts.escortiAddressSummary; if (!sameResult) activityRows[1].summary=researchFacts.escortiPhoneSummary; renderResearchFacts(); } if (activityRows.some(entry => entry.summary)) { if (progress.phase === 'details' && progress.url) detailedUrls.add(normalizeEscortiAdUrl(progress.url) || progress.url); for (const entry of activityRows) { if (!entry.summary) continue; const checked=entry.urls.filter(url => detailedUrls.has(normalizeEscortiAdUrl(url) || url)).length; const detailsTotal=entry.urls.length; const pending=progress.phase !== 'details' ? (progress.checked < progress.total ? ` • dostępność ${progress.checked}/${progress.total}…`:'') :checked < detailsTotal ? ` • dane ${checked}/${detailsTotal}`:''; setTwoLineButton(entry.row,entry.title,formatEscortiAdsAndActivity(entry.result,entry.summary) + pending); setButtonColor(entry.row,entry.summary.staleCount || entry.summary.unknownCount || entry.summary.totalChecked < Number(entry.result?.adLinks) ? 'stale':entry.summary.activeCount > 0 ? 'found':'empty'); } return; } const progressText=`${progress.phase === 'details' ? 'dane':'dostępność'} ${progress.checked}/${progress.total}…`; setTwoLineButton( escortiAddressRow, sameResult ? 'Escorti.pl':'Escorti.pl - adres anonsu', progressText ); if (!sameResult) { setTwoLineButton( escortiPhoneRow, 'Escorti.pl - nr tel.', progressText ); } }, forceRefresh, {headFirst:true,cacheActivity:true,requestContext} ); if (run !== escortiRefreshRun) return; const phoneSummary=summarizeActiveAdsSubset(summary,phoneAdUrls); const addressSummary=summarizeActiveAdsSubset(summary,addressAdUrls); saveEscortiActivitySummary( adId, ['phone-escorti'], phoneAdUrls, phoneSummary ); saveEscortiActivitySummary( adId, ['address-escorti'], addressAdUrls, addressSummary ); researchFacts.escortiPhoneSummary=phoneSummary; researchFacts.escortiAddressSummary=addressSummary; activityRows[0].summary=addressSummary; if (!sameResult) activityRows[1].summary=phoneSummary; for (const entry of activityRows) { setButtonColor(entry.row,entry.summary.staleCount ? 'stale':entry.summary.activeCount > 0 ? 'found':'empty'); setTwoLineButton(entry.row,entry.title,formatEscortiAdsAndActivity(entry.result,entry.summary)); const title=sameResult ? 'Wyszukiwanie Escorti.pl po adresie anonsu i nr tel. zwróciło ten sam wynik.' :entry.row === escortiAddressRow ? 'Escorti.pl po adresie anonsu.':'Escorti.pl po nr tel.'; entry.row.title=buildActiveAdsTitle(title,entry.result,entry.summary); } researchFacts.escortiWarnings= getEscortActiveAdsConsistencyWarnings(summary); renderWarnings(); renderResearchFacts(); } catch (error) { if (run !== escortiRefreshRun) return; updateGarsoExtendedButton('nie udało się pobrać listy Escorti'); log('Błąd odświeżania panelu powiązanych wyników',error); if (cachedActivity) { setButtonColor(escortiAddressRow,'stale'); setTwoLineButton( escortiAddressRow, 'Escorti.pl', `${cachedActivity.activeCount} aktywnych` ); } else { setButtonColor(escortiAddressRow,'error'); setTwoLineButton(escortiAddressRow,'Escorti.pl','błąd'); } renderResearchFacts(); } finally { if (run === escortiRefreshRun) { researchFacts.escortiLoading=false; renderResearchFacts(); } } }; const refreshEscortPhone=async (forceRefresh=false) => { const run=++escortPhoneRefreshRun; researchFacts.escortPhoneLoading=true; renderResearchFacts(); setButtonColor(escortPhoneRow,'checking'); setTwoLineButton(escortPhoneRow,'Escort - nr tel.','wyszukiwanie…'); try { const search=await fetchEscortClubPhoneSearchAds(phone); if (run !== escortPhoneRefreshRun) return; setListCache(adId,{status:'ok',profileUrls:[],adUrls:search.adUrls},'phone-escort-search'); escortPhoneRow.dataset.searchUrl=search.searchUrl; const total=search.adUrls.length; let completed=0; const dataResults=await Promise.all(search.adUrls.map(async url => { const result=await getEscortAdData( parseAdIdFromUrl(url), url, forceRefresh, null, null, {needPhone:false} ); completed++; if (run === escortPhoneRefreshRun && completed < total) { setTwoLineButton( escortPhoneRow, 'Escort - nr tel.', `${total} ${polishAdWord(total)} • dane ${completed}/${total}…` ); } return {url,result}; })); if (run !== escortPhoneRefreshRun) return; const results=dataResults .filter(item => item.result?.status === 'ok') .map(item => ({ url:item.url, active:true, city:item.result.location?.city || null, adData:item.result })); escortPhoneWarningSummary=summarizeActiveAdResults(results); researchFacts.escortPhoneSummary=escortPhoneWarningSummary; renderWarnings(); renderResearchFacts(); setButtonColor(escortPhoneRow,total > 0 ? 'found':'empty'); setTwoLineButton( escortPhoneRow, 'Escort - nr tel.', `${total} ${polishAdWord(total)}` ); escortPhoneRow.title= `Aktualnie znalezione na Escort.club anonse z tym samym numerem telefonu: ${total}.`; } catch (error) { if (run !== escortPhoneRefreshRun) return; log('Błąd wyszukiwania numeru telefonu na Escort.club',error); setButtonColor(escortPhoneRow,'error'); setTwoLineButton(escortPhoneRow,'Escort - nr tel.','błąd'); } finally { if (run === escortPhoneRefreshRun) { researchFacts.escortPhoneLoading=false; renderResearchFacts(); } } }; const refreshPhotoDateRange=async () => { researchFacts.photoDatesLoading=true; renderResearchFacts(); try { researchFacts.photoRange=await getEscortGalleryPhotoDateRange(document); const stored=getStoredEscortAdData(adId),oldestPhotoDate=researchFacts.photoRange?.oldest?.getTime(); if (stored && oldestPhotoDate) { const saved={...stored,oldestPhotoDate}; writePersistentCacheValue(escortAdDataCacheKey(adId),saved); escortAdDataMemoryCache.set(String(adId),saved); storeWatchedAdDataMirror(adId,saved); } } catch (error) { researchFacts.photoRange=null; log('Nie udało się przygotować zakresu dat zdjęć',error); } finally { researchFacts.photoDatesLoading=false; renderResearchFacts(); } }; const refreshAll=async () => { if (refreshButton.disabled) return; setResearchRefreshState(refreshButton,true,'Pobieranie danych…'); try { await Promise.allSettled([ updateGarsoStatus( garsoPhoneRow, garsoPhoneTerm, 'phone', true, true, false ), updateGarsoStatus( garsoAddressRow, garsoAddressTerm, 'address', true, true, false ), (async () => { await refreshEscorti(true); await refreshEscortPhone(true); })(), refreshPhotoDateRange(), phoneReputationLinksUi?.refresh(true) ]); } finally { setResearchRefreshState( refreshButton, false, 'Pobierz ponownie dane z sekcji „Linki” (Garsoniera: tylko sprawdzenie, czy są tematy; sprawdzanie numeru: jeśli włączone automatyczne wyszukiwanie)' ); } }; const loadGarsoTopicsForContent=(row,term,mode,cancelToken,forceRefresh=false) => updateGarsoStatus(row,term,mode,forceRefresh,true,false,cancelToken); const refreshGarsoContent=async (forceRefresh=true) => { if (garsoDownloadButton.disabled) return; const operationCancelToken=createOperationCancelToken( 'Pełna analiza Garso' ); garsoCombinedSummaryState._vmGarsoSummaryCancelToken= operationCancelToken; if (forceRefresh) { garsoSummaryCard.collapsible?.setExpanded(true); } setResearchRefreshState( garsoDownloadButton, true, 'Pobieranie i analizowanie tematów Garsoniery…' ); const previousResult=garsoCombinedSummaryState._vmGarsoSummaryResult || null; const previousCheckedAt=Number( garsoCombinedSummaryState.dataset.garsoSummaryCheckedAt ) || null; let failureMessage=''; if (previousResult) { setGarsoSummaryState( garsoCombinedSummaryState, 'refreshing', summaryToHtml(previousResult), { checkedAt:previousCheckedAt, cacheState:'refreshing' } ); } else { setGarsoSummaryState( garsoCombinedSummaryState, 'loading', '<b>Garso</b><br>Łączenie list tematów…' ); } try { const [phoneResult,addressResult]=await Promise.all([ loadGarsoTopicsForContent( garsoPhoneRow, garsoPhoneTerm, 'phone', operationCancelToken, forceRefresh ), loadGarsoTopicsForContent( garsoAddressRow, garsoAddressTerm, 'address', operationCancelToken, forceRefresh ) ]); const failed=[phoneResult,addressResult].filter(result => result?.status !== 'ok'); if (failed.length) { const messages=[...new Set(failed.map(result => result?.errorMessage).filter(Boolean))]; const error=new Error(messages.join(' | ') || 'Nie udało się pobrać obu list tematów Garso'); error.garsoAlreadyLogged=failed.every(result => result?.errorLogged); throw error; } const combinedTopics=mergeUniqueGarsoTopics( phoneResult.topics || [], addressResult.topics || [] ); await prepareGarsoSummary( garsoCombinedSummaryState, garsoCombinedSummaryTerm, combinedTopics, forceRefresh, operationCancelToken ); if (forceRefresh) { garsoSummaryCard.collapsible?.setExpanded(true); } } catch (error) { if (isOperationCancelledError(error)) { garsoCombinedSummaryState._vmGarsoSummaryResult= previousResult; setGarsoSummaryState( garsoCombinedSummaryState, 'cancelled', previousResult ? summaryToHtml(previousResult):'', { checkedAt:previousCheckedAt, cacheState:previousResult ? 'stale':null } ); } else { failureMessage=String(error?.message || 'Nie udało się przygotować analizy Garso'); if (!error?.garsoAlreadyLogged) log('Błąd wspólnej analizy Garso',error); if (previousResult) { applySummaryToButton( garsoCombinedSummaryState, previousResult, { checkedAt:previousCheckedAt, cacheState:'stale' } ); } else { setGarsoSummaryState( garsoCombinedSummaryState, 'error', '' ); } } } finally { if ( garsoCombinedSummaryState._vmGarsoSummaryCancelToken === operationCancelToken ) { garsoCombinedSummaryState._vmGarsoSummaryCancelToken=null; } setResearchRefreshState( garsoDownloadButton, false, failureMessage || 'Odśwież i przeanalizuj treść tematów Garsoniery dla numeru telefonu i adresu anonsu.' ); } }; const initializeGarsoChecks=() => { if (!SETTINGS.autoGarsoCheck) return; const analyzeContent=normalizeAutoGarsoAnalysisMode( SETTINGS.autoGarsoAnalysisMode ) === 'content'; if (analyzeContent && !cachedCombinedGarsoSummary?.recentCheck) { refreshGarsoContent(false); return; } Promise.allSettled([ updateGarsoStatus( garsoPhoneRow, garsoPhoneTerm, 'phone', false, true, false ), updateGarsoStatus( garsoAddressRow, garsoAddressTerm, 'address', false, true, false ) ]).then(() => { if (garsoCombinedSummaryState.dataset.garsoSummaryState) return; if ( garsoPhoneRow.dataset.garsoStatus !== 'result' || garsoAddressRow.dataset.garsoStatus !== 'result' ) return; const checkedAt=Math.max( Number(garsoPhoneRow.dataset.garsoCountCheckedAt) || 0, Number(garsoAddressRow.dataset.garsoCountCheckedAt) || 0 ); setGarsoSummaryState( garsoCombinedSummaryState, 'count-only', '', { checkedAt:checkedAt || Date.now(), cacheState:'fresh' } ); }); }; refreshButton.addEventListener('click',event => { stopUiEvent(event); refreshAll(); }); garsoDownloadButton.addEventListener('click',event => { stopUiEvent(event); refreshGarsoContent(true); }); garsoDownloadButton.addEventListener('keydown',event => { event.stopPropagation(); }); if (archiveRuntime) { renderArchiveCachedResearchRows(); renderWarnings(); renderResearchFacts(); refreshButton.disabled=true; setControlTitle(refreshButton,'Widok archiwalny korzysta z zapisanego podsumowania z cache.'); refreshButton.style.opacity='.45'; garsoDownloadButton.disabled=true; setControlTitle(garsoDownloadButton,'Widok archiwalny korzysta z zapisanej analizy Garso z cache.'); garsoDownloadButton.style.opacity='.45'; } else { setTimeout(() => { if (!panel.isConnected) return; initializeGarsoChecks(); refreshEscorti(false); refreshEscortPhone(false); refreshPhotoDateRange(); },50); } return panel; } function initEscortGarsoAutoChecks() { const adId=getEscortAdIdFromPath(); if (!IS_ESCORT_CLUB || window.top!==window.self || !adId || !SETTINGS.autoGarsoCheck || isEscortCachedArchiveRuntime()) return; const start=(mode,term)=>{ const key=garsoSearchCountCacheKey(mode,term); if (!term || !key || garsoAutoSearchJobs.has(key)) return; const keys=[garsoSearchCountCacheKey(mode,term),garsoSummaryCacheKey(mode,term)].filter(Boolean); if (keys.some(key => !persistentCacheKeyIsLoaded(key))) {ensurePersistentCacheKeys(keys).then(() => start(mode,term)).catch(error => log('Nie udało się odczytać cache Garso',error)); return;} const cached=getGarsoSearchCountCache(mode,term); if (cached?.fresh && cached.topicLinksFresh) return; const job={cancelToken:createOperationCancelToken('Automatyczne sprawdzanie Garso')}; job.promise=checkGarsoTerm(term,true,job.cancelToken,mode).then(result=>{ job.cancelToken.throwIfCancelled(); job.checkedAt=cacheGarsoSearchResult(mode,term,result); job.result=result; job.completedAt=Date.now(); return result; }); garsoAutoSearchJobs.set(key,job); job.promise.catch(()=>{if(garsoAutoSearchJobs.get(key)===job)garsoAutoSearchJobs.delete(key);}); }; start('address',buildGarsoAdLinkSearchTerm(adId)); let observer=null; const checkPhone=()=>{ if (!SETTINGS.autoGarsoCheck) {observer?.disconnect(); return true;} const phone=getFormattedPhoneNumber(); if (!phone) return false; start('phone',getGarsoSearchTerms(phone,'phone')[0]); observer?.disconnect(); return true; }; if (!checkPhone()) { observer=new MutationObserver(checkPhone); observer.observe(document.body || document.documentElement,{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['href','data-phone']}); } window.addEventListener('pagehide',event=>{ if(event.persisted)return; observer?.disconnect(); for(const job of garsoAutoSearchJobs.values())job.cancelToken.cancel('Karta została zamknięta.'); }); } const buttonsAdded={panel:false}; function ensureEscortResearchPanel(phone,adUrl,container) { if (buttonsAdded.panel || !adUrl) return; const id=parseAdIdFromUrl(adUrl); if (persistentCacheUsesIndexedDb && getPersistentGarsoKeys(id,phone).some(key => !persistentCacheKeyIsLoaded(key))) { if (!buttonsAdded.loading) {buttonsAdded.loading=true; ensureEscortPageCache([id],phone).then(() => {buttonsAdded.loading=false; checkAndAddButtons();}).catch(error => {buttonsAdded.loading=false; log('Nie udało się odczytać danych panelu',error);});} return; } const researchPanel=createEscortResearchPanel(phone,adUrl); placeEscortResearchPanelAboveActionButtons(researchPanel,container); buttonsAdded.panel=true; } function checkAndAddButtons() { if (!SETTINGS.showSummarySidePanel) { removeEscortSummarySidePanel(); } if (!SETTINGS.showCompactSummaryAboveDescription) { detachHelperNode(document.getElementById( ESCORT_COMPACT_SUMMARY_ID )); } const phoneLink=getEscortPhoneLink(); const container= getEscortButtonsContainer(phoneLink); if (!container) return; const adId=getAdIdFromUrl(); const adUrl=adId ? `https://pl.escort.club/anons/${adId}.html` :null; const archiveRuntime= isEscortCachedArchiveRuntime(); if (!archiveRuntime) { const phone= getFormattedPhoneNumber(phoneLink); if (phone) { renderEscortPhoneCopyButton(phone); ensureEscortResearchPanel(phone,adUrl,container); } else if ( phoneLink && phoneLink.getAttribute('href') === '#' && !phoneLink.dataset.scriptClicked ) { phoneLink.click(); phoneLink.dataset.scriptClicked= 'true'; } return; } const archiveData= adId ? getStoredEscortAdData(adId) :null; const phone= getFormattedPhoneNumber(phoneLink) || normalizeEscortCachedPhone( archiveData?.phone ) || ''; if (phone) { renderEscortPhoneCopyButton(phone); } ensureEscortResearchPanel(phone,adUrl,container); } const persistentReadBridgeTabs=new Map(); function persistentCacheReadTabKey(host) {return 'vm_helper_cache_read_tab_' + host;} function findPendingPersistentCacheReadToken(host) { for (const key of GM_listValues()) { if (!key.startsWith(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX)) continue; const request=GM_getValue(key,null); if (request?.action !== 'read-ad-data' || request.host !== host || !(Date.now() - Number(request.createdAt) < 15000)) continue; const token=key.slice(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX.length); const response=GM_getValue(persistentCacheBridgeResponseKey(token),null); if (response?.protocol !== 1 || response.working) return token; } return ''; } function closePersistentCacheReadTab(host,state) { clearInterval(state.timer); state.timer=null; try {state.tab?.close?.();} catch {} const key=persistentCacheReadTabKey(host),ownerKey=persistentCacheReaderKey(host); if (GM_getValue(key,null)?.token === state.token) deleteHelperStorageValue(key); const owner=GM_getValue(ownerKey,null); if (owner?.auxiliary && owner.token === state.token) deleteHelperStorageValue(ownerKey); state.tab=null; state.token=''; } async function ensurePersistentCacheReadTab(host,token,unresponsiveSession='') { let state=persistentReadBridgeTabs.get(host); if (!state) { state={opening:null,tab:null,timer:null,token:''}; persistentReadBridgeTabs.set(host,state); } if (state.tab?.closed) closePersistentCacheReadTab(host,state); const ready=() => {const value=getReadyPersistentCacheReader(host); return value && value.session !== unresponsiveSession;}; if (ready()) return; if (state.opening) return state.opening; if (state.tab) return; const current=GM_getValue(persistentCacheReadTabKey(host),null); if (current?.token && Date.now() - current.at < 45000) return; state.opening=(async () => { const key=persistentCacheReadTabKey(host); // Dzierżawa obejmuje też uruchamianie karty przed zgłoszeniem gotowości. writeHelperStorageValue(key,{token,at:Date.now()}); await new Promise(resolve => setTimeout(resolve,60)); if (GM_getValue(key,null)?.token !== token) return; if (ready()) {deleteHelperStorageValue(key); return;} const pending=findPendingPersistentCacheReadToken(host); if (!pending) {deleteHelperStorageValue(key); return;} token=pending; state.token=token; writeHelperStorageValue(key,{token,at:Date.now()}); try { state.tab=GM_openInTab(`https://${host}/?${PERSISTENT_CACHE_BRIDGE_PARAM}=${encodeURIComponent(token)}`,{active:false,insert:true,setParent:true}); if (!state.tab) throw new Error('Nie ma gotowej karty obsługującej cache. Otwórz Escort.club i ponów odczyt.'); const tab=state.tab,openedAt=Date.now(); let idleSince=0; state.timer=setInterval(() => { if (state.tab !== tab) return; const now=Date.now(),pending=findPendingPersistentCacheReadToken(host); if (pending) idleSince=0; else if (!idleSince) idleSince=now; if (tab.closed || (idleSince && now - idleSince >= 2000) || (now - openedAt >= 20000 && !getReadyPersistentCacheReader(host))) { closePersistentCacheReadTab(host,state); return; } const lease=GM_getValue(key,null); if (pending && lease?.token === token && now - lease.at >= 10000) writeHelperStorageValue(key,{token,at:now}); },1000); } catch (error) { closePersistentCacheReadTab(host,state); throw error; } })(); try {await state.opening;} finally {state.opening=null;} } const persistentReadBridgeJobs=new Map(); let persistentCacheReaderInstalled=false,persistentCacheReaderBusy=0; function persistentCacheReaderKey(host) {return PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX + 'reader_' + host;} function getReadyPersistentCacheReader(host) { const value=GM_getValue(persistentCacheReaderKey(host),null); return value?.protocol === 1 && value.host === host && value.ready && Date.now() - value.at < 15000 ? value:null; } async function servePersistentCacheAdRequest(token,allowOwnerChange=false) { const key=persistentCacheBridgeRequestKey(token),replyKey=persistentCacheBridgeResponseKey(token); const request=GM_getValue(key,null),reply=GM_getValue(replyKey,null); if (request?.action !== 'read-ad-data' || request.host !== location.hostname || Date.now() - Number(request.createdAt) >= 25000 || (reply?.protocol === 1 && !reply.working)) return; if (reply?.working && reply.session === GARSO_SEARCH_INSTANCE_ID) return; if (!allowOwnerChange && request.owner && request.owner !== GARSO_SEARCH_INSTANCE_ID) return; if (!allowOwnerChange && !request.owner && getReadyPersistentCacheReader(location.hostname)?.session !== GARSO_SEARCH_INSTANCE_ID) return; let response; persistentCacheReaderBusy++; writeHelperStorageValue(replyKey,{protocol:1,working:true,session:GARSO_SEARCH_INSTANCE_ID,at:Date.now()}); try { const ids=[...new Set(request.adIds || [])].map(String).filter(id => /^\d+$/.test(id)); await persistentCacheDbOperationChain; await ensurePersistentCacheKeys(ids.flatMap(id => [escortAdDataCacheKey(id),listCacheKey(id)]),{refresh:true}); await ensureEscortPageCache(ids); response={protocol:1,session:GARSO_SEARCH_INSTANCE_ID,ok:true,ads:ids.map(readEscortAdCacheBundle).filter(Boolean),completedAt:Date.now()}; } catch (error) {response={protocol:1,session:GARSO_SEARCH_INSTANCE_ID,ok:false,error:error?.message || String(error)};} finally {persistentCacheReaderBusy--;} if (GM_getValue(key,null)) writeHelperStorageValue(replyKey,response); } function requestPersistentCacheBridgeDirect(action,options={}) { if (IS_ESCORT_CLUB) return Promise.reject(new Error('Bridge cache nie jest potrzebny na Escort.club.')); if (options.cancelToken?.cancelled) return Promise.reject(createOperationCancelledError(options.cancelToken.reason)); const host=String(options.host || 'pl.escort.club').toLowerCase(); if (!isEscortClubHost(host)) return Promise.reject(new Error('Nieprawidłowa domena cache.')); const token=`${Date.now()}-${Math.random().toString(36).slice(2)}`; const requestKey=persistentCacheBridgeRequestKey(token),responseKey=persistentCacheBridgeResponseKey(token); const owner=action === 'read-ad-data' ? getReadyPersistentCacheReader(host):null; const payload={protocol:1,action,host,owner:owner?.session || '',createdAt:Date.now(), garsoTtlMs:Number(options.garsoTtlMs) || 0,escortTtlMs:Number(options.escortTtlMs) || 0, adIds:[...new Set((options.adIds || []).map(String).filter(id => /^\d+$/.test(id)))]}; writeHelperStorageValue(requestKey,payload); deleteHelperStorageValue(responseKey); return new Promise((resolve,reject) => { let settled=false,listenerId=null,bridgeTab=null,pollTimer=null,timeoutTimer=null,fallbackTimer=null,unsubscribe=null,workingAt=0,fallbackStarted=false; const cleanup=() => { unsubscribe?.(); if (listenerId != null && typeof GM_removeValueChangeListener === 'function') GM_removeValueChangeListener(listenerId); clearInterval(pollTimer); clearTimeout(timeoutTimer); clearTimeout(fallbackTimer); try {bridgeTab?.close?.();} catch {} deleteHelperStorageValue(requestKey); deleteHelperStorageValue(responseKey); }; const finish=(response,error=null) => { if (settled) return; settled=true; cleanup(); if (error) reject(error); else if (response?.ok) resolve(response); else reject(new Error(response?.error || 'Nie udało się odczytać danych z cache Escort.club.')); }; const onResponse=response => { if (!response || (action === 'read-ad-data' && response.protocol !== 1)) return; if (response.working) {workingAt=Number(response.at) || Date.now(); return;} finish(response); }; const openFallback=() => { if (settled || bridgeTab || fallbackStarted) return; fallbackStarted=true; try { payload.owner=''; writeHelperStorageValue(requestKey,payload); if (action === 'read-ad-data') { void ensurePersistentCacheReadTab(host,token,owner?.session).then(() => { if (!settled) writeHelperStorageValue(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX + 'signal',token); },error => finish(null,error)); return; } bridgeTab=GM_openInTab(`https://${host}/?${PERSISTENT_CACHE_BRIDGE_PARAM}=${encodeURIComponent(token)}`,{active:false,insert:true,setParent:true}); if (!bridgeTab) finish(null,new Error('Nie ma gotowej karty obsługującej cache. Otwórz Escort.club i ponów odczyt.')); } catch (error) {finish(null,error);} }; try { if (typeof GM_addValueChangeListener === 'function') listenerId=GM_addValueChangeListener(responseKey,(_key,_old,value) => onResponse(value)); pollTimer=setInterval(() => { onResponse(GM_getValue(responseKey,null)); if (!settled && action === 'read-ad-data' && !workingAt && getReadyPersistentCacheReader(host)) writeHelperStorageValue(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX + 'signal',token); if (!settled && owner && !bridgeTab && workingAt && Date.now() - workingAt > 3000 && getReadyPersistentCacheReader(host)?.session !== owner.session) openFallback(); },250); timeoutTimer=setTimeout(() => finish(null,new Error(action === 'read-ad-data' ? 'Nie udało się uruchomić odbiorcy cache Escort.club. Otwórz stronę Escort.club i ponów odczyt.' : 'Przekroczono czas oczekiwania na operację na cache Escort.club.')),action === 'read-ad-data' ? 15000:20000); unsubscribe=options.cancelToken?.onCancel(reason => finish(null,createOperationCancelledError(reason))); if (settled) return; if (action === 'read-ad-data') { writeHelperStorageValue(PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX + 'signal',token); fallbackTimer=setTimeout(() => {if (!workingAt) openFallback();},owner ? 1000:0); } else openFallback(); onResponse(GM_getValue(responseKey,null)); } catch (error) {finish(null,error);} }); } function formatHelperDiagnosticError(error) { if (error == null) return ''; if (typeof error !== 'object') return String(error); const parts=[]; if (error.name && error.name !== 'Error') parts.push(String(error.name)); if (error.message) parts.push(String(error.message)); if (error.type) parts.push('type=' + String(error.type)); if (Number.isFinite(Number(error.status))) parts.push('status=' + Number(error.status)); if (error.statusText) parts.push(String(error.statusText)); if (error.readyState != null) parts.push('readyState=' + String(error.readyState)); const address=error.finalUrl || error.url; if (address) {try {const url=new URL(address,location.href); parts.push(url.origin + url.pathname);} catch {}} if (error.timeout != null) parts.push('timeout=' + String(error.timeout)); if (!parts.length) parts.push('Błąd bez komunikatu (' + Object.keys(error).slice(0,12).join(', ') + ')'); return parts.join(' | ').slice(0,1200); } const sharedHelperRequests=new Map(); let watchButtonsRefreshScheduled=false; let escortLeafletPromise=null; function runSharedHelperRequest(key,work,cancelToken=null) { if (cancelToken?.cancelled) return Promise.reject(createOperationCancelledError(cancelToken.reason)); let job=sharedHelperRequests.get(key); if (!job) { job={token:createOperationCancelToken(key),subscribers:new Set()}; sharedHelperRequests.set(key,job); Promise.resolve().then(() => {job.token.throwIfCancelled(); return work(job.token);}).then( value => {if (sharedHelperRequests.get(key) === job) sharedHelperRequests.delete(key); for (const sub of job.subscribers) {sub.off?.(); sub.resolve(copyWatchedValue(value));}}, error => {if (sharedHelperRequests.get(key) === job) sharedHelperRequests.delete(key); for (const sub of job.subscribers) {sub.off?.(); sub.reject(error);}} ).finally(() => {job.subscribers.clear(); if (sharedHelperRequests.get(key) === job) sharedHelperRequests.delete(key);}); } return new Promise((resolve,reject) => { const sub={resolve,reject}; job.subscribers.add(sub); sub.off=cancelToken?.onCancel(reason => { job.subscribers.delete(sub); reject(createOperationCancelledError(reason)); if (!job.subscribers.size) { if (sharedHelperRequests.get(key) === job) sharedHelperRequests.delete(key); job.token.cancel(reason); } }); }); } function loadEscortRadiusLeaflet() { if (!escortLeafletPromise) escortLeafletPromise=Promise.resolve().then(() => { const source=GM_getResourceText('escortRadiusLeafletJs'); if (!source) throw new Error('Brak zasobu mapy. Zainstaluj ponownie pełną wersję skryptu.'); const exports={}; new Function('exports','module',source)(exports,{exports}); if (typeof exports.map !== 'function') throw new Error('Nie udało się uruchomić mapy.'); return exports; }).catch(error => {escortLeafletPromise=null; throw error;}); return escortLeafletPromise; } initLiveDiagnosticsReports(); probeEscortCooldownOnStartup().catch(error => log('Nie udało się sprawdzić dostępu do Escort.club',error)); let earlyPageUi=false; try { const pageParams=new URL(location.href).searchParams; earlyPageUi=IS_ESCORT_CLUB && window.top === window.self && !getEscortCachedArchiveMarkerFromHash() && !(location.pathname === '/' && readRecentEscortCachedAdNavigation()) && !sessionStorage.getItem(FAVORITE_SEARCH_EDIT_KEY) && (!getEscortAdIdFromPath() || !!document.querySelector('.content-info-col .content-name h1, .content-name.-desc-name h1')) && ![PERSISTENT_CACHE_BRIDGE_PARAM,FAVORITE_SEARCH_EDIT_PARAM] .some(param => pageParams.has(param)); } catch {} if (earlyPageUi) { try { initHelperToolbar(); initEscortSectionVisibility(); } catch (error) { earlyPageUi=false; log('Nie udało się wcześniej uruchomić interfejsu',error); } } const cacheBridgeStartupTimer=IS_ESCORT_CLUB && new URL(location.href).searchParams.has(PERSISTENT_CACHE_BRIDGE_PARAM) ? setTimeout(() => {try {window.close();} catch {}},20000):null; await persistentCacheInitialization; clearRemovedDiagnosticStorage(); if (IS_ESCORT_CLUB && await handlePersistentCacheBridgePage(true)) { clearTimeout(cacheBridgeStartupTimer); return; } clearTimeout(cacheBridgeStartupTimer); initPersistentCacheChanges(); try {await rebuildWatchedAdDataMirrorFromCentralCache();} catch (error) {log('Nie udało się odbudować pomocniczego mirrora Ulubionych',error);} initEscortAdCacheReader(); acknowledgeWatchEventOnArrival(); try {saveEscortAdTemplateFromPage();} catch (error) {log('Nie udało się zapisać szablonu anonsu',error);} installEscortCachedAdNavigationMarker(); if (await handleEscortCachedAdArchivePage()) return; if (initFavoriteSearchView()) return; if (initFavoriteSearchEdit()) return; try {initEscortGarsoAutoChecks();} catch(error) {log('Nie udało się rozpocząć sprawdzania Garso',error);} if (IS_ESCORT_CLUB && location.pathname === '/' && !getFavoriteSearchSession()) { const pageUrl=new URL(location.href); if (!pageUrl.searchParams.has(PERSISTENT_CACHE_BRIDGE_PARAM)) { try { sessionStorage.removeItem(ESCORT_MULTI_LOCATION_SESSION_KEY); } catch {} } } if (initGarsoTopicCacheBridge()) return; maybeOpenSettingsOnFirstRun(); initHelperToolbar(); try { applyWatchFeatureState(); } catch (error) { log( 'Nie udało się uruchomić systemu Ulubionych/alertów', error ); } try { initLiveDiagnosticsPanel(); initQuickAdPreview(); initGlobalQuickSearch(); } catch (error) { log( 'Nie udało się uruchomić szybkiego wyszukiwania', error ); } if (['1','table'].includes(getFavoritePageMode())) { try {if(getFavoritePageMode()==='table')openWatchedSettingsModal(false,true);else await renderFavoriteTilesView();} catch (error) {log('Nie udało się otworzyć Ulubionych',error); showWatchToast('Nie udało się otworzyć Ulubionych.',true);} const url=new URL(location.href);for(const key of [FAVORITE_TILES_PARAM,FAVORITE_TILES_FIELDS_PARAM,FAVORITE_TILES_EXTRA_PARAM])url.searchParams.delete(key); history.replaceState(history.state,'',url.href); } if (GARSO_MENU_ONLY_MODE) { try {initGarsoInlineTools();} catch (error) {log('Nie udało się uruchomić skrótów Garsoniery',error);} initGarsoTopicSummary().catch(error => log('Nie udało się uruchomić podsumowania tematu Garso',error)); initGarsoTrackedTopics().catch(error => log('Nie udało się pokazać zapisanych danych przy tematach Garso',error)); } if ( location.hostname === 'escorti.pl' || location.hostname === 'www.escorti.pl' ) { initEscortiProfileAdLinkStatuses(); initEscortiSearchSettingsGear(); handleEscortiPage(); return; } if (IS_ESCORT_CLUB) { const isSingleEscortAd=!!getEscortAdIdFromPath(location.pathname); if (!earlyPageUi) initEscortSectionVisibility(); if (!isSingleEscortAd) { initEscortMultiLocationControls(); initEscortNativeAdvancedFilterCounter(); initEscortSavedSearchFilters(); initEscortNativeFilterResetFix(); } if (isSingleEscortAd) { for (const [label,initializer] of [ ['data publikacji anonsu',initEscortClubDatePostedDisplay], ['stan dostępności anonsu',initEscortClubCurrentAvailabilityDot], ['kolorowanie tagów',initEscortClubTagHighlighting], ['porównanie z poprzednią wizytą',initEscortClubVisitChangeTracking] ]) { try { initializer(); } catch (error) { log( `Nie udało się uruchomić: ${label}`, error ); } } } } if ( IS_ESCORT_CLUB && (getFavoriteSearchSession() || location.pathname.startsWith('/anonse/') || isEscortSearchPath() || (CURRENT_HOST !== 'pl.escort.club' && location.pathname !== '/' && !getEscortAdIdFromPath() && !!getEscortClubListContext(document,location.href)?.anchors?.length)) ) { try { await initEscortClubListPage(); if (favoriteSearchEditFrameNotify) { trimFavoriteSearchEditPage(); favoriteSearchEditFrameNotify(favoriteSearchEditReadFilters && findEscortSearchLocationControls() ? 'ready':'error', {error:'Nie udało się odczytać formularza lub wyników. Poczekaj na wyszukiwarkę albo anuluj edycję.'}); } } catch (error) { log('Nie udało się uruchomić funkcji strony wyników Escort.club',error); showEscortListInitializationError(error); favoriteSearchEditFrameNotify?.('error',{error:error?.message || 'Nie udało się przygotować wyszukiwarki.'}); } return; } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded',syncGarsoNativeSearchCooldown,{once:true}); else syncGarsoNativeSearchCooldown(); if (!GARSO_MENU_ONLY_MODE) { try { initEscortGalleryWatcher(); } catch (error) { log( 'Nie udało się uruchomić obserwatora galerii', error ); } try { checkAndAddButtons(); } catch (error) { recordDiagnosticError( 'Escort.club - panel Linki/podsumowania', error ); console.error( '[GARSO-ESCORTI] Nie udało się utworzyć panelu Linki/podsumowania', error ); } } })();