// ==UserScript== // @name CF-Submissions-Ratings // @name:zh-CN Codeforces 提交页/状态页 难度分显示 // @namespace https://github.com/GodExious/CF-Submissions-Ratings // @version 1.5.3 // @description Fetches and displays problem difficulty ratings. Adds a new Rating column to status and submissions tables with color-coded backgrounds. // @description:zh-CN 自动获取并显示 Codeforces 题目难度分。在 Status 和 Submissions 表格最右侧新增 Rating 列并带有 Codeforces Analytics 风格的色彩高亮,同时完美兼容个人提交记录背景。 // @author GodExious & Antigravity // @supportURL https://github.com/GodExious/CF-Submissions-Ratings/issues // @match *://codeforces.com/* // @match *://*.codeforces.com/* // @icon https://codeforces.com/favicon.ico // @updateURL https://raw.githubusercontent.com/GodExious/CF-Submissions-Ratings/main/cf-submission-ratings.user.js // @downloadURL https://raw.githubusercontent.com/GodExious/CF-Submissions-Ratings/main/cf-submission-ratings.user.js // @run-at document-end // @require https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js // @license MIT // @grant none // ==/UserScript== /* * GitHub Repository: https://github.com/GodExious/CF-Submissions-Ratings * If you have any suggestions or find any bugs, please feel free to open an issue! * 如果你对本插件有改进建议,欢迎通过 GitHub Issue 提出建议或反馈! * * Inspired by Codeforces-Helper (https://chromewebstore.google.com/detail/codeforces-helper/ahoeafmlmoohkkalcickdnkifpfnolpj) * 灵感来源于 Codeforces-Helper,由于其不支持 status 页面,故仿写并实现了本项目。 * * Mainly implemented by Antigravity 1.23.2 * 主要由 Antigravity 1.23.2 完成实现 */ (function () { 'use strict'; const CACHE_KEY = 'cf_problems_ratings'; const CACHE_TIME_KEY = 'cf_problems_ratings_time'; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 1 day in milliseconds // Settings Management const SETTINGS_KEY = 'cf_submissions_settings'; const DEFAULT_SETTINGS = { acBgColor: '#d4edc9', show: { submissions: true, status: true, hacks: true, problemset: true, contestProblems: true, standings: true, problemTags: true, userAvatar: true, langIcon: true, shortVerdict: false }, avatarSize: 1.4, langIconSize: 1.0, timeFormat: { enabled: true, format: 'YYYY/MM/DD HH:mm' }, displayStyle: 'block', lang: 'zh' }; function hexToRgba(hex, alpha) { if (!hex || !hex.startsWith('#')) return hex || DEFAULT_SETTINGS.acBgColor; let r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function saveSettings(settings) { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function getSettings() { const saved = localStorage.getItem(SETTINGS_KEY); let settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); // deep clone if (saved) { try { let parsed = JSON.parse(saved); // Migrate from split hex+alpha to unified format if (parsed.acBgAlpha !== undefined && parsed.acBgColor && parsed.acBgColor.startsWith('#')) { if (parsed.acBgAlpha < 1) { parsed.acBgColor = hexToRgba(parsed.acBgColor, parsed.acBgAlpha); } delete parsed.acBgAlpha; } settings.acBgColor = parsed.acBgColor || settings.acBgColor; if (parsed.show) Object.assign(settings.show, parsed.show); settings.avatarSize = parsed.avatarSize !== undefined ? parsed.avatarSize : settings.avatarSize; settings.langIconSize = parsed.langIconSize !== undefined ? parsed.langIconSize : settings.langIconSize; if (settings.langIconSize >= 5) { settings.langIconSize = parseFloat((settings.langIconSize / 14).toFixed(1)); } if (parsed.timeFormat) Object.assign(settings.timeFormat, parsed.timeFormat); settings.lang = parsed.lang || settings.lang; settings.displayStyle = parsed.displayStyle || settings.displayStyle; saveSettings(settings); } catch (e) { console.error('Failed to parse settings', e); } } return settings; } function getLanguageIconName(langStr) { langStr = langStr.toLowerCase(); if (langStr.includes('c++') || langStr.includes('g++')) return 'cplusplus'; if (langStr.includes('c#')) return 'csharp'; if (langStr.includes('python') || langStr.includes('pypy')) return 'python'; if (langStr.includes('java') && !langStr.includes('javascript')) return 'java'; if (langStr.includes('rust')) return 'rust'; if (/\bgo\b/.test(langStr)) return 'go'; if (langStr.includes('kotlin')) return 'kotlin'; if (langStr.includes('ruby')) return 'ruby'; if (langStr.includes('node.js') || langStr.includes('nodejs')) return 'nodejs'; if (langStr.includes('javascript') || langStr.includes('v8')) return 'javascript'; if (langStr.includes('php')) return 'php'; if (langStr.includes('haskell')) return 'haskell'; if (langStr.includes('scala')) return 'scala'; if (langStr.includes('ocaml')) return 'ocaml'; if (langStr.includes('perl')) return 'perl'; if (langStr.includes('f#')) return 'fsharp'; if (langStr.includes('delphi')) return 'delphi'; if (/\bd\b/.test(langStr) || langStr.includes('dmd')) return 'd'; if (langStr.includes('gcc') || langStr.includes('clang') || /\bc(?:89|99|11|17|18|23|2x)?\b/.test(langStr)) return 'c'; return null; } const appSettings = getSettings(); function customFormatTime(d, formatStr) { const yyyy = d.getFullYear().toString(); const MM = String(d.getMonth() + 1).padStart(2, '0'); const DD = String(d.getDate()).padStart(2, '0'); const HH = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); return formatStr .replace(/YYYY/g, yyyy) .replace(/MM/g, MM) .replace(/DD/g, DD) .replace(/HH/g, HH) .replace(/mm/g, mm) .replace(/ss/g, ss); } // Inject Global Custom CSS for AC Background const customStyle = document.createElement('style'); customStyle.innerHTML = ` tr.accepted-problem td { background-color: ${appSettings.acBgColor} !important; } .pcr-app { z-index: 9999999 !important; opacity: 0; visibility: hidden; pointer-events: none; } .pcr-app.visible { opacity: 1; visibility: visible; pointer-events: auto; } /* Widen the Pickr nano theme and adjust interaction layout */ .pcr-app[data-theme="nano"] { width: 240px !important; } .pcr-app[data-theme="nano"] .pcr-interaction { flex-wrap: wrap !important; } .pcr-app[data-theme="nano"] .pcr-interaction .pcr-result { flex: 1 1 100% !important; width: 100% !important; min-width: 100% !important; margin-top: 8px !important; } /* Custom Toggle Switch */ .cf-toggle-switch { position: relative; display: inline-block; width: 34px; height: 18px; margin-right: 8px; flex-shrink: 0; vertical-align: middle; } .cf-toggle-switch input { opacity: 0; width: 0; height: 0; } .cf-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .2s; border-radius: 18px; } .cf-toggle-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 2px; bottom: 2px; background-color: white; transition: .2s; border-radius: 50%; } .cf-toggle-switch input:checked + .cf-toggle-slider { background-color: #1890ff; } .cf-toggle-switch input:checked + .cf-toggle-slider:before { transform: translateX(16px); } `; document.head.appendChild(customStyle); // Inject Pickr CSS const pickrCss = document.createElement('link'); pickrCss.rel = 'stylesheet'; pickrCss.href = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css'; document.head.appendChild(pickrCss); // Auto dark theme detection const isDarkTheme = () => { // Dark Reader will handle inverting our light colors automatically as long as we don't use !important if (document.querySelector('.darkreader') || document.querySelector('meta[name="darkreader"]')) return false; if (document.documentElement.getAttribute('data-theme') === 'dark' || document.body.classList.contains('dark')) return true; try { const bodyBg = window.getComputedStyle(document.body).backgroundColor; const match = bodyBg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (match) { const brightness = (parseInt(match[1]) * 299 + parseInt(match[2]) * 587 + parseInt(match[3]) * 114) / 1000; if (brightness < 128) return true; } } catch (e) { } return false; }; // Fine-grained background colors matching Codeforces Analytics / extended rating systems function getRatingBgColor(rating) { if (isDarkTheme()) { if (rating < 1200) return '#444444'; // Gray if (rating < 1400) return '#1A4D1A'; // Green if (rating < 1600) return '#1A4D4D'; // Cyan if (rating < 1900) return '#1A1A4D'; // Blue if (rating < 2100) return '#4D1A4D'; // Violet if (rating < 2300) return '#4D331A'; // Light Orange if (rating < 2400) return '#66331A'; // Orange if (rating < 2600) return '#4D1A1A'; // Light Red if (rating < 3000) return '#661A1A'; // Red return '#800000'; // Dark Red } if (rating < 1200) return '#CCCCCC'; // Gray (Newbie) if (rating < 1400) return '#77FF77'; // Green (Pupil) if (rating < 1600) return '#77DDBB'; // Cyan (Specialist) if (rating < 1900) return '#AAAAFF'; // Blue (Expert) if (rating < 2100) return '#FF88FF'; // Violet (Candidate Master) if (rating < 2300) return '#FFCC88'; // Light Orange (Master) if (rating < 2400) return '#FFBB55'; // Orange (International Master) if (rating < 2600) return '#FF7777'; // Light Red (Grandmaster) if (rating < 3000) return '#FF3333'; // Red (International Grandmaster) return '#CC2222'; // Dark Red (Legendary Grandmaster+) } // Darker border colors for the roundbox tags function getRatingBorderColor(rating) { if (rating < 1200) return '#AAAAAA'; // Gray if (rating < 1400) return '#44CC44'; // Green if (rating < 1600) return '#44AA88'; // Cyan if (rating < 1900) return '#7777CC'; // Blue if (rating < 2100) return '#CC55CC'; // Violet if (rating < 2300) return '#CC9955'; // Light Orange if (rating < 2400) return '#CC8822'; // Orange if (rating < 2600) return '#CC4444'; // Light Red if (rating < 3000) return '#CC0000'; // Red return '#990000'; // Dark Red } // Fine-grained text colors when appending rating as text next to standalone links function getRatingTextColor(rating) { if (rating < 1200) return '#808080'; if (rating < 1400) return '#008000'; if (rating < 1600) return '#03A89E'; if (rating < 1900) return '#0000FF'; if (rating < 2100) return '#AA00AA'; if (rating < 2300) return '#FF8C00'; if (rating < 2400) return '#FF8C00'; if (rating < 2600) return '#FF0000'; if (rating < 3000) return '#FF0000'; return '#AA0000'; } // IViewUI / Ant Design style aesthetic tags function getRatingTagStyle(rating) { let bg, border, text; const isDark = isDarkTheme(); if (rating < 1200) { bg = isDark ? '#262626' : '#f7f7f7'; border = isDark ? '#434343' : '#e6e6e6'; text = isDark ? '#bfbfbf' : '#808080'; } // Gray else if (rating < 1400) { bg = isDark ? '#135200' : '#f6ffed'; border = isDark ? '#237804' : '#b7eb8f'; text = isDark ? '#73d13d' : '#389e0d'; } // Green else if (rating < 1600) { bg = isDark ? '#00474f' : '#e6fffb'; border = isDark ? '#006d75' : '#87e8de'; text = isDark ? '#36cfc9' : '#08979c'; } // Cyan else if (rating < 1900) { bg = isDark ? '#002c8c' : '#e6f7ff'; border = isDark ? '#003eb3' : '#91d5ff'; text = isDark ? '#40a9ff' : '#096dd9'; } // Blue else if (rating < 2100) { bg = isDark ? '#531dab' : '#f9f0ff'; border = isDark ? '#722ed1' : '#d3adf7'; text = isDark ? '#b37feb' : '#531dab'; } // Violet else if (rating < 2400) { bg = isDark ? '#873800' : '#fff2e8'; border = isDark ? '#ad4e00' : '#ffd8bf'; text = isDark ? '#ff7a45' : '#d4380d'; } // Orange else if (rating < 3000) { bg = isDark ? '#a8071a' : '#fff1f0'; border = isDark ? '#cf1322' : '#ffa39e'; text = isDark ? '#ff4d4f' : '#cf1322'; } // Red else { bg = isDark ? '#434343' : '#fff0f6'; border = isDark ? '#8c8c8c' : '#ffadd2'; text = isDark ? '#eb2f96' : '#c41d7f'; } // Dark Red (Legendary) return { bg, border, text }; } // Fetch ratings from CF API or LocalStorage cache async function getRatings() { const cached = localStorage.getItem(CACHE_KEY); const cachedTime = localStorage.getItem(CACHE_TIME_KEY); const now = Date.now(); // Use cache if it's fresh if (cached && cachedTime && (now - cachedTime < CACHE_EXPIRY)) { try { return JSON.parse(cached); } catch (e) { console.error('Codeforces Rating Helper: Failed to parse cached ratings', e); } } // Fetch new ratings try { console.log('Codeforces Rating Helper: Fetching problem ratings...'); const response = await fetch('https://codeforces.com/api/problemset.problems'); const data = await response.json(); if (data.status === 'OK') { const ratingsMap = {}; for (const p of data.result.problems) { if (p.rating) { ratingsMap[`${p.contestId}${p.index}`] = p.rating; } } // Save to localStorage localStorage.setItem(CACHE_KEY, JSON.stringify(ratingsMap)); localStorage.setItem(CACHE_TIME_KEY, now); console.log('Codeforces Rating Helper: Ratings fetched and cached successfully.'); return ratingsMap; } else { console.error('Codeforces Rating Helper: API returned status', data.status); } } catch (e) { console.error('Codeforces Rating Helper: Failed to fetch ratings API', e); } // Fallback to cached if fetch failed but we have something if (cached) { try { return JSON.parse(cached); } catch (e) { } } return {}; } // Apply ratings to tables and standalone links function applyRatings(ratingsMap) { if (!ratingsMap || Object.keys(ratingsMap).length === 0) return; function applyRatingStyle(cell, rating) { cell.style.textAlign = 'center'; cell.style.verticalAlign = 'middle'; if (appSettings.displayStyle === 'tag') { cell.textContent = ''; cell.style.setProperty('background-color', 'transparent', 'important'); const tagStyle = getRatingTagStyle(rating); const tagSpan = document.createElement('span'); tagSpan.textContent = rating; tagSpan.style.cssText = ` display: inline-block !important; padding: 1px 6px !important; border-radius: 4px !important; border: 1px solid ${tagStyle.border} !important; background-color: ${tagStyle.bg} !important; color: ${tagStyle.text} !important; font-size: 12px !important; font-weight: 500 !important; `; cell.appendChild(tagSpan); } else { cell.textContent = rating; cell.style.setProperty('background-color', getRatingBgColor(rating), 'important'); cell.style.setProperty('color', isDarkTheme() ? '#EEEEEE' : (rating >= 1600 ? 'white' : 'black'), 'important'); cell.style.setProperty('font-weight', 'normal', 'important'); } } const regexes = [ /\/contest\/(\d+)\/problem\/([A-Za-z0-9_]+)/i, /\/problemset\/problem\/(\d+)\/([A-Za-z0-9_]+)/i, /\/gym\/(\d+)\/problem\/([A-Za-z0-9_]+)/i ]; function getProblemRatingFromHref(href) { for (const regex of regexes) { const match = href.match(regex); if (match) { return { contestId: match[1], index: match[2], rating: ratingsMap[`${match[1]}${match[2]}`] }; } } return null; } // Walk through nodes to replace verdict text with abbreviations function walkAndReplaceVerdict(node) { if (node.nodeType === Node.TEXT_NODE) { let txt = node.textContent; if (!txt.trim()) return; const map = { 'Accepted': 'AC', 'Wrong answer': 'WA', 'Time limit exceeded': 'TLE', 'Memory limit exceeded': 'MLE', 'Runtime error': 'RE', 'Compilation error': 'CE', 'Idleness limit exceeded': 'ILE', 'Presentation error': 'PE', 'Skipped': 'SK' }; let matched = false; let htmlStr = txt; for (let key in map) { const regex = new RegExp(key, 'gi'); if (regex.test(htmlStr)) { matched = true; htmlStr = htmlStr.replace(regex, `${map[key]}`); } } if (matched) { const span = document.createElement('span'); span.innerHTML = htmlStr; node.parentNode.replaceChild(span, node); } } else { const children = Array.from(node.childNodes); for (let i = 0; i < children.length; i++) { walkAndReplaceVerdict(children[i]); } } } // 1. Handle Status Tables and Hacks Tables by adding a new Rating column const statusTables = document.querySelectorAll('table.status-frame-datatable, div.datatable table:not(.standings):not(.problems)'); statusTables.forEach(table => { const headerRow = table.querySelector('tr'); if (!headerRow) return; // Find Time/When column index let timeColIdx = -1; let langColIdx = -1; let verdictColIdx = -1; let isHacks = window.location.href.includes('/hacks'); let isStatusOrHacks = false; Array.from(headerRow.cells).forEach((th, idx) => { const text = th.textContent.toLowerCase(); if (timeColIdx === -1 && (text.includes('when') || text.includes('time') || text.includes('时间') || text.includes('когда') || text.includes('date'))) { timeColIdx = idx; } if (langColIdx === -1 && (text.includes('lang') || text.includes('语言') || text.includes('язык'))) { langColIdx = idx; } if (verdictColIdx === -1 && (text.includes('verdict') || text.includes('结果') || text.includes('вердикт'))) { verdictColIdx = idx; } if (text.includes('hacker') || text.includes('defender')) { isHacks = true; isStatusOrHacks = true; } if (text.includes('problem') || text.includes('题目') || text.includes('задача')) { isStatusOrHacks = true; } }); if (!isStatusOrHacks) return; // Skip if it's not a status or hacks table (e.g., contest list) const path = window.location.pathname.toLowerCase(); const isSubmissionsPage = path.includes('/my') || path.includes('/submissions'); let shouldShowRating = false; if (isHacks) { shouldShowRating = appSettings.show.hacks; timeColIdx = -1; // Skip time formatting for hacks page } else if (isSubmissionsPage) { shouldShowRating = appSettings.show.submissions; } else { shouldShowRating = appSettings.show.status; } if (!shouldShowRating && !appSettings.timeFormat.enabled && langColIdx === -1) return; // Process Header (Rating Column and Time) if (!headerRow.hasAttribute('data-cf-rating-processed')) { headerRow.setAttribute('data-cf-rating-processed', 'true'); // Append timezone to Time column header if (timeColIdx !== -1 && appSettings.timeFormat && appSettings.timeFormat.enabled) { const th = headerRow.cells[timeColIdx]; let tzStr = 'UTC+3'; // Codeforces default server time (MSK) const firstDataRow = table.querySelector('tr:not(:first-child)'); if (firstDataRow && firstDataRow.cells[timeColIdx]) { const tzMatch = firstDataRow.cells[timeColIdx].textContent.match(/UTC[+-]?\d*(:\d+)?/i); if (tzMatch) { tzStr = tzMatch[0].toUpperCase(); } } th.innerHTML = `${th.innerHTML}
(${tzStr})`; if (!isSubmissionsPage) { th.style.setProperty('white-space', 'nowrap', 'important'); } } if (langColIdx !== -1 && appSettings.show && appSettings.show.langIcon !== false) { headerRow.cells[langColIdx].style.setProperty('text-align', 'left', 'important'); headerRow.cells[langColIdx].style.setProperty('white-space', 'nowrap', 'important'); } if (verdictColIdx !== -1 && appSettings.show && appSettings.show.shortVerdict) { headerRow.cells[verdictColIdx].style.setProperty('white-space', 'nowrap', 'important'); } if (shouldShowRating) { // Remove 'right' class from the previous last header cell const prevTh = headerRow.querySelector('th.right'); if (prevTh) prevTh.classList.remove('right'); // Create new Rating header const th = document.createElement('th'); th.className = 'top right'; th.style.textAlign = 'center'; th.style.width = '60px'; th.innerHTML = 'Rating'; headerRow.appendChild(th); } } // Process data rows const dataRows = table.querySelectorAll('tr:not(:first-child)'); dataRows.forEach(row => { // Time Formatting if (timeColIdx !== -1) { const timeCell = row.cells[timeColIdx]; if (timeCell && appSettings.timeFormat && appSettings.timeFormat.enabled) { const text = timeCell.textContent.trim(); // Prevent re-formatting if it already has our format (starts with YYYY/MM/DD or YYYY-MM-DD) if (!/^\d{4}[-\/]\d{2}[-\/]\d{2}/.test(text) && text.length >= 8 && /\d/.test(text)) { const newTime = formatTimeStr(text); if (newTime) { timeCell.innerHTML = newTime; } } if (!isSubmissionsPage) { timeCell.style.setProperty('white-space', 'nowrap', 'important'); } } } // Language Icon Formatting if (langColIdx !== -1) { const langCell = row.cells[langColIdx]; if (langCell && appSettings.show && appSettings.show.langIcon !== false) { langCell.style.setProperty('text-align', 'left', 'important'); if (!langCell.hasAttribute('data-cf-lang-icon-processed')) { langCell.setAttribute('data-cf-lang-icon-processed', 'true'); const langText = langCell.textContent.trim(); const iconName = getLanguageIconName(langText); if (iconName) { const sizePx = 14 * (appSettings.langIconSize || 1.0); const img = document.createElement('img'); let svgName = `${iconName}-original.svg`; let customSrc = null; if (iconName === 'go') svgName = 'go-original-wordmark.svg'; if (iconName === 'c') { customSrc = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMmM5YTQyIiBkPSJNMTE4Ljc2NiA5NS44MmMuODktMS41NDMgMS40NDEtMy4yOCAxLjQ0MS00Ljg0M1YzNi43OGMwLTEuNTU4LS41NS0zLjI5Ny0xLjQ0MS00Ljg0bC01NS4zMiAzMS45NFptMCAwIi8+PHBhdGggZmlsbD0iIzFiNmQyZSIgZD0ibTY4LjM2IDEyNi41ODYgNDYuOTMzLTI3LjA5NGMxLjM1Mi0uNzgxIDIuNTgyLTIuMTI5IDMuNDczLTMuNjcybC01NS4zMi0zMS45NEw4LjEyIDk1LjgyYy44OSAxLjU0MyAyLjEyMSAyLjg5IDMuNDczIDMuNjcybDQ2LjkzMyAyNy4wOTRjMi43MDMgMS41NjIgNy4xMyAxLjU2MiA5LjgzMiAwWm0wIDAiLz48cGF0aCBmaWxsPSIjNWNjYjc0IiBkPSJNMTE4Ljc2NiAzMS45NDFjLS44OTEtMS41NDYtMi4xMjEtMi44OTQtMy40NzMtMy42NzFMNjguMzU5IDEuMTcyYy0yLjcwMy0xLjU2My03LjEyOS0xLjU2My05LjgzMiAwTDExLjU5NCAyOC4yN0M4Ljg5IDI5LjgyOCA2LjY4IDMzLjY2IDYuNjggMzYuNzh2NTQuMTk2YzAgMS41NjIuNTUgMy4zIDEuNDQxIDQuODQzTDYzLjQ0NSA2My44OFptMCAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjQ0NSAyNi4wMzVjLTIwLjg2NyAwLTM3Ljg0MyAxNi45NzctMzcuODQzIDM3Ljg0NHMxNi45NzYgMzcuODQ0IDM3Ljg0MyAzNy44NDRjMTMuNDY1IDAgMjYuMDI0LTcuMjQ3IDMyLjc3LTE4LjkxTDc5Ljg0IDczLjMzNWMtMy4zOCA1Ljg0LTkuNjYgOS40NjUtMTYuMzk1IDkuNDY1LTEwLjQzMyAwLTE4LjkyMi04LjQ4OC0xOC45MjItMTguOTIyIDAtMTAuNDM0IDguNDktMTguOTIyIDE4LjkyMi0xOC45MjIgNi43MyAwIDEzLjAxNyAzLjYyOSAxNi4zOSA5LjQ2NWwxNi4zOC05LjQ3N2MtNi43NS0xMS42NjQtMTkuMzA1LTE4LjkxLTMyLjc3LTE4LjkxeiIvPjwvc3ZnPg=='; } if (iconName === 'd') { customSrc = 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjYjAxYzJlIiBkPSJNMTE4Ljc2NiA5NS44MmMuODktMS41NDMgMS40NDEtMy4yOCAxLjQ0MS00Ljg0M1YzNi43OGMwLTEuNTU4LS41NS0zLjI5Ny0xLjQ0MS00Ljg0bC01NS4zMiAzMS45NFptMCAwIi8+PHBhdGggZmlsbD0iIzhhMTIyMSIgZD0ibTY4LjM2IDEyNi41ODYgNDYuOTMzLTI3LjA5NGMxLjM1Mi0uNzgxIDIuNTgyLTIuMTI5IDMuNDczLTMuNjcybC01NS4zMi0zMS45NEw4LjEyIDk1LjgyYy44OSAxLjU0MyAyLjEyMSAyLjg5IDMuNDczIDMuNjcybDQ2LjkzMyAyNy4wOTRjMi43MDMgMS41NjIgNy4xMyAxLjU2MiA5LjgzMiAwWm0wIDAiLz48cGF0aCBmaWxsPSIjZDkzODRkIiBkPSJNMTE4Ljc2NiAzMS45NDFjLS44OTEtMS41NDYtMi4xMjEtMi44OTQtMy40NzMtMy42NzFMNjguMzU5IDEuMTcyYy0yLjcwMy0xLjU2My03LjEyOS0xLjU2My05LjgzMiAwTDExLjU5NCAyOC4yN0M4Ljg5IDI5LjgyOCA2LjY4IDMzLjY2IDYuNjggMzYuNzh2NTQuMTk2YzAgMS41NjIuNTUgMy4zIDEuNDQxIDQuODQzTDYzLjQ0NSA2My44OFptMCAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzUgMjYuMyB2NzUuNCBoMjAgYSAzNy43IDM3LjcgMCAwIDAgMCAtNzUuNCB6IE01MCA0MS4zIGg1IGEgMjIuNyAyMi43IDAgMCAxIDAgNDUuNCBoLTUgeiIvPjwvc3ZnPg=='; } img.src = customSrc || `https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/${iconName}/${svgName}`; img.style.cssText = `width: ${sizePx}px; height: ${sizePx}px; vertical-align: middle; margin-right: 5px;`; const textSpan = document.createElement('span'); textSpan.textContent = langText; textSpan.style.cssText = 'display: inline-block; max-width: 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle;'; langCell.innerHTML = ''; langCell.appendChild(img); langCell.appendChild(textSpan); langCell.title = langText; langCell.style.setProperty('white-space', 'nowrap', 'important'); langCell.style.setProperty('max-width', '140px', 'important'); } } } } // Verdict Abbreviation if (verdictColIdx !== -1 && appSettings.show && appSettings.show.shortVerdict) { const verdictCell = row.cells[verdictColIdx]; if (verdictCell && !verdictCell.hasAttribute('data-cf-verdict-processed')) { verdictCell.setAttribute('data-cf-verdict-processed', 'true'); walkAndReplaceVerdict(verdictCell); verdictCell.style.setProperty('white-space', 'nowrap', 'important'); } } if (row.hasAttribute('data-cf-rating-processed')) return; row.setAttribute('data-cf-rating-processed', 'true'); // Skip empty/info rows (like "No submissions found") if (row.cells.length <= 1) { if (row.cells.length === 1 && shouldShowRating) { row.cells[0].colSpan = (parseInt(row.cells[0].colSpan) || 1) + 1; } return; } if (!shouldShowRating) return; // Remove 'right' class from the previous last data cell const prevTd = row.querySelector('td.right'); if (prevTd) prevTd.classList.remove('right'); // Find rating from links in the row let problemRating = null; const links = row.querySelectorAll('a[href*="/problem/"]'); for (const link of links) { const info = getProblemRatingFromHref(link.href); if (info && info.rating) { problemRating = info.rating; } // Mark ALL problem links in the datatable so the standalone logic ignores them link.setAttribute('data-cf-rating-added', 'true'); } // Create new Rating cell const td = document.createElement('td'); td.className = 'right'; td.style.textAlign = 'center'; td.style.verticalAlign = 'middle'; if (problemRating) { applyRatingStyle(td, problemRating); } else { td.textContent = ''; } row.appendChild(td); }); }); // 1.5 Handle Standings tables specifically (adding a whole new row under the header) const standingsTables = document.querySelectorAll('table.standings:not([data-cf-rating-standings-processed])'); standingsTables.forEach(table => { if (!appSettings.show.standings) return; table.setAttribute('data-cf-rating-standings-processed', 'true'); const headerRow = table.querySelector('tr'); if (!headerRow) return; const ratingRow = document.createElement('tr'); ratingRow.className = 'cf-rating-standings-row'; let hasRatings = false; Array.from(headerRow.cells).forEach(cell => { const newCell = document.createElement('th'); newCell.style.padding = '0.3em'; // minimal padding const link = cell.querySelector('a[href*="/problem/"]'); if (link) { const info = getProblemRatingFromHref(link.href); if (info && info.rating) { hasRatings = true; applyRatingStyle(newCell, info.rating); if (appSettings.displayStyle === 'block') { newCell.style.setProperty('font-size', '0.9em', 'important'); newCell.style.setProperty('padding', '0.2em', 'important'); } // Mark the link so it's skipped by standalone processor link.setAttribute('data-cf-rating-added', 'true'); } } ratingRow.appendChild(newCell); }); if (hasRatings) { // Insert the new rating row right below the header row headerRow.parentNode.insertBefore(ratingRow, headerRow.nextSibling); } }); // 1.8 Handle Contest Problems tables specifically (adding a new column to the left of '#') const problemsTables = document.querySelectorAll('table.problems:not([data-cf-rating-problems-processed])'); problemsTables.forEach(table => { const isProblemset = window.location.pathname.toLowerCase().includes('/problemset'); const shouldShowRating = isProblemset ? appSettings.show.problemset : appSettings.show.contestProblems; table.setAttribute('data-cf-rating-problems-processed', 'true'); const headerRow = table.querySelector('tr'); if (headerRow && shouldShowRating) { const th = document.createElement('th'); th.className = 'top left'; th.style.width = '4em'; th.style.textAlign = 'center'; th.innerHTML = 'Rating'; const prevTh = headerRow.firstElementChild; if (prevTh && prevTh.classList.contains('left')) { prevTh.classList.remove('left'); } headerRow.insertBefore(th, headerRow.firstElementChild); } const dataRows = table.querySelectorAll('tr:not(:first-child)'); dataRows.forEach(row => { if (row.cells.length < 2) return; let td = null; const idCell = row.querySelector('td.id'); if (shouldShowRating) { td = document.createElement('td'); td.className = 'left'; td.style.textAlign = 'center'; td.style.verticalAlign = 'middle'; const prevTd = row.firstElementChild; if (prevTd && prevTd.classList.contains('left')) { prevTd.classList.remove('left'); } const link = idCell ? idCell.querySelector('a') : row.querySelector('a[href*="/problem/"]'); if (link) { const info = getProblemRatingFromHref(link.href); if (info && info.rating) { applyRatingStyle(td, info.rating); } const rowLinks = row.querySelectorAll('a[href*="/problem/"]'); rowLinks.forEach(l => l.setAttribute('data-cf-rating-added', 'true')); } row.insertBefore(td, row.firstElementChild); } // Fix the CF accepted/rejected status styling if (row.classList.contains('accepted-problem')) { if (shouldShowRating && idCell) { idCell.style.setProperty('border-left', '1px solid #e1e1e1', 'important'); } Array.from(row.cells).forEach(cell => { if (shouldShowRating && cell === td) return; cell.style.setProperty('background-color', appSettings.acBgColor, 'important'); }); } else if (row.classList.contains('rejected-problem')) { if (shouldShowRating && idCell) { idCell.style.setProperty('border-left', '1px solid #e1e1e1', 'important'); } Array.from(row.cells).forEach(cell => { if (shouldShowRating && cell === td) return; cell.style.setProperty('background-color', '#ffe3e3', 'important'); }); } }); }); // 3. Handle actual Problem Page tags (sidebar tags) const tagBoxes = document.querySelectorAll('span.tag-box:not([data-cf-rating-added])'); tagBoxes.forEach(tag => { if (!appSettings.show.problemTags) return; const text = tag.textContent.trim(); if (text.startsWith('*')) { const ratingMatch = text.match(/^\*\s*(\d+)$/); if (ratingMatch && ratingMatch[1]) { const rating = parseInt(ratingMatch[1], 10); tag.setAttribute('data-cf-rating-added', 'true'); const parentBox = tag.closest('.roundbox'); if (appSettings.displayStyle === 'tag') { const tagStyle = getRatingTagStyle(rating); if (parentBox) { parentBox.style.setProperty('background-color', tagStyle.bg, 'important'); parentBox.style.setProperty('border-color', tagStyle.border, 'important'); parentBox.style.setProperty('color', tagStyle.text, 'important'); } tag.style.setProperty('color', tagStyle.text, 'important'); } else { if (parentBox) { parentBox.style.setProperty('background-color', getRatingBgColor(rating), 'important'); parentBox.style.setProperty('border-color', getRatingBorderColor(rating), 'important'); if (rating >= 1600) { parentBox.style.setProperty('color', 'white', 'important'); } } tag.style.setProperty('color', rating >= 1600 ? 'white' : '#000', 'important'); } tag.style.setProperty('background-color', 'transparent', 'important'); } } }); } function formatTimeStr(text) { if (!appSettings.timeFormat.enabled) return null; // Extract any UTC suffix (e.g. "UTC+8", "UTC-5", "UTC+3") let tzSuffix = ''; const tzMatch = text.match(/UTC[+-]?\d*(:\d+)?/i); if (tzMatch) { tzSuffix = tzMatch[0].toUpperCase(); } // Clean text for parsing let cleanText = text.replace(/UTC.*$/i, '').trim(); let d = new Date(cleanText); if (isNaN(d.getTime())) { // Try parsing Russian format: DD.MM.YYYY HH:MM:SS const ruMatch = cleanText.match(/(\d{2})\.(\d{2})\.(\d{4})\s+(\d{2}:\d{2}(:\d{2})?)/); if (ruMatch) d = new Date(`${ruMatch[2]}/${ruMatch[1]}/${ruMatch[3]} ${ruMatch[4]}`); } if (!isNaN(d.getTime())) { return customFormatTime(d, appSettings.timeFormat.format); } return null; } function applyTimeFormatting() { const timeSpans = document.querySelectorAll('.format-time:not([data-custom-formatted]), .format-date:not([data-custom-formatted])'); timeSpans.forEach(span => { const text = span.textContent.trim(); if (text.length < 8) return; const newTime = formatTimeStr(text); if (newTime) { span.innerHTML = newTime; span.setAttribute('data-custom-formatted', 'true'); span.classList.remove('format-time'); span.classList.remove('format-date'); } }); } // Observe DOM changes to apply ratings to newly loaded elements (e.g. via AJAX/PJAX) function setupObserver(ratingsMap) { const observer = new MutationObserver((mutations) => { let shouldApply = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldApply = true; break; } } if (shouldApply) { applyRatings(ratingsMap); applyUserAvatars(); setTimeout(applyTimeFormatting, 500); } }); observer.observe(document.body, { childList: true, subtree: true }); } const AVATAR_CACHE_KEY = 'cf_user_avatars'; async function applyUserAvatars() { if (!appSettings.show.userAvatar) return; const userLinks = document.querySelectorAll('a[href^="/profile/"]:not([data-cf-avatar-processed])'); const handlesToFetch = new Set(); const handleToElements = {}; userLinks.forEach(link => { // Skip if it already has an img (like titlePhoto or similar icon) if (link.querySelector('img')) return; // Skip if it's inside a native CF avatar container or profile main-info if (link.closest('.avatar, .main-info')) return; // Skip if it's a post author or comment author (they already have native CF avatars) // Mentions inside the text body (.ttypography) should still receive avatars. if (link.closest('.comment, .topic') && !link.closest('.ttypography')) return; const href = link.getAttribute('href'); const match = href.match(/^\/profile\/([^/]+)$/i); if (match && link.textContent.trim().toLowerCase() === match[1].toLowerCase()) { link.setAttribute('data-cf-avatar-processed', 'true'); const handle = match[1]; handlesToFetch.add(handle); if (!handleToElements[handle]) handleToElements[handle] = []; handleToElements[handle].push(link); } }); if (handlesToFetch.size === 0) return; let avatarCache = {}; try { const cached = localStorage.getItem(AVATAR_CACHE_KEY); if (cached) avatarCache = JSON.parse(cached); } catch (e) { } const now = Date.now(); let missingHandles = []; for (const handle of handlesToFetch) { const cachedData = avatarCache[handle]; if (cachedData && (now - cachedData.time < CACHE_EXPIRY)) { injectAvatar(handleToElements[handle], cachedData.url); } else { missingHandles.push(handle); } } if (missingHandles.length > 0) { try { while (missingHandles.length > 0) { const url = `https://codeforces.com/api/user.info?handles=${missingHandles.join(';')}`; const response = await fetch(url); const data = await response.json(); if (data.status === 'OK') { for (const user of data.result) { const handle = user.handle; const avatarUrl = user.avatar; avatarCache[handle] = { url: avatarUrl, time: now }; injectAvatar(handleToElements[handle] || [], avatarUrl); } localStorage.setItem(AVATAR_CACHE_KEY, JSON.stringify(avatarCache)); break; } else if (data.status === 'FAILED' && data.comment) { const match = data.comment.match(/User with handle (.*?) not found/i); if (match) { const missing = match[1].toLowerCase(); missingHandles = missingHandles.filter(h => h.toLowerCase() !== missing); } else { break; } } else { break; } } } catch (e) { console.error('Codeforces Rating Helper: Failed to fetch user avatars', e); } } } function injectAvatar(elements, url) { if (!url) return; if (url.startsWith('//')) url = 'https:' + url; else if (url.startsWith('/')) url = 'https://codeforces.com' + url; elements.forEach(el => { const td = el.closest('td, th'); const size = appSettings.avatarSize || 1.4; const img = document.createElement('img'); img.src = url; img.style.cssText = `width: ${size}em; height: ${size}em; border-radius: 50%; vertical-align: middle; margin-right: 4px; border: 1px solid rgba(0,0,0,0.1); display: inline-block; object-fit: cover;`; const isTableLayout = td && el.closest('table.status-frame-datatable, div.datatable table, table.standings, table.rtable') && !el.closest('.ttypography'); if (isTableLayout) { // Ensure the column containing the user is left-aligned td.style.setProperty('text-align', 'left', 'important'); // Fetch or create the avatar container at the very beginning of the cell (before Team Name, flags, etc) let avatarContainer = td.querySelector('.cf-avatar-container'); if (!avatarContainer) { avatarContainer = document.createElement('span'); avatarContainer.className = 'cf-avatar-container'; avatarContainer.style.cssText = 'white-space: nowrap; vertical-align: middle; margin-right: 4px; display: inline-block;'; const lineWrapper = document.createElement('span'); lineWrapper.className = 'cf-avatar-line-wrapper'; lineWrapper.style.cssText = 'white-space: nowrap; display: inline-block; max-width: 100%;'; td.insertBefore(lineWrapper, td.firstChild); lineWrapper.appendChild(avatarContainer); let current = lineWrapper.nextSibling; while (current) { if (current.tagName === 'BR') break; const next = current.nextSibling; lineWrapper.appendChild(current); current = next; } } // Wrap the image in an anchor so it links to the user profile const anchor = document.createElement('a'); anchor.href = el.href; anchor.title = el.textContent.trim(); anchor.appendChild(img); avatarContainer.appendChild(anchor); } else { // Fallback for non-table elements (e.g., profile page, header) el.style.setProperty('white-space', 'nowrap', 'important'); el.insertBefore(img, el.firstChild); } }); } // Initialization async function init() { const ratingsMap = await getRatings(); applyRatings(ratingsMap); applyUserAvatars(); setTimeout(applyTimeFormatting, 500); setupObserver(ratingsMap); } function createSettingsUI() { if (document.getElementById('cf-ratings-settings-btn')) return; const i18n = { zh: { title: 'CFSR 插件设置', displayStyleTitle: 'Ratings 展示形式', styleBlock: '色块', styleTag: '标签', acColor: 'AC 背景色', locationsTitle: '色彩展示难度分的区域', locSubmissions: '提交', locStatus: '状态', locHacks: 'Hack', locProblemset: '题单', locContestProblems: '比赛题单', locStandings: '比赛榜单', locProblemTags: '题目页标签', locUserAvatar: '显示用户头像', locAvatarSize: '头像大小', locLangIcon: '显示语言图标', locLangIconSize: '语言图标大小', locShortVerdict: '显示状态缩写 (AC/WA等)', timeFormatTitle: '自定义时间格式', timeFormatPreview: '预览: ', timeFormatDisabled: '格式化已关闭', saveBtn: '保存并刷新' }, en: { title: 'CFSR Settings', displayStyleTitle: 'Ratings Display Format', styleBlock: 'Block', styleTag: 'Tag', acColor: 'AC Background', locationsTitle: 'Colorized Rating Display Locations', locSubmissions: 'Submissions', locStatus: 'Status', locHacks: 'Hacks', locProblemset: 'ProblemSet', locContestProblems: 'Contest Problems', locStandings: 'Contest Standings', locProblemTags: 'Problem Tags', locUserAvatar: 'User Avatars', locAvatarSize: 'Avatar Size', locLangIcon: 'Language Icons', locLangIconSize: 'Icon Size', locShortVerdict: 'Short Verdicts (AC/WA)', timeFormatTitle: 'Custom Time Format', timeFormatPreview: 'Preview: ', timeFormatDisabled: 'Disabled', saveBtn: 'Save & Reload' } }; let currentLang = appSettings.lang || 'zh'; const t = () => i18n[currentLang]; const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 70px; right: 20px; z-index: 999999; font-family: Arial, sans-serif; `; const btn = document.createElement('div'); btn.innerHTML = '⚙️'; btn.title = 'CF Submissions Ratings Settings'; btn.style.cssText = ` width: 44px; height: 44px; background: #ffffff; border: 1px solid #dcdcdc; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.1); transition: all 0.2s ease; user-select: none; `; btn.onmouseover = () => { btn.style.transform = 'scale(1.1)'; btn.style.boxShadow = '0 4px 15px rgba(0,0,0,0.15)'; }; btn.onmouseout = () => { btn.style.transform = 'scale(1)'; btn.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)'; }; const modal = document.createElement('div'); modal.style.cssText = ` position: absolute; top: 55px; right: 0; width: 320px; max-height: 85vh; overflow-y: auto; background: #ffffff; border: 1px solid #e1e1e1; border-radius: 8px; box-shadow: 0 5px 25px rgba(0,0,0,0.2); padding: 16px; display: none; flex-direction: column; gap: 15px; color: #333; `; const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 5px; `; const headerTitle = document.createElement('div'); headerTitle.style.cssText = 'font-size: 16px; font-weight: bold;'; const langBtn = document.createElement('div'); langBtn.style.cssText = ` display: flex; align-items: center; gap: 6px; padding: 4px 10px; background: #f5f5f5; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; color: #555; transition: all 0.2s ease; user-select: none; `; const globeSvg = ``; langBtn.innerHTML = `${globeSvg} `; langBtn.onmouseover = () => { langBtn.style.background = '#e8e8e8'; langBtn.style.color = '#111'; }; langBtn.onmouseout = () => { langBtn.style.background = '#f5f5f5'; langBtn.style.color = '#555'; }; const updateLangUI = () => { langBtn.querySelector('#cf-lang-text').textContent = currentLang === 'zh' ? '简体中文' : 'English'; }; updateLangUI(); let checkIfChanged = () => { }; langBtn.onclick = () => { currentLang = currentLang === 'zh' ? 'en' : 'zh'; updateTexts(); checkIfChanged(); }; header.appendChild(headerTitle); header.appendChild(langBtn); modal.appendChild(header); // Display Style Setting const rowStyle = document.createElement('div'); rowStyle.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;`; const labelStyle = document.createElement('span'); labelStyle.style.cssText = 'font-size: 14px; font-weight: bold;'; const styleSwitch = document.createElement('div'); styleSwitch.style.cssText = ` display: flex; position: relative; background: #f0f0f0; border-radius: 12px; padding: 2px; cursor: pointer; font-size: 12px; font-weight: bold; user-select: none; width: 120px; `; const slider = document.createElement('div'); slider.style.cssText = ` position: absolute; top: 2px; bottom: 2px; width: calc(50% - 2px); border-radius: 10px; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 1px 3px rgba(0,0,0,0.2); box-sizing: border-box; `; const styleBlockBtn = document.createElement('div'); const styleTagBtn = document.createElement('div'); let currentDisplayStyle = appSettings.displayStyle || 'block'; const updateStyleUI = () => { const btnBase = 'flex: 1; text-align: center; padding: 2px 0; font-size: 12px; z-index: 1; transition: color 0.25s; box-sizing: border-box; margin: 1px;'; if (currentDisplayStyle === 'block') { slider.style.left = '2px'; slider.style.background = getRatingBgColor(2400); slider.style.border = `1px solid ${getRatingBgColor(2400)}`; styleBlockBtn.style.cssText = `${btnBase} color: white;`; styleTagBtn.style.cssText = `${btnBase} color: #888;`; } else { slider.style.left = '50%'; const ts = getRatingTagStyle(2400); slider.style.background = ts.bg; slider.style.border = `1px solid ${ts.border}`; styleBlockBtn.style.cssText = `${btnBase} color: #888;`; styleTagBtn.style.cssText = `${btnBase} color: ${ts.text};`; } }; updateStyleUI(); styleBlockBtn.onclick = () => { currentDisplayStyle = 'block'; updateStyleUI(); checkIfChanged(); }; styleTagBtn.onclick = () => { currentDisplayStyle = 'tag'; updateStyleUI(); checkIfChanged(); }; styleSwitch.appendChild(slider); styleSwitch.appendChild(styleBlockBtn); styleSwitch.appendChild(styleTagBtn); rowStyle.appendChild(labelStyle); rowStyle.appendChild(styleSwitch); modal.appendChild(rowStyle); // AC Bg Color Setting const row1 = document.createElement('div'); row1.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;`; const label1 = document.createElement('span'); label1.style.cssText = 'font-size: 14px; font-weight: bold;'; const colorPickerContainer = document.createElement('div'); row1.appendChild(label1); row1.appendChild(colorPickerContainer); modal.appendChild(row1); // Initialize Pickr unified RGBA picker let selectedColor = appSettings.acBgColor; if (window.Pickr) { const pickr = Pickr.create({ el: colorPickerContainer, theme: 'nano', // Mimics Chrome devtools default: appSettings.acBgColor, position: 'top-end', components: { preview: true, opacity: true, hue: true, interaction: { hex: true, rgba: true, input: true, clear: false, save: false } } }); pickr.on('change', (color) => { selectedColor = color.toRGBA().toString(0); checkIfChanged(); }).on('save', () => { checkIfChanged(); }); } // User Avatar Setting const rowAvatar = document.createElement('label'); rowAvatar.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top: 5px; cursor: pointer; user-select: none;`; const labelAvatar = document.createElement('span'); labelAvatar.style.cssText = 'font-size: 14px; font-weight: bold;'; const toggleContainerAvatar = document.createElement('div'); toggleContainerAvatar.className = 'cf-toggle-switch'; const cbAvatar = document.createElement('input'); cbAvatar.type = 'checkbox'; cbAvatar.checked = appSettings.show.userAvatar; const sliderAvatar = document.createElement('span'); sliderAvatar.className = 'cf-toggle-slider'; toggleContainerAvatar.appendChild(cbAvatar); toggleContainerAvatar.appendChild(sliderAvatar); rowAvatar.appendChild(labelAvatar); rowAvatar.appendChild(toggleContainerAvatar); modal.appendChild(rowAvatar); // Avatar Size Setting const rowAvatarSize = document.createElement('div'); rowAvatarSize.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;`; const labelAvatarSize = document.createElement('span'); labelAvatarSize.style.cssText = 'font-size: 13px; color: #555;'; const avatarSizeInput = document.createElement('input'); avatarSizeInput.type = 'range'; avatarSizeInput.min = '0.8'; avatarSizeInput.max = '3.0'; avatarSizeInput.step = '0.1'; avatarSizeInput.value = appSettings.avatarSize || 1.4; avatarSizeInput.style.cssText = 'width: 100px; cursor: pointer;'; const avatarSizeVal = document.createElement('span'); avatarSizeVal.style.cssText = 'font-size: 12px; width: 28px; text-align: right; display: inline-block;'; avatarSizeVal.textContent = parseFloat(avatarSizeInput.value).toFixed(1) + 'x'; avatarSizeInput.oninput = () => { avatarSizeVal.textContent = parseFloat(avatarSizeInput.value).toFixed(1) + 'x'; checkIfChanged(); }; const sizeWrapper = document.createElement('div'); sizeWrapper.style.display = 'flex'; sizeWrapper.style.alignItems = 'center'; sizeWrapper.style.gap = '5px'; sizeWrapper.appendChild(avatarSizeInput); sizeWrapper.appendChild(avatarSizeVal); rowAvatarSize.appendChild(labelAvatarSize); rowAvatarSize.appendChild(sizeWrapper); modal.appendChild(rowAvatarSize); cbAvatar.onchange = () => { rowAvatarSize.style.display = cbAvatar.checked ? 'flex' : 'none'; checkIfChanged(); }; rowAvatarSize.style.display = cbAvatar.checked ? 'flex' : 'none'; // Lang Icon Setting const rowLangIcon = document.createElement('label'); rowLangIcon.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top: 5px; cursor: pointer; user-select: none;`; const labelLangIcon = document.createElement('span'); labelLangIcon.style.cssText = 'font-size: 14px; font-weight: bold;'; const toggleContainerLangIcon = document.createElement('div'); toggleContainerLangIcon.className = 'cf-toggle-switch'; const cbLangIcon = document.createElement('input'); cbLangIcon.type = 'checkbox'; cbLangIcon.checked = appSettings.show.langIcon !== false; const sliderLangIcon = document.createElement('span'); sliderLangIcon.className = 'cf-toggle-slider'; toggleContainerLangIcon.appendChild(cbLangIcon); toggleContainerLangIcon.appendChild(sliderLangIcon); rowLangIcon.appendChild(labelLangIcon); rowLangIcon.appendChild(toggleContainerLangIcon); modal.appendChild(rowLangIcon); cbLangIcon.onchange = () => { rowLangIconSize.style.display = cbLangIcon.checked ? 'flex' : 'none'; checkIfChanged(); }; // Lang Icon Size Setting const rowLangIconSize = document.createElement('div'); rowLangIconSize.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;`; const labelLangIconSize = document.createElement('span'); labelLangIconSize.style.cssText = 'font-size: 13px; color: #555;'; const langIconSizeInput = document.createElement('input'); langIconSizeInput.type = 'range'; langIconSizeInput.min = '0.5'; langIconSizeInput.max = '2.0'; langIconSizeInput.step = '0.1'; langIconSizeInput.value = appSettings.langIconSize || 1.0; langIconSizeInput.style.cssText = 'width: 100px; cursor: pointer;'; const langIconSizeVal = document.createElement('span'); langIconSizeVal.style.cssText = 'font-size: 12px; width: 28px; text-align: right; display: inline-block;'; langIconSizeVal.textContent = parseFloat(langIconSizeInput.value).toFixed(1) + 'x'; langIconSizeInput.oninput = () => { langIconSizeVal.textContent = parseFloat(langIconSizeInput.value).toFixed(1) + 'x'; checkIfChanged(); }; const sizeWrapperLang = document.createElement('div'); sizeWrapperLang.style.display = 'flex'; sizeWrapperLang.style.alignItems = 'center'; sizeWrapperLang.style.gap = '5px'; sizeWrapperLang.appendChild(langIconSizeInput); sizeWrapperLang.appendChild(langIconSizeVal); rowLangIconSize.appendChild(labelLangIconSize); rowLangIconSize.appendChild(sizeWrapperLang); modal.appendChild(rowLangIconSize); rowLangIconSize.style.display = cbLangIcon.checked ? 'flex' : 'none'; // Short Verdict Setting const rowShortVerdict = document.createElement('label'); rowShortVerdict.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top: 5px; cursor: pointer; user-select: none;`; const labelShortVerdict = document.createElement('span'); labelShortVerdict.style.cssText = 'font-size: 14px; font-weight: bold;'; const toggleContainerShortVerdict = document.createElement('div'); toggleContainerShortVerdict.className = 'cf-toggle-switch'; const cbShortVerdict = document.createElement('input'); cbShortVerdict.type = 'checkbox'; cbShortVerdict.checked = !!appSettings.show.shortVerdict; const sliderShortVerdict = document.createElement('span'); sliderShortVerdict.className = 'cf-toggle-slider'; toggleContainerShortVerdict.appendChild(cbShortVerdict); toggleContainerShortVerdict.appendChild(sliderShortVerdict); rowShortVerdict.appendChild(labelShortVerdict); rowShortVerdict.appendChild(toggleContainerShortVerdict); modal.appendChild(rowShortVerdict); cbShortVerdict.onchange = () => { checkIfChanged(); }; // Toggles Section const showSettingsMap = [ { key: 'submissions', labelKey: 'locSubmissions' }, { key: 'status', labelKey: 'locStatus' }, { key: 'hacks', labelKey: 'locHacks' }, { key: 'problemset', labelKey: 'locProblemset' }, { key: 'contestProblems', labelKey: 'locContestProblems' }, { key: 'standings', labelKey: 'locStandings' }, { key: 'problemTags', labelKey: 'locProblemTags' } ]; const showGroup = document.createElement('div'); showGroup.style.cssText = 'border-top: 1px solid #eee; padding-top: 10px; margin-top: 5px;'; const showTitle = document.createElement('div'); showTitle.style.cssText = 'font-size: 14px; font-weight: bold; margin-bottom: 8px;'; showGroup.appendChild(showTitle); const checkBoxes = {}; showSettingsMap.forEach(item => { const label = document.createElement('label'); label.style.cssText = 'display: flex; align-items: center; margin-bottom: 6px; font-size: 13px; cursor: pointer; user-select: none;'; const toggleContainer = document.createElement('div'); toggleContainer.className = 'cf-toggle-switch'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = appSettings.show[item.key]; const slider = document.createElement('span'); slider.className = 'cf-toggle-slider'; toggleContainer.appendChild(cb); toggleContainer.appendChild(slider); cb.onchange = () => { checkIfChanged(); }; checkBoxes[item.key] = cb; label.appendChild(toggleContainer); item.textNode = document.createTextNode(''); label.appendChild(item.textNode); showGroup.appendChild(label); }); modal.appendChild(showGroup); // Time Format Section const timeGroup = document.createElement('div'); timeGroup.style.cssText = 'border-top: 1px solid #eee; padding-top: 10px; margin-top: 5px;'; const timeTitleRow = document.createElement('div'); timeTitleRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'; const timeTitle = document.createElement('label'); timeTitle.style.cssText = 'font-size: 14px; font-weight: bold; cursor: pointer; user-select: none;'; const timeTitleTextNode = document.createTextNode(''); timeTitle.appendChild(timeTitleTextNode); const timeToggleContainer = document.createElement('div'); timeToggleContainer.className = 'cf-toggle-switch'; const timeToggle = document.createElement('input'); timeToggle.type = 'checkbox'; timeToggle.checked = appSettings.timeFormat.enabled; const timeSlider = document.createElement('span'); timeSlider.className = 'cf-toggle-slider'; timeToggleContainer.appendChild(timeToggle); timeToggleContainer.appendChild(timeSlider); timeTitle.appendChild(timeToggleContainer); timeTitle.insertBefore(timeToggleContainer, timeTitle.firstChild); timeTitleRow.appendChild(timeTitle); timeGroup.appendChild(timeTitleRow); const timeInput = document.createElement('input'); timeInput.type = 'text'; timeInput.value = appSettings.timeFormat.format; timeInput.placeholder = 'YYYY/MM/DD HH:mm'; timeInput.style.cssText = 'width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; box-sizing: border-box;'; const timePreview = document.createElement('div'); timePreview.style.cssText = 'margin-top: 6px; font-size: 12px; color: #666; font-family: monospace; text-align: right;'; const updatePreview = () => { if (timeToggle.checked) { const d = new Date(); timePreview.textContent = t().timeFormatPreview + customFormatTime(d, timeInput.value || 'YYYY/MM/DD HH:mm'); timeInput.disabled = false; timeInput.style.opacity = '1'; } else { timePreview.textContent = t().timeFormatDisabled; timeInput.disabled = true; timeInput.style.opacity = '0.5'; } }; timeInput.addEventListener('input', () => { updatePreview(); checkIfChanged(); }); timeToggle.addEventListener('change', () => { updatePreview(); checkIfChanged(); }); timeGroup.appendChild(timeInput); timeGroup.appendChild(timePreview); updatePreview(); modal.appendChild(timeGroup); const saveBtn = document.createElement('button'); saveBtn.style.cssText = ` margin-top: 10px; padding: 10px; background: #1890ff; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.2s; `; checkIfChanged = () => { let changed = false; if (currentLang !== appSettings.lang) changed = true; if (currentDisplayStyle !== appSettings.displayStyle) changed = true; if (selectedColor !== appSettings.acBgColor) changed = true; if (timeToggle.checked !== appSettings.timeFormat.enabled) changed = true; if ((timeInput.value || 'YYYY/MM/DD HH:mm') !== appSettings.timeFormat.format) changed = true; if (cbAvatar.checked !== appSettings.show.userAvatar) changed = true; if (cbLangIcon.checked !== (appSettings.show.langIcon !== false)) changed = true; if (cbShortVerdict.checked !== !!appSettings.show.shortVerdict) changed = true; if (parseFloat(langIconSizeInput.value) !== (appSettings.langIconSize || 1.0)) changed = true; if (parseFloat(avatarSizeInput.value) !== (appSettings.avatarSize || 1.4)) changed = true; showSettingsMap.forEach(item => { if (checkBoxes[item.key] && checkBoxes[item.key].checked !== (appSettings.show[item.key] ?? DEFAULT_SETTINGS.show[item.key])) { changed = true; } }); if (changed) { saveBtn.style.background = '#1890ff'; saveBtn.style.cursor = 'pointer'; saveBtn.style.opacity = '1'; saveBtn.disabled = false; saveBtn.onmouseover = () => saveBtn.style.background = '#40a9ff'; saveBtn.onmouseout = () => saveBtn.style.background = '#1890ff'; } else { saveBtn.style.background = '#d9d9d9'; saveBtn.style.cursor = 'not-allowed'; saveBtn.style.opacity = '0.7'; saveBtn.disabled = true; saveBtn.onmouseover = null; saveBtn.onmouseout = null; } }; saveBtn.onmouseover = () => saveBtn.style.background = '#40a9ff'; saveBtn.onmouseout = () => saveBtn.style.background = '#1890ff'; const updateTexts = () => { headerTitle.textContent = t().title; label1.textContent = t().acColor; labelAvatar.textContent = t().locUserAvatar; labelAvatarSize.textContent = t().locAvatarSize; labelLangIcon.textContent = t().locLangIcon; labelLangIconSize.textContent = t().locLangIconSize; labelShortVerdict.textContent = t().locShortVerdict; showTitle.textContent = t().locationsTitle; showSettingsMap.forEach(item => { if (item.textNode) item.textNode.textContent = t()[item.labelKey]; }); timeTitleTextNode.textContent = t().timeFormatTitle; labelStyle.textContent = t().displayStyleTitle; styleBlockBtn.textContent = t().styleBlock; styleTagBtn.textContent = t().styleTag; saveBtn.textContent = t().saveBtn; updatePreview(); updateLangUI(); checkIfChanged(); }; updateTexts(); saveBtn.onclick = () => { appSettings.acBgColor = selectedColor; if (!appSettings.show || typeof appSettings.show !== 'object') { appSettings.show = { ...DEFAULT_SETTINGS.show }; } appSettings.show.userAvatar = cbAvatar.checked; appSettings.show.langIcon = cbLangIcon.checked; appSettings.show.shortVerdict = cbShortVerdict.checked; appSettings.avatarSize = parseFloat(avatarSizeInput.value); appSettings.langIconSize = parseFloat(langIconSizeInput.value); showSettingsMap.forEach(item => { if (!appSettings.show || typeof appSettings.show !== 'object') { appSettings.show = { ...DEFAULT_SETTINGS.show }; } appSettings.show[item.key] = checkBoxes[item.key].checked; }); if (!appSettings.timeFormat || typeof appSettings.timeFormat !== 'object') { appSettings.timeFormat = { ...DEFAULT_SETTINGS.timeFormat }; } appSettings.timeFormat.enabled = timeToggle.checked; appSettings.timeFormat.format = timeInput.value || 'YYYY/MM/DD HH:mm'; appSettings.lang = currentLang; appSettings.displayStyle = currentDisplayStyle; saveSettings(appSettings); location.reload(); }; modal.appendChild(saveBtn); btn.onclick = () => { modal.style.display = modal.style.display === 'none' ? 'flex' : 'none'; }; container.appendChild(modal); container.appendChild(btn); document.body.appendChild(container); } // Run when the page is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { init(); createSettingsUI(); }); } else { init(); createSettingsUI(); } })();