// ==UserScript==
// @name The West - Experience
// @namespace http://tampermonkey.net/
// @version 6.8
// @description Tracks and displays gained experience in The West with activity details, including reset functionality and draggable window
// @author DK, Shikokuchuo
// @include https://*.the-west.*/game.php*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Inicjalizacja zmiennych
let totalExperience = JSON.parse(localStorage.getItem('totalExperience')) || 0;
let experienceLog = JSON.parse(localStorage.getItem('experienceLog')) || [];
let savedPosition = JSON.parse(localStorage.getItem('experienceTrackerPosition')) || { top: '50px', left: 'auto', right: '310px' };
let isCollectingHistory = false;
let isPaused = false;
let shouldCancel = false;
let collectionStartTime = null;
let processedPagesTime = [];
let tempExpLog = [];
let messageTimeout = null;
let isTrackerVisible = JSON.parse(localStorage.getItem('experienceTrackerVisible')) !== false; // domyślnie widoczny
// Dodajemy zmienną na nazwę gracza
let playerName = Character.name;
// Dodajemy nowe zmienne do przechowywania stanu kolekcji
let collectionState = JSON.parse(localStorage.getItem('collectionState')) || {
inProgress: false,
currentPage: 1,
folder: null,
tempLog: [],
totalPages: 0
};
// Funkcja do wyświetlania komunikatów
function showError(message, duration = 5000) {
if (messageTimeout) {
clearTimeout(messageTimeout);
messageTimeout = null;
}
const statusElement = document.querySelector('#collection-status');
if (statusElement) {
statusElement.innerHTML = `
<div style="
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 5px;
margin-top: 10px;
color: white;
text-align: center;
animation: fadeIn 0.3s ease-in-out;
">
${message}
</div>
`;
if (duration > 0) {
messageTimeout = setTimeout(() => {
if (statusElement) {
statusElement.innerHTML = '';
}
messageTimeout = null;
}, duration);
}
}
}
// Funkcja do pokazywania komunikatu o postępie
function showProgress(message) {
const statusElement = document.querySelector('#collection-status');
if (statusElement) {
statusElement.innerHTML = `
<div style="
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 5px;
margin-top: 10px;
color: white;
text-align: center;
">
${message}
</div>
`;
}
}
// Dodaj style CSS dla animacji
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
// Funkcja do aktualizacji statusu kolekcji
function updateCollectionStatus(processed, total, foundEntries) {
let statusElement = document.querySelector('#collection-status');
const experienceTracker = document.querySelector('#experience-tracker');
const controlButtons = document.querySelector('#collection-controls');
// Jeśli element nie istnieje lub nie ma właściwej klasy, tworzymy go na nowo
if (!statusElement || !statusElement.classList.contains('collection-status-fixed')) {
// Usuń stary element jeśli istnieje
if (statusElement) {
statusElement.remove();
}
// Stwórz nowy element
statusElement = document.createElement('div');
statusElement.id = 'collection-status';
statusElement.classList.add('collection-status-fixed');
statusElement.style.cssText = `
margin: 15px 0;
opacity: 0;
transition: opacity 0.3s ease;
`;
// Dodaj status przed przyciskami
if (experienceTracker && controlButtons) {
experienceTracker.insertBefore(statusElement, controlButtons);
} else {
experienceTracker.appendChild(statusElement);
}
// Pokaż element z animacją
setTimeout(() => {
statusElement.style.opacity = '1';
}, 100);
}
const percent = Math.round((processed / total) * 100);
// Używamy tempExpLog zamiast experienceLog podczas zbierania danych
const workEntries = tempExpLog.filter(e => e.type === 'work').length;
const duelEntries = tempExpLog.filter(e => e.type === 'duel').length;
const battleEntries = tempExpLog.filter(e => e.type === 'battle').length;
// Dodajmy też szacowany pozostały czas
const timeEstimate = calculateTimeEstimate(processed, total);
const timeInfo = timeEstimate ? `<div class="time-estimate">⏱️ ${timeEstimate}</div>` : '';
statusElement.innerHTML = `
<div class="status-container" style="
background: rgba(20, 20, 20, 0.95);
padding: 15px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
">
<div class="status-header" style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
">
<div style="
color: #F1C40F;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
">Status pobierania</div>
<div style="
background: rgba(241, 196, 15, 0.1);
color: #F1C40F;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">${percent}%</div>
</div>
<div class="status-grid" style="
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
">
<div class="stat-box" style="
background: rgba(46, 204, 113, 0.1);
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(46, 204, 113, 0.2);
">
<div style="color: #2ecc71; font-size: 12px; margin-bottom: 5px;">Prace</div>
<div style="font-size: 14px; font-weight: 600; color: #fff;">
${workEntries}<br>
<span style="font-size: 12px; color: #2ecc71;">${tempExpLog.filter(e => e.type === 'work').reduce((sum, e) => sum + e.amount, 0)} XP</span>
</div>
</div>
<div class="stat-box" style="
background: rgba(231, 76, 60, 0.1);
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(231, 76, 60, 0.2);
">
<div style="color: #e74c3c; font-size: 12px; margin-bottom: 5px;">Pojedynki</div>
<div style="font-size: 14px; font-weight: 600; color: #fff;">
${duelEntries}<br>
<span style="font-size: 12px; color: #e74c3c;">${tempExpLog.filter(e => e.type === 'duel').reduce((sum, e) => sum + e.amount, 0)} XP</span>
</div>
</div>
<div class="stat-box" style="
background: rgba(155, 89, 182, 0.1);
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(155, 89, 182, 0.2);
">
<div style="color: #9b59b6; font-size: 12px; margin-bottom: 5px;">Bitwy</div>
<div style="font-size: 14px; font-weight: 600; color: #fff;">
${battleEntries}<br>
<span style="font-size: 12px; color: #9b59b6;">${tempExpLog.filter(e => e.type === 'battle').reduce((sum, e) => sum + e.amount, 0)} XP</span>
</div>
</div>
</div>
<div class="progress-info" style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 12px;
color: #95a5a6;
">
<div>Postęp: ${processed}/${total} stron</div>
<div>Znalezione wpisy: ${foundEntries}</div>
</div>
<div class="progress-container" style="
background: rgba(255, 255, 255, 0.1);
height: 6px;
border-radius: 3px;
overflow: hidden;
margin-bottom: 10px;
">
<div class="progress-bar" style="
width: ${percent}%;
background: linear-gradient(90deg, #2ecc71, #27ae60);
height: 100%;
transition: width 0.3s ease;
border-radius: 3px;
"></div>
</div>
${timeInfo ? `
<div style="
text-align: center;
color: #95a5a6;
font-size: 12px;
margin-top: 10px;
padding: 5px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
">${timeInfo}</div>
` : ''}
</div>
`;
}
// Dodaj nową funkcję do obsługi przeciągania statusu
function makeStatusDraggable(element) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
element.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
if (e.target.closest('.status-container')) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, element);
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function setTranslate(xPos, yPos, el) {
// Zabezpieczenie przed wyjściem poza ekran
const rect = el.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (xPos < -rect.width + 50) xPos = -rect.width + 50;
if (xPos > windowWidth - 50) xPos = windowWidth - 50;
if (yPos < 0) yPos = 0;
if (yPos > windowHeight - 50) yPos = windowHeight - 50;
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
}
// Funkcja do aktualizacji statystyk XP
function updateXPStats() {
const workXP = experienceLog.reduce((sum, entry) => {
if (entry.type === 'work') {
return sum + entry.amount;
}
return sum;
}, 0);
const duelXP = experienceLog.reduce((sum, entry) => {
if (entry.type === 'duel') {
return sum + entry.amount;
}
return sum;
}, 0);
const battleXP = experienceLog.reduce((sum, entry) => {
if (entry.type === 'battle') {
return sum + entry.amount;
}
return sum;
}, 0);
const workXPElement = document.querySelector('#work-xp');
const duelXPElement = document.querySelector('#duel-xp');
const battleXPElement = document.querySelector('#battle-xp');
if (workXPElement) workXPElement.textContent = workXP + ' XP';
if (duelXPElement) duelXPElement.textContent = duelXP + ' XP';
if (battleXPElement) battleXPElement.textContent = battleXP + ' XP';
}
// Funkcja do debugowania systemu raportów
function debugReportSystem() {
// Sprawdź różne parametry zapytań
const testQueries = [
{ page: 1, folder: 'all' },
{ page: 1, folder: 'all', offset: 0 },
{ page: 1, folder: 'all', offset: 0, limit: 50 }
];
function makeTestQuery(params, index) {
setTimeout(() => {
Ajax.remoteCall('reports', 'get_reports', params, function(response) {
});
}, index * 1000);
}
testQueries.forEach(makeTestQuery);
}
function formatTimeRemaining(milliseconds) {
if (!milliseconds || isNaN(milliseconds)) return "obliczanie...";
const seconds = milliseconds / 1000;
if (seconds < 60) return Math.round(seconds) + ' sekund';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return minutes + ' min ' + remainingSeconds + ' sek';
}
function calculateTimeEstimate(processedPages, totalPages) {
if (processedPages < 2) return null;
const currentTime = Date.now();
const timeElapsed = currentTime - collectionStartTime;
// Oblicz średni czas na stronę w milisekundach
const avgTimePerPage = timeElapsed / processedPages;
if (isNaN(avgTimePerPage)) return null;
// Oblicz pozostały czas w milisekundach
const remainingPages = totalPages - processedPages;
const estimatedTimeRemaining = remainingPages * avgTimePerPage;
return formatTimeRemaining(estimatedTimeRemaining);
}
// Funkcja do tłumaczenia nazw kategorii
function getCategoryName(folder) {
switch(folder) {
case 'all': return 'WSZYSTKIE RAPORTY';
case 'job': return 'PRACE';
case 'duel': return 'POJEDYNKI';
case 'fortbattle': return 'BITWY FORTOWE';
default: return folder;
}
}
// Funkcja do zapisywania stanu kolekcji
function saveCollectionState(state) {
localStorage.setItem('collectionState', JSON.stringify(state));
}
// Funkcja do czyszczenia stanu kolekcji
function clearCollectionState() {
collectionState = {
inProgress: false,
currentPage: 1,
folder: null,
tempLog: [],
totalPages: 0
};
saveCollectionState(collectionState);
}
function toggleMainElements(show) {
const experienceHeader = document.querySelector('#experience-tracker > div:first-child');
const statsGrid = document.querySelector('#experience-tracker > div:nth-child(2)');
const statusElement = document.querySelector('#collection-status');
if (experienceHeader && statsGrid) {
if (show) {
// Pokazujemy główne elementy z animacją
experienceHeader.style.display = 'flex';
statsGrid.style.display = 'grid';
setTimeout(() => {
experienceHeader.style.opacity = '1';
statsGrid.style.opacity = '1';
if (statusElement) {
statusElement.style.opacity = '0';
setTimeout(() => {
statusElement.remove();
}, 300);
}
}, 50);
} else {
// Ukrywamy główne elementy z animacją
experienceHeader.style.opacity = '0';
statsGrid.style.opacity = '0';
setTimeout(() => {
experienceHeader.style.display = 'none';
statsGrid.style.display = 'none';
}, 300);
}
}
}
// Funkcja do zbierania historii doświadczenia
function collectExperienceHistory(maxPages = null, folder = 'all', callback = null) {
if (isCollectingHistory) {
showError('Pobieranie danych jest już w trakcie. Poczekaj na zakończenie.');
if (callback) callback();
return;
}
// Ukryj główne elementy przed rozpoczęciem pobierania
toggleMainElements(false);
isCollectingHistory = true;
isPaused = false;
shouldCancel = false;
collectionStartTime = Date.now();
processedPagesTime = [];
// Sprawdzamy, czy mamy zapisany stan kolekcji do wznowienia
const shouldResume = collectionState.inProgress &&
collectionState.folder === folder &&
collectionState.tempLog.length > 0;
if (shouldResume) {
tempExpLog = collectionState.tempLog;
showError(`Wznawianie zbierania danych dla ${getCategoryName(folder)} od strony ${collectionState.currentPage}`);
} else {
tempExpLog = [];
collectionState.currentPage = 1;
collectionState.folder = folder;
collectionState.tempLog = [];
}
// Zachowujemy istniejące wpisy z innych kategorii
let existingEntries = [];
if (folder !== 'all') {
existingEntries = experienceLog.filter(entry => {
const entryCategory =
entry.type === 'work' ? 'job' :
entry.type === 'duel' ? 'duel' :
entry.type === 'battle' ? 'fortbattle' : 'other';
return entryCategory !== folder;
});
}
// Pokaż przyciski kontrolne i ukryj standardowe
const controlButtons = document.querySelector('#collection-controls');
const standardButtons = document.querySelector('#standard-buttons');
if (controlButtons) {
controlButtons.style.display = 'grid';
}
if (standardButtons) {
standardButtons.style.display = 'none';
}
// Dostosowane opóźnienia
const MIN_DELAY = 2400;
const MAX_DELAY = 3000;
const RETRY_DELAY = 3000;
let processedPages = shouldResume ? collectionState.currentPage - 1 : 0;
let failedAttempts = 0;
const MAX_RETRIES = 3;
function getRandomDelay() {
return Math.floor(Math.random() * (MAX_DELAY - MIN_DELAY + 1)) + MIN_DELAY;
}
function finishCollection(wasSuccessful = true) {
isCollectingHistory = false;
isPaused = false;
// Pokaż z powrotem główne elementy
toggleMainElements(true);
// Przywróć standardowe przyciski i ukryj kontrolne
const controlButtons = document.querySelector('#collection-controls');
const standardButtons = document.querySelector('#standard-buttons');
if (controlButtons) {
controlButtons.style.display = 'none';
}
if (standardButtons) {
standardButtons.style.display = 'grid';
}
// Ukryj status pobierania
const statusElement = document.querySelector('#collection-status');
if (statusElement) {
statusElement.innerHTML = '';
}
if (!wasSuccessful) {
// Zapisz stan kolekcji do późniejszego wznowienia
collectionState.inProgress = true;
collectionState.tempLog = tempExpLog;
saveCollectionState(collectionState);
showError('Przerwano zbieranie danych. Możesz wznowić je później.');
if (callback) callback();
return;
}
// Wyczyść stan kolekcji po udanym zakończeniu
clearCollectionState();
if (tempExpLog.length === 0 && existingEntries.length === 0) {
showError('Nie znaleziono żadnych wpisów z doświadczeniem.');
if (callback) callback();
return;
}
// Łączymy nowe wpisy z zachowanymi wpisami z innych kategorii
if (folder === 'all') {
experienceLog = tempExpLog;
} else {
experienceLog = [...tempExpLog, ...existingEntries];
}
totalExperience = experienceLog.reduce((sum, entry) => sum + entry.amount, 0);
localStorage.setItem('experienceLog', JSON.stringify(experienceLog));
localStorage.setItem('totalExperience', JSON.stringify(totalExperience));
updateDisplay();
updateXPStats();
const newEntriesCount = tempExpLog.length;
showError(`Zakończono pobieranie ${getCategoryName(folder)}! ${folder === 'all' ? 'Znaleziono' : 'Dodano'} ${newEntriesCount} wpisów.`);
if (callback) {
callback();
}
}
// Pobierz wszystkie raporty
Ajax.remoteCall('reports', 'get_reports', {
page: 1,
folder: folder
}, function(initialData) {
if (!initialData || initialData.error) {
isCollectingHistory = false;
showError('Nie udało się pobrać informacji o raportach. Spróbuj ponownie.');
return;
}
const totalPages = initialData.count;
collectionState.totalPages = totalPages;
const pagesToProcess = maxPages ? Math.min(maxPages, totalPages) : totalPages;
function processPage(page) {
if (shouldCancel) {
finishCollection(false);
return;
}
if (!isCollectingHistory || page > pagesToProcess) {
finishCollection(true);
return;
}
if (isPaused) {
// Zapisz aktualny stan przed zatrzymaniem
collectionState.currentPage = page;
collectionState.tempLog = tempExpLog;
collectionState.inProgress = true;
saveCollectionState(collectionState);
setTimeout(() => processPage(page), 500);
return;
}
collectionState.currentPage = page;
saveCollectionState(collectionState);
Ajax.remoteCall('reports', 'get_reports', {
page: page,
folder: folder
}, function(data) {
if (!isCollectingHistory || shouldCancel) return;
if (data && !data.error && data.reports && data.reports.length > 0) {
failedAttempts = 0;
data.reports.forEach(report => {
// Najpierw sprawdzamy typ raportu
const reportType = report.title.includes('Raport dot. pracy') ? 'work' :
report.title.includes('Pojedynek') ? 'duel' :
(report.title.includes('Bitwa') || report.title.includes('Fort')) ? 'battle' : 'other';
// Domyślnie zakładamy, że nie powinniśmy zliczyć XP
let shouldCount = false;
// Dla pojedynków sprawdzamy szczegóły
if (reportType === 'duel') {
// Jeśli to pojedynek z bandytą, zawsze zliczamy
if (report.title.includes('bandytą') || report.title.includes('bandit')) {
shouldCount = true;
} else {
// Dla pojedynków z graczami sprawdzamy zwycięzcę
const winnerMatch = report.popupData.match(/Zwycięzca:\s*([^<]+)/) ||
report.popupData.match(/(\w+)\s+wygrywa pojedynek!/);
if (winnerMatch) {
const winner = winnerMatch[1].trim();
shouldCount = winner === playerName;
} else {
shouldCount = false;
}
}
} else {
// Dla nie-pojedynków zawsze zliczamy
shouldCount = true;
}
// Tylko jeśli powinniśmy zliczyć XP, sprawdzamy czy jest XP do dodania
if (shouldCount) {
const expMatch = report.popupData.match(/experience.png[^>]*>(?:[^<]*<\/[^>]+>)*[^<]*<td>(\d+)<\/td>/) ||
report.popupData.match(/Doświadczenie<\/span>\s*<span[^>]*>(\d+)\s*punktów/);
const exp = expMatch ? parseInt(expMatch[1]) : 0;
if (exp > 0) {
tempExpLog.push({
amount: exp,
source: report.title,
timestamp: report.date_received,
page: page,
type: reportType
});
// Aktualizuj zapisany stan
collectionState.tempLog = tempExpLog;
saveCollectionState(collectionState);
}
}
});
processedPages++;
updateCollectionStatus(processedPages, pagesToProcess, tempExpLog.length);
setTimeout(() => processPage(page + 1), getRandomDelay());
} else {
failedAttempts++;
if (failedAttempts < MAX_RETRIES) {
setTimeout(() => processPage(page), RETRY_DELAY);
} else {
showError(`Nie udało się przetworzyć strony ${page} po ${MAX_RETRIES} próbach`);
finishCollection(true);
}
}
});
}
processPage(shouldResume ? collectionState.currentPage : 1);
});
}
// Funkcja do aktualizacji wyświetlania
function updateDisplay() {
const totalExperienceElement = document.querySelector('#total-experience');
if (totalExperienceElement) {
totalExperienceElement.textContent = totalExperience;
}
}
// Funkcja do dodawania okna z doświadczeniem
function addExperienceWindow() {
const existingWindow = document.querySelector('#experience-tracker');
if (existingWindow) {
existingWindow.style.display = isTrackerVisible ? 'block' : 'none';
return;
}
// Pobierz zapisaną pozycję
const savedPosition = JSON.parse(localStorage.getItem('experienceTrackerPosition')) || { top: '50px', left: 'auto', right: '310px' };
const trackerDiv = document.createElement('div');
trackerDiv.id = 'experience-tracker';
trackerDiv.style.cssText = `
position: fixed;
top: ${savedPosition.top};
left: ${savedPosition.left};
right: ${savedPosition.right};
width: 280px;
padding: 15px;
background: rgba(20, 20, 20, 0.95);
color: #fff;
border: 1px solid #444;
border-radius: 8px;
z-index: 1000;
cursor: move;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
font-family: Arial, sans-serif;
display: ${isTrackerVisible ? 'block' : 'none'};
`;
// Dodaj style dla animacji
const style = document.createElement('style');
style.textContent = `
#experience-tracker > div {
transition: opacity 0.3s ease;
}
#collection-status {
transition: opacity 0.3s ease;
}
`;
document.head.appendChild(style);
trackerDiv.innerHTML = `
<div style="background: linear-gradient(135deg, rgba(241, 196, 15, 0.1), rgba(241, 196, 15, 0.05)); border-radius: 10px; padding: 15px; margin-bottom: 15px; border: 1px solid rgba(241, 196, 15, 0.2); display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; flex-direction: column;">
<span style="font-size: 12px; color: #F1C40F; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;">Całkowite doświadczenie</span>
<span id="total-experience" style="font-size: 24px; font-weight: bold; color: #F1C40F; transition: all 0.3s ease;">${totalExperience} XP</span>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px; font-size: 13px;">
<div style="background: rgba(39, 174, 96, 0.2); padding: 8px; border-radius: 8px; text-align: center; border: 1px solid rgba(39, 174, 96, 0.3);">
<div style="color: #2ecc71; font-weight: bold;">Prace</div>
<span id="work-xp" style="color: #fff;">0 XP</span>
</div>
<div style="background: rgba(231, 76, 60, 0.2); padding: 8px; border-radius: 8px; text-align: center; border: 1px solid rgba(231, 76, 60, 0.3);">
<div style="color: #e74c3c; font-weight: bold;">PvP</div>
<span id="duel-xp" style="color: #fff;">0 XP</span>
</div>
<div style="background: rgba(155, 89, 182, 0.2); padding: 8px; border-radius: 8px; text-align: center; border: 1px solid rgba(155, 89, 182, 0.3);">
<div style="color: #9b59b6; font-weight: bold;">Bitwy</div>
<span id="battle-xp" style="color: #fff;">0 XP</span>
</div>
</div>
<div id="collection-status"></div>
<div id="collection-controls" style="display: none; grid-template-columns: 1fr 1fr; gap: 10px; margin: 10px 0;">
<button id="pause-collection" style="
padding: 10px 15px;
background: rgba(241, 196, 15, 0.1);
color: #F1C40F;
border: 1px solid rgba(241, 196, 15, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">⏸️</span>
Wstrzymaj
</button>
<button id="cancel-collection" style="
padding: 10px 15px;
background: rgba(231, 76, 60, 0.1);
color: #E74C3C;
border: 1px solid rgba(231, 76, 60, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">✖️</span>
Anuluj
</button>
</div>
<div id="standard-buttons" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
<button id="update-history" style="
padding: 10px 0px;
background: rgba(33, 150, 243, 0.1);
color: #2196F3;
border: 1px solid rgba(33, 150, 243, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">🔄</span>
Aktualizuj dane
</button>
<button id="collect-history" style="
padding: 10px 0px;
background: rgba(130, 224, 170, 0.1);
color: #82E0AA;
border: 1px solid rgba(130, 224, 170, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">📊</span>
Zbierz dane
</button>
<button id="show-experience-log" style="
padding: 10px 15px;
background: rgba(230, 126, 34, 0.1);
color: #E67E22;
border: 1px solid rgba(230, 126, 34, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">📋</span>
Szczegóły
</button>
<button id="reset-experience" style="
padding: 10px 15px;
background: rgba(255, 148, 148, 0.1);
color: #FF9494;
border: 1px solid rgba(255, 148, 148, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">🗑️</span>
Kasuj
</button>
</div>
`;
// Modyfikacja efektu hover dla przycisków
const buttons = trackerDiv.getElementsByTagName('button');
for (let button of buttons) {
button.addEventListener('mouseover', function() {
this.style.transform = 'translateY(-1px)';
this.style.boxShadow = '0 4px 12px ' + (
this.id === 'pause-collection' ? 'rgba(241, 196, 15, 0.2)' :
this.id === 'cancel-collection' ? 'rgba(231, 76, 60, 0.2)' :
'rgba(255, 255, 255, 0.1)'
);
});
button.addEventListener('mouseout', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = 'none';
});
}
document.body.appendChild(trackerDiv);
// Dodanie obsługi przycisków
document.querySelector('#collect-history').addEventListener('click', () => {
if (!isCollectingHistory) {
showCategorySelectionPanel('collect');
}
});
document.querySelector('#pause-collection').addEventListener('click', function() {
isPaused = !isPaused;
if (isPaused) {
this.innerHTML = `
<span style="font-size: 16px;">▶️</span>
Wznów
`;
this.style.background = 'rgba(46, 204, 113, 0.1)';
this.style.color = '#2ECC71';
this.style.border = '1px solid rgba(46, 204, 113, 0.2)';
} else {
this.innerHTML = `
<span style="font-size: 16px;">⏸️</span>
Wstrzymaj
`;
this.style.background = 'rgba(241, 196, 15, 0.1)';
this.style.color = '#F1C40F';
this.style.border = '1px solid rgba(241, 196, 15, 0.2)';
}
});
document.querySelector('#cancel-collection').addEventListener('click', function() {
shouldCancel = true;
});
document.querySelector('#show-experience-log').addEventListener('click', showExperienceDetails);
document.querySelector('#reset-experience').addEventListener('click', resetExperience);
// Dodaj obsługę przycisku aktualizacji
document.querySelector('#update-history').addEventListener('click', () => {
if (!isCollectingHistory) {
showCategorySelectionPanel('update');
}
});
makeDraggable(trackerDiv);
// Aktualizuj statystyki przy tworzeniu okna
updateXPStats();
// Modyfikacja funkcji addExperience aby aktualizowała statystyki
const originalAddExperience = addExperience;
addExperience = function(amount, source) {
originalAddExperience.call(this, amount, source);
updateXPStats();
};
// Dodanie aktualizacji statystyk po zebraniu danych
const originalCollectExperienceHistory = collectExperienceHistory;
collectExperienceHistory = function(maxPages, folder, callback = null) {
originalCollectExperienceHistory.call(this, maxPages, folder, callback);
setTimeout(updateXPStats, 1000); // Aktualizuj po zakończeniu zbierania
};
}
// Funkcja przeciągania elementu
function makeDraggable(element) {
let isDragging = false;
let startX, startY, initialX, initialY;
// Znajdujemy element nagłówka (cały górny obszar z doświadczeniem)
const headerArea = element.querySelector('div:first-child');
if (headerArea) {
headerArea.style.cursor = 'move';
}
element.onmousedown = (event) => {
let targetElement = event.target;
let isInHeader = false;
while (targetElement && targetElement !== element) {
if (targetElement === headerArea) {
isInHeader = true;
break;
}
targetElement = targetElement.parentElement;
}
if (isInHeader && event.target.tagName !== 'BUTTON') {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
initialX = parseInt(window.getComputedStyle(element).left, 10) || 0;
initialY = parseInt(window.getComputedStyle(element).top, 10) || 0;
document.onmousemove = onMouseMove;
document.onmouseup = onMouseUp;
}
};
function onMouseMove(event) {
if (!isDragging) return;
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
element.style.left = `${initialX + deltaX}px`;
element.style.top = `${initialY + deltaY}px`;
element.style.right = 'auto';
}
function onMouseUp() {
if (isDragging) {
const position = {
top: element.style.top,
left: element.style.left,
right: element.style.right
};
localStorage.setItem('experienceTrackerPosition', JSON.stringify(position));
}
isDragging = false;
document.onmousemove = null;
document.onmouseup = null;
}
}
// Funkcja zapisywania pozycji okna
function savePosition(element) {
const top = element.style.top;
const left = element.style.left;
const right = element.style.right;
localStorage.setItem('experienceTrackerPosition', JSON.stringify({ top, left, right }));
alert('Pozycja okna została zapisana!');
}
// Funkcja do zapisywania zdobytego doświadczenia
function addExperience(amount, source) {
const now = new Date();
const day = String(now.getDate()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const year = now.getFullYear();
const formattedDate = `${day}.${month}.${year}`;
totalExperience += amount;
experienceLog.push({ amount, source, timestamp: formattedDate });
// Aktualizuj lokalne przechowywanie
localStorage.setItem('totalExperience', JSON.stringify(totalExperience));
localStorage.setItem('experienceLog', JSON.stringify(experienceLog));
// Zaktualizuj wyświetlaną wartość
const totalExperienceElement = document.querySelector('#total-experience');
if (totalExperienceElement) {
totalExperienceElement.textContent = totalExperience;
}
}
// Funkcja do pokazywania szczegółów doświadczenia
function showExperienceDetails() {
const logWindow = window.open('', '_blank');
// Ensure we have a valid window reference
if (logWindow === null) {
alert('Proszę zezwolić na otwieranie nowych kart w przeglądarce.');
return;
}
logWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Historia doświadczenia</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
}
html {
min-width: 1300px;
}
body {
background: #f5f6fa;
color: #2d3436;
line-height: 1.6;
padding: 20px;
min-width: 1300px;
width: 100%;
}
.container {
min-width: 1300px;
width: 100%;
margin: 0 auto;
padding: 20px;
}
.controls {
background: white;
padding: 15px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.controls select,
.controls input,
.controls button {
padding: 8px 12px;
border: 1px solid #dfe6e9;
border-radius: 6px;
font-size: 13px;
width: 100%;
}
.controls button {
background: #2196F3;
color: white;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.controls button:hover {
background: #1976D2;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
height: 400px;
margin: 20px 0;
}
.content-wrapper {
display: grid;
grid-template-columns: 20% 35% 45%;
gap: 20px;
margin-top: 20px;
width: 100%;
height: calc(100vh - 220px); /* Dodajemy stałą wysokość dla wrappera */
}
.left-column, .middle-column, .right-column {
width: 100%;
min-width: 0;
height: 100%; /* Ustawiamy pełną wysokość dla kolumn */
}
.right-column {
display: flex;
flex-direction: column;
gap: 15px;
height: 560px; /* Całkowita wysokość kolumny */
}
.right-content {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
height: 500px !important;
overflow: hidden;
min-height: 500px !important;
max-height: 500px !important;
display: flex;
flex-direction: column;
}
.right-content h2 {
margin: 0 0 15px 0;
color: #2d3436;
font-size: 18px;
flex: 0 0 auto;
}
.entries-list {
flex: 1;
overflow-y: auto;
}
.entries-header {
display: grid;
grid-template-columns: 100px 1fr 100px;
gap: 15px;
padding: 10px 15px;
background: white;
font-weight: bold;
border-bottom: 2px solid #ddd;
position: sticky;
top: 0;
z-index: 1;
flex: 0 0 auto; /* Dodajemy flex aby header nie rozpychał kontenera */
}
.entry {
display: grid;
grid-template-columns: 100px 1fr 100px;
gap: 15px;
padding: 15px;
border-bottom: 1px solid #f1f2f6;
transition: all 0.2s ease;
align-items: center;
}
.entry:hover {
background: #f8f9fa;
}
.entry:last-child {
border-bottom: none;
}
.entry .timestamp {
color: #636e72;
font-size: 14px;
white-space: nowrap;
}
.entry .source {
color: #2d3436;
font-weight: 500;
}
.entry .amount {
color: #00b894;
font-weight: 700;
font-size: 16px;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
margin-right: 8px;
}
.badge-church { background: #81ecec; color: #00cec9; }
.badge-work { background: #55efc4; color: #00b894; }
.badge-duel-player { background: #ff7675; color: #d63031; }
.badge-duel-bandit { background: #fab1a0; color: #e17055; }
.badge-battle { background: #9b59b6; color: #8e44ad; }
.badge-other { background: #81ecec; color: #00cec9; }
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
height: 45px;
flex: 0 0 auto; /* Dodajemy flex aby paginacja nie rozpychała kontenera */
}
.pagination button {
padding: 8px 12px;
border: 1px solid #dfe6e9;
background: white;
color: #2d3436;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.pagination button:hover {
border-color: #0984e3;
color: #0984e3;
}
.pagination button.active {
background: #0984e3;
color: white;
border-color: #0984e3;
}
.pagination .info {
color: #636e72;
font-size: 14px;
}
.daily-xp-container {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
height: 500px !important;
overflow: hidden;
min-height: 500px !important;
max-height: 500px !important;
display: flex;
flex-direction: column;
}
.daily-xp-container h2 {
margin: 0 0 15px 0;
color: #2d3436;
font-size: 18px;
flex: 0 0 auto;
}
.daily-xp-header {
display: grid;
grid-template-columns: 100px minmax(150px, 1fr) 100px;
gap: 15px;
padding: 10px 15px;
background: white;
font-weight: bold;
border-bottom: 2px solid #ddd;
flex: 0 0 auto;
}
.daily-xp-header > div:nth-child(3) {
text-align: right;
padding-right: 30px;
}
.daily-xp-entries {
flex: 1;
overflow-y: auto;
padding-right: 5px;
}
.daily-xp-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.daily-xp-table td {
padding: 10px 15px;
border-bottom: 1px solid #f1f2f6;
}
.daily-xp-table td:first-child {
width: 100px;
}
.daily-xp-table td:nth-child(2) {
width: auto;
min-width: 150px;
}
.daily-xp-table td:last-child {
width: 100px;
}
.daily-xp-entry {
transition: all 0.2s ease;
}
.daily-xp-entry:hover {
background: #f8f9fa;
}
.daily-xp-entry:last-child td {
border-bottom: none;
}
.daily-xp-entry .date {
color: #636e72;
font-size: 14px;
}
.daily-xp-entry .xp {
color: #2d3436;
font-weight: 500;
text-align: center;
}
.daily-xp-entry .difference {
text-align: right;
font-weight: 700;
font-size: 14px;
padding-right: 30px;
}
.daily-xp-entry .difference.positive {
color: #27ae60;
}
.daily-xp-entry .difference.negative {
color: #e74c3c;
}
.daily-xp-entry .difference.neutral {
color: #666;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
transition: all 0.3s ease;
margin-bottom: 15px;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.stat-card h3 {
color: #636e72;
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 24px;
font-weight: 700;
color: #2d3436;
margin-bottom: 5px;
}
.stat-card .sub-value {
font-size: 14px;
color: #636e72;
}
</style>
</head>
<body>
<div class="container">
<div class="controls">
<select id="categoryFilter">
<option value="all">Wszystkie źródła</option>
<option value="Prace">Zwykłe prace</option>
<option value="Kosciol">Budowa kościoła</option>
<option value="PojedynkiGracze">Pojedynki z graczami</option>
<option value="PojedynkiBandyci">Pojedynki z bandytami</option>
<option value="Bitwy">Bitwy fortowe</option>
<option value="Inne">Inne</option>
</select>
<select id="sortBy">
<option value="date-desc">Od najnowszych</option>
<option value="exp-desc">Najwięcej XP</option>
<option value="exp-asc">Najmniej XP</option>
</select>
<input type="text" id="searchInput" placeholder="Szukaj...">
<button onclick="applyFilters()">Filtruj</button>
</div>
<div class="chart-container">
<canvas id="dailyExpChart"></canvas>
</div>
<div class="content-wrapper">
<div class="left-column">
<div id="summary" class="stats-grid"></div>
</div>
<div class="middle-column">
<div class="daily-xp-container">
<h2>Dzienne XP</h2>
<div class="daily-xp-header">
<div>Data</div>
<div style="text-align: center;">XP</div>
<div style="text-align: right;">Różnica</div>
</div>
<div class="daily-xp-entries">
<table class="daily-xp-table">
<tbody id="daily-xp-tbody"></tbody>
</table>
</div>
</div>
</div>
<div class="right-column">
<div class="right-content">
<h2>Raporty</h2>
<div class="entries-header">
<div>Data</div>
<div>Raport</div>
<div>XP</div>
</div>
<div class="entries-list" id="experience-list"></div>
</div>
<div id="pagination" class="pagination"></div>
</div>
</div>
</div>
<script>
let currentPage = 1;
const itemsPerPage = 25;
let filteredData = [];
const originalData = ${JSON.stringify(experienceLog)};
function updateSummary() {
const summary = {
total: 0,
categories: {
'Zwykłe prace': { exp: 0, count: 0, max: 0 },
'Budowa kościoła': { exp: 0, count: 0, max: 0 },
'Pojedynki z graczami': { exp: 0, count: 0, max: 0 },
'Pojedynki z bandytami': { exp: 0, count: 0, max: 0 },
'Bitwy fortowe': { exp: 0, count: 0, max: 0 },
'Inne': { exp: 0, count: 0, max: 0 }
}
};
// Funkcja pomocnicza do formatowania daty
function formatDate(timestamp) {
if (!timestamp) return '';
// Jeśli timestamp zaczyna się od "Godzina:"
if (timestamp.startsWith('Godzina:')) {
const today = new Date();
const day = String(today.getDate()).padStart(2, '0');
const month = String(today.getMonth() + 1).padStart(2, '0');
const year = today.getFullYear();
return day + '.' + month + '.' + year;
}
// Jeśli data jest w formacie DD.MM.YYYY, HH:mm:ss
if (timestamp.includes('.') && timestamp.includes(':')) {
const [datePart] = timestamp.split(',');
return datePart.trim();
}
// Jeśli data jest w formacie "DD MMM YYYY"
const monthMap = {
'sty': '01', 'lut': '02', 'mar': '03', 'kwi': '04',
'maj': '05', 'cze': '06', 'lip': '07', 'sie': '08',
'wrz': '09', 'paź': '10', 'lis': '11', 'gru': '12'
};
const parts = timestamp.split(' ');
if (parts.length === 3) {
const day = String(parseInt(parts[0])).padStart(2, '0');
const monthAbbr = parts[1].toLowerCase();
const year = parts[2];
if (monthMap[monthAbbr]) {
return day + '.' + monthMap[monthAbbr] + '.' + year;
}
}
// Dla pozostałych przypadków zwróć oryginalny timestamp
return timestamp;
}
// Analiza dziennego XP
const dailyExp = {};
const dailyMaxNpcExp = {};
originalData.forEach(entry => {
const formattedDate = formatDate(entry.timestamp);
if (!dailyExp[formattedDate]) {
dailyExp[formattedDate] = 0;
dailyMaxNpcExp[formattedDate] = 0;
}
dailyExp[formattedDate] += entry.amount;
if (entry.source.includes('Pojedynek z bandytą')) {
dailyMaxNpcExp[formattedDate] = Math.max(dailyMaxNpcExp[formattedDate], entry.amount);
}
// Aktualizacja statystyk ogólnych
let category = 'Inne';
if (entry.type === 'work') {
if (entry.source.includes('rozbudowa Kościół')) {
category = 'Budowa kościoła';
} else {
category = 'Zwykłe prace';
}
} else if (entry.type === 'duel') {
if (entry.source.includes('bandytą') || entry.source.includes('bandit')) {
category = 'Pojedynki z bandytami';
} else {
category = 'Pojedynki z graczami';
}
} else if (entry.type === 'battle') {
category = 'Bitwy fortowe';
}
summary.categories[category].exp += entry.amount;
summary.categories[category].count++;
summary.categories[category].max = Math.max(summary.categories[category].max, entry.amount);
summary.total += entry.amount;
});
// Sortowanie dat
const sortedDates = Object.keys(dailyExp).sort((a, b) => {
const [dayA, monthA, yearA] = a.split(' ');
const [dayB, monthB, yearB] = b.split(' ');
const monthMap = {
'sty': 1, 'lut': 2, 'mar': 3, 'kwi': 4, 'maj': 5, 'cze': 6,
'lip': 7, 'sie': 8, 'wrz': 9, 'paź': 10, 'lis': 11, 'gru': 12
};
if (yearA !== yearB) return yearB - yearA;
if (monthMap[monthA] !== monthMap[monthB]) return monthMap[monthB] - monthMap[monthA];
return dayB - dayA;
});
// Aktualizacja HTML
const summaryDiv = document.getElementById('summary');
const summaryHtml = [
'<div class="stats-grid">',
'<div class="stat-card">',
'<h3>Całkowite XP</h3>',
'<div class="value">' + summary.total.toLocaleString() + ' XP</div>',
'<div class="sub-value">' + originalData.length + ' wpisów</div>',
'</div>',
Object.entries(summary.categories).map(([category, data]) =>
data.count > 0 ? [
'<div class="stat-card">',
'<h3>' + category + '</h3>',
'<div class="value">' + data.exp.toLocaleString() + ' XP</div>',
'<div class="sub-value">',
'Ilość: ' + data.count + '<br>',
'Średnio: ' + Math.round(data.exp / data.count).toLocaleString() + ' XP<br>',
'Max: ' + data.max.toLocaleString() + ' XP',
'</div>',
'</div>'
].join('') : ''
).join(''),
'</div>'
].join('');
summaryDiv.innerHTML = summaryHtml;
// Aktualizacja tabeli dziennego XP
const dailyXpTbody = document.getElementById('daily-xp-tbody');
if (dailyXpTbody) {
let htmlContent = '';
htmlContent += sortedDates.map((date, index) => {
const prevDayXP = index < sortedDates.length - 1 ? dailyExp[sortedDates[index + 1]] : 0;
const difference = dailyExp[date] - prevDayXP;
const differenceText = index < sortedDates.length - 1 ?
(difference > 0 ? '+' + difference.toLocaleString() : difference.toLocaleString()) : '-';
const differenceClass = difference > 0 ? 'positive' : difference < 0 ? 'negative' : 'neutral';
return '<tr class="daily-xp-entry">' +
'<td class="date">' + date + '</td>' +
'<td class="xp">' + dailyExp[date].toLocaleString() + '</td>' +
'<td class="difference ' + differenceClass + '">' + differenceText + ' XP</td>' +
'</tr>';
}).join('');
dailyXpTbody.innerHTML = htmlContent;
}
// Tworzenie wykresu
const ctx = document.getElementById('dailyExpChart');
new Chart(ctx, {
type: 'line',
data: {
labels: sortedDates.slice(0, 30), // Ostatnie 30 dni
datasets: [{
label: 'XP dziennie',
data: sortedDates.slice(0, 30).map(date => dailyExp[date]),
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
tension: 0.1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
callbacks: {
label: function(context) {
return context.raw.toLocaleString() + ' XP';
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value.toLocaleString();
}
}
}
}
}
});
}
function applyFilters() {
const category = document.getElementById('categoryFilter').value;
const sortBy = document.getElementById('sortBy').value;
const searchText = document.getElementById('searchInput').value.toLowerCase();
filteredData = originalData.filter(entry => {
const matchesCategory =
category === 'all' ? true :
category === 'Kosciol' ? (entry.type === 'work' && entry.source.includes('rozbudowa Kościół')) :
category === 'Prace' ? (entry.type === 'work' && !entry.source.includes('rozbudowa Kościół')) :
category === 'PojedynkiGracze' ? (entry.type === 'duel' && !entry.source.includes('bandytą') && !entry.source.includes('bandit')) :
category === 'PojedynkiBandyci' ? (entry.type === 'duel' && (entry.source.includes('bandytą') || entry.source.includes('bandit'))) :
category === 'Bitwy' ? entry.type === 'battle' :
category === 'Inne' ? (entry.type !== 'work' && entry.type !== 'duel' && entry.type !== 'battle') : false;
const matchesSearch = searchText === '' ||
entry.source.toLowerCase().includes(searchText);
return matchesCategory && matchesSearch;
});
filteredData.sort((a, b) => {
switch(sortBy) {
case 'date-desc':
return new Date(b.timestamp) - new Date(a.timestamp);
case 'exp-desc':
return b.amount - a.amount;
case 'exp-asc':
return a.amount - b.amount;
default:
return 0;
}
});
currentPage = 1;
renderPage();
}
function renderPage() {
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageItems = filteredData.slice(start, end);
const listDiv = document.getElementById('experience-list');
listDiv.innerHTML = '';
if (pageItems.length === 0) {
listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">Brak wpisów spełniających kryteria</div>';
return;
}
// Funkcja pomocnicza do formatowania daty
function formatDate(timestamp) {
if (!timestamp) return '';
// Jeśli timestamp zaczyna się od "Godzina:"
if (timestamp.startsWith('Godzina:')) {
const today = new Date();
const day = String(today.getDate()).padStart(2, '0');
const month = String(today.getMonth() + 1).padStart(2, '0');
const year = today.getFullYear();
return day + '.' + month + '.' + year;
}
// Jeśli data jest w formacie DD.MM.YYYY, HH:mm:ss
if (timestamp.includes('.') && timestamp.includes(':')) {
const [datePart] = timestamp.split(',');
return datePart.trim();
}
// Jeśli data jest w formacie "DD MMM YYYY"
const monthMap = {
'sty': '01', 'lut': '02', 'mar': '03', 'kwi': '04',
'maj': '05', 'cze': '06', 'lip': '07', 'sie': '08',
'wrz': '09', 'paź': '10', 'lis': '11', 'gru': '12'
};
const parts = timestamp.split(' ');
if (parts.length === 3) {
const day = String(parseInt(parts[0])).padStart(2, '0');
const monthAbbr = parts[1].toLowerCase();
const year = parts[2];
if (monthMap[monthAbbr]) {
return day + '.' + monthMap[monthAbbr] + '.' + year;
}
}
// Dla pozostałych przypadków zwróć oryginalny timestamp
return timestamp;
}
pageItems.forEach(entry => {
const entryDiv = document.createElement('div');
entryDiv.className = 'entry';
let badgeClass = '';
let badgeText = '';
if (entry.type === 'work') {
if (entry.source.includes('rozbudowa Kościół')) {
badgeClass = 'badge-church';
badgeText = 'Kościół';
} else {
badgeClass = 'badge-work';
badgeText = 'Praca';
}
} else if (entry.type === 'duel') {
if (entry.source.includes('bandytą')) {
badgeClass = 'badge-duel-bandit';
badgeText = 'Bandyta';
} else {
badgeClass = 'badge-duel-player';
badgeText = 'PvP';
}
} else if (entry.type === 'battle') {
badgeClass = 'badge-battle';
badgeText = 'Bitwa';
} else {
badgeClass = 'badge-other';
badgeText = 'Inne';
}
entryDiv.innerHTML = [
'<span class="timestamp">' + formatDate(entry.timestamp) + '</span>',
'<span class="source">',
'<span class="badge ' + badgeClass + '">' + badgeText + '</span>',
entry.source,
'</span>',
'<span class="amount">+' + entry.amount.toLocaleString() + ' XP</span>'
].join('');
listDiv.appendChild(entryDiv);
});
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
const paginationDiv = document.getElementById('pagination');
let html = [
'<div class="pagination">',
'<span class="info">Strona ' + currentPage + ' z ' + totalPages + ' (' + filteredData.length + ' wpisów)</span>'
];
if (totalPages > 1) {
if (currentPage > 1) {
html.push('<button onclick="changePage(1)">«</button>');
html.push('<button onclick="changePage(' + (currentPage - 1) + ')">‹</button>');
}
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
html.push('<button onclick="changePage(' + i + ')"' +
(i === currentPage ? ' class="active"' : '') + '>' + i + '</button>');
}
if (currentPage < totalPages) {
html.push('<button onclick="changePage(' + (currentPage + 1) + ')">›</button>');
html.push('<button onclick="changePage(' + totalPages + ')">»</button>');
}
}
html.push('</div>');
paginationDiv.innerHTML = html.join('');
}
function changePage(newPage) {
currentPage = newPage;
renderPage();
window.scrollTo(0, 0);
}
// Inicjalizacja
filteredData = [...originalData];
updateSummary();
applyFilters();
</script>
<script>
// Add resize handling
window.addEventListener('resize', function() {
if (window.outerWidth < 1300) {
window.resizeTo(1300, window.outerHeight);
}
});
</script>
</body>
</html>
`);
logWindow.document.close();
}
// Funkcja resetująca doświadczenie do 0
function resetExperience() {
totalExperience = 0;
experienceLog = [];
// Czyścimy również stan przerwanej kolekcji
clearCollectionState();
// Zapisz zresetowane wartości w localStorage
localStorage.setItem('totalExperience', JSON.stringify(totalExperience));
localStorage.setItem('experienceLog', JSON.stringify(experienceLog));
// Zaktualizuj wyświetlaną wartość doświadczenia
const totalExperienceElement = document.querySelector('#total-experience');
if (totalExperienceElement) {
totalExperienceElement.textContent = totalExperience + ' XP';
}
// Zaktualizuj statystyki z prac i pojedynków
const workXPElement = document.querySelector('#work-xp');
const duelXPElement = document.querySelector('#duel-xp');
const battleXPElement = document.querySelector('#battle-xp');
if (workXPElement) workXPElement.textContent = '0 XP';
if (duelXPElement) duelXPElement.textContent = '0 XP';
if (battleXPElement) battleXPElement.textContent = '0 XP';
}
// Nasłuchiwanie na zmiany w doświadczeniu (np. praca, bitwa)
const originalSetExperience = window.Character.setExperience;
window.Character.setExperience = function(newExperience) {
const currentExperience = this.experience;
const gainedExperience = newExperience - currentExperience;
if (gainedExperience > 0) {
addExperience(gainedExperience, 'Aktywność w grze'); // Domyślna aktywność
}
originalSetExperience.call(this, newExperience);
};
// Dodajemy nową funkcję do tworzenia panelu wyboru
function showCategorySelectionPanel(action) {
const existingPanel = document.querySelector('#category-selection-panel');
if (existingPanel) existingPanel.remove();
const experienceTracker = document.querySelector('#experience-tracker');
if (!experienceTracker) return;
const panel = document.createElement('div');
panel.id = 'category-selection-panel';
panel.style.cssText = `
color: white;
width: 100%;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
margin-top: 15px;
`;
const title = action === 'collect' ? 'Zbieranie danych' : 'Aktualizacja danych';
// Dodajemy informację o przerwanej kolekcji
const hasInterruptedCollection = collectionState.inProgress && collectionState.tempLog.length > 0;
panel.innerHTML = `
<div style="margin-bottom: 15px; text-align: center;">
<span style="font-size: 18px; color: #F1C40F; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; display: block;">
${title}
</span>
</div>
<div style="display: grid; gap: 12px;">
${hasInterruptedCollection ? `
<div style="margin-bottom: 15px; padding: 15px; background: rgba(241, 196, 15, 0.1); border: 1px solid rgba(241, 196, 15, 0.2); border-radius: 8px;">
<div style="margin-bottom: 8px; color: #F1C40F;">Wykryto przerwaną kolekcję:</div>
<div style="color: #DDD; font-size: 13px; line-height: 1.5;">
Kategoria: ${getCategoryName(collectionState.folder)}<br>
Postęp: ${collectionState.currentPage}/${collectionState.totalPages} stron<br>
Zebrane wpisy: ${collectionState.tempLog.length}
</div>
</div>
<button id="resume-collection" style="
padding: 10px 15px;
background: rgba(130, 224, 170, 0.1);
color: #82E0AA;
border: 1px solid rgba(130, 224, 170, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">▶️</span>
Wznów przerwaną kolekcję
</button>
<button id="clear-interrupted" style="
padding: 10px 15px;
background: rgba(255, 148, 148, 0.1);
color: #FF9494;
border: 1px solid rgba(255, 148, 148, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">🗑️</span>
Wyczyść przerwaną kolekcję
</button>
` : `
<label style="
display: flex;
align-items: center;
padding: 12px 15px;
background: rgba(241, 196, 15, 0.1);
border: 1px solid rgba(241, 196, 15, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
gap: 10px;
">
<input type="checkbox" data-folder="all" class="category-checkbox" style="
width: 16px;
height: 16px;
cursor: pointer;
">
<span style="color: #F1C40F; font-weight: 600;">WSZYSTKIE RAPORTY</span>
</label>
<label style="
display: flex;
align-items: center;
padding: 12px 15px;
background: rgba(46, 204, 113, 0.1);
border: 1px solid rgba(46, 204, 113, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
gap: 10px;
">
<input type="checkbox" data-folder="job" class="category-checkbox" style="
width: 16px;
height: 16px;
cursor: pointer;
">
<span style="color: #2ecc71; font-weight: 600;">PRACE</span>
</label>
<label style="
display: flex;
align-items: center;
padding: 12px 15px;
background: rgba(231, 76, 60, 0.1);
border: 1px solid rgba(231, 76, 60, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
gap: 10px;
">
<input type="checkbox" data-folder="duel" class="category-checkbox" style="
width: 16px;
height: 16px;
cursor: pointer;
">
<span style="color: #e74c3c; font-weight: 600;">POJEDYNKI</span>
</label>
<label style="
display: flex;
align-items: center;
padding: 12px 15px;
background: rgba(155, 89, 182, 0.1);
border: 1px solid rgba(155, 89, 182, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
gap: 10px;
">
<input type="checkbox" data-folder="fortbattle" class="category-checkbox" style="
width: 16px;
height: 16px;
cursor: pointer;
">
<span style="color: #9b59b6; font-weight: 600;">BITWY FORTOWE</span>
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 5px;">
<button id="start-collection" style="
padding: 10px 15px;
background: rgba(130, 224, 170, 0.1);
color: #82E0AA;
border: 1px solid rgba(130, 224, 170, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">▶️</span>
Start
</button>
<button id="cancel-selection" style="
padding: 10px 15px;
background: rgba(255, 148, 148, 0.1);
color: #FF9494;
border: 1px solid rgba(255, 148, 148, 0.2);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<span style="font-size: 16px;">✖️</span>
Anuluj
</button>
</div>
`}
</div>
`;
// Dodaj panel do głównego interfejsu
experienceTracker.appendChild(panel);
// Dodaj obsługę zdarzeń
const buttons = panel.getElementsByTagName('button');
for (let button of buttons) {
button.addEventListener('click', function() {
if (this.id === 'resume-collection') {
panel.remove();
collectExperienceHistory(null, collectionState.folder);
} else if (this.id === 'clear-interrupted') {
clearCollectionState();
panel.remove();
} else if (this.id === 'start-collection') {
const selectedFolders = Array.from(panel.querySelectorAll('.category-checkbox:checked'))
.map(cb => cb.dataset.folder);
if (selectedFolders.length === 0) {
showError('Wybierz przynajmniej jedną kategorię!');
return;
}
panel.remove();
if (selectedFolders.includes('all')) {
if (action === 'collect') {
collectExperienceHistory(null, 'all');
} else {
updateExperienceHistory('all');
}
} else {
processCategories(selectedFolders, action);
}
} else if (this.id === 'cancel-selection') {
panel.remove();
}
});
}
// Obsługa checkboxów
const allCheckbox = panel.querySelector('input[data-folder="all"]');
if (allCheckbox) {
allCheckbox.addEventListener('change', function() {
const checkboxes = panel.querySelectorAll('.category-checkbox:not([data-folder="all"])');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
checkbox.disabled = this.checked;
});
});
}
const otherCheckboxes = panel.querySelectorAll('.category-checkbox:not([data-folder="all"])');
otherCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
if (this.checked && allCheckbox) {
allCheckbox.checked = false;
}
});
});
}
// Dodaj funkcję do przetwarzania kategorii
async function processCategories(folders, action) {
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
showError(`Rozpoczynam zbieranie danych dla ${getCategoryName(folder)}`);
await new Promise((resolve) => {
if (action === 'collect') {
collectExperienceHistory(null, folder, () => {
showError(`Zakończono przetwarzanie ${getCategoryName(folder)}`);
setTimeout(resolve, 2000);
});
} else {
updateExperienceHistory(folder, () => {
showError(`Zakończono przetwarzanie ${getCategoryName(folder)}`);
setTimeout(resolve, 2000);
});
}
});
if (shouldCancel) {
showError('Anulowano zbieranie pozostałych danych.');
break;
}
}
if (!shouldCancel) {
showError('Zakończono przetwarzanie wszystkich wybranych kategorii!');
}
}
// Modyfikacja funkcji updateExperienceHistory
function updateExperienceHistory(folder = 'all', callback = null) {
if (isCollectingHistory) {
showError('Aktualizacja danych jest już w trakcie. Poczekaj na zakończenie.');
if (callback) callback();
return;
}
// Ukryj główne elementy przed rozpoczęciem aktualizacji
toggleMainElements(false);
showError(`Rozpoczynam aktualizację danych dla ${getCategoryName(folder)}`);
isCollectingHistory = true;
isPaused = false;
shouldCancel = false;
tempExpLog = [];
let processedPages = 0;
let failedAttempts = 0;
const MAX_RETRIES = 3;
const MIN_DELAY = 2000;
const MAX_DELAY = 2200;
const RETRY_DELAY = 3000;
// Pokaż przyciski kontrolne i ukryj standardowe
const controlButtons = document.querySelector('#collection-controls');
const standardButtons = document.querySelector('#standard-buttons');
if (controlButtons) {
controlButtons.style.display = 'grid';
}
if (standardButtons) {
standardButtons.style.display = 'none';
}
// Tworzymy zbiór istniejących raportów (unikalny po source+timestamp)
const existingKeys = new Set(experienceLog.map(e => `${e.source}|${e.timestamp}`));
let foundDuplicate = false;
function getRandomDelay() {
return Math.floor(Math.random() * (MAX_DELAY - MIN_DELAY + 1)) + MIN_DELAY;
}
Ajax.remoteCall('reports', 'get_reports', {
page: 1,
folder: folder
}, function(initialData) {
if (!initialData || initialData.error) {
isCollectingHistory = false;
showError('Nie udało się pobrać informacji o raportach. Spróbuj ponownie.');
return;
}
const totalPages = initialData.count;
collectionState.totalPages = totalPages;
function processPage(page) {
if (!isCollectingHistory || shouldCancel || page > totalPages || foundDuplicate) {
finishUpdate(shouldCancel);
return;
}
if (isPaused) {
setTimeout(() => processPage(page), 500);
return;
}
Ajax.remoteCall('reports', 'get_reports', {
page: page,
folder: folder
}, function(data) {
if (!isCollectingHistory || shouldCancel || foundDuplicate) {
finishUpdate(shouldCancel);
return;
}
if (data && !data.error && data.reports && data.reports.length > 0) {
failedAttempts = 0;
for (const report of data.reports) {
const expMatch = report.popupData.match(/experience.png[^>]*>(?:[^<]*<\/[^>]+>)*[^<]*<td>(\d+)<\/td>/) ||
report.popupData.match(/Doświadczenie<\/span>\s*<span[^>]*>(\d+)\s*punktów/);
const exp = expMatch ? parseInt(expMatch[1]) : 0;
if (exp > 0) {
const key = `${report.title}|${report.date_received}`;
if (existingKeys.has(key)) {
foundDuplicate = true;
break;
}
const reportType = report.title.includes('Raport dot. pracy') ? 'work' :
report.title.includes('Pojedynek') ? 'duel' :
(report.title.includes('Bitwa') || report.title.includes('Fort')) ? 'battle' : 'other';
// Dodajemy sprawdzanie zwycięzcy dla pojedynków
let shouldCount = true;
if (reportType === 'duel') {
if (report.title.includes('bandytą') || report.title.includes('bandit')) {
shouldCount = true;
} else {
// Dla pojedynków z graczami sprawdzamy zwycięzcę
const winnerMatch = report.popupData.match(/Zwycięzca:\s*([^<]+)/) ||
report.popupData.match(/(\w+)\s+wygrywa pojedynek!/);
if (winnerMatch) {
const winner = winnerMatch[1].trim();
shouldCount = winner === playerName;
} else {
shouldCount = false;
}
}
}
if (shouldCount) {
tempExpLog.push({
amount: exp,
source: report.title,
timestamp: report.date_received,
page: page,
type: reportType
});
// Aktualizuj zapisany stan
collectionState.tempLog = tempExpLog;
saveCollectionState(collectionState);
}
}
}
processedPages++;
updateCollectionStatus(processedPages, totalPages, tempExpLog.length);
if (!foundDuplicate && !shouldCancel) {
setTimeout(() => processPage(page + 1), getRandomDelay());
} else {
finishUpdate(shouldCancel);
}
} else {
failedAttempts++;
if (failedAttempts < MAX_RETRIES && !shouldCancel) {
setTimeout(() => processPage(page), RETRY_DELAY);
} else {
showError(`Nie udało się przetworzyć strony ${page} po ${MAX_RETRIES} próbach`);
finishUpdate(shouldCancel);
}
}
});
}
processPage(1);
});
function finishUpdate(wasCancelled = false) {
isCollectingHistory = false;
isPaused = false;
// Pokaż z powrotem główne elementy
toggleMainElements(true);
// Przywróć standardowe przyciski i ukryj kontrolne
const controlButtons = document.querySelector('#collection-controls');
const standardButtons = document.querySelector('#standard-buttons');
if (controlButtons) {
controlButtons.style.display = 'none';
}
if (standardButtons) {
standardButtons.style.display = 'grid';
}
if (wasCancelled) {
showError('Anulowano aktualizację danych.');
if (callback) callback();
return;
}
if (tempExpLog.length === 0) {
showError(`Brak nowych wpisów w kategorii ${getCategoryName(folder)}.`);
if (callback) callback();
return;
}
// Dodaj nowe wpisy na początek loga (bo są najnowsze)
experienceLog = tempExpLog.concat(experienceLog);
totalExperience = experienceLog.reduce((sum, entry) => sum + entry.amount, 0);
localStorage.setItem('experienceLog', JSON.stringify(experienceLog));
localStorage.setItem('totalExperience', JSON.stringify(totalExperience));
updateDisplay();
updateXPStats();
showError(`Zaktualizowano ${getCategoryName(folder)}! Dodano ${tempExpLog.length} nowych wpisów.`);
if (callback) {
callback();
}
}
}
// Funkcja do sprawdzania ilości stron dla różnych typów raportów
function checkReportPages() {
const types = [
{ name: 'Wszystkie raporty', params: { folder: 'all' } },
{ name: 'Raporty z pracy', params: { folder: 'job' } },
{ name: 'Raporty z pojedynków', params: { folder: 'duel' } },
{ name: 'Raporty z bitew fortowych', params: { folder: 'fortbattle' } }
];
let results = {};
let completed = 0;
showError('Rozpoczynam sprawdzanie raportów...');
types.forEach(type => {
Ajax.remoteCall('reports', 'get_reports', {
page: 1,
...type.params
}, function(response) {
completed++;
if (response && !response.error) {
results[type.name] = {
totalPages: response.pages || response.count || 0,
reportsPerPage: response.reports ? response.reports.length : 0,
totalReports: response.total || (response.reports ? response.reports.length * (response.pages || response.count || 0) : 0)
};
} else {
results[type.name] = {
error: response ? response.error : 'Brak odpowiedzi'
};
}
if (completed === types.length) {
let resultMessage = 'Statystyki raportów:\n\n';
Object.entries(results).forEach(([name, data]) => {
resultMessage += `${name}:\n`;
if (data.error) {
resultMessage += ` Błąd: ${data.error}\n`;
} else {
resultMessage += ` Liczba stron: ${data.totalPages}\n`;
resultMessage += ` Raportów na stronę: ${data.reportsPerPage}\n`;
resultMessage += ` Łączna liczba raportów: ${data.totalReports}\n`;
if (data.reportsPerPage > 0) {
resultMessage += ` Szacowana łączna liczba raportów: ${data.totalPages * data.reportsPerPage}\n`;
}
resultMessage += '------------------------\n';
}
});
showError(resultMessage, 10000); // Pokazuj przez 10 sekund
}
});
});
return "Sprawdzanie ilości stron... Wyniki pojawią się w oknie statusu.";
}
// Dodaj do window aby było dostępne w konsoli
window.checkReportPages = checkReportPages;
// Dodanie okna śledzenia doświadczenia
setInterval(() => {
addExperienceWindow();
createVisibilityToggle();
}, 1000);
// Funkcja do przełączania widoczności
function toggleTrackerVisibility() {
const tracker = document.querySelector('#experience-tracker');
const toggleCheckbox = document.querySelector('#toggle-tracker-visibility');
if (tracker && toggleCheckbox) {
isTrackerVisible = toggleCheckbox.checked;
tracker.style.display = isTrackerVisible ? 'block' : 'none';
localStorage.setItem('experienceTrackerVisible', JSON.stringify(isTrackerVisible));
}
}
// Funkcja do tworzenia checkboxa widoczności
function createVisibilityToggle() {
const existingToggle = document.querySelector('#tracker-visibility-toggle');
if (existingToggle) return;
const toggleContainer = document.createElement('div');
toggleContainer.id = 'tracker-visibility-toggle';
// Pozycjonowanie względem ui_topbar
const uiTopbar = document.querySelector('#ui_topbar');
const topbarRect = uiTopbar ? uiTopbar.getBoundingClientRect() : null;
toggleContainer.style.cssText = `
position: fixed;
top: ${topbarRect ? topbarRect.top + 'px' : '10px'};
right: ${topbarRect ? (window.innerWidth - topbarRect.left) + 'px' : '310px'};
background: rgba(20, 20, 20, 0.95);
padding: 5px 10px;
border-radius: 4px;
border: 1px solid #444;
z-index: 1000;
color: white;
font-family: Arial, sans-serif;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
`;
toggleContainer.innerHTML = `
<input type="checkbox" id="toggle-tracker-visibility" ${isTrackerVisible ? 'checked' : ''}>
`;
document.body.appendChild(toggleContainer);
// Aktualizacja pozycji przy zmianie rozmiaru okna
window.addEventListener('resize', () => {
const uiTopbar = document.querySelector('#ui_topbar');
const topbarRect = uiTopbar ? uiTopbar.getBoundingClientRect() : null;
if (topbarRect && toggleContainer) {
toggleContainer.style.top = topbarRect.top + 'px';
toggleContainer.style.right = (window.innerWidth - topbarRect.left) + 'px';
}
});
const checkbox = toggleContainer.querySelector('#toggle-tracker-visibility');
checkbox.addEventListener('change', toggleTrackerVisibility);
}
})();