Filter Collections

Adds [<10], [10-99], [100-999], [>1000], Not dungeon and D1-4 filter buttons to Collections

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Filter Collections
// @namespace    http://tampermonkey.net/
// @version      2025-12-12
// @description  Adds [<10], [10-99], [100-999], [>1000], Not dungeon and D1-4 filter buttons to Collections
// @license      MIT
// @author       sentientmilk
// @match        https://www.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

/*
	Changelog
	=========

	v2025-12-06
		- Initial version
	v2025-12-06-2
		- FIXED: "<10" button was showing 10s
	v2025-12-06-3
		- Made checkbox label clickable
	v2025-12-11
		- Save and restore "Show Uncollected Items" state when opening Collections again
		- FIXED: Items that can be in 2 dungeons (like Pestilent Shot) were attributed to only a single dungeon
	v2025-12-11-2
		- FIXED: Not showin non dungeon drops
	v2025-12-12
		- Show collection badges on skilling actions
		- FIXED: Sometimes no/almost no items were shown after opeming Collections again

	        TODO
	====================
*/


(function() {
	async function waitFnRepeatedFor (selector, callback) {
		let notified = false;
		return new Promise((resolve) => {
			let lastEl = null
			function check () {
				const el = selector();
				if (el && el != lastEl) {
					lastEl = el;
					notified = false;
				}
				setTimeout(check, 1000/30); // Schedule first to allow the callback to throw
				if (el && !notified) {
					notified = true;
					resolve(el);
					if (callback) {
						callback(el);
					}
				} else if (el && notified) {
					// Skip, wait for cond to be false again
				} else {
					notified = false;
				}
			}
			check();
		});
	}

	function unformatNumber (s) {
		if (s.endsWith("M")) {
			return parseFloat(s) * 1000 * 1000;
		} else if (s.endsWith("K")) {
			return parseFloat(s) * 1000;
		} else if (("" + parseFloat(s)) == s) {
			return parseFloat(s);
		}
	}

	function f (n) {
		if (typeof n != "number") {
			return "NaN";
		} else if (n == 0) {
			return "" + n;
		} else if (Math.abs(n) < 1) {
			return n.toFixed(2);
		} else if (Math.abs(n) < 10*1000) {
			if (n % 1 == 0) {
				return "" + n;
			} else {
				return n.toFixed(1);
			}
		} else if (Math.abs(n) <= 1*1000*1000) {
			const k = n/1000;
			if (k % 1 == 0) {
				return k + "K";
			} else {
				return k.toFixed(1) + "K";
			}
		} else if (Math.abs(n) > 1*1000*1000) {
			const m = n/(1000*1000);
			if (m % 1 == 0) {
				return m + "M";
			} else if (m % 0.1 == 0) {
				return m.toFixed(1) + "M";
			} else {
				return m.toFixed(2) + "M";
			}
		} else {
			return "" + n;
		}
	}

	const dungeonsItems = {
		"d1": new Set([
			"chimerical_chest",
			"chimerical_refinement_chest",
			"chimerical_token",
			"chimerical_quiver",
			"chimerical_quiver_refined",
			"griffin_leather",
			"manticore_sting",
			"jackalope_antler",
			"dodocamel_plume",
			"griffin_talon",
			"chimerical_refinement_shard",
			"chimerical_essence",
			"shield_bash",
			"crippling_slash",
			"pestilent_shot",
			"griffin_tunic",
			"griffin_chaps",
			"manticore_shield",
			"jackalope_staff",
			"dodocamel_gauntlets",
			"griffin_bulwark",
		]),
		"d2": new Set([
			"sinister_chest",
			"sinister_refinement_chest",
			"sinister_token",
			"sinister_cape",
			"sinister_cape_refined",
			"acrobats_ribbon",
			"magicians_cloth",
			"chaotic_chain",
			"cursed_ball",
			"sinister_refinement_shard",
			"sinister_essence",
			"penetrating_strike",
			"pestilent_shot",
			"smoke_burst",
			"acrobatic_hood",
			"magicians_hat",
			"chaotic_flail",
			"cursed_bow",
		]),
		"d3": new Set([
			"enchanted_chest",
			"enchanted_refinement_chest",
			"enchanted_token",
			"enchanted_cloak",
			"enchanted_cloak_refined",
			"royal_cloth",
			"knights_ingot",
			"bishops_scroll",
			"regal_jewel",
			"sundering_jewel",
			"enchanted_refinement_shard",
			"enchanted_essence",
			"crippling_slash",
			"penetrating_shot",
			"retribution",
			"mana_spring",
			"knights_aegis",
			"bishops_codex",
			"royal_water_robe_top",
			"royal_water_robe_bottoms",
			"royal_nature_robe_top",
			"royal_nature_robe_bottoms",
			"royal_fire_robe_top",
			"royal_fire_robe_bottoms",
			"furious_spear",
			"regal_sword",
			"sundering_crossbow",
		]),
		"d4": new Set([
			"pirate_chest",
			"pirate_refinement_chest",
			"pirate_token",
			"marksman_brooch",
			"corsair_crest",
			"damaged_anchor",
			"maelstrom_plating",
			"kraken_leather",
			"kraken_fang",
			"pirate_refinement_shard",
			"pirate_essence",
			"shield_bash",
			"fracturing_impact",
			"life_drain",
			"marksman_bracers",
			"corsair_helmet",
			"anchorbound_plate_body",
			"anchorbound_plate_legs",
			"maelstrom_plate_body",
			"maelstrom_plate_legs",
			"kraken_tunic",
			"kraken_chaps",
			"rippling_trident",
			"blooming_trident",
			"blazing_trident",
		]),
	};

	function checkbox ({ label, className, checked }) {
		return `<div class="AchievementsPanel_checkboxControl__3e6CJ ${className}">
				<label class="MuiFormControlLabel-root MuiFormControlLabel-labelPlacementEnd Checkbox_checkbox__dP0DH css-1jaw3da">
					<span class="MuiButtonBase-root MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeSmall PrivateSwitchBase-root MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeSmall ${checked ? "Mui-checked" : ""} MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeSmall css-zun73v">`
						+ (checked ? `<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeSmall css-1k33q06" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="CheckBoxIcon">
							<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
						</svg>` : "")
						+ (!checked ? `<svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeSmall css-1k33q06" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="CheckBoxOutlineBlankIcon">
							<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path>
						</svg>` : "")
					+ `</span>
					<span class="MuiTypography-root MuiTypography-body1 MuiFormControlLabel-label css-9l3uo3">${label}</span>
				</label>
			</div>`;
	}

	let flags = [
		{ label: "<10", className: "c10", from: 0, to: 9, checked: true },
		{ label: "10-99", className: "c99", from: 10, to: 99, checked: true },
		{ label: "100-999", className: "c999", from: 100, to: 999, checked: true },
		{ label: "1000+", className: "c1000", from: 1000, to: Infinity, checked: true },
		{ label: "Not dungeon", className: "nod", checked: true },
		{ label: "D1", className: "d1", dungeon: "d1", checked: true },
		{ label: "D2", className: "d2", dungeon: "d2", checked: true },
		{ label: "D3", className: "d3", dungeon: "d3", checked: true },
		{ label: "D4", className: "d4", dungeon: "d4", checked: true },
	];

	const characterID = unsafeWindow.location.search.split("=")[1];
	let collections = GM_getValue("collections_" + characterID, {});

	function rerenderCollectionsFiltering (panelEl) {
		const catsEl = panelEl.parentElement.querySelector(".AchievementsPanel_categories__34hno");

		catsEl.querySelectorAll(".Collection_collectionContainer__3ZlUO").forEach((el) => {
			const itemId = el.querySelector("use").getAttribute("href").split("#")[1];
			const n = unformatNumber(el.querySelector(".Collection_count__3oj-t")?.textContent ?? "0");
			const f = flags.find((f) => (f.from || f.to) && f.from <= n && n <= f.to);
			el.dataset.count = f.className;

			collections[itemId] = n;

			let isDungeonItem = false;
			for (let d in dungeonsItems) {
				if (dungeonsItems[d].has(itemId)) {
					el.classList.add(d + "-ucf-userscript");
					isDungeonItem = true;
				}
			}
			if (!isDungeonItem) {
				el.classList.add("nod" + "-ucf-userscript");
			}
		});

		GM_setValue("collections_" + characterID, collections);

		let containerEl = panelEl.querySelector(".ucf-userscript");

		if (!containerEl) {
			panelEl.insertAdjacentHTML("beforeend", `<div class="ucf-userscript" style="display: flex"></div>`);
			containerEl = panelEl.querySelector(".ucf-userscript");
		}

		function updateShowClass (f) {
			if (f.checked) {
				catsEl.classList.add(`show-${f.className}-ucf-userscript`);
			} else {
				catsEl.classList.remove(`show-${f.className}-ucf-userscript`);
			}
		}

		containerEl.innerHTML = flags.map(checkbox).join("");

		flags.forEach((f) => {
			containerEl.querySelector("." + f.className).onclick = (event) => {
				event.stopPropagation();
				f.checked = !f.checked;
				rerenderCollectionsFiltering(panelEl);
			};

			updateShowClass(f);
		});

		catsEl.classList.add("ucf-userscript");
	}

	let showUncollected = false;
	function saveShowUncollected () {
		showUncollected = document.querySelector(".AchievementsPanel_collections__qA6CY .AchievementsPanel_controls__3bGFT > .AchievementsPanel_checkboxControl__3e6CJ > label > span").classList.contains("Mui-checked")
	}

	function restoreShowUncollected () {
		if (showUncollected) {
			document.querySelector(".AchievementsPanel_collections__qA6CY .AchievementsPanel_controls__3bGFT > .AchievementsPanel_checkboxControl__3e6CJ input").click();
		}
	}

	function isCollectionsOnScreen () {
		return document.querySelector(".TabPanel_tabPanel__tXMJF:not(.TabPanel_hidden__26UM3) .AchievementsPanel_collections__qA6CY .Collection_collectionContainer__3ZlUO");
	}

	function addCollectionsFiltering () {
		const panelEl =  document.querySelector(".TabPanel_tabPanel__tXMJF:not(.TabPanel_hidden__26UM3) .AchievementsPanel_collections__qA6CY .AchievementsPanel_controls__3bGFT");
		restoreShowUncollected();

		saveShowUncollected();
		rerenderCollectionsFiltering(panelEl);

		panelEl.querySelector(".AchievementsPanel_refreshButton__3RYCh").onclick = () => {
			setTimeout(() => {
				rerenderCollectionsFiltering(panelEl);
			}, 500);
		};

		panelEl.parentElement.querySelector(".AchievementsPanel_controls__3bGFT > .AchievementsPanel_checkboxControl__3e6CJ").addEventListener("click", () => {
			setTimeout(() => {
				saveShowUncollected();
			}, 500);
		}, true);

		panelEl.parentElement.querySelector(".AchievementsPanel_controls__3bGFT > .AchievementsPanel_checkboxControl__3e6CJ").onclick = () => {
			requestAnimationFrame(() => {
				rerenderCollectionsFiltering(panelEl);
			});
		};
	}

	const actionToItem = {
		"cow": "milk",
		"verdant_cow": "verdant_milk",
		"azure_cow": "azure_milk",
		"burble_cow": "burble_milk",
		"crimson_cow": "crimson_milk",
		"unicow": "rainbow_milk",
		"holy_cow": "holy_milk",
		"tree": "log",
		"birch_tree": "birch_log",
		"cedar_tree": "cedar_log",
		"purpleheart_tree": "purpleheart_log",
		"ginkgo_tree": "ginkgo_log",
		"redwood_tree": "redwood_log",
		"arcane_tree": "arcane_log",
	};

	function isSkillingScreen () {
		return document.querySelector(".TabPanel_tabPanel__tXMJF:not(.TabPanel_hidden__26UM3) .SkillActionGrid_skillActionGrid__1tJFk");
	}

	function tierColorClass (n) {
		if (n == 0) {
			return "Collection_tierGray__279Mp";
		} else if (n < 10) {
			return "Collection_tierWhite__2m0_1";
		} else if (n < 100) {
			return "Collection_tierGreen__ExgCi";
		} else if (n < 1000) {
			return "Collection_tierBlue__3uYl-";
		} else if (n < 10000) {
			return "Collection_tierPurple__13F_l";
		} else if (n < 100000) {
			return "Collection_tierRed__3dV_1";
		} else {
			return "Collection_tierRainbow__1eS_P";
		}
	}

	function addSkillingCollectionCounts (containerEl) {
		const itemsEls = containerEl.querySelectorAll(".SkillAction_skillAction__1esCp");
		itemsEls.forEach((el) => {
			let itemId = el.querySelector("use").getAttribute("href").split("#")[1];

			if (itemId in actionToItem) {
				itemId = actionToItem[itemId];
			}

			if (itemId in collections) {
				const n = collections[itemId];
				el.querySelector(".ucf-userscript")?.remove();
				el.querySelector(".SkillAction_name__2VPXa").insertAdjacentHTML("beforeend", `
					<span class="ucf-userscript Collection_collection__3H6c8 ${tierColorClass(n)}">
						<span class="Collection_count__3oj-t">${f(n)}</span>
					</span>
				`.replace(/[\t\n]+/g, ""));
			}
		});
	}

	document.body.insertAdjacentHTML("beforeend", `<style class="ucf-userscript">`
		+ `.AchievementsPanel_categories__34hno.ucf-userscript .Collection_collectionContainer__3ZlUO { display: none; }\n`
		+ `.ucf-userscript.Collection_collection__3H6c8 { border-radius: var(--radius-sm); margin-left: 4px; padding: 2px; }\n`
		+ flags.map((f) => {
			return ["nod", ...Object.keys(dungeonsItems)].map((d) => {
				return `.AchievementsPanel_categories__34hno.ucf-userscript.show-${f.className}-ucf-userscript.show-${d}-ucf-userscript .Collection_collectionContainer__3ZlUO[data-count="${f.className}"].${d}-ucf-userscript { display: initial; }\n`
			}).join("");
		}).join("")
		+ ` </style>`);

	waitFnRepeatedFor(isCollectionsOnScreen, addCollectionsFiltering);
	waitFnRepeatedFor(isSkillingScreen, addSkillingCollectionCounts);
})();