您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Keeps track of games rewards history (Community sharing can be disabled in the settings)
// ==UserScript== // @name Grundo's Cafe - Games Journal // @namespace https://www.grundos.cafe/ // @version 0.9 // @description Keeps track of games rewards history (Community sharing can be disabled in the settings) // @author yon // @match *://*.grundos.cafe/games* // @icon https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @require https://unpkg.com/jquery/dist/jquery.min.js // @require https://unpkg.com/gridjs/dist/gridjs.umd.js // ==/UserScript== var version = 0.9; (async function() { 'use strict'; try { if (document.URL.includes('grundos.cafe/games/#journal')) { await showJournal(); return; } if (document.URL.endsWith('grundos.cafe/games/')) { showJournalLink(); } else if (document.URL.includes('grundos.cafe/games/html5/')) { showJournalLink(); await displayPastRewardsIfNeeded(); await interceptAndLogReward(); } } catch (error) { showError(); throw error; } })(); async function interceptAndLogReward() { var originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { try { if (method === 'POST' && url.endsWith('/games/process/')) { this.addEventListener('load', async function() { // Example: "Success! You get a x2 NP \nFeatured Game Bonus!\nYou've also been awarded\na Secret Laboratory Map 3!" let responseText = this.responseText.replaceAll('\n', ' '); if (responseText.includes("You've also been awarded")) { let reward = responseText; let match = responseText.match(/You've also been awarded a(?:n)? (.+?)(?=\!)/i); if (match) { reward = match[1]; } else { match = responseText.match(/You've also been awarded (.+?)(?=\!)/i); if (match) { reward = match[1]; } } let isFeaturedGame = (responseText.includes('Featured Game') || $('main > div > strong:contains(featured today)').length == 1) ? 'TRUE' : 'FALSE'; let game = $('h1[id="game-header"]')[0]?.textContent; await logReward(game, reward, isFeaturedGame); } }); } } catch (error) { showError(); } originalOpen.apply(this, arguments); }; } async function logReward(game, reward, isFeaturedGame) { let newReward = {'date': getDateString(), 'game': game, 'url': document.URL, 'reward': reward, 'is_featured_game': isFeaturedGame}; console.log(newReward); let gamesData = await GM.getValue('gc_games_rewards', {'version': version}); if (gamesData['game'] === undefined) { gamesData['game'] = []; } gamesData['game'].push(newReward); await GM.setValue('gc_games_rewards', gamesData); await sendRewardIfNeeded(newReward); } function getDateString() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); const second = String(now.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`; } async function sendRewardIfNeeded(newReward) { let settings = await GM.getValue('gc_games_settings', getDefaultGamesSettings()); let setting = settings["share_rewards"]; if (setting === "public" || setting === "private") { let username = $('div[id="userinfo"] a[href^="/userlookup/?user="]')[0].href.split('/?user=')[1]; let privacy = setting; let formId = '1FAIpQLSfS9W682NADVesEGVN_VO0ZBjgE7PRBPUDj3Qpmnu9sZjVWzA'; const query = { "1350867233": version, "1534307698": newReward['date'], "1053758418": newReward['game'], "153236007": newReward['url'], "1007726898": newReward['reward'], "1344427827": newReward['is_featured_game'], "1550065838": username, "815139160": privacy, }; let formLink = `https://docs.google.com/forms/d/e/${formId}/formResponse?usp=pp_url`; for (const [key, value] of Object.entries(query)) { formLink += `&entry.${key}=${value}`; } let opts = { mode: 'no-cors', referrer: 'no-referrer', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } let response = fetch(formLink, opts) .then(response => { console.log("Game data submitted"); }) .catch(error => { console.log("Error:", error); console.log(response) }); } } async function displayPastRewardsIfNeeded() { let settings = await GM.getValue('gc_games_settings', getDefaultGamesSettings()); let setting = settings["display_rewards"]; if (setting !== "yes") { return; } let game = $('h1[id="game-header"]')[0]?.textContent; if (!game) { return; } let gamesData = await GM.getValue('gc_games_rewards', {'version': version}); let gameData = gamesData[game]; if (!gameData) { return; } await displayPastRewardsFromGameData(gameData); } async function displayPastRewardsFromGameData(gameData) { const prizesElement = document.createElement('div'); prizesElement.className = 'prizes'; const prizesTextElement = document.createElement('p'); prizesTextElement.className = 'prizes-text center'; prizesTextElement.textContent = 'Here are all the rewards you have got so far:'; prizesElement.appendChild(prizesTextElement); const prizesListElement = document.createElement('div'); prizesListElement.className = 'itemList'; let settings = await GM.getValue('gc_games_settings', getDefaultGamesSettings()); let setting = settings["rewards_sort_order"]; if (setting === "desc") { gameData = gameData.reverse(); } setting = settings["aggregate_rewards"]; if (setting === "yes") { let aggregatedCounts = {}; let gameDataFiltered = []; for (let rowData of gameData) { let prizeName = rowData['reward']; if (prizeName in aggregatedCounts) { aggregatedCounts[prizeName] += 1; } else { aggregatedCounts[prizeName] = 1; gameDataFiltered.push(rowData); } } for (let rowData of gameDataFiltered) { let prizeName = rowData['reward']; rowData['count'] = aggregatedCounts[prizeName]; } gameData = gameDataFiltered; } gameData.forEach(rowData => { if (!rowData['reward']) { return; } let prizeName = rowData['reward']; let count = rowData['count'] ?? 0; // generated from aggregate_rewards setting, 0 means 1 but the count won't show let shownPrizeName = prizeName; if (count > 0) { shownPrizeName += ' x' + count; } const invItem = document.createElement('div'); invItem.className = 'shop-item'; /* TODO? const img = document.createElement('img'); img.className = 'med-image border-1'; img.src = prizeImage;*/ const itemDiv = document.createElement('div'); itemDiv.className = 'item-info'; itemDiv.innerHTML = `<span>${shownPrizeName}</span>`; const linksDiv = document.createElement('div'); linksDiv.id = prizeName + '-links'; linksDiv.className = 'searchhelp'; linksDiv.setAttribute('style', `display: flex; justify-content: center; align-items: center; gap: 4px;`); const formattedName = prizeName.replaceAll(' ', '%20'); const swLink = document.createElement('a'); swLink.href = `/market/wizard/?query=${formattedName}`; swLink.target = '_blank'; const swImg = document.createElement('img'); swImg.src = 'https://grundoscafe.b-cdn.net/misc/wiz.png'; swLink.appendChild(swImg); linksDiv.appendChild(swLink); const sdbLink = document.createElement('a'); sdbLink.href = `/safetydeposit/?page=1&query=${formattedName}&exact=1`; sdbLink.target = '_blank'; const sdbImg = document.createElement('img'); sdbImg.src = 'https://grundoscafe.b-cdn.net/misc/sdb.gif'; sdbLink.appendChild(sdbImg); linksDiv.appendChild(sdbLink); const tpLink = document.createElement('a'); tpLink.href = `/island/tradingpost/browse/?query=${formattedName}`; tpLink.target = '_blank'; const tpImg = document.createElement('img'); tpImg.src = 'https://grundoscafe.b-cdn.net/misc/tp.png'; tpLink.appendChild(tpImg); linksDiv.appendChild(tpLink); const wlLink = document.createElement('a'); wlLink.href = `/wishlist/search/?query=${formattedName}`; wlLink.target = '_blank'; const wlImg = document.createElement('img'); wlImg.src = 'https://grundoscafe.b-cdn.net/misc/wish_icon.png'; wlLink.appendChild(wlImg); linksDiv.appendChild(wlLink); //invItem.appendChild(img); invItem.appendChild(itemDiv); invItem.appendChild(linksDiv); prizesListElement.appendChild(invItem); }); prizesElement.appendChild(prizesListElement); $('div[id="page_content"]').append(prizesElement); return prizesElement; } function showJournalLink() { let pageContents = $('div[id="page_content"]'); if (pageContents.length > 0) { const htmlString = ` <div id="games_journal_header" style="display: flex; justify-content: flex-end; margin-bottom: 12px;"> <div id="games_journal_redirection"> <a href="https://www.grundos.cafe/games/#journal" target="_blank">Go to Journal</a> </div> </div>`; pageContents[0].insertAdjacentHTML("beforeend", htmlString); } } async function showJournal() { let journalElement = getJournalElement(); replacePageWithElement(journalElement); await addExportButtonListener(journalElement); await addResetButtonListener(journalElement); await addSettingsButtonListener(journalElement); await loadGrid(); } function getJournalElement() { let journalElement = document.createElement('div'); let html = ` <center> <div id="games_journal"> <h1>Games Journal</h1> <button id="games_export">Export</button> <button id="games_reset">Reset</button> <div id="games_history"></div> <div id="games_settings"></div> </div> </center>`; journalElement.innerHTML = html; let cssLink = document.createElement("link"); cssLink.rel = "stylesheet"; cssLink.href = "https://unpkg.com/gridjs/dist/theme/mermaid.min.css"; journalElement.appendChild(cssLink); var styleElement = document.createElement('style'); styleElement.textContent = ` #games_journal { h1 { margin-bottom: 24px; } #games_export { font-size: 16px; padding: 8px 16px; } #games_reset { font-size: 16px; padding: 8px 16px; } #games_history { margin: 24px; } #games_settings { margin: 24px; } .gridjs-search { float: initial; width: "100%" } .gridjs-search-input { width: 100% } }`; journalElement.appendChild(styleElement); return journalElement; } async function loadGrid() { let rowsPerPage = 15; let rows = await getJournalHistoryRows(); // start loading grid data let grid = new gridjs.Grid({ columns: getJournalHistoryColumns(), data: rows, pagination: { limit: rowsPerPage, summary: true }, resizable: true, search: { debounceTimeout: 0 }, sort: { multiColumn: true }, autoWidth: true }).render(document.getElementById("games_history")); // show loading message let loadingElement = showLoadingMessage(); // wait for grid to finish loading let expectedRows = Math.min(rows.length, rowsPerPage); await waitForGridCompleteLoad(expectedRows); // hide loading message hideLoadingMessage(loadingElement); fixGridJsTable(); } function getJournalHistoryColumns() { return ["Date", "Game", "Reward", "Is Featured?"]; } async function getJournalHistoryRows() { let gamesData = await GM.getValue('gc_games_rewards', {'version': version}); let tableData = []; let games = Object.keys(gamesData).filter(name => gamesData.hasOwnProperty(name) && name !== 'version').sort(); for (const game of games) { let gameData = gamesData[game]; for (let i in gameData) { let rowData = gameData[i]; let date = rowData['date']; let gameName = rowData['game']; let reward = rowData['reward'] ?? ''; let isFeaturedGame = rowData['is_featured_game']; tableData.push([date, gameName, reward, isFeaturedGame]); } } return tableData; } function showLoadingMessage() { let loadingElement = document.createElement('div'); loadingElement.innerHTML = ` <h2>Loading...</h2> <div class="loader"></div> `; var styleElement = document.createElement('style'); styleElement.textContent = ` h2 { color: #3498db; /* Blue */ } .loader { border: 16px solid #f3f3f3; /* Light grey */ border-top: 16px solid #3498db; /* Blue */ border-radius: 50%; width: 120px; height: 120px; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; loadingElement.appendChild(styleElement); $('div[class="gridjs-head"]')[0].parentNode.appendChild(loadingElement); return loadingElement; } function hideLoadingMessage(loadingElement) { loadingElement.style.display = "none"; } async function waitForGridCompleteLoad(expectedRows) { let done = false; while (!done) { // wait 100 ms await new Promise(r => setTimeout(r, 100)); // check let currentRows = $('table[role="grid"] > tbody > tr'); if (currentRows && currentRows.length === expectedRows) { done = true; } } } function fixGridJsTable() { // issue: the table does not load fully before interacting with it // small hack: interacting by sorting by date descending // todo: I need to either change library or find a better fix // sort asc $('table[role="grid"] > thead > tr > th[data-column-id="date"] > button')[0].click(); // sort desc $('table[role="grid"] > thead > tr > th[data-column-id="date"] > button')[0].click(); } function replacePageWithElement(element) { let page = document.querySelector('html'); page.parentNode.replaceChild(element, page); } async function addExportButtonListener(journalElement) { let gamesData = await GM.getValue('gc_games_rewards', {'version': version}); let exportElement = $('button[id="games_export"]'); exportElement.click(exportFunction); function exportFunction() { const filename = `${getDateString()}_games_journal.json`; const jsonString = JSON.stringify(gamesData, null, 2); // The third argument is for indentation const blob = new Blob([jsonString], { type: 'application/json' }); const downloadLink = document.createElement('a'); downloadLink.href = window.URL.createObjectURL(blob); downloadLink.download = filename; journalElement.appendChild(downloadLink); downloadLink.click(); journalElement.removeChild(downloadLink); }; } async function addResetButtonListener(journalElement) { let resetElement = $('button[id="games_reset"]'); resetElement.click(resetFunction); async function resetFunction() { if (confirm("Do you really want to reset your Games Journal? This action cannot be undone (but you can export it first).") == true) { await GM.deleteValue('gc_games_rewards'); } }; } async function addSettingsButtonListener(journalElement) { let settingsElement = $('div[id="games_settings"]')[0]; let html = ` <details> <summary>Settings</summary> <div id="games_settings_parameters"></div> </details>`; settingsElement.innerHTML = html; var styleElement = document.createElement('style'); styleElement.textContent = ` summary { font-weight: bold; font-size: 28px; } label { font-size: 24px; } select { font-size: 20px; margin-left: 8px; } #games_settings_save { font-size: 16px; padding: 8px 16px; }`; settingsElement.appendChild(styleElement); let settingsDetailsElement = $('div[id="games_settings_parameters"]')[0]; let defaultSettings = getDefaultGamesSettings(); let currentSettings = await GM.getValue('gc_games_settings', defaultSettings); let parameters = { "share_rewards": { "label": "ε(´。•᎑•`)っ 💕 Share your future prize rewards with the community 💕", "possibleValues": { "public": "Yes", "private": "Yes but keep my name hidden (only visible to admin)", "no": "No" }, "defaultValue": defaultSettings["share_rewards"] }, "display_rewards": { "label": "Display rewards below games", "possibleValues": { "yes": "Yes", "no": "No" }, "defaultValue": defaultSettings["display_rewards"] }, "rewards_sort_order": { "label": "Display rewards in the following order", "possibleValues": { "desc": "Latest first", "asc": "Oldest first" }, "defaultValue": defaultSettings["rewards_sort_order"] }, "aggregate_rewards": { "label": "Aggregate game rewards per item", "possibleValues": { "yes": "Yes", "no": "No" }, "defaultValue": defaultSettings["aggregate_rewards"] }, }; for (const [parameter, values] of Object.entries(parameters)) { const label = document.createElement("label"); label.setAttribute("for", parameter); label.textContent = values["label"] + ":"; const select = document.createElement("select"); select.id = parameter; let parameterCurrentSetting = currentSettings[parameter] ?? values["defaultValue"]; for (const [value, label] of Object.entries(values["possibleValues"])) { const option = document.createElement("option"); option.value = value; option.textContent = label; if (value == parameterCurrentSetting) { option.selected = true; } select.appendChild(option); } const paragraph = document.createElement("p"); paragraph.appendChild(label); paragraph.appendChild(select); settingsDetailsElement.appendChild(paragraph); } var saveSettingsElement = document.createElement('button'); saveSettingsElement.id = 'games_settings_save'; saveSettingsElement.textContent = 'Save settings'; saveSettingsElement.addEventListener('click', saveSettingsFunction); async function saveSettingsFunction() { let newSettings = {}; for (const [parameter, values] of Object.entries(parameters)) { let selectElement = document.getElementById(parameter); newSettings[parameter] = selectElement.value; } await GM.setValue('gc_games_settings', newSettings); console.log('New settings:', newSettings); alert('Settings have been saved.') }; settingsDetailsElement.appendChild(saveSettingsElement); } function getDefaultGamesSettings() { let defaultSettings = { "share_rewards": "private", "display_rewards": "yes", "rewards_sort_order": "desc", "aggregate_rewards": "yes" }; return defaultSettings; } function showError() { let errorMessage = ` <div class="error-message"> <p>Oops! Something went wrong with the Games Journal script.</p> <p>Please check you have the latest option from <a href="https://greasyfork.org/en/scripts/488478-grundo-s-cafe-games-journal">here</a></p> <p>If it still does not work, please keep the tab open (or save the HTML) and contact Yon#epyslone, the script probably needs an update.</p> </div> `; $('div[id="page_content"]').prepend(errorMessage); }