您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A script for listing unsolved nonograms of a category
// ==UserScript== // @name Nonogram tweaker // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description A script for listing unsolved nonograms of a category // @author myklosbotond // @match http://www.nonograms.org/ // @match http://www.nonograms.org/nonograms* // @match http://www.nonograms.org/nonograms2* // @exclude http://www.nonograms.org/nonograms/i/* // @exclude http://www.nonograms.org/nonograms2/i/* // @require https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== /* * jshint esversion: 6 * jshint esnext: true */ const DEVELOPEMENT = false; /* FIXME: fix loading: - implement a hasChanged(): compare last page contents with cahce for last page - always parse current page to update array - on load all, query categories separately and update them on return */ const STAT_KEY = "ng_stats"; const TIME_LIMIT_IN_DAYS = 2; (function () { 'use strict'; if (isFrontPage()) { runFrontPage(); } else { runNonogramList(); } })(); function runFrontPage() { setupFrontHtml(); const stats = getStatsFromStorage(); updateUi(stats); } function runNonogramList() { setupListHtml(); populateNonogramList(); } //color in just the curs / laboratory column on unselected function setupCss() { 'use strict'; //Insert the styles from the css: GM_addStyle(` .dashboard-item { border: 1px solid #b6b6b6; display: inline-block; padding: 5px; border-radius: 5px; min-width: 60px; text-align: center; } .dash-data { position: relative; } #dash-spinner { --size: 8px; --border: 3px; --inner-size: calc(var(--size) - 2 * var(--border)); --color: #6d844f; --spin-dur: 1.5s; display: inline-block; position: absolute; left: 0; width: var(--size); height: var(--size); border: solid; border-color: var(--color) var(--color) transparent var(--color); border-width: var(--border); border-radius: 50%; animation: spin var(--spin-dur) infinite linear; } #dash-spinner::after { content: ""; display: inline-block; position: absolute; top: 0; left: 0; width: var(--inner-size); height: var(--inner-size); border: solid; border-color: transparent var(--color) var(--color) var(--color); border-width: var(--border); border-radius: 50%; animation: spin calc(var(--spin-dur) / 2) infinite reverse linear; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .rel { position: relative; } #dashboard-header { margin-bottom: 10px; } #spinner-wrapper { position: relative; display: inline-block; width: 10px; height: 10px; } .faded { opacity: 0.5; filter: saturate(42%); } /* */ :root { --transition-s: .3s; } #unsolved-list-wrapper { position: absolute; top: 275px; right: 50px; } #unsolved-list { list-style-type: none; padding: 0; max-height: 500px; overflow: auto; } #unsolved-counter { position: relative; display: inline-block; text-align: center; height: 11px; min-width: 17px; } @media (max-width: 1200px) { #unsolved-list-wrapper { position: relative; top: 0; right: 0; max-width: 500px; } #unsolved-list { max-height: 210px; margin-bottom: 14px; border-bottom: 1px solid; } } #unsolved-list li { text-align: center; border-bottom: 1px solid #eeeeee; } #unsolved-list li a { padding: 3px; display: block; transition-duration: var(--transition-s); } #unsolved-list li a:hover { background: #f4f4f4; } #unsolved-list li .page-link { padding: 6px 0; } li.page-link-li { border-bottom: 1px solid #9e9e9e !important; } a.page-link { color: #04259a; font-weight: bold; } #unsolved-list li:not(:last-child) { border-bottom: 1px solid #eeeeee; } .begun { position: relative; } .begun::after { content: ""; display: inline-block; --size: 5px; width: var(--size); height: var(--size); background-color: #efc447; border-radius: 50%; position: absolute; top: 7px; margin-left: 4px; border: 1px solid #c4891d; transition-duration: var(--transition-s); } .begun:hover::after { background-color: #fad059; border-color: #dbc34e; } `); } if (!Array.prototype.last) { Array.prototype.last = function () { return this[this.length - 1]; }; }; function toHex(num) { let hexString = num.toString(16); if (hexString.length % 2) { hexString = '0' + hexString; } return hexString; } /** * * @param {*} url * @returns { {type: string, size: string} } * type and size information from url * or `null` if url is not of correct format */ function dataFromUrl(url) { const urlRegex = /(nonograms2?)\/size\/([a-z]*)(?:.*)?/; const matched = url.match(urlRegex); if (!matched) { return null; } return { type: matched[1], size: matched[2] }; } function isFrontPage() { return window.location.pathname === "/"; } function getCurrentBase() { 'use strict'; const regex = /\/p\/[0-9]+$/; return window.location.href.replace(regex, ''); } function queryDatePassedLimit(date) { if (!date) { return true; } const ONE_DAY = 1000 * 60 * 60 * 24; const diff = (new Date() - new Date(date)) / ONE_DAY; return diff > TIME_LIMIT_IN_DAYS; } async function refreshStats() { startPending(); const list = [...$(".dashboard-item")]; const listDetails = await Promise.all(list.map(toDetailed)); const detailsWithList = await fetchPagesForAll(listDetails); const finalDetails = await Promise.all(detailsWithList); saveStats(finalDetails); stopPending(); } async function fetchPagesForAll(listDetails) { return listDetails.map(async details => await fetchPagesFor(details)); } async function getFirstPageHtml(url) { const resp = await fetch(url); return await resp.text(); } async function fetchAll(details) { return [...Array(details.lastPage).keys()] .map(async i => ({ html: await fetch(`${details.href}/p/${i + 1}`) .then(resp => resp.text()), page: i + 1 })); } async function toDetailed(item) { const $item = $(item); const href = $item.find("a").attr("href"); const firstPageHtml = await getFirstPageHtml(href); const $html = $(firstPageHtml); const lastPage = getLastPage($html); const urldata = dataFromUrl(href); return { href, lastPage, type: urldata.type, size: urldata.size }; } function extractList(pageData) { const $page = $(pageData.html); return [...$page.find(".nonogram_title")] .map(a => a.id) .map(id => parseInt(id.replace(/[^0-9]/g, ""), 10)) .map(id => ({ page: pageData.page, id })); } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ function populateNonogramList() { const stats = getStatsFromStorage(); const data = dataFromUrl(location.href); const pageStats = stats[data.type][data.size]; if (queryDatePassedLimit(pageStats.queried)) { startPageSpin(); fetchCategoryDetails(data) .then(newPageStats => { saveSingleStat(newPageStats, data); const stats1 = getStatsFromStorage(); const pageStats1 = stats1[data.type][data.size]; refreshListUi(pageStats1, data); }); } else { const currentPage = getCurrentPageList(); const newIds = mergeIdList(pageStats.ids, currentPage); saveSingleStat_page(newIds, data); const stats1 = getStatsFromStorage(); const pageStats1 = stats1[data.type][data.size]; console.log("from cache"); refreshListUi(pageStats1, data); } } async function fetchCategoryDetails(data) { const detail = { href: location.href, lastPage: getLastPage(), type: data.type, size: data.size }; const detailPromise = await fetchPagesFor(detail); return await detailPromise; } async function fetchPagesFor(details) { const promises = await fetchAll(details); const pages = await Promise.all(promises); const puzzleList = pages .map(extractList) .reduce((acc, cur) => [...acc, ...cur], []); return { puzzleList, ...details }; } function getLastPage($dom) { 'use strict'; return getPagingData($dom).total; // const navWrapper = $dom.find('.pager div'); // const navLinks = navWrapper.children(); // const lastLink = navLinks.last(); // const lastText = lastLink.text(); // const nextRegex = /Next/; // if (nextRegex.test(lastText)) { // const lastNumber = navLinks.eq(-2); // const lastNumText = lastNumber.text(); // if (lastNumText == '...') { // const sPageNum = lastNumber.attr('href').match(/[0-9]+$/)[0]; // return parseInt(sPageNum, 10); // } // else { // return parseInt(lastNumText, 10); // } // } else { // const sPageNum = lastText.replace(/[\[\]]/g, ''); // return parseInt(sPageNum, 10); // } } function getPagingData($dom) { 'use strict'; if (!$dom) { $dom = $("body"); } const navSpan = $dom.find('.pager > span').eq(0); const navText = navSpan.text(); const match = navText.match(/[^0-9]*([0-9]+)[^0-9]*([0-9]+)/); return { current: parseInt(match[1], 10), total: parseInt(match[2], 10) } } function getCurrentPageList() { 'use strict'; const pageData = { html: $('body').html(), page: getPagingData().current } return extractList(pageData); } function mergeIdList(original, newData) { const ids = newData.map(data => data.id); const pruned = original.filter(idEntry => !ids.includes(idEntry.id)); const curPage = newData[0].page; let index = 0; while (index < pruned.length && pruned[index].page < curPage) { ++index; } pruned.splice(index, 0, ...newData); return pruned; } function refreshStatsAndUi() { refreshStats() .then(() => { updateUi(getStatsFromStorage()); }); } function getStatsFromStorage() { const base = getBaseStats(); const stored = _getStatsFromStorage(); if (!stored.v || stored.v != base.v) { return base; } return { ...base, ...stored }; } function _getStatsFromStorage() { try { const savedStats = GM_getValue(STAT_KEY, "{}"); return JSON.parse(savedStats); } catch (err) { return {}; } } function getBaseStats() { return { v: 1, nonograms: { small: { queried: null, ids: [] }, medium: { queried: null, ids: [] }, large: { queried: null, ids: [] } }, nonograms2: { small: { queried: null, ids: [] }, medium: { queried: null, ids: [] }, large: { queried: null, ids: [] } } }; } function saveStats(details) { const stats = getBaseStats(); const time = new Date(); details.forEach(stat => { stats[stat.type][stat.size] = setDataForCategory(time, stat.puzzleList); }); GM_setValue(STAT_KEY, JSON.stringify(stats)); } function saveSingleStat(newPageStats, data) { const stats = getStatsFromStorage(); stats[data.type][data.size] = setDataForCategory(new Date(), newPageStats.puzzleList) GM_setValue(STAT_KEY, JSON.stringify(stats)); } function saveSingleStat_page(modifiedIds, data) { const stats = getStatsFromStorage(); stats[data.type][data.size].ids = modifiedIds; GM_setValue(STAT_KEY, JSON.stringify(stats)); } function setDataForCategory(queried, ids) { return { queried: queried, ids: ids }; } function clearStats() { GM_setValue(STAT_KEY, "{}"); } function setupFrontHtml() { const pages = [...$(".menu .menu_sub2")]; const pageData = pages.map(li => { const $li = $(li); const $a = $li.find("a"); const url = new URL($a.attr("href")); const data = dataFromUrl(url.href); return { href: url.href, path: url.pathname, title: $a.text().replace(/[^a-zA-Z]/g, ""), ...data } }); setupCss(); const html = ` <div id="dashboard-wrapper"> <div id="dashboard-header"> <span> Last updated: <span id="dashboard-updated"/> </span> <button id="dash-refresh">Refresh</button> <span id="spinner-wrapper"> <span id="dash-spinner" /> </span> </div> <div id="dashboard-items"> ${pageData.map(page => ` <div class="dashboard-item" id="item-${page.type}-${page.size}"> <a href="${page.href}">${page.title}</a> <div class="dash-data"> <span>Solved: </span> <br/> <span class="dash-counter">?</span> </div> </div> `).join("")} </div> </div> `; $(html).insertAfter(".content h1"); $("#dash-spinner").hide(); $("body").on("click", "#dash-refresh", () => refreshStatsAndUi()); } function updateUi(stats) { const queried = stats.queried ? new Date(stats.queried) : null; $("#dashboard-updated") .text(queried ? `${queried.toLocaleDateString()} - ${queried.toLocaleTimeString()}` : "never"); const types = ["nonograms", "nonograms2"]; for (let type of types) { const sizes = stats[type]; for (let size in sizes) { const currentSize = sizes[size]; if (!currentSize.queried) { continue; } const list = currentSize.ids.map(item => item.id); const $item = $(`#item-${type}-${size}`); const solved = list.filter(id => usrsvl.includes(id)); const solvedNo = solved.length; const totalNo = list.length; const colorModifier = Math.floor(255 * solvedNo / totalNo); const color = `#${toHex(255 - colorModifier)}${toHex(colorModifier)}00`; $item .find(".dash-counter") .html(`<span style="color: ${color}; font-weight:bold;">${solvedNo}</span>/${totalNo}`); } } } function startPending() { $("#dash-spinner").show(); $("#dash-refresh").prop("disabled", true); $("#dashboard-items").addClass("faded"); } function stopPending() { $("#dash-spinner").hide(); $("#dash-refresh").prop("disabled", false); $("#dashboard-items").removeClass("faded"); } function setupListHtml() { setupCss(); const html = ` <div id="unsolved-list-wrapper"> <section id="unsolved-header"> <h2>List of unsolved puzzles (<span id="unsolved-counter">0</span>):</h2> </section> <ul id="unsolved-list"> </ul> </div> `; $('.content').prepend(html); } function refreshListUi(details, data) { const unsolved = details.ids .filter(idObj => !usrsvl.includes(idObj.id)); const listHtml = unsolved .map(idObj => ({ url: `/${data.type}/i/${idObj.id}`, page: idObj.page })) .map(item => toLiFromUrl(item.url, item.page)); const $list = $('#unsolved-list'); $list.html(listHtml); $("#unsolved-counter").text(unsolved.length); addPageEntries(data); } function toLiFromUrl(url, pageNo) { const sId = url.match(/[0-9]+$/)[0]; const begun = usrbgl.includes(parseInt(sId, 10)); const clazz = begun ? 'begun' : ''; return `<li data-page="${pageNo}"> <a href="${url}" class="${clazz}">#${sId}</a> </li>`; } function addPageEntries(data) { const $list = $('#unsolved-list'); const items = [...$list.find("li")]; let processedPages = []; items.forEach(item => { const $item = $(item); const page = $item.attr("data-page"); if (!processedPages.includes(page)) { processedPages.push(page); pageLi(data, page).insertBefore($item); } }); } function pageLi(data, pageNo) { const pageUrl = `/${data.type}/size/${data.size}/p/${pageNo}`; return $(`<li class="page-link-li" data-page="${pageNo}"> <a href="${pageUrl}" class="page-link">Page ${pageNo}:</a> </li>`); } function startPageSpin() { const counter = $("#unsolved-counter"); counter.html('<span id="dash-spinner" />'); counter.find("#dash-spinner").show(); }