Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name duels stats csv
- // @namespace http://tampermonkey.net/
- // @version 6.0
- // @description Fetches Geoguessr API data to get advanced stats for duel games and displays them on the profile page.
- // @author You
- // @match https://www.geoguessr.com/*/me/profile
- // @match https://www.geoguessr.com/me/profile
- // @icon https://www.geoguessr.com/_next/static/media/favicon.bffdd9d3.png
- // @grant GM_xmlhttpRequest
- // @grant GM_addStyle
- // @require https://code.jquery.com/jquery-3.6.0.min.js
- // @require https://unpkg.com/[email protected]/dist/axios.min.js
- // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/progressbar.min.js
- // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
- // @connect https://game-server.geoguessr.com/
- // ==/UserScript==
- (function() {
- 'use strict';
- //*** Constants and Variables ***//
- // Constants
- const API_BASE_URL = 'https://game-server.geoguessr.com/api/duels';
- // Variables
- let playerId = '';
- //*** Helper Functions ***//
- // Function to convert JSON to CSV
- function jsonToCsv(json) {
- console.log(json)
- const keys = Object.keys(json[0]);
- const csv = json.map(row =>
- keys.map(key => JSON.stringify(row[key], (_, value) => value === null ? '' : value)).join(',')
- );
- return [keys.join(','), ...csv].join('\r\n');
- }
- //delay setup
- function delay(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- // Function to download a CSV file
- function downloadCsv(data, filename) {
- const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.setAttribute("href", url);
- // Correct replacement string
- const baseFilename = filename.replace(/ - (\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})\.\d{3}/, ' - $1-$2$3');
- link.setAttribute("download", baseFilename);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- // Function to count new duels
- function countNewDuels(gameIds, lastDownloadDate) {
- if (!lastDownloadDate) {
- // If no previous timestamp is found, all duels are considered "new"
- return gameIds.length;
- }
- // Count the duels with timestamps later than the last download timestamp
- const newDuels = gameIds.filter(game => new Date(game.time) > lastDownloadDate);
- console.log(`${newDuels.length} new duels found since last download.`);
- return newDuels.length;
- }
- //*** API Interactions ***//
- // Function to get game IDs - Phase 1
- async function getGameIds(session) {
- const startTime = new Date(); // Start time of getGameIds
- console.log("getGameIds started at:", startTime.toLocaleString());
- const gameIds = [];
- let paginationToken = null;
- try {
- while (true) {
- const response = await session.get('https://www.geoguessr.com/api/v4/feed/private', { params: { paginationToken } });
- const data = response.data;
- paginationToken = data.paginationToken;
- if (!playerId && data.entries.length > 0) {
- playerId = data.entries[0].user.id;
- console.log("Player ID:", playerId);
- }
- data.entries.forEach(entry => {
- try {
- extractGameIds(entry, gameIds);
- } catch (error) {
- console.error(error);
- }
- });
- if (!paginationToken) break;
- }
- } catch (error) {
- console.error("An error occurred while fetching game IDs:", error);
- }
- console.log("Number of Game IDs found:", gameIds.length);
- const endTimePhase1 = new Date(); // End time after game IDs are fetched
- const durationPhase1 = (endTimePhase1 - startTime) / 1000; // Duration in seconds
- console.log("getGameIds finished at:", endTimePhase1.toLocaleString());
- console.log("Time taken for getGameIds:", durationPhase1, "seconds");
- gameIds.sort((a, b) => new Date(a.time) - new Date(b.time)); // Crucial sort!
- const gameIdsCsv = jsonToCsv(gameIds.map((gameId, index) => ({
- duel_id: gameId.id,
- duel_datetime: gameId.time,
- duel_type: gameId.gameMode,
- duel_mode: gameId.competitiveGameMode
- })));
- downloadCsv(gameIdsCsv, 'newDuels.csv');
- return gameIds;
- }
- // extract game IDs
- function extractGameIds(entry, gameIds) {
- try {
- const payloadJson = JSON.parse(entry.payload);
- if (Array.isArray(payloadJson)) {
- payloadJson.forEach(payloadItem => {
- let gameData = null;
- if (payloadItem.payload && payloadItem.payload.gameMode === 'Duels') {
- gameData = payloadItem.payload;
- } else if (payloadItem.gameMode === 'Duels') {
- gameData = payloadItem;
- }
- if (gameData) { // Only proceed if gameData is found
- const gameTime = payloadItem.time || "Not Applicable";
- const gameMode = gameData.gameMode || "Not Applicable";
- const competitiveGameMode = gameData.competitiveGameMode || "Not Applicable";
- const gameId = gameData.gameId; // Extract gameId
- if (gameMode === 'Duels' && gameData.partyId === undefined) {
- gameIds.push({
- id: gameId,
- time: gameTime,
- gameMode: gameMode,
- competitiveGameMode: competitiveGameMode
- });
- }
- }
- });
- } else if (payloadJson && payloadJson.gameMode === 'Duels') { // Handle single object case
- const gameTime = entry.time || "Not Applicable"; // Use entry.time for single object case
- const gameMode = payloadJson.gameMode || "Not Applicable";
- const competitiveGameMode = payloadJson.competitiveGameMode || "Not Applicable";
- const gameId = payloadJson.gameId;
- if (payloadJson.gameMode === 'Duels' && payloadJson.partyId === undefined) {
- gameIds.push({
- id: gameId,
- time: gameTime,
- gameMode: gameMode,
- competitiveGameMode: competitiveGameMode
- });
- }
- }
- } catch (error) {
- console.error("Error parsing payload:", error, entry.payload);
- }
- }
- // function to fetch duels - Phase 2 - iterates through the gameIds and feches data
- async function fetchDuels(session, gameIds) {
- const duels = [];
- for (const gameId of gameIds) {
- let response = null; // Declare response here, initialize to null
- try {
- response = await session.get(`${API_BASE_URL}/${gameId}`); // Assign inside try
- duels.push(response.data);
- await delay(1000);
- }
- catch (error) {
- console.error(`Error fetching duel data for game ${gameId}:`, error, response ? response.status : "No Response"); // Check if response exists
- // Handle error as needed (e.g., retry, skip)
- await delay(1000);
- }
- }
- return duels;
- }
- //***Data Processing ***//
- // Function to extract player round data from duel JSON - Phase 2 - Processes data from fetchduels
- function extractPlayerRoundData(duels, playerId) {
- return duels.flatMap(duel => {
- const playerTeam = duel.teams.find(team => team.players.some(p => p.playerId === playerId));
- const opponentTeam = duel.teams.find(team => team.players.some(p => p.playerId !== playerId));
- if (!playerTeam || !opponentTeam) {
- console.error("Player or opponent not found in duel:", duel);
- return [];
- }
- const player = playerTeam.players.find(p => p.playerId === playerId);
- const opponent = opponentTeam.players.find(p => p.playerId !== playerId);
- if (!player || !opponent) {
- console.error("Player or opponent data missing:", duel);
- return [];
- }
- return duel.rounds.map(round => {
- const playerGuess = player.guesses.find(g => g.roundNumber === round.roundNumber);
- const opponentGuess = opponent.guesses.find(g => g.roundNumber === round.roundNumber);
- const playerRoundResult = playerTeam.roundResults.find(r => r.roundNumber === round.roundNumber);
- const opponentRoundResult = opponentTeam.roundResults.find(r => r.roundNumber === round.roundNumber);
- const playerGuessData = {
- GuessLat: playerGuess ? playerGuess.lat : null,
- GuessLng: playerGuess ? playerGuess.lng : null,
- distance: playerGuess ? playerGuess.distance : null,
- score: playerRoundResult ? playerRoundResult.score : null,
- playerHealthBefore: playerRoundResult ? playerRoundResult.healthBefore : null,
- playerHealthAfter: playerRoundResult ? playerRoundResult.healthAfter : null,
- playerRatingBefore: player.progressChange?.rankedSystemProgress?.ratingBefore ?? null, // Correct path!
- playerRatingAfter: player.progressChange?.rankedSystemProgress?.ratingAfter ?? null, // Correct path!
- playerGameMode: player.progressChange?.rankedSystemProgress?.gameMode ?? null, // Correct path!
- playerGameModeRatingBefore: player.progressChange?.rankedSystemProgress?.gameModeRatingBefore ?? null, // Correct path!
- playerGameModeRatingAfter: player.progressChange?.rankedSystemProgress?.gameModeRatingAfter ?? null, // Correct path!
- playerGameModeGamesPlayed: player.progressChange?.rankedSystemProgress?.gameModeGamesPlayed ?? null,
- };
- const opponentGuessData = {
- opponentGuessLat: opponentGuess ? opponentGuess.lat : null,
- opponentGuessLng: opponentGuess ? opponentGuess.lng : null,
- opponentDistance: opponentGuess ? opponentGuess.distance : null,
- opponentScore: opponentRoundResult ? opponentRoundResult.score : null,
- opponentHealthBefore: opponentRoundResult ? opponentRoundResult.healthBefore : null,
- opponentHealthAfter: opponentRoundResult ? opponentRoundResult.healthAfter : null,
- opponentRatingBefore: opponent.progressChange?.rankedSystemProgress?.ratingBefore ?? null, // Correct path!
- opponentRatingAfter: opponent.progressChange?.rankedSystemProgress?.ratingAfter ?? null, // Correct path!
- opponentGameMode: opponent.progressChange?.rankedSystemProgress?.gameMode ?? null, // Correct path!
- opponentGameModeRatingBefore: opponent.progressChange?.rankedSystemProgress?.gameModeRatingBefore ?? null, // Correct path!
- opponentGameModeRatingAfter: opponent.progressChange?.rankedSystemProgress?.gameModeRatingAfter ?? null, // Correct path!
- opponentGameModeGamesPlayed: opponent.progressChange?.rankedSystemProgress?.gameModeGamesPlayed ?? null,
- };
- return {
- gameId: duel.gameId,
- mapName: duel.options.map.name,
- gameMode: duel.options.competitiveGameMode ?? "Not Available",
- forbidMoving: duel.options.movementOptions.forbidMoving,
- forbidZooming: duel.options.movementOptions.forbidZooming,
- forbidRotating: duel.options.movementOptions.forbidRotating,
- roundStartTime: round.startTime,
- roundEndTime: round.endTime,
- roundNumber: round.roundNumber,
- roundMultiplier: round.multiplier,
- roundDamageMultiplier: round.damageMultiplier ?? "Not Available",
- LocLat: round.panorama.lat,
- LocLng: round.panorama.lng,
- panoramaCountryCode: round.panorama.countryCode,
- playerId: player.playerId,
- ...playerGuessData,
- opponentPlayerId: opponent.playerId,
- ...opponentGuessData,
- };
- }).filter(Boolean);
- });
- }
- //*** Event Handlers ***//
- //function to handle CSV button click - Phase 2 - called when user confirms number duels to download
- async function handleDownloadCsv(session, gameIds) {
- const numDuels = gameIds.length; // Store the number of duels to fetch
- const progressPopup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- progressPopup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff;
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10003; /* Ensure it's on top */
- width: auto;
- min-width: 300px; /* Adjust as needed */
- `;
- progressPopup.innerHTML = `
- <p><span id="duelsFetchedCount">0</span> of ${numDuels} fetched</p>
- <div id="progressBar"></div>
- <button id="cancelFetchButton" style="background-color: red; color: white; padding: 10px 20px; margin-top: 10px;">Cancel</button>
- `;
- document.body.appendChild(progressPopup);
- const progressBar = new ProgressBar.Line('#progressBar', {
- strokeWidth: 4,
- easing: 'easeInOut',
- duration: 1400,
- color: '#007bff', // Blue color for progress
- trailColor: '#eee',
- svgStyle: { width: '100%', height: '10px' }, // Make the progress bar fill the container
- text: {
- style: {
- color: '#333', // Darker text color
- position: 'absolute',
- left: '50%',
- top: '50%',
- padding: 0,
- margin: 0,
- transform: {
- prefix: true,
- x: '-50%',
- y: '-50%'
- }
- },
- autoStyleContainer: false
- },
- step: (state, bar) => {
- bar.setText(Math.round(bar.value() * 100) + ' %');
- }
- });
- let fetchedCount = 0;
- const duels = [];
- let cancelled = false; // Flag to track cancellation
- const cancelFetchButton = document.getElementById('cancelFetchButton');
- cancelFetchButton.onclick = () => {
- cancelled = true;
- document.body.removeChild(progressPopup); // Close the popup immediately
- console.log("Duel fetching cancelled by user.");
- };
- for (let i = 0; i < numDuels; i++) {
- if (cancelled) break; // Check for cancellation before each fetch
- let response = null;
- try {
- response = await session.get(`${API_BASE_URL}/${gameIds[i].id}`);
- duels.push(response.data);
- fetchedCount++;
- document.getElementById('duelsFetchedCount').textContent = fetchedCount;
- progressBar.set(fetchedCount / numDuels); // Update progress bar
- await delay(1000);
- } catch (error) {
- console.error(`Error fetching duel data for game ${gameIds[i].id}:`, error, response ? response.status : "No Response");
- await delay(1000);
- }
- }
- if (!cancelled) { // Only proceed if not cancelled
- document.body.removeChild(progressPopup); // Remove the progress popup after completion
- const playerRoundData = extractPlayerRoundData(duels, playerId);
- const csvData = jsonToCsv(playerRoundData.map(round => ({
- duel_id: round.gameId,
- map: round.mapName,
- duel_mode: round.gameMode,
- forbid_moving: round.forbidMoving,
- forbid_zooming: round.forbidZooming,
- forbid_rotating: round.forbidRotating,
- round_start_datetime: round.roundStartTime,
- round_end_datetime: round.roundEndTime,
- duel_round_number: round.roundNumber,
- round_multiplier: round.roundMultiplier,
- round_damage: round.roundDamageMultiplier,
- loc_lat: round.LocLat,
- loc_lng: round.LocLng,
- loc_cc: round.panoramaCountryCode,
- player_id: round.playerId,
- player_guess_lat: round.GuessLat,
- player_guess_lng: round.GuessLng,
- player_distance: round.distance,
- player_score: round.score,
- player_health_before: round.playerHealthBefore,
- player_health_after: round.playerHealthAfter,
- player_rating_before: round.playerRatingBefore,
- player_rating_after: round.playerRatingAfter,
- player_game_mode: round.playerGameMode,
- player_game_mode_rating_before: round.playerGameModeRatingBefore,
- player_game_mode_rating_after: round.playerGameModeRatingAfter,
- player_game_mode_games_played: round.playerGameModeGamesPlayed,
- opponent_player_id: round.opponentPlayerId,
- opponent_guess_lat: round.opponentGuessLat,
- opponent_guess_lng: round.opponentGuessLng,
- opponent_distance: round.opponentDistance,
- opponent_score: round.opponentScore,
- opponent_health_before: round.opponentHealthBefore,
- opponent_health_after: round.opponentHealthAfter,
- opponent_rating_before: round.opponentRatingBefore,
- opponent_rating_after: round.opponentRatingAfter,
- opponent_game_mode: round.opponentGameMode,
- opponent_game_mode_rating_before: round.opponentGameModeRatingBefore,
- opponent_game_mode_rating_after: round.opponentGameModeRatingAfter,
- opponent_game_mode_games_played: round.opponentGameModeGamesPlayed
- })));
- downloadCsv(csvData, 'newDuelsRounds.csv');
- console.log("Duel stats download complete.");
- }
- }
- //*** UI Popups ***//
- //Show confirmation popup at page load
- function showConfirmationPopup() {
- return new Promise((resolve) => {
- const popup = document.createElement('div');
- const topOffset = 80; // 80px from the top
- const rightOffset = 80; // 80px from the right
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0); /* Remove default centering transform */
- background-color: #fff;
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10000;
- `;
- popup.innerHTML = `
- <p>Do You Want to Fetch Duels Data?</p>
- <button id="confirmButton" style="background-color: green; color: white; padding: 10px 20px; margin-right: 10px;">Yes ✓</button>
- <button id="cancelButton" style="background-color: red; color: white; padding: 10px 20px;">No X</button>
- `;
- document.body.appendChild(popup);
- const confirmButton = document.getElementById('confirmButton');
- const cancelButton = document.getElementById('cancelButton');
- confirmButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(true); // Resolve with true when confirmed
- };
- cancelButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(false); // Resolve with false when canceled
- };
- });
- }
- //Sow searching popup
- function showSearchingPopup() {
- const popup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff; /* White background */
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10001;
- width: auto; /* Adjust width as needed */
- min-width: 200px; /* Minimum width to prevent it from being too small */
- `;
- popup.innerHTML = `<p>Searching For Duels...</p>`;
- document.body.appendChild(popup);
- return popup;
- }
- //Hide popup
- function hidePopup(popup) {
- if (popup && popup.parentNode) { // Check if the popup exists and is in the DOM
- popup.parentNode.removeChild(popup);
- }
- }
- //Show phase 1 complete popup
- function showPhase1CompletePopup(gameIds, lastDownloadDate) {
- return new Promise((resolve) => {
- const popup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff !important; /* White background - override other styles */
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10002;
- width: auto;
- min-width: 250px;
- `;
- const newDuelsCount = countNewDuels(gameIds, lastDownloadDate); // Use the helper function
- const lastDownloadText = lastDownloadDate
- ? `Last download occurred on ${lastDownloadDate.toLocaleString()}`
- : "No previous downloads detected"; // Dynamic message
- popup.innerHTML = `
- <p>${gameIds.length} Duels found and downloaded</p>
- <p>${lastDownloadText}</p>
- <p>${newDuelsCount} new duels have occurred since then</p>
- <p>How many do you want to fetch?</p>
- <input type="number" id="numDuels" value="${newDuelsCount}" min="1" max="${gameIds.length}" style="width: 100px; margin-right: 10px;">
- <button id="confirmButton" style="background-color: green; color: white; padding: 10px 20px; margin-right: 10px;">Confirm</button>
- <button id="cancelButton" style="background-color: red; color: white; padding: 10px 20px;">Cancel</button>
- `;
- document.body.appendChild(popup);
- const confirmButton = document.getElementById('confirmButton');
- const cancelButton = document.getElementById('cancelButton');
- const numDuelsInput = document.getElementById('numDuels');
- confirmButton.onclick = () => {
- let numDuels = parseInt(numDuelsInput.value, 10);
- if (isNaN(numDuels) || numDuels < 1) {
- alert("Please enter a valid number greater than 0.");
- return;
- }
- numDuels = Math.min(numDuels, gameIds.length);
- // Save the current timestamp in local storage
- localStorage.setItem("lastDownloadTimestamp", JSON.stringify(new Date()));
- document.body.removeChild(popup);
- resolve(numDuels);
- };
- cancelButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(false);
- };
- });
- }
- //*** Initialization ***//
- //*** Initialization ***//
- async function init() {
- // Retrieve the last download timestamp from local storage
- const lastDownloadTimestamp = localStorage.getItem("lastDownloadTimestamp");
- let lastDownloadDate = null;
- if (lastDownloadTimestamp) {
- lastDownloadDate = new Date(JSON.parse(lastDownloadTimestamp)); // Parse the timestamp into a Date object
- console.log("Last download occurred on:", lastDownloadDate.toLocaleString());
- } else {
- console.log("No previous downloads detected.");
- }
- const userConfirmed = await showConfirmationPopup();
- if (userConfirmed) {
- const scriptStartTime = performance.now(); // Capture start time using performance.now()
- console.time('total_script_time'); // Start timer *after* confirmation
- console.log("Script execution started at:", new Date().toLocaleString()); // Log start time
- const session = axios.create({
- withCredentials: true
- });
- const searchingPopup = showSearchingPopup();
- getGameIds(session).then(gameIds => {
- const phase1EndTime = performance.now(); // Capture end time using performance.now()
- const phase1Duration = (phase1EndTime - scriptStartTime) / 1000; // Calculate duration (using performance.now consistently)
- console.log("Phase 1 (Game IDs and CSV Download) finished at:", new Date().toLocaleString());
- hidePopup(searchingPopup);
- showPhase1CompletePopup(gameIds, lastDownloadDate).then(numDuels => {
- if (numDuels) {
- const recentGameIds = gameIds.slice(-numDuels);
- const phase2StartTime = performance.now(); // Capture Phase 2 start time
- console.time('phase2_loading'); // Phase 2 start time
- console.log("Phase 2 started at:", new Date().toLocaleString()); // Human-readable start time
- handleDownloadCsv(session, recentGameIds).then(() => {
- const phase2EndTime = performance.now(); // Capture Phase 2 end time
- const phase2Duration = (phase2EndTime - phase2StartTime) / 1000; // Calculate phase 2 duration
- console.timeEnd('phase2_loading'); // Phase 2 end time
- console.timeEnd('total_script_time'); // Total time end
- const scriptEndTime = performance.now(); // Capture script end time
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000; // Calculate total script duration
- console.log("Phase 2 finished at:", new Date().toLocaleString());
- console.log("Total time for Phase 2:", phase2Duration, "seconds");
- console.log("Total script execution time:", scriptDuration, "seconds");
- }).catch(error => {
- console.timeEnd('phase2_loading'); // End timer on error in Phase 2
- console.timeEnd('total_script_time'); // Total time end
- // ... (Phase 2 error handling)
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- console.log(`Fetching ${numDuels} duels...`);
- } else {
- console.log("User chose not to proceed to Phase 2.");
- console.timeEnd('total_script_time'); // End timer if no Phase 2
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- }
- }).catch(error => {
- console.timeEnd('total_script_time'); // End timer on error in Phase 1
- // ... (Phase 1 error handling)
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- }).catch(error => {
- console.timeEnd('total_script_time'); // End timer on error in getGameIds
- // ... (getGameIds error handling)
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- } else {
- console.log("Script execution cancelled by user.");
- }
- }
- $(document).ready(init);
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement