// ==UserScript==
// @name Folo Colorful Listview
// @namespace http://folo.colorful.list.view
// @description Colorizes items based on their source feed
// @author ObenK
// @license MIT
// @match https://app.follow.is/*
// @match https://follow.is/*
// @match https://*.follow.is/*
// @version 1.0.0
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const colors = {};
const processedEntries = new Set();
// Function to add CSS styles
const addStyle = (styleText) => {
const style = document.createElement('style');
style.appendChild(document.createTextNode(styleText));
document.head.appendChild(style);
};
// Clean title for consistent coloring
const cleanTitle = (title) => {
return title?.replace?.(/[^\p{L}\s]/gu, '') || title;
};
// Compute color based on source title - Morandi colors (muted, desaturated)
const computeColor = (title) => {
if (colors[title]) return colors[title];
let h = 0;
const clean = cleanTitle(title);
for (let i = 0; i < clean.length; i++) {
let s = i !== 0 ? clean.length % i : 1;
let r = s !== 0 ? clean.charCodeAt(i) % s : clean.charCodeAt(i);
h += r;
}
// Morandi color palette - muted, soft tones
const morandiColors = [
{ h: 210, s: 15, l: 85 }, // Soft blue-gray
{ h: 25, s: 20, l: 88 }, // Warm beige
{ h: 160, s: 18, l: 82 }, // Sage green
{ h: 45, s: 22, l: 86 }, // Warm gray
{ h: 280, s: 16, l: 84 }, // Soft lavender
{ h: 200, s: 14, l: 87 }, // Powder blue
{ h: 350, s: 18, l: 85 }, // Dusty rose
{ h: 170, s: 15, l: 83 }, // Mint gray
{ h: 30, s: 16, l: 89 }, // Cream
{ h: 220, s: 12, l: 86 }, // Steel blue
{ h: 260, s: 14, l: 84 }, // Mauve
{ h: 15, s: 19, l: 87 } // Warm taupe
];
const index = h % morandiColors.length;
const color = morandiColors[index];
colors[title] = color;
return color;
};
// Add base styles
addStyle(`
.folo-colored-item {
transition: background-color 0.2s ease !important;
}
.folo-colored-item:hover {
background-color: rgba(0, 0, 0, 0.05) !important;
}
.dark .folo-colored-item:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
}
`);
// Function to apply color to an entry
const colorizeEntry = (entry) => {
if (!entry || processedEntries.has(entry)) return;
// Find the feed title element - look for the actual feed name
const feedTitleElements = entry.querySelectorAll('.truncate, [class*="truncate"], .text-xs.font-bold');
let feedTitle = null;
for (const el of feedTitleElements) {
const text = el.textContent?.trim();
if (text && !text.includes('·') && text.length > 0) {
feedTitle = text;
break;
}
}
if (!feedTitle) return;
const color = computeColor(feedTitle);
// Apply color directly to the element
entry.style.backgroundColor = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
entry.style.borderRadius = '8px';
entry.style.margin = '2px 0';
entry.style.padding = '8px';
// Store the color info
entry.setAttribute('data-folo-source', feedTitle);
entry.setAttribute('data-folo-color', `hsl(${color.h}, ${color.s}%, ${color.l}%)`);
processedEntries.add(entry);
};
// Function to find and colorize all entries
const colorizeAllEntries = () => {
// Use the same selector as the working merger script
const entries = document.querySelectorAll('[class*="group"][class*="relative"][class*="flex"]');
entries.forEach(entry => {
if (!processedEntries.has(entry)) {
colorizeEntry(entry);
}
});
};
// Removed unused observer code
function waitForContent() {
let attempts = 0;
const maxAttempts = 20;
const checkInterval = setInterval(() => {
attempts++;
const hasEntries = document.querySelectorAll('[class*="group"]').length > 5;
if (hasEntries || attempts > maxAttempts) {
clearInterval(checkInterval);
colorizeAllEntries();
// 设置观察器
const observer = new MutationObserver(() => {
setTimeout(colorizeAllEntries, 500);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}, 500);
}
// 启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForContent);
} else {
waitForContent();
}
// 处理SPA路由变化
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(waitForContent, 1000);
}
}, 1000);
// Expose for debugging
window.foloColorful = {
colors,
processedEntries,
colorizeAllEntries,
recompute: () => {
processedEntries.clear();
colorizeAllEntries();
}
};
})();