Filter Collections

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();