您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add "Sort By" dropdown to connected accounts page
// ==UserScript== // @name Tiller.com - Sort Connected Accounts // @namespace http://tampermonkey.net/ // @version 2025-06-17 // @description Add "Sort By" dropdown to connected accounts page // @author You // @match https://my.tiller.com/ // @icon https://www.google.com/s2/favicons?sz=64&domain=tiller.com // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; console.log('[TM] Script loaded'); let currentSortBy = 'lastRefreshedAt'; let lastRenderedState = ''; let mutationTimeout = null; function computeDomStateHash() { const elements = Array.from(document.querySelectorAll('[data-testid="connected-institution"]')); return elements.map(el => el.querySelector('[data-testid="institution-name"]')?.textContent?.trim()).join('|'); } function parseStatus(status, lastRefreshedAt) { const now = new Date(); const refreshedDate = new Date(lastRefreshedAt); const diff = (now - refreshedDate) / (1000 * 60 * 60); const UNKNOWN = 4; const RED = 3; const YELLOW = 2; const GREEN = 1; if (status === 'FAILURE') return RED; if (status === 'SUCCESS' && diff < 36) return GREEN; if (status === 'SUCCESS' && diff >= 36) return YELLOW; return UNKNOWN; } function calculateTotalBalance(accountData) { let totalBalance = 0; accountData.forEach(account => { const balance = account.balance?.amount?.value || 0; totalBalance += parseFloat(balance); }); return totalBalance; } function gatherConnectionData(apiData) { return apiData.items.map(item => { const totalBalance = calculateTotalBalance(item.accounts); const statusValue = parseStatus(item.refreshInfo.status, item.refreshInfo.lastRefreshedAt); return { id: item.id, name: item.name, totalBalance: totalBalance, statusValue: statusValue, lastRefreshedAt: item.refreshInfo.lastRefreshedAt, accounts: item.accounts }; }); } function reorderConnections(sortBy) { console.log('[TM] Reordering by:', sortBy); const domElements = Array.from(document.querySelectorAll('[data-testid="connected-institution"]')); console.log(`[TM] Found ${domElements.length} connected institution elements`); if (!window.apiData) { console.warn('[TM] apiData not loaded'); return; } const matched = []; domElements.forEach(el => { const institutionName = el.querySelector('[data-testid="institution-name"]')?.textContent?.trim(); let accountKeys = Array.from(el.querySelectorAll('[data-testid="account-number"]')) .map(n => n.textContent.trim()) .filter(n => n.length > 0); let matchKeyType = 'number'; if (accountKeys.length === 0) { accountKeys = Array.from(el.querySelectorAll('[data-testid="account-name"]')) .map(n => n.textContent.trim()) .filter(n => n.length > 0); matchKeyType = 'name'; } if (!institutionName || accountKeys.length === 0) { console.warn('[TM] Could not extract identifiers from element', el); return; } const match = window.apiData.find(data => { if (data.name !== institutionName) return false; const dataKeys = data.accounts.map(a => matchKeyType === 'number' ? a.number : a.name ); return accountKeys.every(k => dataKeys.includes(k)); }); if (match) { matched.push({ el, ...match }); } }); if (matched.length === 0) { console.warn('[TM] No DOM elements matched to API data'); return; } const parent = matched[0].el.parentElement; matched.sort((a, b) => { const aVal = a[sortBy]; const bVal = b[sortBy]; if (sortBy === 'lastRefreshedAt') { return new Date(aVal) - new Date(bVal); } return bVal - aVal; }); matched.forEach(d => { parent.appendChild(d.el); console.log(`[TM] Moved: ${d.name} (${sortBy} = ${d[sortBy]})`); }); lastRenderedState = computeDomStateHash(); console.log('[TM] Sorting complete'); } function createDropdown() { const sel = document.createElement('select'); sel.id = 'tm-sort-dropdown'; ['lastRefreshedAt', 'statusValue', 'totalBalance'].forEach(val => { const opt = document.createElement('option'); opt.value = val; opt.text = { lastRefreshedAt: 'Last Refreshed At', statusValue: 'Connection Status', totalBalance: 'Total Balance' } [val]; if (val === currentSortBy) opt.selected = true; sel.add(opt); }); sel.style.margin = '10px'; sel.addEventListener('change', () => { currentSortBy = sel.value; lastRenderedState = ''; reorderConnections(currentSortBy); }); return sel; } function insertDropdown() { const headers = Array.from(document.querySelectorAll('h3')); const targetHeader = headers.find(h => h.textContent.trim().startsWith('Connected Account')); if (targetHeader) { console.log('[TM] Found target header:', targetHeader.textContent.trim()); } else { console.warn('[TM] Could not find "Connected Account" header'); return; } const existingDropdown = document.getElementById('tm-sort-dropdown'); if (existingDropdown) { console.log('[TM] Dropdown already inserted'); return; } const parent = targetHeader.closest('.mt-0.mb-3.d-flex.flex-column'); if (!parent) { console.warn('[TM] Parent element not found for sorting'); return; } window.tmParentElement = parent; const dropdown = createDropdown(); targetHeader.insertAdjacentElement('afterend', dropdown); console.log('[TM] Dropdown inserted after "Connected Account" header'); } const originalFetch = window.fetch; window.fetch = async function(...args) { const url = args[0]; try { if (url.includes('/api/v2/provider-accounts')) { const response = await originalFetch.apply(this, args); const clonedResponse = response.clone(); const data = await clonedResponse.json(); window.apiData = gatherConnectionData(data); return response; } return originalFetch.apply(this, args); } catch (error) { console.error('[TM] Error in fetch interception:', error); throw error; } }; const observer = new MutationObserver(() => { if (mutationTimeout) return; mutationTimeout = setTimeout(() => { mutationTimeout = null; const headers = Array.from(document.querySelectorAll('h3')); const targetHeader = headers.find(h => h.textContent.trim().startsWith('Connected Account')); if (targetHeader && window.apiData) { insertDropdown(); const currentHash = computeDomStateHash(); if (currentHash !== lastRenderedState) { reorderConnections(currentSortBy); } } }, 200); }); observer.observe(document.body, { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址