您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Uses the kudos/hits ratio, number of chapters, and statistical evaluation to score and sort AO3 works.
- // ==UserScript==
- // @name AO3: Quality score (Adjusted Kudos/Hits ratio)
- // @description Uses the kudos/hits ratio, number of chapters, and statistical evaluation to score and sort AO3 works.
- // @namespace https://greasyfork.org/scripts/3144-ao3-kudos-hits-ratio
- // @author cupkax
- // @version 2.2
- // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
- // @include http://archiveofourown.org/*
- // @include https://archiveofourown.org/*
- // @license MIT
- // ==/UserScript==
- // Configuration object: centralizes all settings for easier management
- const CONFIG = {
- alwaysCount: true, // count kudos/hits automatically
- alwaysSort: false, // sort works on this page by kudos/hits ratio automatically
- hideHitcount: true, // hide hitcount
- colourBackground: true, // colour background depending on percentage
- thresholds: {
- low: 4, // percentage level separating red and yellow background
- high: 7 // percentage level separating yellow and green background
- },
- colors: {
- red: '#8b0000', // background color for low scores
- yellow: '#994d00', // background color for medium scores
- green: '#006400' // background color for high scores
- }
- };
- // Main function: wraps all code to avoid polluting global scope
- (($) => {
- 'use strict'; // Enables strict mode to catch common coding errors
- // Variables to track the state of the page
- let countable = false; // true if kudos/hits can be counted on this page
- let sortable = false; // true if works can be sorted on this page
- let statsPage = false; // true if this is a statistics page
- // Load user settings from localStorage
- const loadUserSettings = () => {
- if (typeof Storage !== 'undefined') {
- CONFIG.alwaysCount = localStorage.getItem('alwaysCountLocal') !== 'no';
- CONFIG.alwaysSort = localStorage.getItem('alwaysSortLocal') === 'yes';
- CONFIG.hideHitcount = localStorage.getItem('hideHitcountLocal') !== 'no';
- }
- };
- // Check if it's a list of works or bookmarks, or header on work page
- const checkCountable = () => {
- const foundStats = $('dl.stats');
- if (foundStats.length) {
- if (foundStats.closest('li').is('.work') || foundStats.closest('li').is('.bookmark')) {
- countable = sortable = true;
- addRatioMenu();
- } else if (foundStats.parents('.statistics').length) {
- countable = sortable = statsPage = true;
- addRatioMenu();
- } else if (foundStats.parents('dl.work').length) {
- countable = true;
- addRatioMenu();
- }
- }
- };
- // Count the kudos/hits ratio for each work
- const countRatio = () => {
- if (!countable) return;
- $('dl.stats').each(function () {
- const $this = $(this);
- const $hitsValue = $this.find('dd.hits');
- const $kudosValue = $this.find('dd.kudos');
- const $chaptersValue = $this.find('dd.chapters');
- // Improved error handling
- try {
- const chaptersString = $chaptersValue.text().split("/")[0];
- if (!$hitsValue.length || !$kudosValue.length || !chaptersString) {
- throw new Error("Missing required statistics");
- }
- const hitsCount = parseInt($hitsValue.text().replace(/,/g, ''));
- const kudosCount = parseInt($kudosValue.text().replace(/,/g, ''));
- const chaptersCount = parseInt(chaptersString);
- if (isNaN(hitsCount) || isNaN(kudosCount) || isNaN(chaptersCount)) {
- throw new Error("Invalid numeric values");
- }
- const newHitsCount = hitsCount / Math.sqrt(chaptersCount);
- let percents = 100 * kudosCount / newHitsCount;
- if (kudosCount < 11) {
- percents = 1;
- }
- const pValue = getPValue(newHitsCount, kudosCount, chaptersCount);
- if (pValue < 0.05) {
- percents = 1;
- }
- const percents_print = percents.toFixed(1).replace(',', '.');
- // Add ratio stats
- const $ratioLabel = $('<dt class="kudoshits">').text('Score:');
- const $ratioValue = $('<dd class="kudoshits">').text(`${percents_print}`);
- $hitsValue.after($ratioLabel, $ratioValue);
- if (CONFIG.colourBackground) {
- if (percents >= CONFIG.thresholds.high) {
- $ratioValue.css('background-color', CONFIG.colors.green);
- } else if (percents >= CONFIG.thresholds.low) {
- $ratioValue.css('background-color', CONFIG.colors.yellow);
- } else {
- $ratioValue.css('background-color', CONFIG.colors.red);
- }
- }
- if (CONFIG.hideHitcount && !statsPage) {
- $this.find('.hits').hide();
- }
- $this.closest('li').attr('kudospercent', percents);
- } catch (error) {
- console.error(`Error processing work stats: ${error.message}`);
- $this.closest('li').attr('kudospercent', 0);
- }
- });
- };
- // Sort works by kudos/hits ratio
- const sortByRatio = (ascending = false) => {
- if (!sortable) return;
- $('dl.stats').closest('li').parent().each(function () {
- const $list = $(this);
- const listElements = $list.children('li').get();
- listElements.sort((a, b) => {
- const aPercent = parseFloat(a.getAttribute('kudospercent'));
- const bPercent = parseFloat(b.getAttribute('kudospercent'));
- return ascending ? aPercent - bPercent : bPercent - aPercent;
- });
- $list.append(listElements);
- });
- };
- // Statistical functions
- const nullHyp = 0.04;
- const getPValue = (hits, kudos, chapters) => {
- const testProp = kudos / hits;
- const zValue = (testProp - nullHyp) / Math.sqrt((nullHyp * (1 - nullHyp)) / hits);
- return normalcdf(0, -1 * zValue, 1);
- };
- const normalcdf = (mean, upperBound, standardDev) => {
- const z = (standardDev - mean) / Math.sqrt(2 * upperBound * upperBound);
- const t = 1 / (1 + 0.3275911 * Math.abs(z));
- const a1 = 0.254829592;
- const a2 = -0.284496736;
- const a3 = 1.421413741;
- const a4 = -1.453152027;
- const a5 = 1.061405429;
- const erf = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
- const sign = z < 0 ? -1 : 1;
- return (1 / 2) * (1 + sign * erf);
- };
- // Add the ratio menu to the page
- const addRatioMenu = () => {
- const $headerMenu = $('ul.primary.navigation.actions');
- const $ratioMenu = $('<li class="dropdown">').html('<a>Kudos/hits</a>');
- $headerMenu.find('li.search').before($ratioMenu);
- const $dropMenu = $('<ul class="menu dropdown-menu">');
- $ratioMenu.append($dropMenu);
- const $buttonCount = $('<li>').html('<a>Count on this page</a>');
- $buttonCount.click(countRatio);
- $dropMenu.append($buttonCount);
- if (sortable) {
- const $buttonSort = $('<li>').html('<a>Sort on this page</a>');
- $buttonSort.click(() => sortByRatio());
- $dropMenu.append($buttonSort);
- }
- if (typeof Storage !== 'undefined') {
- const $buttonSettings = $('<li>').html('<a style="padding: 0.5em 0.5em 0.25em; text-align: center; font-weight: bold;">— Settings (click to change): —</a>');
- $dropMenu.append($buttonSettings);
- const createToggleButton = (text, storageKey, onState, offState) => {
- const $button = $('<li>').html(`<a>${text}: ${CONFIG[storageKey] ? 'YES' : 'NO'}</a>`);
- $button.click(function () {
- CONFIG[storageKey] = !CONFIG[storageKey];
- localStorage.setItem(storageKey + 'Local', CONFIG[storageKey] ? onState : offState);
- $(this).find('a').text(`${text}: ${CONFIG[storageKey] ? 'YES' : 'NO'}`);
- if (storageKey === 'hideHitcount') {
- $('.stats .hits').toggle(!CONFIG.hideHitcount);
- }
- });
- return $button;
- };
- $dropMenu.append(createToggleButton('Count automatically', 'alwaysCount', 'yes', 'no'));
- $dropMenu.append(createToggleButton('Sort automatically', 'alwaysSort', 'yes', 'no'));
- $dropMenu.append(createToggleButton('Hide hitcount', 'hideHitcount', 'yes', 'no'));
- }
- // Add button for statistics page
- if ($('#main').is('.stats-index')) {
- const $buttonSortStats = $('<li>').html('<a>↓ Kudos/hits</a>');
- $buttonSortStats.click(function () {
- sortByRatio();
- $(this).after($buttonSortStatsAsc).detach();
- });
- const $buttonSortStatsAsc = $('<li>').html('<a>↑ Kudos/hits</a>');
- $buttonSortStatsAsc.click(function () {
- sortByRatio(true);
- $(this).after($buttonSortStats).detach();
- });
- $('ul.sorting.actions li:nth-child(3)').after($buttonSortStats);
- }
- };
- // Main execution
- loadUserSettings();
- checkCountable();
- if (CONFIG.alwaysCount) {
- countRatio();
- if (CONFIG.alwaysSort) {
- sortByRatio();
- }
- }
- })(jQuery); sc