// ==UserScript==
// @name GGn Control Panel
// @namespace http://tampermonkey.net/
// @version 0.42
// @description A beautiful control panel that displays advanced capabilities you won't find on the site.
// @author Animaker
// @icon https://icons.duckduckgo.com/ip3/gazellegames.net.ico
// @match https://gazellegames.net/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const API_KEY = ""; // Replace with your API key
// Global
let isDebug = false;
let globalDialog = null;
let globalSnackbar = null;
let loadingScreen = null;
let minimized = null
// Best Torrents Section
let currentTorrentPage = 1; // Initialize pagination when searching the best torrent
let currentSortBy = "seeders";
let bestTorrents = []; // Initialize an empty array to hold best torrents
let minimum = 0;
// Check Inbox Section
let checkInboxButton = null;
// Crafting Simulator Section
let selectedItems = []; // Initialize an empty array to hold selected items
function youWish(){
alert("Oops, ask Santa.");
}
/**
* Displays a full-screen loading overlay.
**/
function showLoadingScreen() {
if (!loadingScreen) {
loadingScreen = document.createElement("div");
loadingScreen.style.position = "fixed";
loadingScreen.style.top = "0";
loadingScreen.style.left = "0";
loadingScreen.style.width = "100%";
loadingScreen.style.height = "100%";
loadingScreen.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
loadingScreen.style.display = "flex";
loadingScreen.style.alignItems = "center";
loadingScreen.style.justifyContent = "center";
loadingScreen.style.zIndex = "10000";
// You can customize this inner HTML to include a spinner icon if desired.
loadingScreen.innerHTML = `<div style="color: #fff; font-size: 24px;">Loading...</div>`;
document.body.appendChild(loadingScreen);
}
}
/**
* Hides and removed the loading overlay.
**/
function hideLoadingScreen() {
if (loadingScreen) {
document.body.removeChild(loadingScreen);
loadingScreen = null;
}
}
// Function to adjust the scale of the control panel based on window width
function adjustControlPanelScale() {
const panel = document.getElementById("control-panel");
if (!panel) return;
// For instance, suppose 1920px is our ideal width; scale down if the window is smaller.
// The scale factor will never exceed 1.
const scaleFactor = Math.min(window.innerWidth / 2160, 1);
// Retrieve and clean position values
const rawRight = Math.max(0, Math.round(parseFloat(localStorage.getItem("panelRight") || "50")));
const rawBottom = Math.max(0, Math.round(parseFloat(localStorage.getItem("panelBottom") || "20")));
console.log(`Right: ${rawRight} Bottom: ${rawBottom}`);
panel.style.right = `${rawRight}px`;
panel.style.bottom = `${rawBottom}px`;
panel.style.transform = `scale(${scaleFactor})`;
panel.style.transformOrigin = "bottom right"; // Adjust the origin as needed
}
async function updateSimulateButtonState() {
const simulateAllButton = window.simulateAllButton;
if (!simulateAllButton) return;
// If there are selected items, enable and show the button
if (selectedItems.length > 0) {
simulateAllButton.disabled = false;
} else {
simulateAllButton.disabled = true;
}
// If 9 items are selected, hide the button
if (selectedItems.length >= 9) {
simulateAllButton.style.display = "none";
}
}
async function addItemToSelectedList(item, header, container, isReflecting = false) {
// Prevent adding the same item more than once
if (selectedItems.some(selectedItem => selectedItem.item.id === item.item.id)) {
alert("This item is already in the simulation.");
return;
}
// Limit to 9 items in the simulation
if (selectedItems.length >= 9 && !isReflecting) {
alert("You can only select up to 9 items.");
return;
}
// Add the item to the list
selectedItems.push(item);
// Sort the selected items by amount (descending), name (alphabetical), and id (numeric)
selectedItems.sort((a, b) => {
if (Number(b.amount) !== Number(a.amount)) {
return Number(b.amount) - Number(a.amount); // Sort by amount descending
}
if (a.item.name !== b.item.name) {
return a.item.name.localeCompare(b.item.name); // Sort by name alphabetically
}
return Number(a.item.id) - Number(b.item.id); // Sort by id numerically
});
// Ensure header exists and create one if necessary
if (!header) {
const dialog = document.querySelector("dialog");
header = document.createElement("h2");
dialog.insertBefore(header, dialog.firstChild);
}
// Create a div to hold both the image and the remove button
const itemDiv = document.createElement("div");
itemDiv.style.display = "inline-flex";
itemDiv.style.alignItems = "center";
itemDiv.style.marginRight = "10px";
itemDiv.style.marginBottom = "10px";
// Add the image only if it's not already in the header
const existingImages = Array.from(header.getElementsByTagName("img"));
if (!existingImages.some(img => img.alt === item.item.name)) {
const img = document.createElement("img");
img.src = getImageUrl(item.item.image);
img.alt = item.item.name;
img.style.maxWidth = "30px";
img.style.maxHeight = "30px";
img.style.marginRight = "5px";
itemDiv.appendChild(img);
}
// Add a remove button
const removeButton = document.createElement("button");
removeButton.textContent = "X";
removeButton.style.backgroundColor = "#dc3545";
removeButton.style.color = "#fff";
removeButton.style.border = "none";
removeButton.style.borderRadius = "50%";
removeButton.style.cursor = "pointer";
removeButton.style.padding = "5px 10px";
removeButton.style.marginLeft = "5px";
// Add the event listener to remove the item
removeButton.addEventListener("click", () => {
// Remove item from the selectedItems array
selectedItems = selectedItems.filter(selectedItem => selectedItem.item.id !== item.item.id);
// Remove the div (UI element) from the container
itemDiv.remove();
});
itemDiv.appendChild(removeButton);
container.appendChild(itemDiv);
}
async function removeItemFromSelectedList(item, header, itemDiv, container) {
selectedItems = selectedItems.filter(selectedItem => selectedItem.item.id !== item.item.id);
itemDiv.remove();
container.removeChild(itemDiv);
await updateSimulateButtonState();
}
async function simulateCrafts(dialog) {
const crafts = await getCraftsForItems(selectedItems);
const resultsDiv = document.createElement("div");
resultsDiv.style.marginTop = "20px";
const resultsTitle = document.createElement("h3");
resultsTitle.textContent = "Crafting Results";
resultsDiv.appendChild(resultsTitle);
if (crafts.length === 0) {
const noCraftsMessage = document.createElement("p");
noCraftsMessage.textContent = "No crafts found.";
resultsDiv.appendChild(noCraftsMessage);
} else {
crafts.forEach(craft => {
const p = document.createElement("p");
p.textContent = `Craft Found: ${craft}`;
resultsDiv.appendChild(p);
});
}
dialog.appendChild(resultsDiv);
}
async function getCraftsForItems(items) {
return items.map(item => `recipe for ${item.item.name}`);
}
// Utility function to show a notification snack bar.
// The third argument 'position' can be either "top" or "bottom".
function showSnackbar(message, imageSrc, position = "bottom") {
// If the snackbar doesn't exist yet, create it once.
if (!globalSnackbar) {
globalSnackbar = document.createElement("div");
globalSnackbar.style.position = "fixed";
// Set default position based on 'position' argument.
if (position === "top") {
globalSnackbar.style.top = "20px";
globalSnackbar.style.bottom = "";
} else {
globalSnackbar.style.bottom = "20px";
globalSnackbar.style.top = "";
}
globalSnackbar.style.left = "50%";
globalSnackbar.style.transform = "translateX(-50%)";
globalSnackbar.style.backgroundColor = "#333";
globalSnackbar.style.color = "white";
globalSnackbar.style.padding = "10px";
globalSnackbar.style.borderRadius = "3px";
globalSnackbar.style.boxShadow = "0px 0px 10px rgba(0, 0, 0, 0.1)";
globalSnackbar.style.fontSize = "14px";
globalSnackbar.style.zIndex = "9999";
// Set up flexbox to arrange the text and image
globalSnackbar.style.display = "flex";
globalSnackbar.style.alignItems = "center";
// Add transition for a fade-out effect
globalSnackbar.style.transition = "opacity 0.5s ease-out";
// Start hidden
globalSnackbar.style.opacity = "0";
document.body.appendChild(globalSnackbar);
} else {
// If snackbar already exists, update its position
if (position === "top") {
globalSnackbar.style.top = "20px";
globalSnackbar.style.bottom = "";
} else {
globalSnackbar.style.bottom = "20px";
globalSnackbar.style.top = "";
}
}
// Clear the snackbar content before updating it
globalSnackbar.innerHTML = "";
// If an image source is provided, create the image element and append it
if (imageSrc) {
const img = document.createElement("img");
img.src = imageSrc;
img.alt = "icon";
img.style.width = "24px";
img.style.height = "24px";
// Add a margin to separate the image from the text
img.style.marginLeft = "8px";
globalSnackbar.appendChild(img);
}
// Create a span for the message text and append it
const messageSpan = document.createElement("span");
messageSpan.textContent = message;
globalSnackbar.appendChild(messageSpan);
// Immediately show the snackbar
globalSnackbar.style.opacity = "1";
// Clear any existing hide timeout to avoid overlapping fadeouts
if (globalSnackbar.hideTimeout) {
clearTimeout(globalSnackbar.hideTimeout);
}
// Set a new timeout to fade out the snackbar after 2.5 seconds
globalSnackbar.hideTimeout = setTimeout(() => {
globalSnackbar.style.opacity = "0";
}, 2500);
}
/**
* Function to log debug messages if debugging is enabled.
* @param {string} message - The debug message to log.
* @param {...any} optionalParams - Additional parameters to log.
**/
function debugConsole(message, ...optionalParams) {
if (isDebug) {
console.log(`[DEBUG]: ${message}`, ...optionalParams);
}
}
function decodeHTML(text) {
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
// Function to determine the full image URL
function getImageUrl(imagePath) {
// Check if the imagePath starts with 'http' indicating it's an absolute URL
if (imagePath.startsWith('http')) {
return imagePath; // Return the absolute URL as is
} else {
// Otherwise, treat it as a relative path and prepend the base URL
const baseUrl = 'https://gazellegames.net/';
return `${baseUrl}${imagePath.replace(/\\/g, '/')}`;
}
}
// Helper function to sort a table by a given column
function sortTableByColumn(table, columnIndex, asc = true) {
const tbody = table.querySelector("tbody");
const rows = Array.from(tbody.querySelectorAll("tr"));
rows.sort((a, b) => {
// Get text content of the target cells
const aText = a.querySelectorAll("td")[columnIndex].textContent.trim();
const bText = b.querySelectorAll("td")[columnIndex].textContent.trim();
// Try to parse as numbers first
const aNum = parseFloat(aText.replace(/[^0-9\.-]+/g, ""));
const bNum = parseFloat(bText.replace(/[^0-9\.-]+/g, ""));
if (!isNaN(aNum) && !isNaN(bNum)) {
return asc ? aNum - bNum : bNum - aNum;
}
// Fall back to string comparison
return asc ? aText.localeCompare(bText) : bText.localeCompare(aText);
});
// Reattach rows in sorted order
rows.forEach(row => tbody.appendChild(row));
}
// Function to add sorting to your table headers
function addSortingToTable(table) {
const thead = table.querySelector("thead");
if (!thead) return;
// For each header cell, add a click listener
const headers = thead.querySelectorAll("th");
headers.forEach((th, index) => {
// We'll store the sort order for each column in a data attribute.
th.style.cursor = "pointer";
th.dataset.asc = "true";
th.addEventListener("click", () => {
const asc = th.dataset.asc === "true";
sortTableByColumn(table, index, asc);
// Toggle the sort order for next click
th.dataset.asc = asc ? "false" : "true";
});
});
}
// Utility: Create a search text filter to use in any display
function createSearchFilter(placeholder) {
const searchField = document.createElement("input");
searchField.type = "text";
searchField.placeholder = placeholder;
searchField.classList.add("search-filter");
return searchField;
}
// Utility: Create a dialog box with a custom title and a close button
function createDialog(titleText, isPopup=false) {
if(!isPopup && globalDialog){ globalDialog.remove(); }
const dialog = document.createElement("div");
dialog.style.display = "flex";
dialog.style.flexDirection = "column";
dialog.style.position = "fixed";
dialog.style.top = "50%";
dialog.style.left = "50%";
dialog.style.transform = "translate(-50%, -50%)";
dialog.style.backgroundColor = "#333";
dialog.style.borderRadius = "10px";
dialog.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.5)";
dialog.style.padding = "20px";
dialog.style.width = "1200px"; // Fixed width
dialog.style.height = "800px"; // Fixed height
dialog.style.zIndex = "10000";
dialog.style.overflow = "hidden"; // Prevent dialog from growing
const title = document.createElement("h2");
title.display = "block";
title.textContent = titleText;
title.style.color = "#fff";
title.style.textAlign = "center";
dialog.appendChild(title);
const closeButton = document.createElement("button");
closeButton.textContent = "×";
closeButton.style.position = "absolute";
closeButton.style.top = "10px";
closeButton.style.right = "10px";
closeButton.style.border = "none";
closeButton.style.backgroundColor = "transparent";
closeButton.style.color = "#fff";
closeButton.style.fontSize = "24px";
closeButton.style.fontWeight = "bold";
closeButton.style.padding = "5px 10px";
closeButton.style.cursor = "pointer";
closeButton.addEventListener("click", () => {
dialog.remove();
selectedItems = [];
});
dialog.appendChild(closeButton);
document.body.appendChild(dialog);
return dialog;
}
function displayCraftingSimulator(craftableItems) {
globalDialog = createDialog("Simulate All Possible Crafts");
const header = document.createElement("h2");
globalDialog.appendChild(header); // Ensure header exists in the dialog
// Add space between the selected items and the table
const selectedItemsContainer = document.createElement("div");
selectedItemsContainer.style.marginBottom = "20px"; // Add space between icon list and table
globalDialog.appendChild(selectedItemsContainer);
// Add the search bar
const searchBar = createSearchFilter("Search items...");
globalDialog.appendChild(searchBar);
// Create table container for growing table
const tableContainer = document.createElement("div");
tableContainer.style.overflowY = "auto"; // Enable vertical scrolling
tableContainer.style.flexGrow = "1"; // Allow container to grow
tableContainer.style.border = "1px solid #444"; // Optional border
// Apply flex layout to the dialog
globalDialog.style.display = "flex";
globalDialog.style.flexDirection = "column";
globalDialog.style.height = "600px"; // Make dialog occupy full height
globalDialog.style.overflow = "hidden"; // Avoid overflow issues
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.marginTop = "20px";
// table.style.tableLayout = "fixed"; // Columns share equal space
const headers = ["Item Name", "Image", "Quantity", "Add to Simulation"];
const headerRow = table.insertRow();
headers.forEach(header => {
const th = document.createElement("th");
th.textContent = header;
th.style.border = "1px solid #444";
th.style.padding = "10px";
th.style.color = "#fff";
th.style.backgroundColor = "#333";
headerRow.appendChild(th);
});
// Function to filter and display the items based on search input
const filterItems = (searchTerm) => {
const filteredItems = craftableItems.filter(item =>
item.item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
displayItems(filteredItems); // Call the function to display the filtered items
};
// Display function for items
const displayItems = (items) => {
// Clear the table before appending the filtered items
table.innerHTML = "";
const headerRow = table.insertRow();
headers.forEach(header => {
const th = document.createElement("th");
th.textContent = header;
th.style.border = "1px solid #444";
th.style.padding = "10px";
th.style.color = "#fff";
th.style.backgroundColor = "#333";
headerRow.appendChild(th);
});
items.forEach(item => {
const row = table.insertRow();
const nameCell = row.insertCell();
nameCell.textContent = item.item.name;
nameCell.style.border = "1px solid #444";
nameCell.style.padding = "10px";
const imageCell = row.insertCell();
const link = document.createElement("a"); // Create the link for shop URL
link.href = `https://gazellegames.net/shop.php?ItemID=${item.itemid}`; // Set the href to the shop URL (ensure it's available in your data)
link.target = "_blank"; // Open the link in a new tab
const img = document.createElement("img");
img.src = getImageUrl(item.item.image);
img.alt = item.item.name;
img.style.maxWidth = "50px";
img.style.maxHeight = "50px";
img.style.objectFit = "contain";
img.setAttribute("loading", "lazy"); // Add lazy loading attribute
link.appendChild(img); // Append the image to the link
imageCell.appendChild(link); // Append the link to the cell
imageCell.style.border = "1px solid #444";
imageCell.style.padding = "10px";
const quantityCell = row.insertCell();
quantityCell.textContent = item.amount;
quantityCell.style.border = "1px solid #444";
quantityCell.style.padding = "10px";
const actionCell = row.insertCell();
const addButton = document.createElement("button");
addButton.textContent = "Add to Simulation";
addButton.style.padding = "5px 10px";
addButton.style.backgroundColor = "#28a745";
addButton.style.color = "#fff";
addButton.style.border = "none";
addButton.style.borderRadius = "5px";
addButton.style.cursor = "pointer";
addButton.addEventListener("click", () => {
addItemToSelectedList(item, header, selectedItemsContainer); // Pass header and container to update
});
actionCell.appendChild(addButton);
actionCell.style.border = "1px solid #444";
actionCell.style.padding = "10px";
actionCell.style.textAlign = "center";
});
};
// Initialize the table with all items
displayItems(craftableItems);
// Attach event listener to search bar
searchBar.addEventListener("input", (event) => {
filterItems(event.target.value); // Filter items as user types
});
// Reflect already selected items if there are any
selectedItems.forEach(item => {
addItemToSelectedList(item, header, selectedItemsContainer, true); // Pass `true` to indicate it’s being reflected
});
// Create Simulate All button (always present but disabled until list is not empty)
const simulateAllButton = document.createElement("button");
simulateAllButton.style.cursor = "pointer";
simulateAllButton.textContent = "Simulate All";
simulateAllButton.style.display = "block";
simulateAllButton.style.margin = "0 auto";
simulateAllButton.style.padding = "5px 10px";
simulateAllButton.style.marginTop = "10px";
simulateAllButton.style.backgroundColor = "#007bff";
simulateAllButton.style.color = "#fff";
simulateAllButton.style.border = "none";
simulateAllButton.style.borderRadius = "5px";
simulateAllButton.disabled = true; // Initially disabled
simulateAllButton.addEventListener("click", simulateCrafts(globalDialog));
// Append the table to the container
tableContainer.appendChild(table);
// Add the table container to the dialog
globalDialog.appendChild(tableContainer);
globalDialog.appendChild(simulateAllButton);
}
// Create a table with recipe details
function createRecipeTable(recipeParts, itemDetailsMap) {
const table = document.createElement("table");
table.style.width = "100%";
table.style.marginTop = "20px";
table.style.borderCollapse = "collapse";
table.style.tableLayout = "fixed"; // Ensures all cells are the same size
const cellSize = "100px"; // Set a fixed size for cells
recipeParts.forEach((part, index) => {
const row = Math.floor(index / 3);
const col = index % 3;
const cell = document.createElement("td");
cell.style.border = "1px solid #444";
cell.style.width = cellSize; // Set fixed width
cell.style.height = cellSize; // Set fixed height
cell.style.padding = "10px";
cell.style.textAlign = "center";
cell.style.verticalAlign = "middle"; // Center content vertically
if (part === "EEEEE") {
cell.textContent = "Empty";
} else {
const cleanedPart = part.replace(/^0+/, "");
const item = itemDetailsMap[cleanedPart];
if (item) {
const itemName = document.createElement("p");
itemName.textContent = item.name;
itemName.style.fontWeight = "bold";
itemName.style.color = "#fff";
itemName.style.margin = "0"; // Remove default margin for consistent spacing
const itemImage = document.createElement("img");
itemImage.src = item.image;
itemImage.alt = item.name;
itemImage.style.maxWidth = "50px";
itemImage.style.maxHeight = "50px";
itemImage.style.marginTop = "5px";
itemImage.style.objectFit = "contain";
itemImage.setAttribute("loading", "lazy");
// Create the link around the image
const itemLink = document.createElement("a");
itemLink.href = `https://gazellegames.net/shop.php?ItemID=${item.id}`;
itemLink.target = "_blank"; // Open in a new tab
// Append the image inside the link
itemLink.appendChild(itemImage);
// Append the item name and image link inside the cell
cell.appendChild(itemName);
cell.appendChild(itemLink);
} else {
cell.textContent = "Item not found";
}
}
const rowElement = table.rows[row] || table.insertRow(row);
rowElement.appendChild(cell);
});
return table;
}
// Translate a recipe string into a table format
function translateRecipe(recipeString, recipeResult) {
const recipeParts = recipeString.match(/.{1,5}/g);
if (!recipeParts || recipeParts.length !== 9) {
console.error("[GGn Control Panel] Recipe string format is invalid.");
return null;
}
const uniqueItemIds = [...new Set(recipeParts.filter(part => part !== "EEEEE")), recipeResult];
return fetchItemDetails(uniqueItemIds).then(itemDetailsArray => {
const itemDetailsMap = itemDetailsArray.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
// Create the recipe table
const recipeTable = createRecipeTable(recipeParts, itemDetailsMap);
// Create the result div
const resultItem = itemDetailsMap[recipeResult];
const resultDiv = document.createElement("div");
resultDiv.style.marginTop = "20px";
if (resultItem) {
const resultLabel = document.createElement("p");
resultLabel.textContent = `Result: ${resultItem.name}`;
resultLabel.style.fontWeight = "bold";
resultLabel.style.color = "#fff";
// Create the anchor tag for the shop URL
const resultLink = document.createElement("a");
resultLink.href = `https://gazellegames.net/shop.php?ItemID=${resultItem.id}`; // Assuming the shop URL is available in resultItem
resultLink.target = "_blank"; // Open the link in a new tab
const resultImage = document.createElement("img");
resultImage.src = resultItem.image;
resultImage.alt = resultItem.name;
resultImage.style.maxWidth = "75px";
resultImage.style.maxHeight = "75px";
resultImage.style.marginTop = "10px";
resultImage.style.objectFit = "contain";
resultImage.setAttribute("loading", "lazy");
// Append the image inside the link
resultLink.appendChild(resultImage);
resultDiv.appendChild(resultLabel);
resultDiv.appendChild(resultLink); // Append the link (with image) to the resultDiv
} else {
const errorLabel = document.createElement("p");
errorLabel.textContent = "Result item not found.";
errorLabel.style.color = "#f00";
resultDiv.appendChild(errorLabel);
}
debugConsole(`[GGn Control Panel] translateRecipe returns:`,{ recipeTable, resultDiv });
return { recipeTable, resultDiv };
});
}
function displayCraftingRecipe(recipe){
const dialog = createDialog(`RecipeId: ${recipe.id}`,true);
dialog.style.overflowY = "auto";
const recipeContainer = document.createElement("div");
recipeContainer.style.borderBottom = "1px solid #444";
recipeContainer.style.marginBottom = "10px";
recipeContainer.style.paddingBottom = "10px";
[
{ label: "Recipe ID", value: recipe.id },
{ label: "Recipe", value: recipe.recipe },
{ label: "Requirement", value: recipe.requirement },
{ label: "Result", value: recipe.result }
].forEach(({ label, value }) => {
const element = document.createElement("p");
element.textContent = `${label}: ${value}`;
element.style.color = "#bbb";
recipeContainer.appendChild(element);
});
translateRecipe(recipe.recipe, recipe.result).then(({ recipeTable, resultDiv }) => {
if (recipeTable && resultDiv) {
recipeContainer.appendChild(recipeTable);
recipeContainer.appendChild(resultDiv);
}
});
dialog.appendChild(recipeContainer);
document.body.appendChild(dialog);
debugConsole(`[GGn Control Panel] displayCraftedRecipe complete.`);
}
// Function to get the list of item IDs from the recipes, result, and inventory
async function getAllItemIds(craftRecipes, inventory) {
const allItemIds = new Set();
// Add item IDs from each recipe
for (let craftRecipe of craftRecipes) {
const recipeParts = craftRecipe.recipe.match(/.{1,5}/g); // Break the recipe into parts
const uniqueRecipeIds = new Set(recipeParts.filter(part => part !== "EEEEE")); // Remove "EEEEE" and add to the set
uniqueRecipeIds.add(craftRecipe.result); // Add the result item ID to the set
uniqueRecipeIds.forEach(id => allItemIds.add(id)); // Add all unique IDs to the overall set
}
// Add item IDs from the inventory (assuming it's an array of item IDs)
inventory.forEach(item => allItemIds.add(item.itemid));
return allItemIds;
}
async function displayCraftedRecipes(recipes, allCraftRecipes) {
globalDialog = createDialog("Crafted Recipes");
// Add the search bar
const searchBar = createSearchFilter("Search recipes...");
globalDialog.appendChild(searchBar);
// Add category filter dropdown
const categoryFilterContainer = document.createElement("div");
categoryFilterContainer.style.marginBottom = "10px";
const categoryFilterLabel = document.createElement("label");
categoryFilterLabel.textContent = "Filter by Category: ";
categoryFilterContainer.appendChild(categoryFilterLabel);
const categoryDropdown = document.createElement("select");
categoryDropdown.style.marginLeft = "10px";
categoryFilterContainer.appendChild(categoryDropdown);
const innerCategoryDropdown = document.createElement("select");
innerCategoryDropdown.style.marginLeft = "10px";
categoryFilterContainer.appendChild(innerCategoryDropdown);
globalDialog.appendChild(categoryFilterContainer);
// --- Add the new filter toggles to your filter container ---
const filterContainer = document.createElement("div");
filterContainer.style.marginBottom = "10px";
// Hide Craftable toggle
const hideCraftableCheckbox = document.createElement("input");
hideCraftableCheckbox.type = "checkbox";
hideCraftableCheckbox.id = "hideCraftable";
const hideCraftableCheckboxLabel = document.createElement("label");
hideCraftableCheckboxLabel.setAttribute("for", "hideCraftable");
hideCraftableCheckboxLabel.textContent = "Hide Craftable Recipes";
hideCraftableCheckboxLabel.style.marginRight = "20px";
// Hide Uncraftable toggle
const hideUncraftableCheckbox = document.createElement("input");
hideUncraftableCheckbox.type = "checkbox";
hideUncraftableCheckbox.id = "hideUncraftable";
const hideUncraftableCheckboxLabel = document.createElement("label");
hideUncraftableCheckboxLabel.setAttribute("for", "hideUncraftable");
hideUncraftableCheckboxLabel.textContent = "Hide Uncraftable Recipes";
hideUncraftableCheckboxLabel.style.marginRight = "20px";
// Hide Untested Crafts toggle
const hideUntestedCheckbox = document.createElement("input");
hideUntestedCheckbox.type = "checkbox";
hideUntestedCheckbox.id = "hideUntested";
const hideUntestedCheckboxLabel = document.createElement("label");
hideUntestedCheckboxLabel.setAttribute("for", "hideUntested");
hideUntestedCheckboxLabel.textContent = "Hide Untested Crafts";
hideUntestedCheckboxLabel.style.marginRight = "20px";
// Hide Tested Crafts toggle
const hideTestedCheckbox = document.createElement("input");
hideTestedCheckbox.type = "checkbox";
hideTestedCheckbox.id = "hideTested";
const hideTestedCheckboxLabel = document.createElement("label");
hideTestedCheckboxLabel.setAttribute("for", "hideTested");
hideTestedCheckboxLabel.textContent = "Hide Tested Crafts";
hideTestedCheckboxLabel.style.marginRight = "20px";
// Append all toggles to filter container
filterContainer.appendChild(hideCraftableCheckbox);
filterContainer.appendChild(hideCraftableCheckboxLabel);
filterContainer.appendChild(hideUncraftableCheckbox);
filterContainer.appendChild(hideUncraftableCheckboxLabel);
filterContainer.appendChild(hideTestedCheckbox);
filterContainer.appendChild(hideTestedCheckboxLabel);
filterContainer.appendChild(hideUntestedCheckbox);
filterContainer.appendChild(hideUntestedCheckboxLabel);
// Append filterContainer to your dialog or control panel container
globalDialog.appendChild(filterContainer);
// Container for the table with scrolling enabled, using flex
const tableContainer = document.createElement("div");
tableContainer.style.overflowY = "auto"; // Enable vertical scrolling
tableContainer.style.flexGrow = "1"; // Allow container to grow
tableContainer.style.border = "1px solid #444"; // Optional border
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
["ID", "Uses", "Name", "Inventory", "Can Craft", "Craft"].forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
th.style.border = "1px solid #444";
th.style.padding = "8px";
th.style.textAlign = "left";
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
// Make displayFilteredRecipes async to use await
const displayFilteredRecipes = async (recipesToDisplay, craftRecipes, inventory, allItems, selectedCategory, selectedInnerCategory) => {
tbody.innerHTML = ""; // Clear existing rows
for (const recipe of recipesToDisplay) {
const row = document.createElement("tr");
row.style.cursor = "pointer";
const craftRecipe = craftRecipes.find(craftRecipe => craftRecipe.id == recipe.id);
row.id = `recipe_${craftRecipe.recipe}`;
if (!craftRecipe) {
console.error("Craft recipe not found for recipe ID:", recipe.id);
continue;
}
// Decode the recipe string to get the required items
const requiredItems = getRequiredItems(craftRecipe.recipe, craftRecipe.result);
// Check if the inventory has enough of the required items
const hasEnoughItems = hasSufficientInventory(inventory, requiredItems.filter(item => item.id)); //filter out the result id
// Highlight the row if enough items are available
if (hasEnoughItems) {
row.style.fontWeight = "bold";
row.style.fontSize = "15px";
}
// Check if recipe should be displayed based on filter toggles
const isCraftable = hasEnoughItems;
const isUncraftable = !hasEnoughItems;
if ((hideCraftableCheckbox.checked && isCraftable) || (hideUncraftableCheckbox.checked && isUncraftable)) {
continue; // Skip rendering the row if the filter doesn't match
}
// Add a column for the Maximum Crafts
const craftCountCell = document.createElement("td");
craftCountCell.style.border = "1px solid #444";
craftCountCell.style.padding = "8px";
craftCountCell.classList.add("craftQuantityColumn");
// Add a column for the Craft Button
const craftButtonCell = document.createElement("td");
craftButtonCell.style.border = "1px solid #444";
craftButtonCell.style.padding = "8px";
craftButtonCell.classList.add("craftButtonColumn");
const craftButton = document.createElement("button");
craftButtonCell.appendChild(craftButton);
const resultItem = allItems.find(allItem => allItem.id == craftRecipe.result);
// Filter based on HideTestedCheckbox and HideUntestedCheckbox
const craftButtonText = await addCraftButtonToTable(craftRecipe.recipe, inventory, craftCountCell, craftButton, resultItem.name, resultItem.image);
if (hideTestedCheckbox.checked && !craftButtonText.includes(`Test Craft`)) {
continue;
}
if (hideUntestedCheckbox.checked && craftButtonText.includes(`Test Craft`)) {
continue;
}
// Collect categories from the required items
const itemCategories = requiredItems
.map(requiredItem => {
const item = allItems.find(allItem => allItem.id == requiredItem.id || allItem.id === requiredItem.resultid);
return item ? item.category : null;
})
.filter(category => category); // Remove null values
const uniqueCategories = [...new Set(itemCategories)]; // Deduplicate categories
// Collect categories from the required items
const itemInnerCategories = requiredItems
.map(requiredItem => {
const item = allItems.find(allItem => allItem.id == requiredItem.id || allItem.id === requiredItem.resultid);
return item ? item.innerCategory : null;
})
.filter(category => category); // Remove null values
const uniqueInnerCategories = [...new Set(itemInnerCategories)]; // Deduplicate inner categories
// Filter recipes based on selected category
if (selectedCategory && !uniqueCategories.includes(selectedCategory)) {
continue; // Skip rendering the row if no matching category is found
}
// Filter recipes based on selected category
if (selectedInnerCategory && !uniqueInnerCategories.includes(selectedInnerCategory)) {
continue; // Skip rendering the row if no matching category is found
}
// Display recipe information in the table row
["id", "uses", "name", "inventory"].forEach(key => {
const cell = document.createElement("td");
if (key === "name") {
const nameContainer = document.createElement("div");
nameContainer.style.display = "flex";
nameContainer.style.alignItems = "center";
// Fetch item image from the result
if (resultItem && resultItem.image) {
const img = document.createElement("img");
img.src = resultItem.image;
img.alt = resultItem.name;
img.style.width = "30px";
img.style.height = "30px";
img.style.marginRight = "10px";
nameContainer.appendChild(img);
}
const nameText = document.createElement("span");
nameText.textContent = recipe[key];
nameContainer.appendChild(nameText);
cell.appendChild(nameContainer);
}
else if (key === "inventory"){
const resultItem = requiredItems.find(item => item.amount === 0);
const resultItemOnInventory = inventory.find(item => Number(item.itemid) === Number(resultItem.id))
cell.textContent = resultItemOnInventory ? resultItemOnInventory.amount : 0;
}else {
cell.textContent = recipe[key];
}
cell.style.border = "1px solid #444";
cell.style.padding = "8px";
row.appendChild(cell);
});
row.appendChild(craftCountCell);
row.appendChild(craftButtonCell);
// Fetch crafting recipe on click
row.addEventListener("click", () => {
getCraftingRecipe(recipe.id);
});
tbody.appendChild(row);
}
};
const inventory = await fetchInventory();
// Fetch all items data to cache immediately
const allItemIds = await getAllItemIds(allCraftRecipes, inventory);
const cleanedItemIds = [...allItemIds].map(id => String(Number(id)));
const allItems = await fetchItemDetails(cleanedItemIds);
// Get all unique categories for the dropdown
const categories = Array.from(new Set(allItems.map(item => item.category))).sort();
// Get all unique inner categories for the dropdown
const innerCategories = Array.from(new Set(allItems.map(item => item.innerCategory))).sort();
const emptyCategoryOption = document.createElement("option");
emptyCategoryOption.value = ""; // Empty value for no filtering
emptyCategoryOption.textContent = "All Categories"; // Label for the empty option
categoryDropdown.appendChild(emptyCategoryOption);
categories.forEach(category => {
const option = document.createElement("option");
option.value = category;
option.textContent = category;
categoryDropdown.appendChild(option);
});
// Set the default option to the empty value
categoryDropdown.value = emptyCategoryOption.value;
const emptyInnerCategoryOption = document.createElement("option");
emptyInnerCategoryOption.value = ""; // Empty value for no filtering
emptyInnerCategoryOption.textContent = "All Inner Categories"; // Label for the empty option
innerCategoryDropdown.appendChild(emptyInnerCategoryOption);
innerCategories.forEach(category => {
const option = document.createElement("option");
option.value = category;
option.textContent = category;
innerCategoryDropdown.appendChild(option);
});
// Set the default option to the empty value
innerCategoryDropdown.value = emptyInnerCategoryOption.value;
// Initialize the table with all recipes
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
// Attach event listener to search bar
searchBar.addEventListener("input", (event) => {
const searchTerm = event.target.value;
const filteredRecipes = recipes.filter(recipe =>
recipe.name.toLowerCase().includes(searchTerm.toLowerCase())
);
displayFilteredRecipes(filteredRecipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
hideCraftableCheckbox.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
hideUncraftableCheckbox.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
hideTestedCheckbox.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
hideUntestedCheckbox.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
categoryDropdown.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
innerCategoryDropdown.addEventListener("change", () => {
displayFilteredRecipes(recipes, allCraftRecipes, inventory, allItems, categoryDropdown.value, innerCategoryDropdown.value);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
globalDialog.appendChild(tableContainer);
document.body.appendChild(globalDialog);
debugConsole(`[GGn Control Panel] displayCraftedRecipes complete.`);
}
/**
* Function to calculate how many times a recipe can be performed based on inventory.
* @param {Array} recipe - The crafting recipe with required item IDs and amounts.
* @param {Array} inventory - The player's inventory containing item amounts.
* @returns {number} - The maximum number of times the recipe can be crafted.
**/
async function calculateCraftingQuantity(recipe, inventory) {
// Get required items from the recipe
const requiredItems = await getRequiredItems(recipe, null); // Only interested in crafting parts
let canCraftTimes = Infinity; // Start with Infinity as a baseline for comparison
for (const requiredItem of requiredItems) {
// Find the corresponding inventory item
const inventoryItem = inventory.find(item => Number(item.item.id) === Number(requiredItem.id));
if (inventoryItem) {
// Calculate how many times this item allows the recipe to be crafted
const maxCraftsWithItem = Math.floor(inventoryItem.amount / requiredItem.amount);
canCraftTimes = Math.min(canCraftTimes, maxCraftsWithItem);
} else {
// If an item is missing, crafting is not possible
canCraftTimes = 0;
break;
}
}
debugConsole(`[GGn Control Panel] calculateCraftingQuantity returns ${canCraftTimes}`);
return canCraftTimes;
}
/**
* Function to determine if inventory has sufficient items.
* @param {Array} inventory - The player's inventory containing item IDs and amounts.
* @param {Array} requiredItems - The required items with id and amount.
* @returns {boolean} - Whether the inventory has sufficient items.
**/
function hasSufficientInventory(inventory, requiredItems) {
for (const requiredItem of requiredItems) {
const inventoryItem = inventory.find(item => Number(item.itemid) === requiredItem.id);
if (!inventoryItem || parseInt(inventoryItem.amount, 10) < requiredItem.amount) {
debugConsole(`[GGn Control Panel] hasSufficientInventory returns false.`);
return false;
}
}
debugConsole(`[GGn Control Panel] hasSufficientInventory returns true.`);
return true;
}
// Function to add Test Craft button to table and handle actions
async function addCraftButtonToTable(recipe, inventory, quantityCell, craftButton, resultItemName, resultItemImage) {
const craftingResult = await fetchCraftingResult(recipe, true, false);
const canCraftTimes = await calculateCraftingQuantity(recipe, inventory);
debugConsole(`[GGn Control Panel] addCraftButtonToTable(recipe=${recipe}, inventory): CraftTimes: ${canCraftTimes} craftingResult: ${JSON.stringify(craftingResult)}`);
const tableRow = document.getElementById(`recipe_${recipe}`);
quantityCell.textContent = canCraftTimes;
craftButton.textContent = `Test Craft`;
if (craftingResult) {
craftButton.textContent = `Craft (${craftingResult.Name})`;
craftButton.disabled = canCraftTimes <= 0; // Disable button if cannot craft
if (craftButton.disabled) {
craftButton.style.opacity = "0.5";
craftButton.style.cursor = "not-allowed";
}
craftButton.onclick = async (event) => {
event.stopPropagation(); // Prevents the event from bubbling up to the row
const actionResult = await fetchCraftingResult(recipe, false, true); // Real Craft with real consequences
if (actionResult) {
showSnackbar(`${resultItemName} has been successfully crafted and added to your inventory.`, getImageUrl(resultItemImage));
localStorage.removeItem("inventoryData");
localStorage.removeItem("inventoryCacheExpiry");
inventory = await fetchInventory();
}
debugConsole("[GGn Control Panel] Inventory Refreshed. Successfully crafted:", resultItemName);
await getCraftedRecipes();
};
debugConsole(`[GGn Control Panel] addCraftButtonToTable added a [Test Craft] Button.`);
} else {
craftButton.onclick = async (event) => {
event.stopPropagation(); // Prevents the event from bubbling up to the row
const actionResult = await fetchCraftingResult(recipe, true, true); // Mock Craft to test consequences
if (actionResult) {
debugConsole("[GGn Control Panel] Successfully crafted (test):", actionResult.name);
showSnackbar(`Test Craft for recipe ${recipe} was successful.`);
await addCraftButtonToTable(recipe, inventory, quantityCell, craftButton, resultItemName, resultItemImage);
}
};
debugConsole(`[GGn Control Panel] addCraftButtonToTable added a [Craft] Button.`);
}
return craftButton.textContent;
}
/**
* Function to get required items for a recipe.
* @param {string} recipeString - The recipe string containing item IDs.
* @param {string|null} recipeResult - The recipe result ID to include (optional).
* @returns {Array} - An array of objects with id and amount.
**/
function getRequiredItems(recipeString, recipeResult) {
if (typeof recipeString !== "string") {
console.error("Invalid recipe string:", recipeString);
return [];
}
const recipeParts = recipeString.match(/.{1,5}/g);
if (!recipeParts || recipeParts.length !== 9) {
console.error("Recipe string format is invalid.");
return [];
}
// Filter and count occurrences of valid parts
const itemCounts = recipeParts
.filter(part => part && part !== "EEEEE" && part !== "00000")
.map(part => parseInt(part, 10))
.filter(Number.isInteger)
.reduce((counts, id) => {
counts[id] = (counts[id] || 0) + 1;
return counts;
}, {});
// Convert the counts object into an array of objects with id and amount
const requiredItems = Object.entries(itemCounts).map(([id, amount]) => ({
id: parseInt(id, 10),
amount
}));
// Optionally include the recipe result if valid
if (recipeResult) {
const resultInt = parseInt(recipeResult, 10);
if (Number.isInteger(resultInt)) {
requiredItems.push({ resultid: resultInt, amount: 0 });
}
}
debugConsole(`[GGn Control Panel] getRequiredItems returns `,requiredItems);
return requiredItems;
}
function extractCategoryFromDescription(description) {
if (!description) return "Unknown"; // Handle missing descriptions
const match = description.match(/Category:\s*(.*?)(?:<br|$)/);
return match ? match[1].trim() : "Unknown"; // Trim whitespace for clean output
}
// Helper function to determine stock status
function determineStockStatus(infStock, stock) {
if (infStock) {
return "Shop Item";
} else if (stock > 0) {
return "Rare";
} else {
return "None";
}
}
function displayInboxMessages(messages) {
globalDialog = createDialog("Inbox");
const tableContainer = document.createElement("div");
tableContainer.style.flexGrow = "1"; // Allow container to grow
tableContainer.style.overflowY = "auto"; // Enable vertical scrolling
tableContainer.style.border = "1px solid #444"; // Optional border
const searchField = createSearchFilter("Search by subject or conversation ID...");
const searchParticipantsField = createSearchFilter("Search by participant username...");
globalDialog.appendChild(searchField);
globalDialog.appendChild(searchParticipantsField);
const hideStickyToggleLabel = document.createElement("label");
hideStickyToggleLabel.style.display = "block";
hideStickyToggleLabel.style.marginBottom = "10px";
const hideStickyToggle = document.createElement("input");
hideStickyToggle.type = "checkbox";
hideStickyToggle.checked = false;
hideStickyToggle.addEventListener("change", function() {
// Re-render the inbox table with the updated filter
const filteredMessages = applyFilters(messages, hideStickyToggle.checked, searchField.value, searchParticipantsField.value);
displayMessages(filteredMessages);
});
hideStickyToggleLabel.appendChild(hideStickyToggle);
hideStickyToggleLabel.appendChild(document.createTextNode("Hide Stickied Messages"));
globalDialog.appendChild(hideStickyToggleLabel);
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.marginTop = "20px";
table.style.backgroundColor = "#222";
table.style.color = "#fff";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
["ConvId", "Subject", "Participants"].forEach((headerText) => {
const th = document.createElement("th");
th.textContent = headerText;
th.style.border = "1px solid #444";
th.style.padding = "8px";
th.style.textAlign = "left";
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
// Function to display messages in the table
const displayMessages = (filteredMessages) => {
tbody.innerHTML = "";
filteredMessages.forEach((msg) => {
const row = document.createElement("tr");
row.style.cursor = "pointer";
if (msg.unread === true) {
row.style.color = "#000";
row.style.backgroundColor = "#6f9";
row.style.fontWeight = "bold";
}
const convIdCell = document.createElement("td");
convIdCell.style.textAlign = "left";
convIdCell.textContent = msg.convId;
convIdCell.style.border = "1px solid #444";
convIdCell.style.padding = "8px";
row.appendChild(convIdCell);
const subjectCell = document.createElement("td");
subjectCell.style.textAlign = "left";
if (msg.sticky) {
const pinEmoji = "📌 ";
subjectCell.textContent = pinEmoji + msg.subject;
subjectCell.style.fontWeight = "bold";
} else {
subjectCell.textContent = msg.subject;
}
subjectCell.style.border = "1px solid #444";
subjectCell.style.padding = "8px";
row.appendChild(subjectCell);
const participantsCell = document.createElement("td");
participantsCell.style.textAlign = "left";
participantsCell.textContent = msg.participants.map(participant => participant.username).join(", ");
participantsCell.style.border = "1px solid #444";
participantsCell.style.padding = "8px";
row.appendChild(participantsCell);
row.addEventListener("click", () => {
fetchConversation(msg.convId);
});
tbody.appendChild(row);
});
};
// Function to apply both sticky and participant filters
const applyFilters = (messages, hideSticky, subjectSearch, participantSearch) => {
return messages.filter((msg) => {
const matchesStickyFilter = !hideSticky || !msg.sticky;
const matchesSubjectSearch = msg.convId.toString().includes(subjectSearch.toLowerCase()) ||
msg.subject.toLowerCase().includes(subjectSearch.toLowerCase());
const matchesParticipantSearch = msg.participants.some(
(participant) => participant.username.toLowerCase().includes(participantSearch.toLowerCase())
);
return matchesStickyFilter && matchesSubjectSearch && matchesParticipantSearch;
});
};
// Initial rendering of all messages
const filteredMessages = applyFilters(messages, hideStickyToggle.checked, searchField.value, searchParticipantsField.value);
displayMessages(filteredMessages);
// Add event listener for subject search input
searchField.addEventListener("input", (event) => {
const filteredMessages = applyFilters(messages, hideStickyToggle.checked, event.target.value, searchParticipantsField.value);
displayMessages(filteredMessages);
});
// Add event listener for participant search input
searchParticipantsField.addEventListener("input", (event) => {
const filteredMessages = applyFilters(messages, hideStickyToggle.checked, searchField.value, event.target.value);
displayMessages(filteredMessages);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
// container.appendChild(tableContainer);
globalDialog.appendChild(tableContainer);
document.body.appendChild(globalDialog);
debugConsole(`[GGn Control Panel] displayInboxMessages complete.`);
}
// Button that downloads all links inside a table.
function createDownloadAllButton(table) {
const downloadAllButton = document.createElement("button");
downloadAllButton.id = "download-all";
downloadAllButton.textContent = "Download All";
downloadAllButton.classList.add("custom-button");
downloadAllButton.style.marginTop = "10px";
downloadAllButton.addEventListener("click", () => {
// Query all links whose text is "Download",
const downloadLinks = table.querySelectorAll('a[title="Download"]');
console.log(`Found ${downloadLinks.length} download links.`);
downloadLinks.forEach(link => {
link.click();
});
console.log(`Attempted to open ${downloadLinks.length} download tabs.`);
});
return downloadAllButton;
}
function createStatusDropdown(labelText, defaultValue) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginRight = "10px";
const label = document.createElement("label");
label.textContent = labelText;
label.style.fontSize = "12px";
label.style.color = "#fff";
label.style.marginRight = "5px";
container.appendChild(label);
const dropdown = document.createElement("select");
dropdown.style.padding = "4px 8px";
dropdown.style.margin = "5px 0 0 10px";
dropdown.style.fontSize = "12px";
dropdown.style.borderRadius = "4px";
dropdown.style.backgroundColor = "#333";
dropdown.style.color = "#fff";
// Default option
const defaultOption = document.createElement("option");
defaultOption.value = defaultValue;
defaultOption.textContent = defaultValue;
dropdown.appendChild(defaultOption);
// Add status options
const options = [
{ value: "Seeding", text: "Seeding" },
{ value: "Leeching", text: "Leeching" },
{ value: "Download", text: "Download" }
];
options.forEach(optData => {
const option = document.createElement("option");
option.value = optData.value;
option.textContent = optData.text;
dropdown.appendChild(option);
});
container.appendChild(dropdown);
return { container, dropdown };
}
function createMinimumGPHDropdown(labelText, defaultValue) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginRight = "10px";
const label = document.createElement("label");
label.textContent = labelText;
label.style.fontSize = "12px";
label.style.color = "#fff";
label.style.marginRight = "5px";
container.appendChild(label);
const dropdown = document.createElement("select");
dropdown.style.padding = "4px 8px"; // smaller padding
dropdown.style.margin = "5px 0 0 10px"; // small top margin and a left margin to separate from the button
dropdown.style.fontSize = "12px"; // smaller text
dropdown.style.borderRadius = "4px";
dropdown.style.backgroundColor = "#333";
dropdown.style.color = "#fff";
// Option for no minimum threshold
const defaultOption = document.createElement("option");
defaultOption.value = defaultValue;
defaultOption.textContent = defaultValue;
dropdown.appendChild(defaultOption);
// Populate the dropdown with options
const options = [
{ value: "0.01", text: "0.01" },
{ value: "0.03", text: "0.03" },
{ value: "0.05", text: "0.05" },
{ value: "0.06", text: "0.06" },
{ value: "0.07", text: "0.07" },
{ value: "0.08", text: "0.08" },
{ value: "0.09", text: "0.09" },
{ value: "0.1", text: "0.1" },
{ value: "0.15", text: "0.15" },
{ value: "0.2", text: "0.2" }
];
options.forEach(optData => {
const option = document.createElement("option");
option.value = optData.value;
option.textContent = optData.text;
dropdown.appendChild(option);
});
container.appendChild(dropdown);
return { container, dropdown };
}
// Helper: Create a dropdown for seeders filter with a label.
function createSeedersDropdown(labelText, defaultValue) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginRight = "10px";
const label = document.createElement("label");
label.textContent = labelText;
label.style.fontSize = "12px";
label.style.color = "#fff";
label.style.marginRight = "5px";
container.appendChild(label);
const dropdown = document.createElement("select");
dropdown.style.padding = "4px 8px";
dropdown.style.fontSize = "12px";
dropdown.style.borderRadius = "4px";
dropdown.style.backgroundColor = "#333";
dropdown.style.color = "#fff";
// Option for no minimum threshold
const defaultOption = document.createElement("option");
defaultOption.value = defaultValue;
defaultOption.textContent = defaultValue;
dropdown.appendChild(defaultOption);
// Populate options 1-50.
for (let i = 1; i <= 50; i++) {
const option = document.createElement("option");
option.value = i;
option.textContent = i;
dropdown.appendChild(option);
}
container.appendChild(dropdown);
return { container, dropdown };
}
// Helper: Create a date picker for filtering with a label.
function createDatePicker(labelText, defaultValue) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginTop = "10px"; // Adds spacing between rows
container.style.marginRight = "10px";
const label = document.createElement("label");
label.textContent = labelText;
label.style.fontSize = "12px";
label.style.color = "#fff";
label.style.marginRight = "5px";
container.appendChild(label);
const datePicker = document.createElement("input");
datePicker.type = "date"; // Set as a date input
datePicker.style.padding = "4px 8px";
datePicker.style.fontSize = "12px";
datePicker.style.borderRadius = "4px";
datePicker.style.backgroundColor = "#333";
datePicker.style.color = "#fff";
datePicker.style.border = "1px solid #444";
if (defaultValue) {
datePicker.value = defaultValue; // Set the default value, if provided
}
container.appendChild(datePicker);
return { container, datePicker };
}
// Helper: Create the div container with a pagination selector
async function createPageIndicatorContainer() {
// Create the page indicator container.
const pageIndicatorContainer = document.createElement("div");
pageIndicatorContainer.style.textAlign = "center";
pageIndicatorContainer.style.margin = "15px 0";
pageIndicatorContainer.style.fontSize = "14px";
pageIndicatorContainer.style.padding = "10px";
pageIndicatorContainer.style.width = "auto";
// Create the dropdown for selecting pages.
const pageDropdown = document.createElement("select");
pageDropdown.style.padding = "5px";
pageDropdown.style.fontSize = "14px";
pageDropdown.style.margin = "0 10px";
pageDropdown.style.width = "80px"; // Increased width to fit larger numbers.
pageDropdown.style.minWidth = "80px"; // Ensures the dropdown won't shrink too much.
pageDropdown.style.height = "30px"; // Explicitly set the height for better visibility.
pageDropdown.style.border = "1px solid #ccc";
pageDropdown.style.borderRadius = "3px";
// Populate the dropdown with values from 1 to 1000.
for (let i = 0; i <= 1000; i++) {
const option = document.createElement("option");
option.value = i;
option.textContent = i;
if (i === currentTorrentPage) {
option.selected = true;
}
pageDropdown.appendChild(option);
}
// Add event listener for dropdown changes.
pageDropdown.addEventListener("change", async () => {
currentTorrentPage = parseInt(pageDropdown.value, 10);
await setCacheData(currentTorrentPage, "currentTorrentPage");
showSnackbar(`Pages Fetched updated to: ${currentTorrentPage}`);
});
// Assemble and append the dropdown to the container.
pageIndicatorContainer.textContent = "Pages Fetched: ";
pageIndicatorContainer.appendChild(pageDropdown);
return pageIndicatorContainer;
}
async function displayTorrentsData() {
globalDialog = createDialog("Torrent Search Results");
// Integrated description with swap search button.
const descriptionParagraph = document.createElement("p");
descriptionParagraph.style.textAlign = "center";
descriptionParagraph.style.fontSize = "14px";
globalDialog.appendChild(descriptionParagraph);
const swapSearchButton = document.createElement("button");
swapSearchButton.classList.add("custom-button");
swapSearchButton.style.marginLeft = "10px";
swapSearchButton.style.padding = "2px 5px";
swapSearchButton.style.fontSize = "inherit";
swapSearchButton.style.display = "inline";
swapSearchButton.style.verticalAlign = "middle";
//Get the last used currentSortBy
currentSortBy = localStorage.getItem('currentSortBy');
// Function to update description and button text.
function updateDescriptionAndButton() {
console.log(`UpdateDescriptionAndButton called with currentSortBy=${currentSortBy}`);
descriptionParagraph.innerHTML = "";
if (currentSortBy === "seeders") {
descriptionParagraph.appendChild(document.createTextNode("Searching all torrents ordered ascending by # of Seeders."));
swapSearchButton.textContent = "Swap Search to Search by Age";
} else {
descriptionParagraph.appendChild(document.createTextNode("Searching all torrents ordered by Age."));
swapSearchButton.textContent = "Swap Search to Search by Seeders";
}
descriptionParagraph.appendChild(swapSearchButton);
}
updateDescriptionAndButton();
swapSearchButton.addEventListener("click", async () => {
currentSortBy = (currentSortBy === "seeders") ? "age" : "seeders";
localStorage.setItem('currentSortBy', currentSortBy);
updateDescriptionAndButton();
});
// The page indicator with a dropdown for selecting Pages Fetched.
let pageIndicatorContainer = await createPageIndicatorContainer();
globalDialog.appendChild(pageIndicatorContainer);
// Create search field for filtering (by Name).
const searchField = createSearchFilter("Filter results by Name...");
globalDialog.appendChild(searchField);
// Count display (distinct groups and torrents).
const countDisplay = document.createElement("div");
countDisplay.style.marginBottom = "10px";
countDisplay.style.fontSize = "14px";
globalDialog.appendChild(countDisplay);
// Container for the table.
const tableContainer = document.createElement("div");
tableContainer.style.flexGrow = "1";
tableContainer.style.overflowY = "auto";
tableContainer.style.border = "1px solid #444";
tableContainer.style.marginBottom = "10px";
globalDialog.appendChild(tableContainer);
// Create the table and header.
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.backgroundColor = "#222";
table.style.color = "#fff";
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
["DateUploaded", "GroupId", "TorrentId", "Name", "Status", "Seeders", "SizeGigabytes", "GPH/GB"].forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
th.style.border = "1px solid #444";
th.style.padding = "8px";
th.style.textAlign = headerText === "Status" ? "center" : "left";
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
table.appendChild(tbody);
tableContainer.appendChild(table);
addSortingToTable(table); // Makes every column in the table sorteable
// The container for filters, allowing for two rows of filters, one for dropdowns, one for datepickers
const filtersContainer = document.createElement("div"); // Container for all filters.
filtersContainer.style.display = "flex";
filtersContainer.style.flexDirection = "column"; // Ensures new filters are in a separate row.
// Create a container for dropdown filters
const dropdownFiltersContainer = document.createElement("div");
dropdownFiltersContainer.style.display = "flex";
dropdownFiltersContainer.style.justifyContent = "center";
dropdownFiltersContainer.style.marginTop = "10px";
// Create a container for datepicker filters
const datePickerFiltersContainer = document.createElement("div");
datePickerFiltersContainer.style.display = "flex";
datePickerFiltersContainer.style.justifyContent = "center";
datePickerFiltersContainer.style.marginTop = "10px";
// Add the first row of filters
const minGPHObj = createMinimumGPHDropdown("Select the minimum GPH/GB you tolerate:", "No minimum");
const minSeedersObj = createSeedersDropdown("Minimum Seeders:", "No minimum");
const maxSeedersObj = createSeedersDropdown("Maximum Seeders:", "No maximum");
const statusObj = createStatusDropdown("Status: ", "");
dropdownFiltersContainer.appendChild(minGPHObj.container);
dropdownFiltersContainer.appendChild(minSeedersObj.container);
dropdownFiltersContainer.appendChild(maxSeedersObj.container);
dropdownFiltersContainer.appendChild(statusObj.container);
// Add "Date After" and "Date Before" filters.
const dateAfterObj = createDatePicker("Date After: ", "");
const dateBeforeObj = createDatePicker("Date Before: ", "");
datePickerFiltersContainer.appendChild(dateAfterObj.container);
datePickerFiltersContainer.appendChild(dateBeforeObj.container);
// Appends the pickers in seperate rows
filtersContainer.appendChild(dropdownFiltersContainer);
filtersContainer.appendChild(datePickerFiltersContainer);
globalDialog.appendChild(filtersContainer);
// Update count display.
function updateCountDisplay() {
const groupIds = new Set(bestTorrents.map(t => t.GroupId));
const torrentIds = new Set(bestTorrents.map(t => t.TorrentId));
countDisplay.textContent = `Total Groups: ${groupIds.size} | Total Torrents: ${torrentIds.size}`;
}
// Filter torrent results by Name, minimum GPH/GB, minimum seeders, and maximum seeders.
function applyLocalFilter(results, searchTerm, minGPH, minSeeders, maxSeeders, status, dateAfter, dateBefore) {
debugConsole(`[GGn Control Panel] Applying Local Filters to TorrentDisplay. MinGPH:${minGPH} MinSeeders: ${minSeeders} MaxSeeders: ${maxSeeders} Status: ${status} DateAfter: ${dateAfter} DateBefore: ${dateBefore}`);
searchTerm = searchTerm.toLowerCase();
let filteredResult = results.filter(t => t.Name && t.Name.toLowerCase().includes(searchTerm));
if(minGPH){
filteredResult = filteredResult.filter(t => Number(t.GPHperGigabyte) > Number(minGPH));
}
if(minSeeders){
filteredResult = filteredResult.filter(t => Number(t.Seeders) >= Number(minSeeders));
}
if(maxSeeders){
filteredResult = filteredResult.filter(t => Number(t.Seeders) <= Number(maxSeeders));
}
if(status){
filteredResult = filteredResult.filter(t => t.Status.toLowerCase() === status.toLowerCase());
}
if (dateAfter) {
filteredResult = filteredResult.filter(t => {
const uploadedDate = new Date(t.DateUploaded); // Convert to Date object
const afterDate = new Date(dateAfter);
return uploadedDate >= afterDate; // Check if uploaded date is after or equal to dateAfter
});
}
if (dateBefore) {
filteredResult = filteredResult.filter(t => {
const uploadedDate = new Date(t.DateUploaded); // Convert to Date object
const beforeDate = new Date(dateBefore);
return uploadedDate <= beforeDate; // Check if uploaded date is before or equal to dateBefore
});
}
return filteredResult;
}
// Render torrent rows.
async function renderTorrentRows(torrentArray) {
tbody.innerHTML = "";
torrentArray.sort((a, b) => b.GPHperGigabyte - a.GPHperGigabyte);
torrentArray.forEach(t => {
const row = document.createElement("tr");
["DateUploaded", "GroupId", "TorrentId", "Name", "Status", "Seeders", "SizeGigabytes", "GPHperGigabyte"].forEach(key => {
const cell = document.createElement("td");
cell.style.border = "1px solid #444";
cell.style.padding = "8px";
if (key === "Status") {
cell.style.textAlign = "center";
if(t.Status === "Download" && t.DownloadUrl) {
const downloadButton = document.createElement("a");
downloadButton.href = t.DownloadUrl;
downloadButton.title = "Download";
downloadButton.textContent = "Download";
downloadButton.target = "_blank";
downloadButton.style.display = "block";
cell.appendChild(downloadButton);
} else if (t.Status === "Leeching"){
cell.style.color = "red";
cell.textContent = t.Status;
} else {
cell.style.color = "green";
cell.textContent = t.Status;
}
} else {
cell.textContent = t[key];
}
row.appendChild(cell);
});
tbody.appendChild(row);
});
updateCountDisplay();
}
// Update displayed torrents based on filter inputs
async function updateFilteredDisplay() {
// Get current filter values
const minGPH = Number(minGPHObj.dropdown.value) || null;
const minSeeders = Number(minSeedersObj.dropdown.value) || null;
const maxSeeders = Number(maxSeedersObj.dropdown.value) || null;
const status = statusObj.dropdown.value || null;
const dateAfter = dateAfterObj.datePicker.value || null;
const dateBefore = dateBeforeObj.datePicker.value || null;
// Apply filters
const filtered = applyLocalFilter(bestTorrents, searchField.value.trim(), minGPH, minSeeders, maxSeeders, status, dateAfter, dateBefore);
// Save current filter values to localStorage
localStorage.setItem('minGPHFilter', minGPH);
localStorage.setItem('minSeedersFilter', minSeeders);
localStorage.setItem('maxSeedersFilter', maxSeeders);
localStorage.setItem('statusFilter', status);
localStorage.setItem('dateAfterFilter', dateAfter);
localStorage.setItem('dateBeforeFilter', dateBefore);
// Render filtered results
await renderTorrentRows(filtered);
}
// Listen for changes to filtering controls.
searchField.addEventListener("input", updateFilteredDisplay);
minGPHObj.dropdown.addEventListener("change", updateFilteredDisplay);
minSeedersObj.dropdown.addEventListener("change", updateFilteredDisplay);
maxSeedersObj.dropdown.addEventListener("change", updateFilteredDisplay);
statusObj.dropdown.addEventListener("change", updateFilteredDisplay);
dateAfterObj.datePicker.addEventListener("change", updateFilteredDisplay);
dateBeforeObj.datePicker.addEventListener("change", updateFilteredDisplay);
// Update filters with their cached result
async function updateFiltersWithCache() {
// Retrieve cached values from localStorage
const cachedMinGPH = localStorage.getItem('minGPHFilter');
const cachedMinSeeders = localStorage.getItem('minSeedersFilter');
const cachedMaxSeeders = localStorage.getItem('maxSeedersFilter');
const cachedStatus = localStorage.getItem('statusFilter');
const cachedDateAfter = localStorage.getItem('dateAfterFilter');
const cachedDateBefore = localStorage.getItem('dateBeforeFilter');
// Update filter dropdowns or date pickers with cached values
if (cachedMinGPH) minGPHObj.dropdown.value = cachedMinGPH;
if (cachedMinSeeders) minSeedersObj.dropdown.value = cachedMinSeeders;
if (cachedMaxSeeders) maxSeedersObj.dropdown.value = cachedMaxSeeders;
if (cachedStatus) statusObj.dropdown.value = cachedStatus;
if (cachedDateAfter) dateAfterObj.datePicker.value = cachedDateAfter;
if (cachedDateBefore) dateBeforeObj.datePicker.value = cachedDateBefore;
}
// Function to fetch torrents and update the global result set.
async function fetchAndRenderTorrents(append = false, sortBy = currentSortBy) {
showLoadingScreen();
try {
if(append){
const result = await fetchTorrents(currentTorrentPage, sortBy);
if (result) {
const newResults = await fetchTorrentsAndExtractGold(result);
const uniqueNewResults = newResults.filter(newItem =>
!bestTorrents.some(existing =>
existing.GroupId === newItem.GroupId && existing.TorrentId === newItem.TorrentId
)
);
bestTorrents.push(...uniqueNewResults);
await updateFilteredDisplay();
} else {
showSnackbar("No torrent data fetched.");
}
}
} catch (error) {
console.error("Error fetching torrents:", error);
} finally {
hideLoadingScreen();
}
}
// Initially load cached torrents then fetch fresh data.
bestTorrents = await loadAllCachedTorrentsInfo();
await renderTorrentRows(bestTorrents);
await fetchAndRenderTorrents(false, currentSortBy);
// Call this function during initialization to load cached values into filters and apply those filters on the rendered torrents
await updateFiltersWithCache();
await updateFilteredDisplay();
// "Fetch More" button.
const fetchMoreButton = document.createElement("button");
fetchMoreButton.textContent = "Fetch More";
fetchMoreButton.classList.add("custom-button");
fetchMoreButton.style.width = "100%";
fetchMoreButton.style.padding = "15px 10px";
fetchMoreButton.style.border = "none";
fetchMoreButton.style.borderRadius = "10px";
fetchMoreButton.style.cursor = "pointer";
fetchMoreButton.style.backgroundColor = "#007BFF";
fetchMoreButton.style.color = "#fff";
fetchMoreButton.style.fontSize = "16px";
fetchMoreButton.style.marginTop = "10px";
fetchMoreButton.addEventListener("click", async () => {
currentTorrentPage += 1;
await setCacheData(currentTorrentPage, "currentTorrentPage");
pageIndicatorContainer.querySelector("select").value = currentTorrentPage;
await fetchAndRenderTorrents(true, currentSortBy);
});
globalDialog.appendChild(fetchMoreButton);
// "Download All" button: create a button that finds all download links in the table and opens them.
function createDownloadAllButton(table) {
const downloadAllButton = document.createElement("button");
downloadAllButton.id = "download-all";
downloadAllButton.textContent = "Download All";
downloadAllButton.classList.add("custom-button");
downloadAllButton.style.marginTop = "10px";
downloadAllButton.addEventListener("click", () => {
const userConfirmed = confirm(
"Ensure that your browser downloads are set to automatic. Do you want to proceed with downloading all items?"
);
if (userConfirmed) {
const downloadLinks = table.querySelectorAll('a[title="Download"]');
downloadLinks.forEach(link => {
window.open(link.href, "_blank");
});
console.log(`Attempted to open ${downloadLinks.length} download tabs.`);
} else {
console.log("User canceled the download process.");
}
});
return downloadAllButton;
}
const downloadAllButton = createDownloadAllButton(table);
globalDialog.appendChild(downloadAllButton);
// Append the dialog to the body.
document.body.appendChild(globalDialog);
}
// Example function to open the torrent dialog.
function openTorrentDialog() {
displayTorrentsData();
}
// Helper: Extract info for all torrents from a group's document
function extractTorrentInfosFromGroupDoc(doc, groupId, group) {
const torrentInfos = [];
// Get all detail rows (assumed to be <tr> with class "pad" and id starting with "torrent_")
const detailRows = doc.querySelectorAll('tr.pad[id^="torrent_"]');
detailRows.forEach(detailRow => {
// Extract torrentId from id attribute, e.g. "torrent_9912" -> "9912"
const idAttr = detailRow.getAttribute("id");
const torrentId = idAttr ? idAttr.replace("torrent_", "") : null;
if (!torrentId) return;
// Find the corresponding torrent row for status
const statusRow = doc.querySelector(`tr.group_torrent#torrent${torrentId}`);
let status = "";
let downloadUrl = null;
let magnet = "";
if (statusRow) {
// Try to get a status from an <a> element with title "Seeding" or "Downloading"
const statusLink = statusRow.querySelector('a[title="Seeding"], a[title="Leeching"]');
if (statusLink) {
status = statusLink.getAttribute("title");
}
// If no seeding or downloading status found, check for a "Download" link.
if (!status) {
const downloadLink = statusRow.querySelector('a[title="Download"]');
if (downloadLink) {
status = "Download";
downloadUrl = downloadLink.href; // Capture the download URL
magnet = downloadLink.outerHTML; // Capture the entire <a> tag HTML
}
}
}
// Find the gold element in the detail row
const goldElem = detailRow.querySelector("#gold_amt");
if (!goldElem) {
console.warn(`[GGn Control Panel] Gold element not found for torrent ${torrentId} in group ${groupId}`);
return;
}
const goldPerHourPool = goldElem.getAttribute("title");
// Get the torrent data from group.Torrents (if available)
const torrent = group.Torrents[torrentId];
if (!torrent) {
console.warn(`[GGn Control Panel] Torrent ${torrentId} not found in group data for group ${groupId}`);
return;
}
// Compute gold per hour and GPH per gigabyte.
const goldPerHour = Number(goldPerHourPool) / Number(torrent.Seeders);
const sizeInGB = Number(torrent.Size) / (1024 ** 3);
const GPHperGigabyte = goldPerHour / sizeInGB;
torrentInfos.push({
DateUploaded: torrent.Time,
GroupId: groupId,
TorrentId: torrentId,
Name: decodeHTML(group.Name),
SizeGigabytes: sizeInGB,
IsSnatched: torrent.IsSnatched || torrent.isSnatched,
Status: status, // "Seeding", "Downloading", or "Download"
DownloadUrl: downloadUrl,
Magnet: magnet, // Full <a> tag HTML if available
Seeders: torrent.Seeders,
LastUpdated: Date.now(),
GPHperGigabyte: GPHperGigabyte
});
});
return torrentInfos;
}
// Fetched torrent information and permanently caches it without an expiration date. The torrent information data can be updated via other function
async function fetchTorrentsAndExtractGold(torrentGroups) {
const options = {
headers: {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-US,en;q=0.9,pt-PT;q=0.8,pt;q=0.7",
"cache-control": "max-age=0",
"priority": "u=0, i",
"sec-ch-ua": "\"Not A(Brand\";v=\"8\", \"Chromium\";v=\"132\", \"Google Chrome\";v=\"132\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
},
referrer: "https://gazellegames.net/torrents.php",
referrerPolicy: "same-origin",
body: null,
method: "GET",
mode: "cors",
credentials: "include"
};
let torrentsWithGoldInfo = [];
// Iterate over each torrent group (torrentGroups is assumed to be an object with keys as group IDs)
for (const groupId in torrentGroups) {
if (!torrentGroups.hasOwnProperty(groupId)) continue;
const group = torrentGroups[groupId];
if (!group.Torrents || typeof group.Torrents !== "object") continue;
// Check if groupId has been processed already
const processedGroupIds = await getProcessedTorrentGroupIds();
if (processedGroupIds.includes(groupId)) {
const cachedGroupData = await getCachedData(`torrent_group_${groupId}`);
if (cachedGroupData && cachedGroupData.response) {
torrentsWithGoldInfo.push(...cachedGroupData.response);
}
continue;
}
// Otherwise, fetch the group page once
const url = `https://gazellegames.net/torrents.php?id=${groupId}`;
let doc;
try {
const response = await fetchWithRateLimit(url, options);
const htmlText = typeof response === "string" ? response : await response.text();
const parser = new DOMParser();
doc = parser.parseFromString(htmlText, "text/html");
} catch (err) {
console.error(`Error fetching group ${groupId}:`, err);
continue;
}
// Extract all torrent info from the fetched document
const groupTorrentInfos = extractTorrentInfosFromGroupDoc(doc, groupId, group);
if (groupTorrentInfos.length > 0) {
updateProcessedTorrentGroupIds(groupId);
await setCacheData(groupTorrentInfos, `torrent_group_${groupId}`); // Cache by groupId
torrentsWithGoldInfo.push(...groupTorrentInfos);
}
}
return torrentsWithGoldInfo;
}
// Function to get cached data with expiry check (handles null cacheExpiryKey)
async function getCachedData(cacheKey, cacheExpiryKey=null, cacheExpiryDuration=null) {
const cachedData = localStorage.getItem(cacheKey);
// If there's no cache, return null and indicate expiration
if (!cachedData) {
console.log(`[GGn Control Panel] Cache[${cacheKey}] is missing.`);
return { response: null, isExpired: true };
}
// If cacheExpiryKey is null, just return the data without checking expiry
if (!cacheExpiryKey) {
console.log(`[GGn Control Panel] Cache[${cacheKey}] fetched without expiration check.`);
debugConsole(`[GGn Control Panel] Cache[${cacheKey}] Response:`, JSON.parse(cachedData));
return { response: JSON.parse(cachedData), isExpired: false };
}
// If cacheExpiryKey exists, check if the cache has expired
const cacheExpiryTime = localStorage.getItem(cacheExpiryKey);
if (cacheExpiryTime && Date.now() < cacheExpiryTime) {
console.log(`[GGn Control Panel] Cache[${cacheKey}] is valid. You have ${((cacheExpiryTime - Date.now()) / 60000)} minutes of cache left.`);
debugConsole(`[GGn Control Panel] Cache[${cacheKey}] Response:`, JSON.parse(cachedData));
return { response: JSON.parse(cachedData), isExpired: false };
} else {
console.log(`[GGn Control Panel] Cache[${cacheKey}] expired or missing.`);
localStorage.removeItem(cacheKey); // Clear expired cache
localStorage.removeItem(cacheExpiryKey); // Clear expired cache expiry
return { response: null, isExpired: true };
}
}
// Function to store new data in cache with expiry
async function setCacheData(data, cacheKey, cacheExpiryKey=null, cacheExpiryDuration=null) {
localStorage.setItem(cacheKey, JSON.stringify(data));
if(cacheExpiryKey && cacheExpiryDuration){
const expiryTime = Date.now() + cacheExpiryDuration;
localStorage.setItem(cacheExpiryKey, expiryTime.toString());
console.log(`[GGn Control Panel] Cache for key "${cacheKey}" set successfully. Expires at: ${new Date(expiryTime).toLocaleString()}`);
}else{
console.log(`[GGn Control Panel] Cache for key "${cacheKey}" set successfully.`);
}
}
async function updateTorrentDictionaryOnCache(newDictionaryData, cacheKey, cacheExpiryKey=null, cacheExpiryDuration=null) {
if(!newDictionaryData){
return;
}
const existingData = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
// Use a Map to store data, where the key is `emitente` and the value is `selectedOption`
const dataMap = new Map();
// Add existing data to the map (this will replace any old selectedOption with the new one)
(existingData.response || []).forEach(item => {
dataMap.set(item.emitente.trim(), { emitente: item.emitente.trim(), selectedOption: item.selectedOption.trim() });
});
// Add new data, replacing the selectedOption for existing emitente
newDictionaryData.forEach(item => {
dataMap.set(item.emitente.trim(), { emitente: item.emitente.trim(), selectedOption: item.selectedOption.trim() });
});
// Convert the map back to an array and store it in cache
const uniqueData = Array.from(dataMap.values());
await setCacheData(uniqueData, cacheKey, cacheExpiryKey, cacheExpiryDuration);
}
async function loadAllCachedTorrentsInfo() {
const processedGroupIds = await getProcessedTorrentGroupIds(); // should return an array of group IDs
let allCachedTorrentInfos = [];
for (const groupId of processedGroupIds) {
const cachedData = await getCachedData(`torrent_group_${groupId}`);
if (cachedData) {
// Depending on how you store your data, it might be stored directly as an array...
if (Array.isArray(cachedData)) {
allCachedTorrentInfos = allCachedTorrentInfos.concat(cachedData);
}
// ...or wrapped in an object (e.g. { response: [...] })
else if (cachedData.response && Array.isArray(cachedData.response)) {
allCachedTorrentInfos = allCachedTorrentInfos.concat(cachedData.response);
}
}
}
return allCachedTorrentInfos;
}
async function updateProcessedTorrentGroupIds(newGroupId) {
let stored = localStorage.getItem("processedTorrentGroupIds");
let processedSet;
if (stored) {
try {
processedSet = new Set(JSON.parse(stored));
} catch (e) {
console.error("Error parsing processedTorrentGroupIds, resetting the set.", e);
processedSet = new Set();
}
} else {
processedSet = new Set();
}
processedSet.add(newGroupId);
localStorage.setItem("processedTorrentGroupIds", JSON.stringify(Array.from(processedSet)));
return processedSet;
}
async function getProcessedTorrentGroupIds(){
const stored = await getCachedData("processedTorrentGroupIds");
if (stored && stored.response) {
try {
const ids = stored.response;
return Array.isArray(ids) ? ids : [];
} catch (e) {
console.error("Error parsing processed torrent group IDs:", e);
return [];
}
}
return [];
}
async function refreshUserProfileCache() {
localStorage.removeItem("userProfileData");
localStorage.removeItem("userProfileCacheExpiry");
console.log("[GGn Control Panel] User Profile cache refreshed.");
}
function refreshTorrentCache() {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key.startsWith("torrent_")) {
localStorage.removeItem(key);
}
}
localStorage.removeItem("processedTorrentGroupIds");
console.log("[GGn Control Panel] Torrents cache refreshed.");
}
async function refreshInboxCache() {
localStorage.removeItem("getInboxMessagesData");
localStorage.removeItem("getInboxMessagesCacheExpiry");
console.log("[GGn Control Panel] Inbox cache refreshed.");
}
async function refreshCraftedRecipesCache() {
localStorage.removeItem("allCraftedRecipesData");
localStorage.removeItem("allCraftedRecipesCacheExpiry");
console.log("[GGn Control Panel] Crafted Recipes cache refreshed.");
localStorage.removeItem("inventoryData");
localStorage.removeItem("inventoryCacheExpiry");
console.log("[GGn Control Panel] Inventory cache refreshed.");
localStorage.removeItem("itemDetailsData");
localStorage.removeItem("itemDetailsCacheExpiry");
console.log("[GGn Control Panel] Item Details cache refreshed.");
}
async function refreshCraftingSimulatorCache() {
localStorage.removeItem("inventoryData");
localStorage.removeItem("inventoryCacheExpiry");
console.log("[GGn Control Panel] Inventory cache refreshed.");
localStorage.removeItem("itemDetailsData");
localStorage.removeItem("itemDetailsCacheExpiry");
console.log("[GGn Control Panel] Item Details cache refreshed.");
}
async function refreshCraftingDataCache() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
// Check if key starts with the desired patterns.
if (key.startsWith("craftingResultData_") || key.startsWith("craftingResultCacheExpiry_")) {
keysToRemove.push(key);
}
}
// Remove all keys that match the patterns.
keysToRemove.forEach(key => {
localStorage.removeItem(key);
console.log(`[GGn Control Panel] Removed key: ${key}`);
});
}
async function validatePagination(data, cacheKey, cacheExpiryKey){
console.log("[GGn Control Panel] Validating data:",data);
try{
if(!data){
console.log("[GGn Control Panel] Data is invalid: ",data);;
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
async function fetchPagination(){
const cacheKey = "currentTorrentPage";
const cachedResponse = await getCachedData(cacheKey);
if (!cachedResponse || !cachedResponse.response) {
try {
await setCacheData(1, cacheKey);
return 1;
} catch (error) {
console.error(`[GGn Control Panel] Error fetching pagination :`, error);
return 1;
}
} else {
await validatePagination(cachedResponse.response);
return cachedResponse.response; // Return cached data
}
}
async function validateItemDetails(data, cacheKey, cacheExpiryKey) {
console.log("[GGn Control Panel] Validating data:", data);
try {
// Check if data is an array
if (!Array.isArray(data)) {
console.log(`Data is invalid: ${JSON.stringify(data)}`);
throw new Error("Invalid cached data structure: data is not an array");
}
// Define the required fields
const requiredFields = ['id', 'name', 'image', 'category', 'description', 'infStock', 'stock', 'notTradeable'];
// Validate each object in the array
const isValid = data.every(item =>
requiredFields.every(field => {
const keys = field.split('.');
return keys.reduce((obj, key) => obj?.[key], item) !== undefined;
})
);
if (!isValid) {
console.log(`Data is invalid: ${JSON.stringify(data)}`);
throw new Error("[GGn Control Panel] Invalid cached data structure: missing required fields");
}
} catch (e) {
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
// Fetch item details for an array of item IDs
async function fetchItemDetails(itemIds) {
const cacheKey = "itemDetailsData";
const cacheExpiryKey = "itemDetailsCacheExpiry";
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
const queryItems = itemIds.map(itemId => parseInt(itemId, 10));
const queryString = queryItems.length > 1
? `itemids=[${queryItems.join(",")}]`
: `itemid=${queryItems[0]}`;
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=items&${queryString}`);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateItemDetails(response.response, cacheKey, cacheExpiryKey);
return response.response.map(item => ({
id: item.id,
name: item.name,
image: getImageUrl(item.image),
description: item.description,
category: item.category,
innerCategory: extractCategoryFromDescription(item.description),
stock: determineStockStatus(item.infStock, item.stock),
tradeable: !item.notTradeable,
value: item.gold
}));
} catch (error) {
console.error(`Error fetching all items from ItemIds=${itemIds} :`, error);
return null; // Or handle the error as needed
}
} else {
await validateItemDetails(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response.map(item => ({
id: item.id,
name: item.name,
image: getImageUrl(item.image),
description: item.description,
category: item.category,
innerCategory: extractCategoryFromDescription(item.description),
stock: determineStockStatus(item.infStock, item.stock),
tradeable: !item.notTradeable,
value: item.gold
})); // Return cached data
}
}
async function validateInventory(data, cacheKey, cacheExpiryKey) {
console.log("[GGn Control Panel] Validating data:", data);
try {
// Check if data is an array
if (!Array.isArray(data)) {
console.log(`[GGn Control Panel] Data is invalid: ${JSON.stringify(data)}`);
throw new Error("Invalid cached data structure: data is not an array");
}
// Define the required fields
const requiredFields = ['itemid', 'amount', 'item.id', 'item.name', 'item.image', 'item.category'];
// Validate each object in the array
const isValid = data.every(item =>
requiredFields.every(field => {
const keys = field.split('.');
return keys.reduce((obj, key) => obj?.[key], item) !== undefined;
})
);
if (!isValid) {
console.log(`[GGn Control Panel] Data is invalid: ${JSON.stringify(data)}`);
throw new Error("Invalid cached data structure: missing required fields");
}
} catch (e) {
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
// Function to fetch inventory with caching
async function fetchInventory() {
const cacheKey = "inventoryData";
const cacheExpiryKey = "inventoryCacheExpiry";
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=items&type=inventory&include_info=true`);
debugConsole(`[GGn Control Panel] fetchInventory() API Response:`, response);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateInventory(response.response, cacheKey, cacheExpiryKey);
return response.response;
} catch (error) {
console.error(`[GGn Control Panel] Error fetching inventory :`, error);
return null; // Or handle the error as needed
}
} else {
await validateInventory(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response; // Return cached data
}
}
async function validateCraftingResult(data, cacheKey, cacheExpiryKey){
debugConsole("[GGn Control Panel] Validating data:",data);
try{
if(!data){
console.log("[GGn Control Panel] Data is invalid: ",data);;
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
async function fetchCraftingResult(recipe, isTest=true, updateCache=true) {
debugConsole(`[GGn Control Panel] fetchCraftingResult(recipe=${recipe},isTest=${isTest},updateCache=${updateCache})`);
const cacheKey = `craftingResultData_${recipe}${isTest ? "_test" : ""}`;
const cacheExpiryKey = `craftingResultCacheExpiry_${recipe}`;
const cacheExpiryDuration = 4320000000; // 5000 days (forever)
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
// The cache response for a successful craft has cachedResponse.response = null, so we must make a special case to fetch ALWAYS from the API when it's not a test
if ((updateCache && !isTest) || (updateCache && (!cachedResponse || cachedResponse.isExpired || !cachedResponse.response))) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=items&type=crafting_result&action=${isTest ? "find" : "take"}&recipe=${recipe}`);
console.log(`[GGn Control Panel] fetchCraftingResult(recipe=${recipe},isTest=${isTest},updateCache=${updateCache}) API Response:`, response);
// Check if the response is valid
if (!response || !response.response) {
console.error(`[GGn Control Panel] Invalid response from API for ${isTest ? "(Test)" : ""} recipe=${recipe}:`, response);
return null; // Handle as needed (e.g., return null or show an error to the user)
}
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateCraftingResult(response.response, cacheKey, cacheExpiryKey);
return response.response;
} catch (error) {
console.error(`[GGn Control Panel] Error fetching craft result test from ${isTest ? "(Test)" : ""} recipe=${recipe}:`, error);
return null; // Handle as needed
}
} else {
// Check if cached response is valid
if (!cachedResponse || !cachedResponse.response) {
console.error(`[GGn Control Panel] Invalid cached response for ${isTest ? "(Test)" : ""} recipe=${recipe}:`, cachedResponse);
return null; // Handle as needed
}
await validateCraftingResult(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response;
}
}
async function validateRecipes(data, cacheKey, cacheExpiryKey) {
console.log("[GGn Control Panel] Validating data:",data);
try {
// Check if data is an array
if (!Array.isArray(data)) {
debugConsole(`[GGn Control Panel] Data is invalid:`,data);
throw new Error("Invalid cached data structure: data is not an array");
}
// Define the required fields
const requiredFields = ['id', 'name', 'recipe', 'requirement', 'result'];
// Validate each object in the array
const isValid = data.every(item =>
requiredFields.every(field => item.hasOwnProperty(field))
);
if (!isValid) {
console.log(`[GGn Control Panel] Data is invalid: ${JSON.stringify(data)}`);
throw new Error("Invalid cached data structure: missing required fields");
}
} catch (e) {
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
// Function to fetch all recipes with caching
async function fetchAllRecipesFrom(recipeIds) {
const cacheKey = "allCraftedRecipesData";
const cacheExpiryKey = "allCraftedRecipesCacheExpiry"; // To store the cache expiry time
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=items&type=get_crafting_recipe&recipeids=[${recipeIds}]`);
console.log(`[GGn Control Panel] fetchAllRecipesFrom(recipeIds=${recipeIds}) API Response:`, response);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateRecipes(response.response, cacheKey, cacheExpiryKey);
return response.response;
} catch (error) {
console.error(`[GGn Control Panel] Error fetching all recipes from RecipeIds=${recipeIds} :`, error);
return null; // Or handle the error as needed
}
} else {
await validateRecipes(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response; // Return cached data
}
}
async function validateCraftedRecipes(data, cacheKey, cacheExpiryKey){
console.log("[GGn Control Panel] Validating data:",data);
try{
if(!data || !Array.isArray(data)){
console.log("[GGn Control Panel] Data is invalid: ",data);;
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
// Function to fetch user data with caching
async function fetchCraftedRecipes() {
const cacheKey = "craftedRecipesData";
const cacheExpiryKey = "craftedRecipesCacheExpiry";
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=items&type=crafted_recipes`);
console.log(`[GGn Control Panel] fetchCraftedRecipes() API Response:`, response);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateCraftedRecipes(response.response, cacheKey, cacheExpiryKey);
return response.response; // Return fresh data
} catch (error) {
console.error("[GGn Control Panel] Error fetching crafting recipes:", error);
return null;
}
} else {
await validateCraftedRecipes(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response;
}
}
async function markChecked(messages) {
const cacheKey = "getInboxMessagesData";
const cacheExpiryKey = "getInboxMessagesCacheExpiry";
const cacheExpiryDuration = 100000; // 100 Seconds
const unreadMessages = messages.filter(message => message.unread);
if (unreadMessages.length === 0) {
console.log("[GGn Control Panel] No unread messages to mark as read.");
return;
}
const unreadConvIds = unreadMessages.map(message => message.convId);
const stringifiedConvIds = unreadConvIds.map(convId => convId.toString());
// Get cached data
const messageInbox = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
const url = `https://gazellegames.net/api.php?key=${API_KEY}&request=inbox&type=markread`;
const formData = new FormData();
stringifiedConvIds.forEach(convId => {
formData.append("messages[]", convId);
});
fetch(url, {
method: "POST",
body: formData
})
.then(response => response.json())
.then(data => {
console.log("[GGn Control Panel] markChecked(messages) API Response: ", data);
if (data.status === "success") {
console.log("[GGn Control Panel] Unread messages marked as read successfully.");
if (checkInboxButton) {
checkInboxButton.textContent = "Check Inbox";
}
// Check if the cached data has the expected structure
if (messageInbox && messageInbox.response && messageInbox.response.response && Array.isArray(messageInbox.response.response.messages)) {
// Update the cache to mark the corresponding conversation(s) as read
unreadConvIds.forEach(convId => {
const cachedMessage = messageInbox.response.response.messages.find(message => message.convId === convId);
if (cachedMessage) {
debugConsole("[GGn Control Panel] Marking as read:", cachedMessage.subject);
cachedMessage.unread = false; // Mark as read in cache
} else {
console.error(`[GGn Control Panel] Message with convId ${convId} not found in cache.`);
}
});
setCacheData(messageInbox.response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
console.log("[GGn Control Panel] Local cache updated to reflect marked as read.");
} else {
console.error("[GGn Control Panel] Error: Cached data does not have the expected structure.");
}
} else {
console.error("[GGn Control Panel] Failed to mark unread messages as read. Response status:", data.status);
}
})
.catch(error => {
console.error("[GGn Control Panel] Error marking unread messages as read:", error);
});
}
function fetchConversation(convId) {
const url = `https://gazellegames.net/api.php?key=${API_KEY}&request=inbox&type=viewconv&id=${convId}`;
fetch(url)
.then((response) => response.json())
.then((data) => {
const messages = data.response.messages;
const dialog = createDialog(`Messages in ConvId: ${convId}`, true);
dialog.style.overflowY = "auto";
messages.forEach((msg) => {
const messageContainer = document.createElement("div");
messageContainer.style.borderBottom = "1px solid #444"; // Darker border for message container
messageContainer.style.marginBottom = "10px";
messageContainer.style.paddingBottom = "10px";
const sender = document.createElement("p");
sender.textContent = `Sender: ${msg.senderName}`;
sender.style.fontWeight = "bold";
const sentDate = document.createElement("p");
sentDate.textContent = `Date: ${msg.sentDate}`;
sentDate.style.color = "#bbb"; // Lighter gray for the date
const messageBody = document.createElement("p");
messageBody.innerHTML = msg.body; // Use innerHTML to parse HTML
messageBody.style.backgroundColor = "rgba(0, 105, 140, 0.3)";
messageBody.style.color = "#fff";
messageBody.style.padding = "10px";
messageBody.style.borderRadius = "5px";
messageBody.style.wordWrap = "break-word";
messageBody.style.marginTop = "10px";
messageContainer.appendChild(messageBody);
messageContainer.appendChild(sender);
messageContainer.appendChild(sentDate);
messageContainer.appendChild(messageBody);
dialog.appendChild(messageContainer);
});
document.body.appendChild(dialog);
})
.catch((error) => {
alert("Failed to fetch conversation messages: " + error);
});
}
async function validateInboxMessages(data, cacheKey, cacheExpiryKey){
console.log("[GGn Control Panel] Validating data:",data);
try{
if(!data.messages || !data.pages || !data.currentPage){
console.log("[GGn Control Panel] Data is invalid: ",data);;
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
// Function to fetch inbox messages with caching and expiry
async function fetchInboxMessages() {
const cacheKey = "getInboxMessagesData";
const cacheExpiryKey = "getInboxMessagesCacheExpiry";
const cacheExpiryDuration = 100000; // 100 Seconds
// Get cached data with expiry check
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=inbox`);
debugConsole(`[GGn Control Panel] fetchInboxMessages() API Response:`, response);
// Store new data in cache with expiry
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateInboxMessages(response.response, cacheKey, cacheExpiryKey);
return response.response; // Return fresh data
} catch (error) {
console.error("[GGn Control Panel] Error fetching inbox messages:", error);
return null; // Handle error if necessary
}
} else {
await validateInboxMessages(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response; // Return only the actual cached data
}
}
async function validateTorrents(data, cacheKey, cacheExpiryKey) {
console.log("[GGn Control Panel] Validating data:", data);
try {
// Instead of checking for data.status, check that data is an object
// and contains at least one key (torrent group)
if (typeof data !== "object" || data === null || Object.keys(data).length === 0) {
console.log("[GGn Control Panel] Data is invalid: ", data);
throw new Error("Invalid cached data structure: expected a non-empty object");
}
// Optionally, you can also validate that each torrent group has expected properties
// For example, check that each group has an "ID" property:
for (const key in data) {
if (data.hasOwnProperty(key)) {
const group = data[key];
if (!group.ID || !group.Name) {
console.log("[GGn Control Panel] Data is invalid for group:", key, group);
throw new Error(`Invalid data structure in group ${key}`);
}
}
}
} catch (e) {
console.error("[GGn Control Panel] Error parsing cached data:", e);
}
}
// Function to fetch torrents
async function fetchTorrents(page, sortBy = "seeders") {
try {
const url = `https://gazellegames.net/api.php?key=${API_KEY}&request=search&search_type=torrents&hide_dead=1&order_by=${sortBy}&order_way=asc&page=${page}`;
const response = await fetchWithRateLimit(url);
debugConsole("[GGn Control Panel] fetchTorrents() API Response:", response);
await validateTorrents(response.response);
return response.response;
} catch (error) {
console.error("[GGn Control Panel] Error fetching torrents:", error);
return null;
}
}
async function validateUserProfileData(data, cacheKey, cacheExpiryKey){
console.log("[GGn Control Panel] Validating data:",data);
try{
if(data.id == null || data.username == null || data.title == null || data.personal.class == null || data.community.hourlyGold == null || data.community.seeding == null || data.community.snatched == null || data.community.seedSize == null || data.buffs.TorrentsGold == null){
console.log(`[GGn Control Panel] Data is invalid:`,data);
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
async function fetchUserProfileData(userId) {
const cacheKey = `userProfileData_${userId}`;
const cacheExpiryKey = `userProfileCacheExpiry`;
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
// Fetch cached data if available
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
// Fetch data from the API
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=user&id=${userId}`);
// Validate and cache the response
await validateUserProfileData(response.response, cacheKey, cacheExpiryKey);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
return response.response;
} catch (error) {
console.error(`[GGn Control Panel] Error fetching user data for user ${userId}:`, error);
return null; // Handle the error as needed
}
} else {
// Validate the cached data before using it
await validateUserProfileData(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response; // Return cached data
}
}
async function validateUserData(data, cacheKey, cacheExpiryKey){
console.log("[GGn Control Panel] Validating data:",data);
try{
if(!data.id || !data.username || !data.userstats.class){
console.log(`[GGn Control Panel] Data is invalid: ${JSON.stringify(data)}`);
throw new Error("Invalid cached data structure");
}
}catch(e){
console.error("[GGn Control Panel] Error parsing cached data:", e);
localStorage.removeItem(cacheKey);
localStorage.removeItem(cacheExpiryKey);
console.info("[GGn Control Panel] Cache cleared!");
}
}
async function fetchUserData() {
const cacheKey = "quickUserData";
const cacheExpiryKey = "quickUserCacheExpiry";
const cacheExpiryDuration = 43200000000; // 12 000 hours or 500 days
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if (!cachedResponse || cachedResponse.isExpired || Object.keys(cachedResponse.response || {}).length === 0) {
try {
const response = await fetchWithRateLimit(`https://gazellegames.net/api.php?key=${API_KEY}&request=quick_user`);
debugConsole(`[GGn Control Panel] fetchUserData() API Response: `,response);
await setCacheData(response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
await validateUserData(response.response, cacheKey, cacheExpiryKey);
return response.response;
} catch (error) {
console.error("[GGn Control Panel] Error fetching user data:", error);
return null; // Or handle the error as needed
}
} else {
await validateUserData(cachedResponse.response.response, cacheKey, cacheExpiryKey);
return cachedResponse.response.response; // Return cached data
}
}
// Utility function to handle API requests and catch 429 errors
async function fetchWithRateLimit(url, options = {}) {
try {
const response = await fetch(url, options);
// Check if the response status is 429 (Too Many Requests)
if (response.status === 429) {
alert("Too many requests! Please tone down your actions to avoid further restrictions.");
console.warn("[GGn Control Panel] Rate limit exceeded. Please reduce the frequency of API requests.");
return null;
}
// Ensure the response object has a .json method
if (typeof response.json !== "function") {
console.error("[GGn Control Panel] Invalid response object:", response);
return null;
}
// Check the content type to determine whether to parse as JSON or text.
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
return await response.json();
} else if (contentType.includes("text/html") || contentType.includes("text/plain")) {
return await response.text();
} else {
console.warn("[GGn Control Panel] Unexpected response content type:", contentType);
return null;
}
} catch (error) {
console.error("[GGn Control Panel] Fetch error:", error);
return null;
}
}
async function createControlPanelMinified() {
const controlPanel = document.getElementById('control-panel');
controlPanel.style.display = "none";
// Create the minimized panel
const minifiedPanel = document.createElement("div");
minifiedPanel.id = "control-panel-minified";
document.body.appendChild(minifiedPanel);
// Apply styles for the minimized panel
minifiedPanel.style.position = "fixed";
minifiedPanel.style.bottom = "0px";
minifiedPanel.style.right = "0px";
minifiedPanel.style.backgroundColor = "#222";
minifiedPanel.style.color = "#fff";
minifiedPanel.style.padding = "10px 20px";
minifiedPanel.style.borderRadius = "10px";
minifiedPanel.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.5)";
minifiedPanel.style.width = "auto";
minifiedPanel.style.height = "auto";
minifiedPanel.style.cursor = "pointer";
minifiedPanel.style.display = "flex";
minifiedPanel.style.alignItems = "center";
minifiedPanel.style.justifyContent = "center";
minifiedPanel.style.fontSize = "14px";
minifiedPanel.style.zIndex = "10000";
// Add a label for the minimized panel
const label = document.createElement("span");
label.textContent = "GGn Control Panel";
label.style.color = "#fff";
minifiedPanel.appendChild(label);
// Add a click event to maximize the panel
minifiedPanel.addEventListener("click", async () => {
localStorage.setItem("panelMinimized", "false");
minifiedPanel.remove(); // Remove the minimized panel
controlPanel.style.display = "flex";
});
}
async function createControlPanel() {
// Remove an existing panel if it exists
const existingPanel = document.getElementById("control-panel");
if (existingPanel) existingPanel.remove();
let panel = null;
panel = document.createElement("div");
panel.id = "control-panel"; // Set the ID of the control panel
// Set the styles directly
panel.style.position = "fixed"; // Make sure it is fixed position for dragging
panel.style.bottom = "-10px";
panel.style.right = "-10px";
panel.style.width = "auto";
panel.style.height = "auto";
panel.style.backgroundColor = "#222";
panel.style.color = "#fff";
panel.style.padding = "20px";
panel.style.gap = "20px";
panel.style.borderRadius = "15px";
panel.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.5)";
panel.style.minWidth = "300px"; // Minimum width
panel.style.maxWidth = "420px"; // Maximum width
panel.style.display = "flex";
panel.style.flexDirection = "column";
panel.style.alignItems = "center";
panel.style.justifyContent = "space-evenly";
panel.style.zIndex = "10000";
const header = document.createElement("h2");
header.textContent = "GGn Control Panel";
header.style.textAlign = "center";
panel.appendChild(header);
// Add the Animaker© text below the icon
const animakerText = document.createElement("p");
animakerText.textContent = "Animaker©";
animakerText.style.position = "absolute";
animakerText.style.top = "60px"; // Adjust position as needed
animakerText.style.left = "20px";
animakerText.style.fontSize = "9px"; // Small text
animakerText.style.color = "gold"; // Golden color
panel.appendChild(animakerText);
// Create and add the badge icon in the top right (150x150 size)
const badgeIcon = document.createElement("img");
badgeIcon.alt = "Badge Icon";
badgeIcon.style.position = "absolute";
badgeIcon.style.top = "10px"; // Adjust the position as needed
badgeIcon.style.right = "10px"; // Position it at the top right
badgeIcon.style.width = "50px"; // Set the width to 150px
badgeIcon.style.height = "50px"; // Set the height to 150px
badgeIcon.style.objectFit = "cover"; // Optional: ensures the image doesn't stretch
panel.appendChild(badgeIcon);
// Fetch user data with caching
const userData = await fetchUserData();
let userId;
if (userData) {
const username = userData.username;
const userClass = userData.userstats.class;
userId = userData.id;
// Change the gazelle icon based on user class
changeIconBasedOnClass(userClass, badgeIcon);
// Create and display the personalized message with the username link
const message = document.createElement("p");
message.innerHTML = `${userClass} <a href="https://gazellegames.net/user.php?id=${userId}" target="_blank">${username}</a>!<br>How can I help you?`;
message.style.fontSize = "16px";
message.style.textAlign = "center";
panel.appendChild(message);
} else {
console.error("[GGn Control Panel] Failed to load user data.");
}
if(userId){
const userProfileData = await fetchUserProfileData(userId);
if (userProfileData) {
const statsContainer = createStatsContainer(userProfileData);
panel.appendChild(statsContainer);
}else {
console.error("[GGn Control Panel] Failed to load user profile data.");
}
}
const refreshUserDataIcon = createRefreshIcon("User", refreshUserProfileCache);
refreshUserDataIcon.style.position = "relative";
panel.appendChild(refreshUserDataIcon);
addButtons(panel);
addAuthorLink(panel);
addMinimizeButton(panel);
makePanelDraggable(panel);
document.body.appendChild(panel);
}
function addAuthorLink(panel){
// Create the link for the badge icon
const authorLink = document.createElement("a");
authorLink.href = `https://gazellegames.net/user.php?id=64361`; // Author (Animaker) link, in case you want to submit feedback or report a bug
authorLink.target = "_blank"; // Open the link in a new tab
// Create and add the gazelle icon image (added last)
const icon = document.createElement("img");
icon.src = "https://ptpimg.me/uf35wd.png"; // Your icon URL
icon.alt = "Gazelle Icon";
icon.style.position = "absolute";
icon.style.top = "20px";
icon.style.left = "20px";
icon.style.width = "40px"; /* or any percentage relative to container width */
icon.style.height = "auto"; /* Maintain aspect ratio */
// Set a fallback image in case of an error
icon.onerror = () => {
console.error("[GGn Control Panel] Failed to load Gazelle icon. Using fallback icon.");
icon.src = "https://ptpimg.me/uf35wd.png"; // Use your fallback image URL
};
authorLink.appendChild(icon);
panel.appendChild(authorLink);
}
function addMinimizeButton(panel){
// Add minimize button
const minimizeBtn = document.createElement("button");
minimizeBtn.title = "Minimize";
minimizeBtn.textContent = "➖";
minimizeBtn.style.position = "absolute";
minimizeBtn.style.alignItems = "center";
minimizeBtn.style.top = "5px";
minimizeBtn.style.background = "grey";
minimizeBtn.style.border = "none";
minimizeBtn.style.color = "#fff";
minimizeBtn.style.fontSize = "20px";
minimizeBtn.style.cursor = "pointer";
minimizeBtn.addEventListener("click", async () => {
localStorage.setItem("panelMinimized", "true");
await createControlPanelMinified();
});
panel.appendChild(minimizeBtn);
}
function createStatsContainer(userProfileData){
const uploadedGB = (userProfileData.stats.uploaded / (1024 ** 3)).toFixed(2);
const downloadedGB = (userProfileData.stats.downloaded / (1024 ** 3)).toFixed(2);
const goldPerHourPerGigabyte = (userProfileData.community.hourlyGold / (userProfileData.community.seedSize / (1024 ** 3))).toFixed(2);
const seedingPercentage = ((userProfileData.community.seeding / userProfileData.community.uniqueSnatched) * 100).toFixed(2);
const avgGPHPerTorrent = (userProfileData.community.hourlyGold / userProfileData.community.seeding).toFixed(2);
const dailyGoldPercentage = (( (24 * userProfileData.community.hourlyGold) / userProfileData.stats.gold) * 100).toFixed(2);
const gph = Number(userProfileData.community.hourlyGold/Number(userProfileData.buffs.TorrentsGold)).toFixed(2);
let gold = userProfileData.stats.gold;
// Convert the string to a number
const goldNumber = Number(gold);
// Check if the conversion was successful
if (!isNaN(goldNumber)) {
// Create a NumberFormat instance for the desired locale
const formatter = new Intl.NumberFormat('en-US', {
useGrouping: true,
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
const formattedGold = formatter.format(goldNumber);
gold = formattedGold.replace(/,/g, ' ');
} else {
console.error('[GGn Control Panel] Invalid number format in userProfileData.stats.gold');
}
// Display the uploaded and downloaded stats
const statsContainer = document.createElement("div");
statsContainer.style.display = "flex";
statsContainer.style.flexDirection = "column";
statsContainer.style.alignItems = "center";
statsContainer.style.marginBottom = "5px";
statsContainer.style.fontSize = "14px";
statsContainer.style.textAlign = "center";
const tab = ` `;
const formattedUpload = formatSize(uploadedGB);
const formattedDownload = formatSize(downloadedGB);
const uploadValue = `<span style="color: green;">⇧ ${formattedUpload}</span>`;
const downloadValue = `<span style="color: red;">⇩ ${formattedDownload}</span>`;
const gphPerGigabyteValue = `<span style="color: gold;">${goldPerHourPerGigabyte}</span>`;
const gphPerGigabyteLabel = `<span style="color: gold;">GPH per Gigabyte</span>`;
const goldValue = `<span style="color: gold; align-items: center;"><span>${gold}</span><img src="https://ptpimg.me/yctawt.png" alt="gold" style="margin-left: 8px; width: 20px; height: 20px;"></span>`;
const gphValue = `<span style="color: gold;">${gph}</span>`;
// Seeding % styling (color based on value)
let seedingColor = "green";
if (seedingPercentage < 40) seedingColor = "red";
else if (seedingPercentage < 60) seedingColor = "orange";
const seedingStyled = `<span style="color: ${seedingColor};">${seedingPercentage}%</span>`;
const avgGPHStyled = `<span style="color: gold;">${avgGPHPerTorrent}</span>`;
const dailyGoldStyled = `<span style="font-weight: bold; color: gold;">${dailyGoldPercentage}%</span>`;
// Display stats
statsContainer.innerHTML = `
<div>
<p>${uploadValue} ${tab} ${gphPerGigabyteValue} ${tab} ${downloadValue}<br>${gphPerGigabyteLabel}</p>
<p>${goldValue}</p>
<p>Unbuffed GPH: ${gphValue}<br>Seeding: ${seedingStyled} ${tab} GPH/Torrent: ${avgGPHStyled}<br>Every day your gold grows ${dailyGoldStyled}</p>
</div>
`;
return statsContainer;
}
// Function to add the buttons to the panel
function addButtons(panel) {
const buttons = [
{ text: "Best Torrents", action: getBestTorrents, refreshAction: refreshTorrentCache },
{ text: "Check Inbox", action: getInboxMessages, refreshAction: refreshInboxCache },
{ text: "Crafting Recipes", action: getCraftedRecipes, refreshAction: refreshCraftedRecipesCache },
{ text: "Crafting Simulator", action: getCraftingSimulator, refreshAction: refreshCraftingSimulatorCache },
{ text: "Oracle", action: youWish, refreshAction: null },
{ text: "Upload Manager", action: youWish, refreshAction: null },
{ text: "Shop", action: youWish, refreshAction: null },
{ text: "Your Collection", action: youWish, refreshAction: null },
];
const buttonContainer = createButtonContainer(buttons); // Creates button container
const buttonContainerToggle = createButtonContainerToggle(buttonContainer); // Creates toggle to show/hide button container
panel.appendChild(buttonContainerToggle);
panel.appendChild(buttonContainer);
}
// Function to add a simple toggle button to hide/show the button container.
function createButtonContainerToggle(buttonContainer) {
const toggleButton = document.createElement("button");
toggleButton.id = "toggle-button";
toggleButton.classList.add("custom-button"); // Apply the custom button class
toggleButton.textContent = "Hide Buttons"; // Initial state: buttons visible
// Determine the initial state from localStorage.
const hideButtonsStored = localStorage.getItem("hideButtons") === "true";
if (hideButtonsStored) {
buttonContainer.style.display = "none";
toggleButton.textContent = "Show Buttons";
toggleButton.style.backgroundColor = "#28a745"; // Green indicates "Show"
} else {
buttonContainer.style.display = "flex";
toggleButton.textContent = "Hide Buttons";
toggleButton.style.backgroundColor = "#dc3545"; // Red indicates "Hide"
}
toggleButton.addEventListener("click", function () {
if (!buttonContainer) return;
if (buttonContainer.style.display === "none") {
buttonContainer.style.display = "flex";
toggleButton.textContent = "Hide Buttons";
toggleButton.style.backgroundColor = "#dc3545";
localStorage.setItem("hideButtons", "false");
} else {
buttonContainer.style.display = "none";
toggleButton.textContent = "Show Buttons";
toggleButton.style.backgroundColor = "#28a745";
localStorage.setItem("hideButtons", "true");
}
});
return toggleButton;
}
function makePanelDraggable(panel) {
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
let dragTimeout;
let dragDelay = 500;
// Ensure the panel is fixed positioned and can be dragged.
panel.style.position = 'fixed';
// Function to handle the mousemove event
function onMouseMove(event) {
if (!isDragging) return;
if (isDragging) {
// Disable these styling preferences to avoid inconsistent behavior
panel.style.right = "";
panel.style.bottom = "";
// Calculate the new position while dragging
const newX = event.clientX - offsetX;
const newY = event.clientY - offsetY;
// Optional: Restrict movement to within the bounds of the window
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
// Ensure the panel stays within bounds
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
// Update panel position using right and bottom.
// We compute right and bottom based on the window dimensions.
const right = window.innerWidth - (boundedX + panel.offsetWidth);
const bottom = window.innerHeight - (boundedY + panel.offsetHeight);
panel.style.right = `${Math.max(0, right)}px`;
panel.style.bottom = `${Math.max(0, bottom)}px`;
}
}
// Function to handle the mouseup event
function onMouseUp() {
clearTimeout(dragTimeout);
if (isDragging) {
// Save position to localStorage
const panelRight = parseFloat(panel.style.right || "50");
const panelBottom = parseFloat(panel.style.bottom || "20");
localStorage.setItem("panelRight", panelRight);
localStorage.setItem("panelBottom", panelBottom);
}
isDragging = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
// Event listener for mousedown to start dragging
panel.addEventListener("mousedown", (event) => {
// Calculate the initial offset from the mouse pointer to the panel's top-left corner
offsetX = event.clientX - panel.getBoundingClientRect().left;
offsetY = event.clientY - panel.getBoundingClientRect().top;
// Set a timeout to start dragging after the specified delay
dragTimeout = setTimeout(() => {
isDragging = true;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}, dragDelay);
});
panel.addEventListener("mouseup", onMouseUp);
panel.addEventListener("mouseleave", onMouseUp);
}
function createButtonContainer(buttons) {
const buttonContainer = document.createElement("div");
buttonContainer.id = "button-container";
// Create the container for the forum filter buttons
const filterContainer = document.createElement('div');
filterContainer.id="filter-container";
buttonContainer.appendChild(filterContainer);
// Create the container for the forum filter buttons
const postContainer = document.createElement('div');
postContainer.id="post-container";
buttonContainer.appendChild(postContainer);
buttons.forEach(({ text, action, refreshAction }) => {
// Create the main button
const buttonWrapper = document.createElement("div");
buttonWrapper.style.position = "relative"; // Needed for positioning the refresh icon
const button = document.createElement("button");
button.textContent = text;
button.classList.add("custom-button"); // Apply the custom button class
button.addEventListener("click", action);
buttonWrapper.appendChild(button);
// Create the refresh icon
const refreshIcon = createRefreshIcon(text, refreshAction);
buttonWrapper.appendChild(refreshIcon);
buttonContainer.appendChild(buttonWrapper);
if (text.toLowerCase().includes("inbox")) {
checkInboxButton = button;
}
});
return buttonContainer;
}
function createRefreshIcon(text, refreshAction) {
const refreshIcon = document.createElement("div");
refreshIcon.textContent = "🔄";
refreshIcon.style.position = "absolute";
refreshIcon.style.top = "10px";
refreshIcon.style.right = "10px";
refreshIcon.style.fontSize = "14px";
refreshIcon.style.cursor = "pointer";
refreshIcon.style.color = "#007bff";
refreshIcon.title = `Refresh ${text} Data`;
// Event listener for click (refresh action)
refreshIcon.addEventListener("click", (event) => {
if (refreshAction) {
console.log(`[GGn Control Panel] Refreshing data for ${text}...`);
event.stopPropagation(); // Prevent triggering the main button
refreshAction();
}
});
// Hover effect
refreshIcon.addEventListener("mouseover", () => {
refreshIcon.style.color = "#0056b3";
});
refreshIcon.addEventListener("mouseout", () => {
refreshIcon.style.color = "#007bff";
});
return refreshIcon;
}
// Function to convert bytes to GB or TB
function formatSize(sizeInGB) {
if (sizeInGB >= 1024) {
return (sizeInGB / 1024).toFixed(2) + " TB"; // Convert GB to TB and format to 2 decimal places
}
return sizeInGB + " GB"; // Return in GB if it's less than 1024 GB
}
// Function to change the gazelle icon based on the user class
function changeIconBasedOnClass(userClass, icon) {
const icons = {
"Gaming God": "https://ptpimg.me/i5m52t.png",
"Master Gamer": "https://ptpimg.me/k0gkfm.png",
"Legendary Gamer": "https://ptpimg.me/sr5z38.png",
"Elite Gamer": "https://ptpimg.me/we2t80.png",
"Pro Gamer": "https://ptpimg.me/yyt9c2.png",
"Gamer": "https://ptpimg.me/nsc932.png",
};
if (icons[userClass]) {
icon.src = icons[userClass]; // Set the appropriate icon based on class
icon.title = userClass;
}
}
async function getBestTorrents(){
displayTorrentsData();
}
async function getInboxMessages() {
let data = await fetchInboxMessages();
const messages = data.messages;
await displayInboxMessages(messages);
const unreadMessages = messages.filter(message => message.unread);
debugConsole(`[GGn Control Panel] Unread Messages: ${JSON.stringify(unreadMessages)}`);
await markChecked(unreadMessages);
}
// Fetch and display crafted recipes
async function getCraftedRecipes() {
let data = await fetchCraftedRecipes();
const recipes = data || [];
if (recipes.length > 0) {
const allCraftRecipes = await fetchAllRecipesFrom(recipes.map(recipe => recipe.id));
displayCraftedRecipes(recipes, allCraftRecipes);
} else {
alert("No crafted recipes found.");
}
}
// Fetch and display a crafting recipe for a given recipeId
async function getCraftingRecipe(recipeId) {
if (!recipeId) {
console.error("[GGn Control Panel] Please enter a valid Recipe ID.");
return;
}
const cacheKey = "allCraftedRecipesData";
const cacheExpiryKey = "allCraftedRecipesCacheExpiry"; // To store the cache expiry time
const cacheExpiryDuration = 432000000; // 120 hours or 5 days
const cachedResponse = await getCachedData(cacheKey, cacheExpiryKey, cacheExpiryDuration);
if(cachedResponse.isExpired){
await setCacheData(cachedResponse.response, cacheKey, cacheExpiryKey, cacheExpiryDuration);
}
const recipe = cachedResponse.response.response.find(recipe => Number(recipe.id) === Number(recipeId));
if (recipe) {
displayCraftingRecipe(recipe);
} else {
console.error(`[GGn Control Panel] Recipe with ID ${recipeId} not found.`);
}
}
// Fetch and display the crafting simulator
async function getCraftingSimulator() {
let data = await fetchInventory();
const items = data || [];
// Filter items where amount > 8 (ensure amount is treated as a number)
const craftableItems = items.filter(item => Number(item.amount) > 8);
// Sort items by amount (descending)
craftableItems.sort((a, b) => Number(b.amount) - Number(a.amount));
displayCraftingSimulator(craftableItems);
debugConsole(`[GGn Control Panel] getCraftingSimulator with Sorted Items from Inventory:`, craftableItems);
}
async function checkForUnreadMessages() {
try {
console.log("[GGn Control Panel] Checking Unread Messages...");
// Fetch inbox messages
let inbox = await fetchInboxMessages();
// Check if the 'messages' field exists and is an array
if (inbox && Array.isArray(inbox.messages)) {
// Check for unread messages
const unreadMessages = inbox.messages.filter(message => message.unread);
if (unreadMessages.length > 0) {
if(globalDialog?.querySelector("h2")?.textContent === "Inbox")
{
await markChecked(unreadMessages);
console.log("[GGn Control Panel] Notification automatically read.");
}else{
if (checkInboxButton && !checkInboxButton.textContent.includes("🔔")) {
checkInboxButton.textContent += " 🔔";
}
console.log("CheckInbox", checkInboxButton);
console.log("[GGn Control Panel] Notification received.");
}
} else {
// Remove the bell emoji if there are no unread messages
if (checkInboxButton) {
checkInboxButton.textContent = "Check Inbox";
}
}
} else {
console.error("[GGn Control Panel] checkForUnreadMessages() ERROR - No messages or invalid messages format:", inbox);
}
} catch (error) {
console.error("[GGn Control Panel] ERROR - While fetching inbox messages:", error);
}
}
// Function to dynamically add CSS to the head of the document
function addButtonStyles() {
const style = document.createElement("style");
style.innerHTML = `
#button-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
}
.custom-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 20px;
font-size: 18px;
background-color: #444;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
}
.custom-button:hover {
background-color: #555;
}
`;
document.head.appendChild(style);
}
function addFilterStyles() {
const style = document.createElement('style');
style.innerHTML = `
#filter-container {
display: flex;
justify-content: space-evenly;
gap: 20px;
}
#filter-container:empty {
display: none;
}
#post-container {
display: flex;
justify-content: space-evenly;
gap: 20px;
}
#post-container:empty {
display: none;
}
.search-filter {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #444;
border-radius: 5px;
background-color: #333;
color: #fff;
box-sizing: border-box;
}
`;
document.head.appendChild(style);
}
async function init(){
//Initialize global variables
currentTorrentPage = await fetchPagination();
minimized = localStorage.getItem("panelMinimized") === "true";
// Initialize the control panel
await createControlPanel();
adjustControlPanelScale();
// Minimize it according to memory
if(minimized){
await createControlPanelMinified();
}
// Immediately check for unread messages
await checkForUnreadMessages();
// Start Cron-Jobs
setInterval(checkForUnreadMessages, 100000); // Happens every 100 Seconds
window.addEventListener("resize", adjustControlPanelScale);
}
// Call the function to add styles to the document (so other userscripts can use them to integrate with the control panel)
addButtonStyles();
addFilterStyles();
init();
})();