您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Show tags in the transactions listing on Mint.com.
// ==UserScript== // @name Mint.com tags display // @match https://mint.intuit.com/transactions // @connect mint.intuit.com // @description Show tags in the transactions listing on Mint.com. // @namespace com.warkmilson.mint.js // @author Mark Wilson (update by Shaun Williams) // @version 2.0.1 // @homepage https://github.com/mddub/mint-tags-display // @grant none // @noframes // ==/UserScript== // (function() { //------------------------------------------------------------------------------ // Logging //------------------------------------------------------------------------------ const logging = false function log(...args) { if (logging) console.info('MINT_TAGS', ...args) } //------------------------------------------------------------------------------ // Track state by watching XHR //------------------------------------------------------------------------------ // State const state = { txnTags: {}, // txn id -> [tag name] tagOrder: [], // [tag name] tagName: {}, // tag id -> tag name } window._MINT_TAGS = state // Update state const apiUrl = path => `https://mint.intuit.com/pfm/v1${path}` const apiHooks = { // when transactions are fetched, save tags belonging to each transaction [apiUrl('/transactions/search')]: data => { for (const {id,tagData} of data.Transaction) { state.txnTags[id] = tagData?.tags.map(tag => tag.name) } }, // when the master tag list is fetched, save it [apiUrl('/tags')]: data => { state.tagOrder = data.Tag.map(tag => tag.name) state.tagName = Object.fromEntries(data.Tag.map(tag => [tag.id, tag.name])) }, } // when transactions are edited, update our tag records function handleTxnEdits(edits) { const idsToUpdate = [] for (const {id,tagData} of edits) { if (tagData) { state.txnTags[id] = tagData.tags.map(tag => state.tagName[tag.id]) idsToUpdate.push(id) } } setTimeout(() => idsToUpdate.forEach(updateRowTags), 500) } // hook XHR to intercept api calls function watchXHR() { const origOpen = XMLHttpRequest.prototype.open XMLHttpRequest.prototype.open = function(method, url) { const self = this // save XHR responses when needed self.addEventListener("readystatechange", function() { const hook = apiHooks[url] if (self.readyState === 4 && hook) { const data = JSON.parse(self.responseText) log('HOOKING', url, data) hook(data) } }, false) // intercept edits to transactions const txnsUrl = apiUrl('/transactions') if (method == 'PUT' && url.startsWith(txnsUrl)) { const origSend = self.send self.send = function(body) { const data = JSON.parse(body) const edits = url==txnsUrl ? data.Transaction : [{...data, id:url.slice(txnsUrl.length+1)}] log('HOOKING EDITS', edits) handleTxnEdits(edits) origSend.apply(self, arguments) } } origOpen.apply(self, arguments) } } //------------------------------------------------------------------------------ // Render DOM //------------------------------------------------------------------------------ var TAG_COLORS = [ // source: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 // background, foreground ['#a6cee3', '#000'], ['#b2df8a', '#000'], ['#fb9a99', '#000'], ['#fdbf6f', '#000'], ['#cab2d6', '#000'], ['#ffff99', '#000'], ['#1f78b4', '#fff'], ['#33a02c', '#fff'], ['#e31a1c', '#fff'], ['#ff7f00', '#fff'], ['#6a3d9a', '#fff'], ['#b15928', '#fff'] ]; function getTagStyle(tag) { const i = state.tagOrder.indexOf(tag) const [bg,fg] = TAG_COLORS[i] return `background:${bg}; color:${fg}` } // re-render our custom tag annotations in this row function updateRowTags(id) { log('UPDATING ROW', id) const td = document.querySelector(`tr[data-automation-id$=_${id}] td:nth-child(4)`) if (!td) return const tags = state.txnTags[id] const tagsDiv = () => td.querySelector('.gm-tags') if (tags?.length) { if (!tagsDiv()) td.innerHTML += '<div class="gm-tags" style="font-size:10px; display:inline-block"></div>' tagsDiv().innerHTML = tags.map(tag => `<span class="gm-tag" style="${getTagStyle(tag)}; margin-left:4px; padding:0 2px">${tag}</span>`).join('') } else { tagsDiv()?.remove() } } const rowId = row => row?.dataset?.automationId?.match(/TRANSACTION_TABLE_ROW_(READ|EDIT)_(.*)/)?.[2] function initRender() { if (!document.querySelector('tr[data-automation-id]')) { return setTimeout(initRender, 500) } log('FOUND TABLE') for (const row of document.querySelectorAll('tr[data-automation-id]')) { updateRowTags(rowId(row)) } renderWhenDomChanges() } function renderWhenDomChanges() { log('WATCHING FOR CHANGES') const observer = new MutationObserver(() => { observer.disconnect() document.querySelectorAll('tr[data-automation-id]').forEach(row => updateRowTags(rowId(row))) log('RELAUNCHING WATCH') renderWhenDomChanges() }) observer.observe(document.body, {subtree: true, childList: true, characterData: true}) } //------------------------------------------------------------------------------ // Main //------------------------------------------------------------------------------ watchXHR() initRender() })();