您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
加强东南北+功能: 太多了请看功能简介
// ==UserScript== // @name 南加北加论坛强化脚本(凛+) // @version 94 // @description 加强东南北+功能: 太多了请看功能简介 // @author 遠坂凛 // @namespace tousakarin // @license MIT // @icon data:image/gif;base64,R0lGODlhEAAQAKIAAKIbG8dXV/339+atrfHMzOGXl9d3d7QxMSH5BAAAAAAALAAAAAAQABAAAANSKBPX0Ma8MBR7GBA5y81AdIihgxWW+I0PsURVAQoXNQwtB0EolYOTza0HPI0EtCKug5RlAsjZwtQSPJGEqIljGGa3IN+EJT55XDnUdHwgOCiCBAA7 // @grant unsafeWindow // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM.notification // @grant GM.openInTab // @grant GM.registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_notification // @grant GM_openInTab // @grant GM_registerMenuCommand // @match *://*.east-plus.net/* // @match *://east-plus.net/* // @match *://*.south-plus.net/* // @match *://south-plus.net/* // @match *://*.south-plus.org/* // @match *://south-plus.org/* // @match *://*.white-plus.net/* // @match *://white-plus.net/* // @match *://*.north-plus.net/* // @match *://north-plus.net/* // @match *://*.level-plus.net/* // @match *://level-plus.net/* // @match *://*.soul-plus.net/* // @match *://soul-plus.net/* // @match *://*.snow-plus.net/* // @match *://snow-plus.net/* // @match *://*.spring-plus.net/* // @match *://spring-plus.net/* // @match *://*.summer-plus.net/* // @match *://summer-plus.net/* // @match *://*.blue-plus.net/* // @match *://blue-plus.net/* // @match *://*.imoutolove.me/* // @match *://imoutolove.me/* // @run-at document-start // ==/UserScript== /* jshint undef: true, unused: true */ /* jshint -W107, -W098, -W069 */ /* globals unsafeWindow, window, document, navigator, atob, btoa, location, localStorage, fetch, console, alert, confirm, prompt, setInterval, setTimeout, clearTimeout, FileReader, FormData, Blob, URL, URLSearchParams, TextEncoder, CompressionStream, DecompressionStream, Response, MutationObserver, DOMParser, GM, GM_getValue, GM_setValue, GM_deleteValue, GM_listValues, GM_notification, GM_openInTab, GM_registerMenuCommand */ const VERSION_MAJOR = 94; const VERSION_FULL = 9400; const VERSION_TEXT = '94.0'; function initGM() { let gmExists = false; try { if (typeof GM.getValue == 'function') { gmExists = true; } } catch (ignore) {} if (gmExists) { return { getValue: GM.getValue, setValue: GM.setValue, async deleteValue(key) { return await GM.deleteValue(key); }, listValues: GM.listValues, notification: GM.notification, openInTab: GM.openInTab, registerMenuCommand: GM.registerMenuCommand }; } else { return { getValue: GM_getValue, setValue: GM_setValue, async deleteValue(key) { return GM_deleteValue(key); }, listValues: GM_listValues, notification: GM_notification, openInTab: GM_openInTab, registerMenuCommand: GM_registerMenuCommand }; } } (function() { "use strict"; const GM = initGM(); const INTRO_POST = document.location.origin + '/read.php?tid-2086932.html'; GM.registerMenuCommand('打开功能简介帖 (茶館)', () => GM.openInTab(INTRO_POST, false)); GM.registerMenuCommand('打开设置 (个人首页)', () => GM.openInTab(document.location.origin + '/u.php', false)); const MY_NAME_DISPLAY = 'Me'; const MAIN_CONFIG_KEY = 'my_config'; const DEFAULT_MAIN_CONFIG = { myUserHashId: null, myNickName: null, autoCheckReplyOption: true, enhanceSellFrame: true, buyRefreshFree: true, replyRefreshFree: true, enhancePageTitle: true, highlightMyself: true, heightReductionMode: false, heightReductionMode$LV2: false, showFloatingMessageIndicator: true, showFloatingStoreThreadButton: true, showFloatingWatchIndicator: true, hideWatchButtonIfEmpty: false, subCategoryInheritImageWallMode: true, dontFilterRequestReplyByUser: true, showInputLimit: true, hideDefaultUserpic: false, hideOtherUserpic: false, selfBypassHideUserpic: true, customUserBypassHideUserpic: true, stickyUserInfo: true, showExtendedUserInfo: true, showExtendedUserInfo$HP: false, hideIgnoreContentPost: false, disablePostCollapse$Self: true, treatAllEmojiTheSameWay: true, ignoreContentUseTextOnly: true, customUserBypassIgnoreList: true, customUserpicBypassIgnoreList: false, watchSkipIgnoreContent: true, customUserBypassWatchSkip: true, hideFilteredThread: false, requestThreadHighlightEnded: true, requestThreadShowExtraBounty: true, requestThreadUseHistoryData: true, siteAnnouncementSectionDefaultFolded: false, siteNoticeSectionDefaultFolded: false, showBackToTopButton: true, useCustomUserInfoPopup: true, hideInactivePinnedUsers: true, floatingShortcut: 'tr', textSize: 0, siteThemeDarkerSubjectLine: false, hideRedundantReSubjectLine: true, addQuickJumpReSubjectLine: true, threadListDefaultOpenNewPage: false, infiniteScrollReplaceURL: false, infiniteScrollScrollToNewPage: false, infiniteScroll$usertopics: false, infiniteScroll$userposts: false, infiniteScroll$threads: false, infiniteScroll$thread_posts: false, infiniteScroll$search: false, infiniteScroll$msg_inbox: false, showResourceSpotsFloating: true, showResourceSpots$sells: true, showResourceSpots$attachments: true, showResourceSpots$shares: true, showResourceSpots$images: true, showResourceSpots$links: false, showResourceSpots$likes: false, showFavThreadFloatingList: true, showFavThreadFloatingList$withTitle: true, showActiveRepliers: false, showActiveRepliers$min: 2, showShareTypeFilter: true, hideSettlementPost: true, hideSettlementPost$GreyoutOnly: true, hideSettlementPost$UseDefaultKeywords: true, hideSettlementPost$HighlightMyself: true, customUserBypassThreadFilter: true, showRequestThreadFilters: true, customUserHashIdMappings: { '#4': [4, '1eb7ddbc', '可可萝'], '@1eb7ddbc': [4, '1eb7ddbc', '可可萝'] }, customUserOrderBy: 'uid', userReplyListFolded: false, replyShownAsByOp: false, hideZeroReply: false, keepVisitPostHistory: true, showInitialRememberedTitle: false, hideFrontPageRecentList: false, v15migrationApplied: false, v42migrationApplied: false, openIntroAfterUpdate: true, autoSpTasks: true, showDefaultingPicWallOption: true, hideMobileVerSwitch: true, showSearchBar: false, showSearchBar$align: null, hasSeenAdminRole: false, adminNoScoreNotifByDefault: false, adminHideMarkUnscoreButton: false, adminHideMarkBadFormatButton: false }; const MIN_OVER_HEIGHT_STICKY_MODE_TRIGGER = 75; const THREAD_FILTER_EXEMPTED_USERS = new Set([4, 168153]); // 可可萝(站长), 平安魂加(总版主) const MIN_REQUEST_INTERVAL = 1100; const SYSTEM_MESSAGE_TITLES = new Set([ '您的文章被评分', '您的文章被取消评分', '您的文章标题被加亮显示', '您的文章被置顶.', '您的回复被设为最佳答案!', '您的回复获得热心助人奖 ..' ]); const DEFAULT_SCORING_PRESETS = { 'fid=201': { // COS区: 自购30天 / 优秀30天 headtopDaysBought: 30, headtopDaysCompilation: 30, extraScoreAdjustments: [ { label: '+200', amount: 200, reason: '第一类盘奖励' } ] }, 'fid=*': { // 各区默认: 自购7天 / 优秀30天 headtopDaysBought: 7, headtopDaysCompilation: 30, extraScoreAdjustments: [] } }; const SCORE_DIFF_ALLOWANCE = { 'fid=201': [220, 220], // COS区 'fid=*': [10, 100] // 各区默认 }; const SEARCH_CONFIG_KEY = 'my_search_pref'; const DEFAULT_SEARCH_CONFIG = { defaultSearchAll: false, defaultTimeRange: '31536000', pinnedTopics: {} }; const THREAD_CUSTOM_CATEGORY_CONFIG_KEY = 'my_thread_categories'; const DEFAULT_THREAD_CUSTOM_CATEGORY_CONFIG = { keywords: [] }; const THREAD_FILTER_CONFIG_KEY = 'my_thread_filter'; const DEFAULT_THREAD_FILTER_CONFIG = { dislikes: [], likes: [], settlementKeywords: [] }; const CONTENT_IGNORE_LIST_CONFIG_KEY = 'my_ignore_list'; const DEFAULT_CONTENT_IGNORE_LIST_CONFIG = { terms: [], exceptions: [] }; const USER_FILTER_CONFIG_KEY = 'my_user_filter'; const DEFAULT_USER_FILTER_CONFIG = { users: {} }; const PINNED_USERS_CONFIG_KEY = 'my_pinned_users'; const DEFAULT_PINNED_USERS_CONFIG = { users: {} }; const SHARETYPE_FILTER_CONFIG_KEY = 'my_sharetype_filter'; const DEFAULT_SHARETYPE_FILTER_CONFIG = { hides: [] }; const FAVOR_THREADS_CACHE_CONFIG_KEY = 'my_favor_threads'; const DEFAULT_FAVOR_THREADS_CACHE_CONFIG = { time: 0, tids: [] }; const QUESTION_AND_REQUEST_AREA_ID = 48; const DEFAULT_IGNORABLE_MARKER_TAG = '<IGNORABLE>'; const DEFAULT_SETTLEMENT_STOPWORD_PATTERN = /[谢謝][谢謝]|多[谢謝]|大佬|老哥|兄弟|[請请][进進]?|[给給]|收|用户/g; const DEFAULT_SETTLEMENT_KEYWORD_PATTERN = /^[奖獎][励勵][贴帖]|感[谢謝]|[结結][账賬]|[转轉][账賬]|[约約]定[贴帖]?|[請请][进進]|[结結][贴帖]|[结結]算[贴帖]?|^另?[结結]|[补補][悬懸][赏賞]|[补補]SP|[热熱]心助人|[热熱]心/g; const DEFAULT_SETTLEMENT_BLACKLIST_PATTERN = /^求(?!物)|资源|出处|视频|合集|完整版/; const DEFAULT_SETTLEMENT_TITLE_MAX_OTHER_TEXT_AMOUNT = 10; const ADMIN_TEMPLATE_CONFIG_KEY = 'my_admin_template'; const DEFAULT_ADMIN_TEMPLATEUSER_FILTER_CONFIG = { pingReasons: [ '请注意标题格式', '标题请注明大小', '标题禁用【】《》符号 敬请注意', '转帖不评分', '评分前已失效', '-----', '禁止盗链转存', '禁止净放BT', '-----', '广告帖', '政治贴', '恶意灌水', '无意义话题', '与版规不符', '重复话题', '重复发帖', '询问求物', '分类违规', '-----', '优秀文章', '原创内容', ] }; const PUNISH_TYPE_BAN = '禁言'; const PUNISH_TYPE_HP = '扣血'; const PUNISH_TYPE_DELETE_THREAD = '删帖'; const PUNISH_TYPE_DELETE_REPLY = '删除回复'; const PUNISH_TYPE_SP = '扣分'; const PUNISH_TYPE_SHIELD = '屏蔽'; const PUNISH_TYPE_UNSHIELD = '取消屏蔽'; const PUNISH_TYPE_CUSTOM = '备注'; const PUNISH_TYPES = [ PUNISH_TYPE_BAN, PUNISH_TYPE_HP, PUNISH_TYPE_DELETE_THREAD, PUNISH_TYPE_DELETE_REPLY, PUNISH_TYPE_SP, PUNISH_TYPE_SHIELD, PUNISH_TYPE_UNSHIELD, PUNISH_TYPE_CUSTOM ]; const REQUEST_ZONE_NAME = '询问&求物'; const NEVER_SCORE_USER_PREFIX = '[不评分]'; const TRUSTED_SCORE_USER_PREFIX = '[OK]'; const SYSTEM_SENDER_DISPLAY_NAME = 'SYSTEM'; const customCss = ` :root { --rinsp-active-toggler-bg-color: #EEEE; --rinsp-active-toggler-fg-color: #333; --rinsp-visited-link-color: #AAA; --rinsp-visited-link-hover-color: #333; --rinsp-visited-update-color: #7628A2; --rinsp-visited-update-border-color: #CAB2D7; --rinsp-blocked-row-bg: #F9F9F9; --rinsp-blocked-row-label-color: #AAA; --rinsp-avatar-replace-default-text-color: #AAA; --rinsp-avatar-replace-default-border-color: #EEE; --rinsp-avatar-replace-default-bg: #F8F8F8; --rinsp-cell-background-grey-out: #EEE; --rinsp-cell-background-hgt-new: #FFFFC5; --rinsp-cell-background-hgt-err: #FFC5C5; --rinsp-cell-background-active: #e4f2f7; --rinsp-lightest-bg-color: #FCFCFC; --rinsp-darkest-text-color: #000; --rinsp-text-color-orange: ##ffa500; --rinsp-text-color-green: #06a426; --rinsp-text-color-red: #ea2a2a; --rinsp-text-color-blue: #161884; --rinsp-text-color-violet: #8c19ac; --rinsp-text-color-grey: #999; --rinsp-infscroll-height: 55px; --rinsp-infscroll-divider-text-color: #3a6776; --rinsp-infscroll-divider-border-color: #a3b7b8; --rinsp-infscroll-divider-background: #ebf6f7; --rinsp-infscroll-loader-border-color: #CCC; --rinsp-infscroll-loader-background: #F5F5F5; --rinsp-infscroll-loader-progress: #EEE; --rinsp-image-masking-filter: contrast(0%) brightness(200%) drop-shadow(0px 0px 1px #000); --rinsp-pin-icon: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAARJJREFUeNpi/P//PwMlgImQgn/ffrT/+/a3HacCkAsw8J/X7b96Zv3/ztWLwJ7L/v+886sdXS0WA761/+qaAtW45v+fi7/+///37///3x///6le/v/Xx/8ohmB64dtVnX8NP6EcLgYGARYGBkZGoGeB+N0zhn87X1cgK2fBMODjNyTOdYbfQV8Z/iqxAg3+wPBvP9Asu78EAlFS8QqjJ7IZjxj+bb0L1PwWyGFlYFQT7SAQC7KVrD1GHYzS6OJALzQ4MLAZMVcSjgUg/vf+YfuvyUv+f4ma9f9L0cb/v259bMemDqcBYAw05PqtO/+v33n+H5caJgYKwcAbwIJflp2BixNoBxMHTiWMNM+NhABAgAEAghbydDDaUO8AAAAASUVORK5CYII=); } #mainNav #user-login { white-space: nowrap; } #mainNav #user-login a[href="u.php"] { max-width: 8em; overflow: hidden; text-overflow: ellipsis; display: inline-block; white-space: nowrap; vertical-align: middle; } #guide.guide { padding: 0; } .rinsp-intro-link { margin-left: 7px; color: #288407; font-weight: bold; } .rinsp-dev-toggle { opacity: 0; border: none; outline: none !important; } .rinsp-dev-toggle:focus { opacity: 0.1; } /* === message box add alternating background === */ form[action="message.php"] .set-table2 > tbody > tr:NOT(.gray3):nth-child(odd) { background-color: #b4d8e524; } form[action="message.php"]:not([onsubmit="return checkCnt();"]) .set-table2 > tbody > tr:NOT(.gray3):hover { background-color: #ead67924; } /* ====== truncate long post title in crumbs ====== */ .crumbs-item.current { max-width: 55%; } .crumbs-item.current strong a { max-width: 100%; overflow: hidden; text-overflow: ellipsis; display: inline-block; white-space: nowrap; vertical-align: top; } /* ====== watcher function ====== */ #rinsp-watcher-menu { margin-left: 40px; } .rinsp-common-popup-menu > .bor { max-height: calc(90vh - 100px); overflow: auto; overflow-x: hidden; } .rinsp-notification-item.rinsp-notification-item-type-watch > .rinsp-notification-item-count { color: #ee9e05; } .rinsp-excontrol-item-watch { color: #999; --rinsp-excontrol-base-hover-color: #f06629; --rinsp-excontrol-active-color: #aa0920; --rinsp-excontrol-active-hover-color: #000; } .new-msg-tips, .new-msg-tips + span { transform: translateX(-55px); } .readbot li.rinsp-watch { width: 7em; } .readbot li.rinsp-watch::after { content: "未开启"; color: #BBB; } .readbot li.rinsp-watch:hover::after { color: var(--rinsp-darkest-text-color); } .readbot li.rinsp-running { pointer-events: none; cursor: wait; } .readbot li.rinsp-running::after { content: "工作中"; color: var(--rinsp-text-color-orange); font-weight: bold; } .readbot li.rinsp-watch.rinsp-active::after { content: "已开启"; color: var(--rinsp-text-color-green); font-weight: bold; } tr.rinsp-expired > td { background: var(--rinsp-cell-background-grey-out); } tr.rinsp-expired:hover > td { opacity: 0.5; } tr.rinsp-expired > .rinsp-bounty-cell { color: var(--rinsp-text-color-red); font-weight: bold; } tr.rinsp-new > td { background: var(--rinsp-cell-background-hgt-new); } tr.rinsp-error > td { background: var(--rinsp-cell-background-hgt-err); } tr.rinsp-new > td.rinsp-status-cell { font-weight: bold; font-size: 110%; color: var(--rinsp-text-color-green); } tr.rinsp-ignorable > td.rinsp-status-cell { color: var(--rinsp-text-color-blue); } tr:not(.rinsp-new):not(.rinsp-error):not(.rinsp-ignorable) > td.rinsp-status-cell { font-size: 90%; color: #666; } td.rinsp-action-bar { text-align: left; } .rinsp-check-now { cursor: pointer; padding: 1px 5px; display: inline-block; } .rinsp-check-now:hover { background: #DDD; } html.rinsp-dark-mode .rinsp-check-now:hover { background: var(--rinsp-dm-color-darkbg-hover); } .rinsp-limit-warning { color: red; font-weight: bold; font-size: 1.1em; background: #fffa6a; padding: 1px 5px; display: inline-block; float: right; } .rinsp-notification-container { position: fixed; left: calc(50vw - 515px); top: 100px; display: flex; flex-direction: column; flex-wrap: wrap; z-index: 2; } .rinsp-tpcontrol-container, .rinsp-bmcontrol-container, .rinsp-excontrol-container { position: fixed; left: calc(50vw + 475px); display: flex; flex-direction: column; flex-wrap: wrap; } .rinsp-excontrol-container { top: 50vh; transform: translateY(-50%); } .rinsp-tpcontrol-container { top: 100px; } .rinsp-tpcontrol-container .rinsp-excontrol-item { width: 1.3em; text-align: center; padding: 1px 2px; } .rinsp-bmcontrol-container { bottom: 12px; } @media only screen and (max-width: 1024px) { .rinsp-notification-container { left: 9px; } .rinsp-tpcontrol-container, .rinsp-bmcontrol-container, .rinsp-excontrol-container { left: unset; right: 9px; } } .rinsp-excontrol-item { color: #999; padding: 1px 4px; background: #333C; box-shadow: 2px 2px 1px 0 #0009; white-space: pre; text-align: center; font-size: 14px; line-height: 18px; flex: 0; margin: 8px 0; cursor: pointer; } .rinsp-excontrol-item:hover { text-decoration: none; } .rinsp-excontrol-item-hidden { display: none; } .rinsp-excontrol-item:not(:hover) { opacity: 0.75; } .rinsp-excontrol-item.rinsp-excontrol-item-ticker:not(hover) { opacity: 0.9; } .rinsp-excontrol-item.rinsp-excontrol-item-ticker:hover { outline: 3px solid var(--rinsp-excontrol-base-hover-color); } .rinsp-excontrol-item.rinsp-excontrol-item-ticker:not(.rinsp-active) { margin-bottom: 24px; } .rinsp-excontrol-item.rinsp-excontrol-item-ticker.rinsp-active { outline: 3px solid var(--rinsp-excontrol-active-color); color: var(--rinsp-active-toggler-fg-color); font-weight: bold; background: var(--rinsp-active-toggler-bg-color); } .rinsp-excontrol-item.rinsp-excontrol-item-ticker.rinsp-active:hover { outline: 3px solid var(--rinsp-excontrol-active-hover-color); } .rinsp-excontrol-item.rinsp-excontrol-item-ticker.rinsp-active:after { content: '✔'; display: block; height: 16px; overflow: hidden; font-size: 12px; color: var(--rinsp-excontrol-active-color); } .rinsp-excontrol-item.rinsp-running { pointer-events: none; opacity: 0.3; } .rinsp-notification-item > .rinsp-notification-item-count { font-family: monospace; font-weight: bold; font-size: 17px; } .rinsp-excontrol-item > .rinsp-excontrol-item-count { font-family: monospace; font-weight: bold; font-size: 15px; margin: 0 -0.5em; } .rinsp-excontrol-item > .rinsp-excontrol-item-count-3d { font-size: 13px; transform: scaleX(0.85); display: inline-block; } .rinsp-excontrol-item:after { font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #999; } /* == scoring filter toggler == */ .rinsp-highlight-unscored-thread-toggler:hover, .rinsp-highlight-unscored-thread-mode .rinsp-highlight-unscored-thread-toggler:hover { outline: 5px solid #ce0940; } .rinsp-highlight-unscored-thread-toggler:after { content: '\\a评\\a分\\a模\\a式'; } .rinsp-highlight-unscored-thread-mode .rinsp-highlight-unscored-thread-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #ce0940; opacity: 1.0; } .rinsp-highlight-unscored-thread-mode .rinsp-highlight-unscored-thread-toggler:after { content: '\\a评\\a分\\a模\\a式'; color: var(--rinsp-active-toggler-fg-color); } .rinsp-highlight-unscored-thread-toggler > .rinsp-excontrol-item-count { color: #ce0940; } .rinsp-highlight-unscored-thread-mode .rinsp-thread-filter-scored { display: none; } .rinsp-highlight-unscored-thread-mode .rinsp-thread-filter-scored + .tr3:not(.t_one) { /* this is a management action button row */ display: none; } .rinsp-highlight-unscored-thread-mode .rinsp-thread-filter-miscored h3 + .gray.tpage { color: #ce2323; font-weight: bold; } .rinsp-highlight-unscored-thread-mode .rinsp-thread-filter-unscored > td:first-child { border-left: 4px solid #d90927; } .rinsp-highlight-unscored-thread-mode .rinsp-post-gf.rinsp-post-unscored { border-left: 4px solid #ce0940 !important; } .rinsp-excontrol-totop:hover { outline: 3px solid #CCC; } /* favor button */ .rinsp-excontrol-item-favor { color: #999; --rinsp-excontrol-base-hover-color: #f9c342; --rinsp-excontrol-active-color: #ce972d; --rinsp-excontrol-active-hover-color: #000; } /* == settlement ignore toggler == */ .rinsp-request-settlement-greyout-mode .rinsp-settlement-ignore-toggler { display: none; } .rinsp-settlement-ignore-toggler:hover, .rinsp-settlement-peek-mode .rinsp-settlement-ignore-toggler:hover { outline: 5px solid #2d9126; } .rinsp-settlement-ignore-toggler > .rinsp-excontrol-item-count { color: #09b93d; } .rinsp-settlement-ignore-toggler:after { content: '\\a隐\\a藏\\a结\\a算'; } .rinsp-settlement-peek-mode .rinsp-settlement-ignore-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #2d9126; opacity: 1.0; } .rinsp-settlement-peek-mode .rinsp-settlement-ignore-toggler:after { content: '\\a显\\a示\\a结\\a算'; color: var(--rinsp-active-toggler-fg-color); } /* == content ignore toggler == */ .rinsp-content-ignore-toggler:hover, .rinsp-filter-peek-mode .rinsp-content-ignore-toggler:hover { outline: 5px solid #8873ec; } .rinsp-content-ignore-toggler > .rinsp-excontrol-item-count { color: #8873ec; } .rinsp-content-ignore-toggler:after { content: '\\a屏\\a蔽\\a内\\a容'; } .rinsp-filter-peek-mode .rinsp-content-ignore-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #8873ec; opacity: 1.0; } .rinsp-filter-peek-mode .rinsp-content-ignore-toggler:after { content: '\\a显\\a示\\a屏\\a蔽'; color: var(--rinsp-active-toggler-fg-color); } /* == paywall ignore toggler == */ .rinsp-paywall-ignore-toggler:hover, .rinsp-paywall-peek-mode .rinsp-paywall-ignore-toggler:hover { outline: 5px solid #FF40AC; } .rinsp-paywall-ignore-toggler > .rinsp-excontrol-item-count { color: #FF40AC; } .rinsp-paywall-ignore-toggler:after { content: '\\a网\\a赚\\a区'; } .rinsp-paywall-peek-mode .rinsp-paywall-ignore-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #FF40AC; opacity: 1.0; } .rinsp-paywall-peek-mode .rinsp-paywall-ignore-toggler:after { color: var(--rinsp-active-toggler-fg-color); } /* == visited thread toggler == */ .rinsp-visited-thread-toggler:hover, .rinsp-visited-thread-mask-mode .rinsp-visited-thread-toggler:hover { outline: 5px solid #AE54AE; } .rinsp-visited-thread-toggler > .rinsp-excontrol-item-count { color: #AE54AE; } .rinsp-visited-thread-toggler:after { content: '\\a已\\a读'; } .rinsp-visited-thread-mask-mode .rinsp-visited-thread-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #AE54AE; opacity: 0.7; } .rinsp-visited-thread-mask-mode .rinsp-visited-thread-toggler:after { color: var(--rinsp-active-toggler-fg-color); } /* == closed thread toggler == */ .rinsp-closed-thread-toggler:hover, .rinsp-closed-thread-view-mode .rinsp-closed-thread-toggler:hover { outline: 5px solid #AAA; } .rinsp-closed-thread-toggler > .rinsp-excontrol-item-count { color: #AAA; } .rinsp-closed-thread-toggler:after { content: '\\a已\\a关\\a闭'; } .rinsp-closed-thread-view-mode .rinsp-closed-thread-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #999; color: #999; opacity: 0.8; } .rinsp-closed-thread-view-mode .rinsp-closed-thread-toggler:after { color: var(--rinsp-active-toggler-fg-color); } /* == floating notitication == */ .rinsp-notification-item { background: #FFFE; padding: 1px 3px; box-shadow: 2px 2px 1px 0 #9d9d9d; white-space: pre; text-align: center; font-weight: bold; animation-name: rinsp-watch-new-anim; animation-duration: 1s; animation-direction: alternate; animation-iteration-count: infinite; font-size: 14px; line-height: 20px; flex: 0; margin: 0.5em 0; cursor: pointer; display: none; } .rinsp-notification-item.rinsp-notification-item-type-pm { background-color: #e1feff; color: #354e6f; outline-color: #06c3cc !important; } .rinsp-notification-item:hover { animation: none; outline: 5px solid #f9c23c; text-decoration: none; } .rinsp-notification-item.rinsp-status-new, .rinsp-notification-item.rinsp-status-error { display: block; } .rinsp-notification-item.rinsp-status-error { color: #b91a1a; outline-color: #b91a1a !important; } #user-login .rinsp-watch-menuitem { color: white; padding: 1px 3px; display: inline-block; } .rinsp-watchemnu-autohide .rinsp-watch-menuitem:not(.rinsp-status-enabled) { display: none !important; } .rinsp-watch-menuitem.rinsp-status-new { color: black; font-weight: bold; animation-name: rinsp-watch-new-anim; animation-duration: 1s; animation-direction: alternate; animation-iteration-count: infinite; } .rinsp-watch-menuitem.rinsp-status-new:not(.rinsp-checking)::before { content: attr(data-new-count) " "; color: #000; display: inline-block; font-family: monospace; font-size: 12px; font-weight: bold; border-radius: 16px; background: #f9c23c; padding: 0 0.3em; text-align: center; min-width: 8px; margin-right: 0.2em; text-shadow: 0px 0px 1px white, 0px 0px 2px white, 0px 0px 3px white, 0px 0px 4px white; } .rinsp-watch-menuitem.rinsp-checking { font-weight: normal; } .rinsp-watch-menuitem.rinsp-checking.rinsp-status-new { outline: 3px solid #f9c23c; } #rinsp-watcher-menu .rinsp-checking .rinsp-status-cell::before, .rinsp-watch-menuitem.rinsp-checking::before { content: "⌛"; color: white; display: inline-block; animation-name: rinsp-watch-checking-rotate-anim; animation-duration: 2s; animation-iteration-count: infinite; } .rinsp-reply-self > .rinsp-lastreply-floor { color: var(--rinsp-text-color-violet); } .readbot li.rinsp-running::after { content: "工作中"; color: var(--rinsp-text-color-orange); font-weight: bold; } .rinsp-watch-menuitem.rinsp-status-error::after { content: "⚠️"; } .rinsp-bounty-ended { color: var(--rinsp-text-color-red) !important; font-weight: bold; } .rinsp-bounty-answered { color: var(--rinsp-text-color-green) !important; font-weight: bold; } @keyframes rinsp-watch-new-anim { 50% { outline: none; } 51% { outline: 3px solid #f9c23c; } 100% { outline: 3px solid #f9c23c; } } @keyframes rinsp-watch-checking-rotate-anim { 0% { transform: rotate(0deg); } 5% { transform: rotate(0deg); } 45% { transform: rotate(180deg); } 55% { transform: rotate(180deg); } 95% { transform: rotate(360deg); } 100% { transform: rotate(360deg); } } /* ====== sell-frame ====== */ h6.jumbotron .btn-danger { margin-left: 0.5em; } .jumbotron.rinsp-sell-free .s3 { color: #999; } .jumbotron .btn-danger { line-height: 16px !important; } .jumbotron.rinsp-sell-free .btn-danger { background: #516ba0; } .jumbotron.rinsp-sell-5 .btn-danger { background: #937210; } .jumbotron.rinsp-sell-100 .s3 { color: #DD0000; } .jumbotron.rinsp-sell-high .s3 { color: #DD0000; font-size: 1.2em; background: #fff9c7; outline: 2px solid #fff9c7; } .jumbotron.rinsp-sell-high .btn-danger { background: #DD0000; } .jumbotron.rinsp-sell-99999 .btn-danger { opacity: 0.2; pointer-events: none; background: #999; } .rinsp-sell-relay-button + .spp-buy-refresh-free, .rinsp-sell-enhanced { max-width: 0; max-height: 0; padding: 0 !important; overflow: hidden; z-index: -1; visibility: hidden; position: absolute; pointer-events: none; } .rinsp-sell-buying { opacity: 0.5; pointer-events: none; cursor: wait; } .rinsp-att-enhanced.rinsp-att-sell > a:before { content: "「售价 " attr(data-price) "SP」"; color: #000; font-weight: bold; font-size: 13px; } .rinsp-att-enhanced.rinsp-att-sell-high > a:before { content: "「售价 " attr(data-price) "SP - 高额出售 欢迎举报」"; color: #DD0000; font-weight: bold; font-size: 14px; } .rinsp-att-enhanced.rinsp-att-sell > a { cursor: default; } .rinsp-buy-failed { background-color: #fffdcf; border-color: black; color: red; font-weight: bold; } /* ====== user display ====== */ th.r_two > .user-pic > table { position: relative; z-index: 2; } .rinsp-userframe-userinfo { margin-top: -50px; padding: 50px 9px 3px 9px; background: linear-gradient(to bottom, transparent 0%, #F7F7F7 100%); display: flex; flex-wrap: wrap; position: relative; z-index: 1; margin-left: -5px; max-width: 185px; box-sizing: border-box; } .rinsp-userframe-userinfo > dl { display: inline-flex; align-items: center; margin: 0; } .rinsp-userframe-userinfo > .rinsp-userframe-udata-spacer { margin: 0; flex-basis: 100%; } .rinsp-userframe-userinfo > dl > dt { color: var(--rinsp-text-color-blue); word-break: keep-all; margin: 0; } .rinsp-userframe-userinfo > dl > dd { padding-left: 0.5em; flex: 1; margin: 0; } .rinsp-userframe-udata-sp { flex-grow: 1; } .rinsp-userframe-udata-login { flex-grow: 1; text-align: right; font-size: 0.9em; color: #888; } .rinsp-userframe-udata-login-today { color: var(--rinsp-text-color-green); } .rinsp-userframe-udata-login-yesterday { color: var(--rinsp-text-color-violet); } .rinsp-userframe-udata-hp { color: #DD0000; font-weight: bold; margin-left: 6px; display: inline-block; } .rinsp-userframe-udata-online > dt { display: none; } .rinsp-userframe-udata-online > dd { color: #666; } .r_two.rinsp-userframe-unnamed > .rinsp-userframe-userinfo + div > a { color: #555; } /* ===== user popup action items ===== */ .rinsp-user-popup-action-mailto > a > div { width: 16px; height: 16px; background: url(/images/colorImagination/mail-icon.gif); background-repeat: no-repeat; background-position: 1px 3px; } .rinsp-user-popup-action-topics > a > div { width: 16px; height: 16px; background: url(data:image/gif;base64,R0lGODlhEAAQAOZCAP///1iHuOfu9VqIueny/p241f3+/ouszvP3+rTJ387c6tfo/XCYwuvx94mqzZOx0dvq/WyVwKnB2uDt/uLu/vf5/Pv8/d7s/tDj/dDk/Xugx+Hu/tzm8H2iyNPm/dXm/V+Mu1yKul2Lutzr/drk72+Xwtnp/dzq/ebx/ujx/t3s/tHk/eXw/tHk/PD2/nWcxO/z+L7Q47XK4NTm/ePv/unv9pm21Nbn/aO92G6XwYWny+Tv/trp/d/s/t3r/q3E3Obw/vn7/P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAEIALAAAAAAQABAAAAeAgEKCg4SFhoeIiYUIDyEBj5ABOgKEBxIWAJmaAAkMFoIVAwYuBCg7GxcQJwAaHIINEQApQDQTKhALHwAOCq+xLBQTPjwLMyu7vUKhBgQ9IyY3HhktAB0kg5YGmhQYBJwMQYOMA48FmTAFL5SIJQIFIj+KODYgMopCNTkx9/z9hIEAOw==); } .rinsp-user-action-pinuser-icon, .rinsp-user-popup-action-pinuser > a > div { width: 16px; height: 16px; background: var(--rinsp-pin-icon); } .rinsp-user-action-pinuser-icon { display: inline-block; } .rinsp-user-popup-action-list > li:hover > a > div { outline: 1px solid #DDD; outline-offset: 1px; box-shadow: 2px 1px 3px #DDD; } /* ===== user names ===== */ span.rinsp-nickname-byother { color: #4a4a4a; font-weight: bold; } span.rinsp-nickname-byowner { color: #160; font-weight: bold; } span.rinsp-nickname-byme { color: #c60e87; font-weight: bold; } .rinsp-nickname-bypinned { color: var(--rinsp-darkest-text-color); font-weight: bold; } .rinsp-nickname-bypinned:before { content: ""; display: inline-block; width: 16px; height: 16px; background: var(--rinsp-pin-icon); } a.rinsp-owner-isme { color: #c60e87; font-weight: bold; } a.rinsp-owner-isknown { font-weight: bold; } .rinsp-byop-noreply-hide-mode td:not(:hover) > .rinsp-nickname-byop-only { visibility: hidden; } /* ====== config ====== */ .rinsp-config-panel.rinsp-config-saving { cursor: wait; } .rinsp-config-panel.rinsp-config-saving .rinsp-config-list { opacity: 0.5; pointer-events: none; } .rinsp-config-list { margin: 0 5px; } .rinsp-config-list > dt { margin-top: 5px; } .rinsp-config-list > dt > a { margin-left: 3px; color: #0060df; cursor: pointer; } .rinsp-config-list > dt > a:hover { text-decoration: underline; } .rinsp-config-item-sep { margin-top: 0.5em; padding-top: 0.5em; border-top: 1px dotted #AAA; } .rinsp-config-item-tip { color: #105884; padding-left: 4px; } .rinsp-config-item-lv1 > span { margin-left: 3px; } .rinsp-config-item-lv2 { margin-left: 20px; } .rinsp-config-item-lv3 { margin-left: 40px; } .rinsp-config-item-lv1:not(.rinsp-config-item-checked) + .rinsp-config-item-lv2:not(.rinsp-config-item-tip), .rinsp-config-item-lv1:not(.rinsp-config-item-checked) + .rinsp-config-item-lv2 + .rinsp-config-item-lv2:not(.rinsp-config-item-tip), .rinsp-config-item-lv1:not(.rinsp-config-item-checked) + .rinsp-config-item-lv2 + .rinsp-config-item-lv2 + .rinsp-config-item-lv2:not(.rinsp-config-item-tip) { opacity: 0.2; pointer-events: none; } .rinsp-config-item-lv2:not(.rinsp-config-item-checked) + .rinsp-config-item-lv3:not(.rinsp-config-item-tip), .rinsp-config-item-lv2:not(.rinsp-config-item-checked) + .rinsp-config-item-lv3 + .rinsp-config-item-lv3:not(.rinsp-config-item-tip), .rinsp-config-item-lv2:not(.rinsp-config-item-checked) + .rinsp-config-item-lv3 + .rinsp-config-item-lv3 + .rinsp-config-item-lv3:not(.rinsp-config-item-tip) { opacity: 0.2; pointer-events: none; } .rinsp-user-map-icon { margin-left: 5px; cursor: pointer; } .rinsp-user-map-icon.rinsp-config-saving { opacity: 0.5; pointer-events: none; } .rinsp-user-map-icon::after { content: "➕收藏"; font-weight: normal; font-size: 12px; } .rinsp-user-map-icon.rinsp-user-mapped::after { content: "🔖 " attr(rinsp-nickname); font-weight: normal; font-size: 12px; color: #c30a0a; } .rinsp-user-tag { margin-right: 6px; white-space: nowrap; min-width: 9em; display: inline-block; max-width: 9em; overflow: hidden; text-overflow: ellipsis; } .rinsp-user-tag-icon::before { content: "🔖 "; font-size: 14px; display: inline-block; cursor: pointer; } .rinsp-user-blacklist .rinsp-user-tag-icon::before { content: "🚫 "; } /* ====== search ====== */ .rinsp-fav-search-area-all { font-weight: bold; color: blue; background: aliceblue; } .rinsp-fav-search-area-group { color: darkred; } .rinsp-temp-disabled { pointer-events: none; } .rinsp-fav-search-area-pin { cursor: pointer; } .rinsp-fav-search-setdefault-range { cursor: pointer; } .rinsp-search-keyword-input { width: 100%; } .rinsp-fav-search-setdefault-mode { cursor: pointer; color: #635994; float: right; } /* ====== fix broken image display ====== */ th.r_two > .user-pic > table > tbody > tr > td { max-width: 175px; overflow: hidden; text-overflow: ellipsis; } th.r_two > .user-pic img { font-size: 9px; line-height: 16px; white-space: nowrap; } /* ====== compact mode ====== */ .rinsp-compact-mode th.r_two > .user-pic { margin-top: -6px; } .rinsp-compact-mode th.r_two > .user-pic img { max-width: 150px; max-height: 150px; object-fit: cover; object-position: top; } .rinsp-compact-mode th.r_two > .user-pic > table:hover img { transition: max-height 0.3s ease-out; transition-delay: 0.7s; max-height: 270px; } th.r_two > .user-pic { position: relative; } .rinsp-compact-mode .rinsp-post-userpic-tall .rinsp-userframe-pulldown:after { content: ""; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAANCAYAAACtpZ5jAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMi1jMDAwIDc5LjU2NmViYzViNCwgMjAyMi8wNS8wOS0wODoyNTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjZEQTE2QTZENzJDOTExRURCMUZDQTc0M0NBRkM3OUY0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjZEQTE2QTZFNzJDOTExRURCMUZDQTc0M0NBRkM3OUY0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NkRBMTZBNkI3MkM5MTFFREIxRkNBNzQzQ0FGQzc5RjQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NkRBMTZBNkM3MkM5MTFFREIxRkNBNzQzQ0FGQzc5RjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6KmXcOAAABXElEQVR42rTUzSsEcRzHcTutcvIHKHHmJO2FJCkPB5GaiMJFcfEQUeIiSdRykAOxDuugkZI2iaKUQnKQkvJQ5ODCwUkx3r98pjZNQ7S/erX1nf195vt72A25rpuWihF2HMficwY5aMPzP/Ia0WnbdqmlwhkiuEDLHwIzsYKo15gJ/kAMWVjFBHaR+8vQZlyiDK2o84KTRy9q9JJD9AUEZmMbU1jTnB3voSXLuNV2nCIPswo221TwLbQHRwo3HXapblZ77wWbazGPK2xgWl8aRxFekcAgCnGCAcypgWPVz3U+C8nBZtmVmtCAR1ThDiUYQTc28abamBqIqv6kRkb99jiuLvaxpJPOwKIm9aMY16jAg67YEMrVyNc99jmUFzShWoE3WklcE9P1UhN0gA6/ux8OOPUtHY5Z6iRqsYdhPW/X3vsO64c7+q69rUe+Qtf1K00ETQyl6r/iU4ABAH2/U1xZevwpAAAAAElFTkSuQmCC); height: 13px; background-repeat: no-repeat; background-position: center center; display: block; width: 100%; opacity: 0.5; max-height: 13px; overflow: hidden; } .rinsp-compact-mode .rinsp-post-userpic-tall .user-pic > table:hover + .rinsp-userframe-pulldown:after { transition: all 0.2s ease-out; transition-delay: 0.7s; opacity: 0; max-height: 0; } .rinsp-compact-mode.rinsp-compact-mode-smaller th.r_two > .user-pic img { max-width: 120px; max-height: 120px; } .rinsp-compact-mode.rinsp-compact-mode-smaller th.r_two > .user-pic > table:hover img { transition: max-height 0.3s ease-out; transition-delay: 0.7s; max-width: 150px; max-height: 270px; } .rinsp-compact-mode th[id^="td_"] > .tiptop { margin-bottom: 1em; } .rinsp-compact-mode th[id] > .tpc_content { padding-bottom: 0.5em; } .rinsp-compact-mode .tr1.r_one > th > .tpc_content { padding-bottom: 0; } .rinsp-compact-mode .tr1.r_one > th > .tipad { margin-top: 1em; } .rinsp-filter-default-ignorable .user-pic ~ div[align="center"], .rinsp-compact-mode th.r_two > .user-pic ~ div[align="center"] { position: relative; max-height: 3em; margin-top: -1em; } .rinsp-compact-mode .rinsp-userframe-userinfo { padding-top: 0.2em; margin-top: 0.1em; background: #FFFD; padding-bottom: 0em; } /* ====== ignore marks ====== */ .rinsp-textlist-description { margin: 9px 9px 0 9px; text-align: left; white-space: pre-wrap; } .rinsp-textlist-editor { width: calc(100% - 28px); margin: 9px; height: 50vh; max-height: 80vh; } .rinsp-textlist-popup-table.rinsp-config-saving { cursor: wait; opacity: 0.5; pointer-events: none; } .rinsp-post:not(:hover) .tiptop .rinsp-ignore-switch { visibility: hidden; } .tiptop .rinsp-ignore-switch:after { content: " | "; } .readbot li.rinsp-ignore-switch { width: 6em; } .rinsp-filter-ignored-bykeyword .rinsp-ignore-switch > a { opacity: 1; color: red; } .rinsp-filter-ignored .rinsp-ignore-switch > a:before { content: "🚫"; opacity: 1; } .rinsp-ignore-switch.rinsp-config-saving { opacity: 0.5; pointer-events: none;AAAA } .rinsp-filter-ignored th.r_two, .rinsp-filter-ignored .tpc_content { opacity: 0.3; } .rinsp-filter-ignored:not(.rinsp-filter-bypass) .tiptop > .fr, .rinsp-filter-ignored:not(.rinsp-filter-bypass) .tr1.r_one { visibility: hidden; } .rinsp-filter-ignored:hover th.r_two, .rinsp-filter-ignored:hover .tpc_content { opacity: 0.9; } .rinsp-filter-ignored:not(.rinsp-filter-bypass):hover .tiptop > .fr, .rinsp-filter-ignored:not(.rinsp-filter-bypass):hover .tr1.r_one { visibility: visible; } .rinsp-filter-ignored .tipad .c + .fr.gray { visibility: visible; } body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_one > .tiptop { height: 20px; line-height: 20px; margin-top: 0 !important; margin-bottom: 0 !important; border-bottom: none !important; } .rinsp-filter-ignored-range-end-summary { display: none; } body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_one > .tiptop .rinsp-filter-ignored-range-end-summary { display: inline; } body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_two:after { content: "屏蔽内容"; display: block; font-size: 0.9em; text-align: center; padding-top: 3px; } body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_two[rinsp-filter-group-size]:after { content: "屏蔽内容 ✕ " attr(rinsp-filter-group-size); color: var(--rinsp-darkest-text-color); font-size: 1.1em; } body:not(.rinsp-filter-peek-mode) .t2.t5[rinsp-filter-ignored-cont="1"], body:not(.rinsp-filter-peek-mode).rinsp-filter-hide-mode .rinsp-filter-ignored:not(.rinsp-filter-bypass) { display: none; } body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_two > .user-pic, body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_two > .user-pic ~ *, body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_one > .tiptop ~ *, body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_one > .tiptop > .fr, body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) .r_one > .tiptop > .fl.bianji, body:not(.rinsp-filter-peek-mode) .rinsp-filter-ignored:not(.rinsp-filter-bypass) tr.tr1.r_one { display: none !important; } .rinsp-dialog-modal-mask { position: fixed; top: 0; bottom: 0; left: 0; right: 0; background: #FFF8; z-index: 100; } input.rinsp-input-max-exceeded, textarea.rinsp-input-max-exceeded { outline: 2px solid red !important; } .rinsp-input-max-hint { font-size: 10px; margin-left: 0.5em; background: #f5fafc; color: #255d99; padding: 1px 3px; display: inline-block; } textarea + .rinsp-input-max-hint { transform: translateY(-100%); margin-bottom: -20px; position: relative; top: -5px; } input[name="atc_title"] + .rinsp-input-max-hint { position: absolute; } .rinsp-input-max-hint.rinsp-input-max-exceeded { font-size: 11px; background: #fff; color: #c41212; font-weight: bold; } /* ====== thread list ====== */ .rinsp-thread-paywall { background: #f4f4f4; } .rinsp-thread-paywall > td > a[href^="thread.php?fid-"]:after { content: " (网赚)"; color: #ff219e; } body:not(.rinsp-paywall-peek-mode) .rinsp-thread-paywall { display: none; } .rinsp-thread-filter-like-words, .rinsp-thread-filter-dislike-words { display: inline-block; white-space: nowrap; cursor: pointer; } #subject_tpc .rinsp-thread-filter-like-words, #subject_tpc .rinsp-thread-filter-dislike-words, #subject_tpc .rinsp-thread-filter-menu-button { font-size: 12px; font-weight: normal; } .rinsp-thread-filter-like-words { color: var(--rinsp-text-color-green); } .rinsp-thread-filter-like-bytid .rinsp-thread-filter-like-words { font-weight: bold; font-size: 13px; } .rinsp-thread-filter-dislike-words { color: red; } .rinsp-thread-filter-like-words:hover, .rinsp-thread-filter-dislike-words:hover { color: #666; text-decoration: underline; } .rinsp-dislike-thread-ignore-toggler:hover, .rinsp-dislike-thread-peek-mode .rinsp-dislike-thread-ignore-toggler:hover { outline: 5px solid #ff9e36; } .rinsp-dislike-thread-ignore-toggler > .rinsp-excontrol-item-count { color: #ff9e36; } .rinsp-dislike-thread-ignore-toggler:after { content: '\\a屏\\a蔽\\a帖\\a子'; } .rinsp-dislike-thread-peek-mode .rinsp-dislike-thread-ignore-toggler { background: var(--rinsp-active-toggler-bg-color); outline: 3px solid #ff9e36; opacity: 1.0; } .rinsp-dislike-thread-peek-mode .rinsp-dislike-thread-ignore-toggler:after { content: '\\a屏\\a蔽\\a帖\\a子'; color: var(--rinsp-active-toggler-fg-color); } body.rinsp-dislike-thread-peek-mode .rinsp-thread-filter-masked-bysharetype, body.rinsp-dislike-thread-peek-mode .rinsp-thread-filter-dislike { opacity: 0.3; background: #eee; transition: none; } body.rinsp-threadfilter-hide-mode:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike { display: none; } body.rinsp-dislike-thread-peek-mode .rinsp-thread-filter-masked-bysharetype:hover, .rinsp-thread-filter-dislike.rinsp-thread-filter-bypass:hover, body.rinsp-dislike-thread-peek-mode .rinsp-thread-filter-dislike:hover { opacity: 1.0; transition: 0.1s opacity ease-in; } .rinsp-thread-filter-menu-button { opacity: 0.5; display: inline-block; margin-left: 0.5em; white-space: nowrap; transition-delay: 0s; transition: none; cursor: default; } .rinsp-thread-filter-unhideop-button { margin-left: 0.25em; display: inline-block; cursor: default; } #wall .section-intro .rinsp-thread-filter-hideop-button { position: absolute; left: 0; background: white; padding: 10px 7px 0 7px; height: 100%; top: 0; box-sizing: border-box; } .rinsp-thread-filter-dislike-byuid .rinsp-uid-inspected { color: red; } .rinsp-thread-filter-dislike-byuid .rinsp-thread-filter-hideop-button { display: none; } .rinsp-alert-menu-button, .rinsp-thread-filter-hideop-button, .rinsp-thread-filter-dislike-button { color: var(--rinsp-text-color-red) !important; } .rinsp-thread-filter-like-button { color: var(--rinsp-text-color-green) !important; } .rinsp-thread-filter-liketid-button { color: var(--rinsp-text-color-blue) !important; } .rinsp-alert-menu-message { background: #FFD164; font-size: 16px; font-weight: bold; } .rinsp-thread-filter-menu-button { transform: translateY(1px); float: right; } h1#subject_tpc { margin-right: 12px; } #subject_tpc > .rinsp-thread-filter-menu-button { transform: translateY(4px); } .rinsp-thread-filter-menu-button:hover { opacity: 1; } .rinsp-thread-filter-menu-button:after { content: ""; display: inline-block; width: 14px; height: 14px; background-repeat: no-repeat; /* svg is used to avoid the button text to be copied as part of the content */ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 14 14' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3Etext %7B font: 14px sans-serif; fill: %23666;%0A%7D%3C/style%3E%3Ctext x='3' y='11'%3E≡%3C/text%3E%3C/svg%3E"); } .rinsp-thread-filter-menu-button:hover:after { outline: 1px solid #4e7597; } .rinsp-thread-selected > td[id] { outline: 3px solid #4e7597; } .rinsp-thread-filter-like .rinsp-thread-filter-like-button { display: none; } /* dislike wins over like, so hide like button if filtered out by dislike */ .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike) { background: linear-gradient(to right, var(--rinsp-text-color-green) 2px, #faffe9 3px, transparent 100%); } .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike):hover { background: linear-gradient(to right, var(--rinsp-text-color-green) 2px, #f2ffd6 3px, transparent 100%); } body:not(.rinsp-dislike-thread-peek-mode) .rinsp-thread-filter-masked-bysharetype, body:not(.rinsp-dislike-thread-peek-mode) .rinsp-thread-filter-dislike { opacity: 0.5; } .t5.t2 > .rinsp-thread-filter-masked-bysharetype:hover, .t5.t2 > .rinsp-thread-filter-dislike:hover { opacity: 1.0; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > th, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > td, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > th, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > td { padding: 0; height: 7px; background: var(--rinsp-blocked-row-bg); line-height: 1px !important; overflow: hidden; color: transparent; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > th, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > td, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > th, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > td { font-size: 0; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype-collapse, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > th > *, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype > td > *, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > th > *, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > td > * { display: none; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .tr3.tac.rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > th.y-style:after, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > td[id^="td_"]:after { font-size: 11px; color: var(--rinsp-blocked-row-label-color); display: inline-block; line-height: normal; padding: 1px 0 1px 7px; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .tr3.tac.rinsp-thread-filter-dislike-bytitle:not(.rinsp-thread-filter-bypass) > th.y-style:after, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike-bytitle:not(.rinsp-thread-filter-bypass) > td[id^="td_"]:after { content: "主题已屏蔽"; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .tr3.tac.rinsp-thread-filter-dislike-byuid:not(.rinsp-thread-filter-bypass) > th.y-style:after, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-dislike-byuid:not(.rinsp-thread-filter-bypass) > td[id^="td_"]:after { content: "楼主已屏蔽"; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .tr3.tac.rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy) > th.y-style:after, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy) > td[id^="td_"]:after { content: "不合适网盘"; font-size: 11px; color: var(--rinsp-blocked-row-label-color); display: inline-block; line-height: normal; padding: 1px 0 1px 7px; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .tr3.tac.rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy) > th.y-style[rinsp-sharetype-chain]:after, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) .rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy) > td[id^="td_"][rinsp-sharetype-chain]:after { content: "不合适网盘 ✕ " attr(rinsp-sharetype-chain); } /* image wall mode */ body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-masked-bysharetype, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) { box-shadow: none; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-masked-bysharetype > .inner, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass) > .inner { display: none; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-masked-bysharetype:before, body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-dislike:not(.rinsp-thread-filter-bypass):before { font-size: 11px; color: var(--rinsp-blocked-row-label-color); display: block; text-align: center; line-height: normal; width: 1em; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-dislike-bytitle:not(.rinsp-thread-filter-bypass):before { content: "主题已屏蔽"; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-dislike-byuid:not(.rinsp-thread-filter-bypass):before { content: "楼主已屏蔽"; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy):before { content: "不合适网盘"; } body:not(.rinsp-dislike-thread-peek-mode):not(.rinsp-highlight-unscored-thread-mode) #wall .rinsp-thread-filter-masked-bysharetype:not(.rinsp-dummy)[rinsp-sharetype-chain]:before { content: "不合适网盘 ✕ " attr(rinsp-sharetype-chain); } #wall .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike) { background: none; box-shadow: 0 0 2px 1px #5bbd37; } #wall .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike) .section-intro { background-color: #1d8232 !important; } .rinsp-nickname-list:empty:after, .rinsp-user-blacklist:empty:after { content: "列表为空"; color: #999; margin-left: 1em; } .dropdown-content.menu .rinsp-filter-block-menu-sep { border-top: 1px dotted #CCC; display: block; } .dropdown-content.menu a.rinsp-filter-block-menu-item { white-space: nowrap; } .dropdown > a.rinsp-filter-block-menu-item-active { color: var(--rinsp-text-color-red); } .dropdown-content.menu a.rinsp-filter-block-menu-item-active { padding-left: 0; color: var(--rinsp-text-color-red); } .dropdown-content.menu > .apeacemaker[data-active="true"] { color: var(--rinsp-text-color-red); } .rinsp-userpic-replace .user-pic:not(:hover) > table a > img { display: none; } .rinsp-userpic-replace .user-pic:not(:hover) a[href^="u.php?action-show-uid-"]::before { color: var(--rinsp-avatar-replace-default-text-color); text-align: center; display: block; white-space: nowrap; padding-right: 0.9em; box-sizing: border-box; line-height: 150px; width: 158px; height: 158px; border: 5px solid var(--rinsp-avatar-replace-default-border-color); background: var(--rinsp-avatar-replace-default-bg); } .rinsp-compact-mode.rinsp-compact-mode-smaller .rinsp-userpic-replace .user-pic:not(:hover) a[href^="u.php?action-show-uid-"]::before { line-height: 120px; width: 128px; height: 128px; font-size: 0.9em; } .rinsp-userpic-replace-default .user-pic:not(:hover) td > a[href^="u.php?action-show-uid-"]::before { content: "👤\\FE0E默认头像"; } .rinsp-userpic-replace-custom .user-pic:not(:hover) td > a[href^="u.php?action-show-uid-"]::before { content: "🎭\\FE0E屏蔽头像"; } .rinsp-filter-default-ignorable .tiptop { margin: 0.25em 1em !important; } .rinsp-filter-default-ignorable:not(.rinsp-filter-ignored).rinsp-userpic-replace th.r_two { padding-top: 0.3em !important; } .rinsp-userframe-username { margin-top: -0.75em; margin-bottom: 1em; } .rinsp-userbookmark-takeover > a, .rinsp-userframe-username > a > strong { display: inline-block; max-width: calc(100% - 48px); } .rinsp-userframe-userknown .rinsp-userbookmark > a { font-weight: bold; } .rinsp-userframe-userknown.rinsp-userframe-userrenamed .rinsp-userbookmark > a { color: #e81210; } .rinsp-userframe-userknown .rinsp-userbookmark-takeover + a { display: block; } .rinsp-userframe-userknown .rinsp-userframe-unnamed .rinsp-userbookmark + a { display: none; } .rinsp-userframe-username-sticky .rinsp-userbookmark > span:first-child:before, .rinsp-userframe-username .rinsp-userbookmark > span:first-child:before { content: "➕"; font-weight: normal; margin-left: -15px; margin-right: 2px; opactity: 0.8; visibility: hidden; } .rinsp-userframe-username-sticky .rinsp-userbookmark > span:first-child:hover:before, .rinsp-userframe-username .rinsp-userbookmark > span:first-child:hover:before { opactity: 1; } .user-pic:hover + .rinsp-userframe-username .rinsp-userbookmark > span:first-child:before, .rinsp-userpic-sticky:hover > .rinsp-userframe-username-sticky .rinsp-userbookmark > span:first-child:before, .rinsp-userframe-username-sticky:hover .rinsp-userbookmark > span:first-child:before, .rinsp-userframe-username:hover .rinsp-userbookmark > span:first-child:before { visibility: visible; } .rinsp-userframe-userknown .rinsp-userframe-username-sticky .rinsp-userbookmark > span:first-child:before, .rinsp-userframe-userknown .rinsp-userframe-username .rinsp-userbookmark > span:first-child:before { content: "🔖"; opactity: 1; visibility: visible; } /* ==== sticky user pic ==== */ th.r_two > .user-pic.rinsp-userpic-sticky { position: fixed; top: 0; width: 180px; z-index: 2; background-color: #EEEEEE; } th.r_two > .user-pic.rinsp-userpic-sticky .rinsp-userframe-username-sticky { display: block; } .rinsp-userframe-username-sticky { text-align: center; margin-left: -5px; padding-top: 2px; display: none; } .rinsp-user-pic-dummy { display: none; } .rinsp-userpic-sticky + .rinsp-user-pic-dummy { display: block; } .rinsp-userpic-sticky ~ .rinsp-userframe-username { opacity: 0; } /* ==== message selection ==== */ .rinsp-message-selection-panel { text-align: right; background: #F8F8F8; padding: 0.2em 1.4em; margin-top: -0.5em; } .rinsp-selection-sep, .rinsp-selection-button { display: inline-block; margin-left: 1em; white-space: nowrap; cursor: default; } .rinsp-selection-button:hover { text-decoration: underline; } /* ==== announcement folding ==== */ .rinsp-top-announcement-row.rinsp-announcement-folded { background: linear-gradient(to bottom, white 0, #ddd 100%) } .rinsp-top-announcement-row.rinsp-announcement-folded h3 { display: none; } .rinsp-announcement-toggler { cursor: pointer; position: relative; top: 4px; margin-left: 2px; } .rinsp-announcement-row-folded { display: none; } /* ==== mentioning ==== */ .rinsp-thread-mention-me:not(.rinsp-request-thread-ended) { background: #ffeaf8 !important; } .rinsp-thread-mention-me:not(.rinsp-request-thread-ended) > td[id^="td_"] > h3 > a[id^="a_ajax_"] { color: #C60E87; } .rinsp-thread-mention-me:not(.rinsp-request-thread-ended) > td[id^="td_"] > h3 > a[id^="a_ajax_"]:before { content: "❤️"; font-size: 12px; font-weight: bold; color: #C60E87; margin-right: 0.25em; } /* ==== request visibility toggler ==== */ .rinsp-answered-request-ignore-toggler:hover, .rinsp-answered-request-hide-mode .rinsp-answered-request-ignore-toggler:hover { outline: 5px solid #3f9b15; } .rinsp-answered-request-ignore-toggler { background: #d2ddcdee; outline: 3px solid #407727; opacity: 1.0; } .rinsp-answered-request-ignore-toggler > .rinsp-excontrol-item-count { color: #3f9b15; } .rinsp-answered-request-ignore-toggler:after { content: '\\a有\\a答\\a案'; color: var(--rinsp-active-toggler-fg-color); } .rinsp-answered-request-hide-mode .rinsp-answered-request-ignore-toggler { background: #333C; opacity: 0.75; outline: none; box-shadow: 2px 2px 1px 0 #0009; } .rinsp-answered-request-hide-mode .rinsp-answered-request-ignore-toggler:after { color: #999; } .rinsp-answered-request-hide-mode .rinsp-request-thread.rinsp-request-thread-ended:not(.rinsp-thread-byme):not(.rinsp-thread-mention-me) { display: none; } .rinsp-unanswered-request-ignore-toggler:hover, .rinsp-unanswered-request-hide-mode .rinsp-unanswered-request-ignore-toggler:hover { outline: 5px solid #ce324f; } .rinsp-unanswered-request-ignore-toggler { background: #f0e8ebde; outline: 3px solid #ae3a50; opacity: 1.0; } .rinsp-unanswered-request-ignore-toggler > .rinsp-excontrol-item-count { color: #bd1d3b; } .rinsp-unanswered-request-ignore-toggler:after { content: '\\a悬\\a赏\\a中'; color: var(--rinsp-active-toggler-fg-color); } .rinsp-unanswered-request-hide-mode .rinsp-unanswered-request-ignore-toggler { background: #333C; opacity: 0.75; outline: none; box-shadow: 2px 2px 1px 0 #0009; } .rinsp-unanswered-request-hide-mode .rinsp-unanswered-request-ignore-toggler:after { color: #999; } .rinsp-unanswered-request-hide-mode .rinsp-request-thread.rinsp-request-thread-ongoing:not(.rinsp-thread-byme):not(.rinsp-thread-mention-me) { display: none; } .rinsp-expired-request-ignore-toggler:hover, .rinsp-expired-request-hide-mode .rinsp-expired-request-ignore-toggler:hover { outline: 5px solid #eee; } .rinsp-expired-request-ignore-toggler { background: #ccccccee; outline: 3px solid #828282; opacity: 1.0; } .rinsp-expired-request-ignore-toggler > .rinsp-excontrol-item-count { color: #5c5c5c; } .rinsp-expired-request-ignore-toggler:after { content: '\\a已\\a超\\a时'; color: var(--rinsp-active-toggler-fg-color); } .rinsp-expired-request-hide-mode .rinsp-expired-request-ignore-toggler > .rinsp-excontrol-item-count { color: #aaa; } .rinsp-expired-request-hide-mode .rinsp-expired-request-ignore-toggler { background: #333C; opacity: 0.75; outline: none; box-shadow: 2px 2px 1px 0 #0009; } .rinsp-expired-request-hide-mode .rinsp-expired-request-ignore-toggler:after { color: #999; } .rinsp-expired-request-hide-mode .rinsp-request-thread.rinsp-request-thread-expired:not(.rinsp-thread-byme):not(.rinsp-thread-mention-me) { display: none; } /* ==== to top button ==== */ .rinsp-excontrol-totop { opacity: 100; transition: opacity 0.5s ease-in; } .rinsp-excontrol-totop::after { content: '▲\\a顶\\a部'; font-size: 16px; line-height: 1.2em; } .rinsp-opacity-0 { opacity: 0 !important; transition: opacity 0.1s ease-in; } /* ==== closed threads ==== */ .rinsp-thread-filter-closed { background-color: #FEE; } .tr3.rinsp-thread-filter-closed:hover { background-color: #FEE; } .rinsp-thread-filter-closed > td[id^="td_"] { text-decoration: line-through; color: red; } .rinsp-closed-thread-mask-mode .rinsp-thread-filter-closed { display: none; } /* ==== visited threads ==== */ .rinsp-visited-thread-view-mode .rinsp-thread-visited:not(:hover) { opacity: 0.9; } .rinsp-visited-thread-view-mode.rinsp-uphp-post #u-contentmain .rinsp-thread-visited:not(:hover) th .gray, .rinsp-visited-thread-view-mode.rinsp-uphp-topic #u-contentmain .rinsp-thread-visited:not(:hover) th .gray { opacity: 0.5; } .rinsp-visited-thread-view-mode .rinsp-thread-visited td[id^="td_"] h3 > a:not(:hover), .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="read.php?tid-"]:not(:hover), .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="job.php?action-topost-tid-"]:not(:hover), .rinsp-visited-thread-view-mode .rinsp-thread-visited .inner > .section-title > a[href^="./read.php?tid-"]:not(:hover) { color: var(--rinsp-visited-link-color); } .rinsp-visited-thread-view-mode .rinsp-thread-visited td[id^="td_"] h3 > a:hover, .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="read.php?tid-"]:hover, .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="job.php?action-topost-tid-"]:hover, .rinsp-visited-thread-view-mode .rinsp-thread-visited .inner > .section-title > a[href^="./read.php?tid-"]:hover { color: var(--rinsp-visited-link-hover-color); } .rinsp-visited-thread-view-mode .rinsp-thread-visited td[id^="td_"] h3 > a:not(:hover) > font, .rinsp-visited-thread-view-mode .rinsp-thread-visited td[id^="td_"] h3 > a:not(:hover) > b > font, .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="read.php?tid-"]:not(:hover) > font, .rinsp-visited-thread-view-mode .rinsp-thread-visited th a[href^="read.php?tid-"]:not(:hover) > b > font, .rinsp-visited-thread-view-mode .rinsp-thread-visited .inner > .section-title > a[href^="./read.php?tid-"]:not(:hover) > b > font, .rinsp-visited-thread-view-mode .rinsp-thread-visited .inner > .section-title > a[href^="./read.php?tid-"]:not(:hover) > b > font { filter: saturate(0.7); opacity: 0.3; } /* on search results */ .rinsp-thread-visited-update td.smalltxt.y-style + .y-style { position: relative; color: var(--rinsp-visited-update-color); } .rinsp-thread-visited-update td.smalltxt.y-style + .y-style:before { content: ""; display: inline-block; width: 3em; position: absolute; border: 1px solid var(--rinsp-visited-update-border-color); border-radius: 12px; left: 50%; margin-left: -1.5em; height: 1.2em; margin-top: -0.1em; } /* on thread listing */ .rinsp-thread-visited-update td.f10 > .s8 { border: 1px solid var(--rinsp-visited-update-border-color); color: var(--rinsp-visited-update-color); padding: 1px 3px; border-radius: 12px; margin-left: -4px; } /* thread wall mode */ .rinsp-thread-visited-update .inner > .section-text > span + span { color: var(--rinsp-visited-update-color) !important; font-weight: bold; } /* ==== request threads ==== */ .rinsp-request-highlight-ended-mode .rinsp-request-thread-ended:not(.rinsp-request-settlement-thread) > td[id^="td_"] > h3:before { content: "— 已有答案 —"; font-weight: bold; color: #407727; } .rinsp-request-highlight-ended-mode .rinsp-request-thread-won:not(.rinsp-request-settlement-thread) > td[id^="td_"] > h3:before { content: "— ✓ 最佳 —"; font-weight: bold; color: #407727; } .rinsp-request-highlight-ended-mode .rinsp-request-thread-won.rinsp-request-settlement-thread > td[id^="td_"] > h3:before { content: "— ✓ 已领取 —"; font-weight: bold; color: #999; } .rinsp-request-bounty { color: #c60e87; } .rinsp-request-bounty + .s1 { display: none; } .rinsp-request-bounty .rinsp-extra-bounty { display: none; } .rinsp-request-show-extra-bounty .rinsp-request-bounty .rinsp-extra-bounty { display: inline; } .rinsp-request-thread-expired .rinsp-request-bounty { color: #999; } .rinsp-request-settlement-thread:not(:hover) { background: #EEE; opacity: 0.7; } .rinsp-request-settlement-thread .rinsp-request-bounty { color: #666; font-weight: normal; } body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > th, body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > td { padding: 0; height: 7px; background: var(--rinsp-blocked-row-bg); line-height: 1px; overflow: hidden; color: transparent; } body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > th > *, body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > td > * { display: none; } body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .tr3.tac.rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > th.y-style:after, body.rinsp-request-settlement-hide-mode:not(.rinsp-settlement-peek-mode) .rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass) > td[id^="td_"]:after { content: "结算帖"; font-size: 12px; color: #AAA; display: inline-block; line-height: normal; padding: 1px 0 1px 7px; } /* ==== bookmarked user indications on message list ==== */ .rinsp-message-user-mapped > a::before { content: "🔖"; position: absolute; transform: translateX(-100%); margin-left: -1px; font-weight: normal; font-size: 12px; color: #c30a0a; } .rinsp-message-user-pinned > a { font-weight: bold; } .rinsp-message-user-pinned > a:before { content: ""; width: 16px; height: 16px; background: var(--rinsp-pin-icon); padding-right: 1px; position: absolute; transform: translateX(-100%); } /* ==== improve column width ratio in message list ==== */ form[name="del"][action="message.php"] .set-table2 > tbody > tr > td:nth-child(1) { width: 8%; } form[name="del"][action="message.php"] .set-table2 > tbody > tr > td:nth-child(2) { width: 15%; } form[name="del"][action="message.php"] .set-table2 > tbody > tr > td:nth-child(4) { width: 22%; } /* ==== topic and reply list filter shortcuts ==== */ .rinsp-uphp-topic #u-contentmain > table > tbody > tr + tr:hover, .rinsp-uphp-post #u-contentmain > table > tbody > tr + tr:hover { outline: 1px solid #DDD; } .rinsp-uphp-trade #u-contentmain .rinsp-gf-link, .rinsp-uphp-topic #u-contentmain .rinsp-gf-link, .rinsp-uphp-post #u-contentmain .rinsp-gf-link { float: right; color: #BBB; white-space: nowrap; } .rinsp-uphp-trade #u-contentmain .rinsp-gf-link, .rinsp-uphp-post #u-contentmain .rinsp-gf-link { margin-right: 5px; margin-left: 5px; } .rinsp-uphp-trade #u-contentmain .rinsp-gf-link:hover, .rinsp-uphp-topic #u-contentmain .rinsp-gf-link:hover, .rinsp-uphp-post #u-contentmain .rinsp-gf-link:hover { color: #000; } /* ==== reply list ==== */ .rinsp-reply-list-controls { color: #333; font-size: 11px; display: block; text-align: right; margin-right: -18%; } .rinsp-reply-fold-table { width: 100%; border-collapse: collapse; table-layout: fixed; } .rinsp-reply-fold-table > tbody > tr > td + td { width: 20%; } /* need to keep the old table for soul++ infinite scroll */ .rinsp-reply-fold-table + .u-table { display: none; } .rinsp-reply-fold-item { display: flex; align-items: flex-start; } .rinsp-reply-fold-item > a { flex-shrink: 1; } .rinsp-reply-fold-pages { max-width: 14em; min-width: 3em; display: flex; align-items: flex-start; padding-right: 7px; white-space: nowrap; font-size: 13px; overflow: hidden; } .rinsp-reply-fold-count { display: inline-block; color: #FFF; font-family: monospace; font-weight: bold; background-color: #AAA; padding: 0px 4px; margin-left: 0.5em; border-radius: 20px; line-height: normal; cursor: default; white-space: nowrap; position: relative; top: 1px; } .rinsp-reply-fold-remains { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: monospace; } .rinsp-reply-fold-remains-start { flex-shrink: 1; } .rinsp-reply-fold-remains-end { flex-grow: 0; flex-shrink: 0; } .rinsp-reply-fold-remains > a { margin-left: 4px; color: #999; white-space: nowrap; } .rinsp-reply-fold-remains > a:hover { color: #333; } /* ==== thread history list ==== */ .rinsp-quick-filter-box { display: flex; align-items: center; } .rinsp-quick-filter-box > :first-child { min-width: 3.5em; } .rinsp-quick-filter-box + .rinsp-quick-filter-box { margin-top: 2px; } .rinsp-quick-filter-box > .rinsp-quick-filter-checkbox { margin-right: 0.5em; padding: 0px 6px 0px 4px; border-radius: 16px; color: #333; } .rinsp-quick-filter-box > .rinsp-quick-filter-bought-checkbox { background: #FBEFD9; } .rinsp-quick-filter-box > .rinsp-quick-filter-replied-checkbox { background: #CFE8FF; } .rinsp-quick-filter-box > .rinsp-quick-filter-notmine-checkbox { background: #FBEAF5; } html.rinsp-dark-mode .rinsp-quick-filter-box > .rinsp-quick-filter-bought-checkbox { background: #523706; color: var(--rinsp-text-color-orange); } html.rinsp-dark-mode .rinsp-quick-filter-box > .rinsp-quick-filter-replied-checkbox { background: #092947; color: var(--rinsp-text-color-blue); } html.rinsp-dark-mode .rinsp-quick-filter-box > .rinsp-quick-filter-notmine-checkbox { background: #3c1332; color: var(--rinsp-text-color-violet); } .rinsp-quick-filter-box > input { flex: 1; } .rinsp-thread-history-table { width: 100%; } .rinsp-quick-filter-box > input:focus, .rinsp-table-filtered .rinsp-quick-filter-box > input { outline: 3px solid #c6ae30; outline-offset: -1px; } .rinsp-thread-status-icons { float: right; margin-left: 1em; } .rinsp-thread-replied-icon { color: #0779AA; cursor: default; } .rinsp-thread-bought-icon { color: #9D674C; cursor: default; } .rinsp-thread-byme > th > a[href^="read.php?tid"] { color: #C60E87; } .rinsp-thread-deleted > th > a[href^="read.php?tid"] { color: #C22; text-decoration: line-through; } .rinsp-thread-initial-title::before { content: "原始标题: "; color: #739; } .rinsp-thread-initial-title { color: var(--rinsp-text-color-grey); padding: 1px 0px 1px 5px; margin-left: -5px; background: var(--rinsp-lightest-bg-color); } .rinsp-thread-title-replaced + br { display: block; height: 0; } .rinsp-thread-history-table > tbody > tr:hover { outline: 1px solid #DDD; } .rinsp-thread-record-uid > a::before { content: "by "; font-size: 0.9em; } .rinsp-thread-record-uid { float: right; } .rinsp-thread-populate-button { color: #BB4411; margin-left: 0.5em; } /* ==== friends ==== */ .rinsp-uphp-friend #u-content .u-table td:nth-child(2) > a[href^="u.php?action-show-uid-"] { color: #AAA; } .rinsp-uphp-friend #u-content .u-table .rinsp-user-state-online > td:nth-child(2) > b { color: #11AA44; } /* ==== user bookmark listing action menu ==== */ #rinsp-userbookmark-action-menu .bor td { padding-left: 12px; } .rinsp-mailto-user-bookmark-button::before { content: ""; width: 14px; height: 12px; display: inline-block; background: url(/images/colorImagination/mail-icon.gif); position: absolute; margin-left: -18px; margin-top: 4px; } /* ==== user profile page actions ==== */ #u-portrait ~ .bdbA > table > tbody > tr > td { padding-bottom: 0.5em; } /* ==== paywell area rename ==== */ .rinsp-area-scoped-item-171 a[href="thread.php?fid-171.html"]::after, .rinsp-area-scoped-item-172 a[href="thread.php?fid-172.html"]::after, .rinsp-area-scoped-item-173 a[href="thread.php?fid-173.html"]::after, .rinsp-area-scoped-item-174 a[href="thread.php?fid-174.html"]::after { content: " (网赚)"; } /* ==== area filter ==== */ .rinsp-area-scoped-item-hidden { display: none; } /* ==== share type filter ==== */ #ajaxtable .hthread { height: auto; display: flex; } #ajaxtable .hthread > .threadlist { position: relative; float: none; padding-right: 1em; } #ajaxtable .threadlist li.current a, .threadlist li.current a:hover { height: 18px; } .rinsp-sharetype-filter { flex: 1; padding-top: 0.2em; text-align: right; } .rinsp-sharetype-item { display: inline-block; font-size: 11px; font-weight: normal; line-height: 16px; border: 1px solid #CAB2D7; border-radius: 9px; padding: 1px 5px; background: #FFFE; margin-right: 0.5em; cursor: pointer; margin-bottom: 1px; } .rinsp-sharetype-item[disabled="1"] { opacity: 0.5; border-color: #CCC; color: #999; } .rinsp-sharetype-item[disabled="1"]::after { color: #999; } .rinsp-sharetype-item[count="0"] { display: none; } .rinsp-sharetype-item::after { content: " " attr(count); color: #7628A2; font-size: 10px; } .rinsp-sharetype-filter-clear { cursor: pointer; } .rinsp-sharetype-filter-clear[disabled="1"] { cursor: default; opacity: 0.1; pointer-events: none; } /* ==== category filter ==== */ .rinsp-category-filter { padding-top: 0.2em; } .rinsp-category-toggle-item { display: inline-block; line-height: 16px; color: #666; border: 1px solid #CCC; border-radius: 9px; padding: 1px 5px; background: #FFFE; margin-right: 0.5em; margin-bottom: 0.5em; cursor: pointer; } .rinsp-category-toggle-item.rinsp-category-active { border: 1px solid #613578; background: #CAB2D7; color: #000; } .rinsp-category-toggle-item::after { content: " " attr(count); color: #7628A2; font-size: 10px; } .rinsp-category-hide { display: none; } /* ==== history-based bounty status ==== */ .rinsp-thread-bounty-status { font-size: 0.9em; margin-right: 0.5em; white-space: nowrap; } .rinsp-thread-bounty-status-won > span, .rinsp-thread-bounty-status-ended > span { display: none; } .rinsp-thread-bounty-status > span::after { content: "SP"; } .rinsp-thread-bounty-status-expired.rinsp-thread-bounty-status-own { color: #b10b0b; } .rinsp-thread-bounty-status-ongoing { color: #c11986; } .rinsp-thread-bounty-status-ongoing.rinsp-thread-bounty-status-own::before { content: "悬赏中 "; font-weight: bold; font-size: 1.2em; } .rinsp-thread-bounty-status-expired.rinsp-thread-bounty-status-own::before { content: "⚠️已超时 "; font-size: 1.2em; font-weight: bold; } .rinsp-thread-bounty-status-ended::before, .rinsp-thread-bounty-status-won::before { content: "✓"; font-size: 1.2em; font-weight: bold; } .rinsp-thread-bounty-status-won, .rinsp-thread-bounty-status-ended.rinsp-thread-bounty-status-own { color: #2d8e0d; } .rinsp-thread-bounty-status-ended { color: #136cbb; } .rinsp-thread-bounty-status-won::after { content: " 最佳"; } .rinsp-thread-bounty-status-ended::after { content: " 有答案"; } .rinsp-thread-bounty-status-ended.rinsp-thread-bounty-status-own::after { content: ""; } .rinsp-thread-bounty-status-expired::after { content: " 已结束"; color: #ae700a; } .rinsp-thread-bounty-status-expired.rinsp-thread-bounty-status-own::after { content: ""; } .rinsp-thread-bounty-status-unknown { display: none; } /* ==== other ==== */ .rinsp-filter-multiip-focus-mode .t5.t2:has(> .rinsp-post:not(.rinsp-post-multi-uid-selected)) { display: none; } .rinsp-hide { display: none; } .tiptop > .fr.js-hidepuremark { position: relative; z-index: 1; } .js-puremark-content + .bianji > a:not(:hover) { color: #aaa; } /* override and take over forum default blocking mode */ form > .t5.t2[hidden] { display: block; } .rinsp-fid-search-button { margin-right: 3px; } .rinsp-fid-search-button:not(:hover) { opacity: 0.8; } .rinsp-fid-search-button:before { content: ""; display: inline-block; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoVJREFUeNq8l0loFEEUhtthBEH05goKcQvGxBE0YtBRMEExggsyiIiag7kEEYQQchAhiAeTkweJh6iIuKCgEcRDoogbKGowbigqejCI4IpCgsTlf/A3PIrpnqpqOw8+uupNV9c/XVXvvR5VKBQCCxsLykkWvASvwNcgoWUt7qkC23idwzEy+TPQDa6nJWAxOADqivw2HdSC3eAxaAa9PgIyEf4T4F7E5KbNBz3gxv96Ax1gu+EbADdBHxgGOZAHM9U9y8ElsD6JgOOgQfVfgF3gWsT4heAQWMr+OnCL4pyXYLYx+V0wN2ZysYdgGbiifNJf4SPgmGo/AjUOb3ItuKr6Z1wFzKDy0DZ67Kctqj0F1NsKkH2wU/megHceAj5x2ULbYSugEixSvosJ4so51c7ZCpCjNM04cr7Wp9rjXZZgtPL9SSDgl+tzRMBb8F75JiQQUKnaw7YCnjOeByqY+NomYzNbCfhpxIAlYJxnWF+t+udd4oAEntfKf9JDQKdqfwanXCOhDsOSUM46TH7YiCV7wG9XAXfAbdXfzHNdHjN+KjgCmgx/K6jwyYZ5xvRa9gtEMtxlnnP5Z/PAmphwW8GKqRo8cK0H6rgncoawvMe+uA9WxVVLURXRArAVfLGY5Dv4FvN7T1xyy8QMPM2s1sglkGD1g8f2AxNPC++ZVSKBXYiqlEpVxRJau4jYGF6HjPsGmdLfgLKIZ3XzpBy1fQPFbKjI5KH95eZ7GjO+i2/NW4CNwCqjLjDtIGhLS0BoNSU+WPaBvWBiNkjPVnLdo8r0/ZJzMkG6toGnKcompy0gYDzpLOKXI92bDUbGmjhheAI+MnX3j5QAsXaWaZP43dkvzn8CDAADRnsHJ4sZkAAAAABJRU5ErkJggg==) no-repeat; background-size: 12px 12px; width: 12px; height: 12px; vertical-align: middle; } .rinsp-fid-search-button:hover:before { color: #007788; } /* ==== menu quick search ==== */ #guide.rinsp-quicksearch-added { display: flex; } #guide.rinsp-quicksearch-added > li { float: none; flex: 0 0 auto; } #guide.rinsp-quicksearch-align-center { width: 100%; } #guide.rinsp-quicksearch-align-center .rinsp-spacer { flex: 1 1; } .rinsp-quicksearch { display: flex; align-items: center; padding: 2px 4px 2px 2px; position: relative; } .rinsp-quicksearch-field { flex: 1; font-size: 12px; padding: 1px 22px 1px 5px; margin: 0px; border-width: 1px; border-radius: 0.5em; border-color: transparent; overflow: hidden; width: 14em; position: relative; } .rinsp-quicksearch-button { flex: 0; padding: 0 4px 0 6px; position: absolute; right: 0; } .rinsp-quicksearch-button:after { content: ""; display: inline-block; cursor: pointer; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoVJREFUeNq8l0loFEEUhtthBEH05goKcQvGxBE0YtBRMEExggsyiIiag7kEEYQQchAhiAeTkweJh6iIuKCgEcRDoogbKGowbigqejCI4IpCgsTlf/A3PIrpnqpqOw8+uupNV9c/XVXvvR5VKBQCCxsLykkWvASvwNcgoWUt7qkC23idwzEy+TPQDa6nJWAxOADqivw2HdSC3eAxaAa9PgIyEf4T4F7E5KbNBz3gxv96Ax1gu+EbADdBHxgGOZAHM9U9y8ElsD6JgOOgQfVfgF3gWsT4heAQWMr+OnCL4pyXYLYx+V0wN2ZysYdgGbiifNJf4SPgmGo/AjUOb3ItuKr6Z1wFzKDy0DZ67Kctqj0F1NsKkH2wU/megHceAj5x2ULbYSugEixSvosJ4so51c7ZCpCjNM04cr7Wp9rjXZZgtPL9SSDgl+tzRMBb8F75JiQQUKnaw7YCnjOeByqY+NomYzNbCfhpxIAlYJxnWF+t+udd4oAEntfKf9JDQKdqfwanXCOhDsOSUM46TH7YiCV7wG9XAXfAbdXfzHNdHjN+KjgCmgx/K6jwyYZ5xvRa9gtEMtxlnnP5Z/PAmphwW8GKqRo8cK0H6rgncoawvMe+uA9WxVVLURXRArAVfLGY5Dv4FvN7T1xyy8QMPM2s1sglkGD1g8f2AxNPC++ZVSKBXYiqlEpVxRJau4jYGF6HjPsGmdLfgLKIZ3XzpBy1fQPFbKjI5KH95eZ7GjO+i2/NW4CNwCqjLjDtIGhLS0BoNSU+WPaBvWBiNkjPVnLdo8r0/ZJzMkG6toGnKcompy0gYDzpLOKXI92bDUbGmjhheAI+MnX3j5QAsXaWaZP43dkvzn8CDAADRnsHJ4sZkAAAAABJRU5ErkJggg==) no-repeat; background-size: 14px 14px; width: 18px; height: 14px; } .rinsp-quicksearch-field:invalid { opacity: 0.9; } .rinsp-quicksearch:has(.rinsp-quicksearch-field:invalid):after { content: "搜索"; font-size: 11px; position: absolute; right: 24px; } #guide.rinsp-quicksearch-added a[href="plugin.php?H_name-tasks.html"] { font-size: 0; } #guide.rinsp-quicksearch-added a[href="plugin.php?H_name-tasks.html"]:after { content: "任务"; font-size: 12px; } /* ==== pin users ==== */ .rinsp-pinned-user-list { display: flex; flex-wrap: wrap; width: 530px; } .rinsp-pinuser-main { position: fixed; left: calc(50vw + 510px); transform: translateY(-50%); top: 50vh; max-height: 90vh; overflow: auto; } .rinsp-pinuser-list > div { padding-right: 20px; } .rinsp-pinuser-list > b { display: inline-block; color: #FFF8; border-bottom: 3px solid #FFF8; box-sizing: border-box; font-size: 1.3em; font-weight: normal; min-width: 106px; } .rinsp-pinuser-item { margin: 5px 0; display: flex; } .rinsp-pinuser-item img { width: 100px; height: 100px; object-fit: cover; object-position: top; border: 3px solid #FFF; } .rinsp-pinuser-item.rinsp-pinuser-item-absent img { width: 50px; height: 50px; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face { display: flex; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face > div { min-width: 85px; max-width: 85px; flex-direction: column-reverse; background-color: transparent; background-image: linear-gradient(to right, #FFF8 0%, #FFF1 100%); } .rinsp-pinuser-item.rinsp-pinuser-item-absent:hover .rinsp-pinuser-item-face > div { background: #FFF8; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-unpin-icon { text-align: right; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face > div > a { max-height: 50px; overflow: hidden; } .rinsp-pinuser-item-locs:empty { display: none; } .rinsp-pinuser-item-locs { margin: 0 0 0 5px; display: flex; flex-direction: column; flex-wrap: wrap; font-size: 0.8em; max-height: 130px; width: 5.5em; } .rinsp-pinuser-item-locs > dt { margin-right: 6px; } .rinsp-pinuser-item-locs > dt > a { color: #FFF8; font-weight: bold; margin-bottom: 1px; white-space: nowrap; } .rinsp-pinuser-item-locs > dt.rinsp-pinuser-loc-ignoredmark > a { color: #FF09; } .rinsp-pinuser-item-locs > dt.rinsp-pinuser-loc-gf > a { color: #0F09; } .rinsp-pinuser-item-locs > dt.rinsp-pinuser-loc-banned > a { color: #F44E; } .rinsp-pinuser-item-face > div { max-width: 106px; padding: 2px 4px; margin-right: -20px; display: flex; background: #FFF8; box-sizing: border-box; } .rinsp-pinuser-item:hover > .rinsp-pinuser-item-face > div { background: #FFFC; } .rinsp-pinuser-item-face > div > a { flex: 1; word-break: break-all; } .rinsp-pinuser-unpin-icon { cursor: pointer; height: 0; } .rinsp-pinuser-unpin-icon::before { content: "❌"; font-size: 10px; } a:not([name="tpc"]) + .rinsp-thread-filter-active-reply { outline: 2px solid #36C; outline-offset: -1px; } .rinsp-thread-filter-pinned { outline: 3px solid #F08; outline-offset: -1px; } .rinsp-thread-filter-pinned .tr1 .r_two, .rinsp-thread-filter-pinned th.r_two > .rinsp-userpic-sticky { background-color: #F2CFE0; } .rinsp-pinuser-item-absent { opacity: 0.3; } .rinsp-thread-user-pinned { outline: 2px solid #F08; outline-offset: -3px; } .rinsp-thread-user-pinned .rinsp-uid-inspected { font-weight: bold; color: #F08; } .rinsp-user-action-pinuser-button { display: inline-block; margin-left: 0.5em; vertical-align: top; } .rinsp-user-action-pinuser-button:not([data-pinned]):not(:hover) { filter: grayscale(1); opacity: 0.5; } .rinsp-quick-action-overlay { z-index: 4; } .rinsp-quick-action-overlay:focus > label::after { content: "凛+ (版本" attr(version) ")"; font-size: 13px; color: #BBB; } .rinsp-quick-action-overlay:focus > label { border-bottom: 1px solid #999; } .rinsp-quick-action-overlay { position: fixed; font-size: 14px; } .rinsp-quick-action-overlay-pos-tr { right: 15px; top: 16px; padding-bottom: 32px; padding-left: 32px; text-align: right; } .rinsp-quick-action-overlay-pos-tr:focus { padding: 5px; margin-top: -5px; margin-right: -5px; border-radius: 5px; background: #FFF7; } .rinsp-quick-action-overlay-pos-tl { left: 25px; top: 16px; padding-bottom: 32px; padding-right: 32px; text-align: left; } .rinsp-quick-action-overlay-pos-tl:focus { padding: 5px; margin-top: -5px; margin-left: -5px; border-radius: 5px; background: #FFF7; } .rinsp-quick-action-overlay:focus { border-radius: 5px; background: #FFFA; } .rinsp-quick-action-overlay > label { font-size: 16px; filter: grayscale(1); opacity: 0.2; margin-bottom: 5px; display: block; } .rinsp-quick-action-overlay:focus > label { filter: grayscale(0.5) invert(1); opacity: 1; } .rinsp-quick-action-overlay:not(:focus) > label ~ * { display: none; } .rinsp-quick-action-overlay > label ~ * { margin-bottom: 0.2em; } .rinsp-quick-action-divider { border-top: 1px solid #999; margin-top: 0.3em; margin-bottom: 0.3em; } .rinsp-quick-action { margin-left: 1px; } .rinsp-quick-action::before { cursor: pointer; } .rinsp-quick-action::before { content: attr(label); color: var(--rinsp-darkest-text-color); display: inline-block; } .rinsp-quick-action:not(:hover)::before { opacity: 0.7; } .new-msg-tips, .new-msg-tips + span { z-index: 5; } @media (max-width: 1278px) { .rinsp-pinuser-main { position: absolute; } .rinsp-pinuser-main:not(:empty) { top: 0; left: 0; padding: 5px 0 5px 0; width: 100%; box-sizing: border-box; transform: none; background-image: url(data:image/gif;base64,R0lGODlhEAAFAIAAAE1NTWZmZiH5BAAAAAAALAAAAAAQAAUAAAIOjAOHmtf2lmN0WljxRQUAOw==); background-repeat: repeat; z-index: 2; } .rinsp-pinuser-list { text-align: center; white-space: nowrap; } .rinsp-pinuser-list > b { writing-mode: vertical-rl; min-width: auto; border-bottom: none; border-right: 3px solid #FFF8; min-height: 76px; } .rinsp-pinuser-list > div { display: inline-block; text-align: left; padding: 0; white-space: nowrap; max-width: 80vw; overflow-x: auto; overflow-y: hidden; } .rinsp-pinuser-item { margin: 0 5px; } .rinsp-pinuser-item-face { display: inline-flex; flex-direction: column; background: #FFF3; vertical-align: top; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face { display: inline-flex; } .rinsp-pinuser-item-face > div, .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face > div { max-width: 106px; min-width: 106px; margin: 0; flex-direction: row; } .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face > div { max-width: 78px; min-width: 78px; } .rinsp-pinuser-item .rinsp-pinuser-item-face > div > a, .rinsp-pinuser-item.rinsp-pinuser-item-absent .rinsp-pinuser-item-face > div > a { max-height: 1.4em; overflow: hidden; } .rinsp-pinuser-list .rinsp-pinuser-item { margin-right: 5px; display: inline-block; } .rinsp-pinuser-item-locs { width: auto; vertical-align: top; display: inline-flex; margin-left: -36px; max-height: 66px; overflow: auto; flex-wrap: nowrap; } .rinsp-pinuser-item img { width: 60px; height: 60px; } } /* ==== subject line functions ==== */ .rinsp-theme-darksubject h1#subject_tpc { color: #333; } .rinsp-theme-darksubject h1[id^="subject_"] { color: #999; } .rinsp-theme-darksubject h1#subject_tpc .rinsp-subject-class { color: #BBB; margin-right: 0.2em; font-size: 15px; } .rinsp-subject-redundant { display: none; } .rinsp-subject-floor-link { color: #3399CC; } .blockquote3 .rinsp-subject-floor-link { font-weight: bold; color: #3399CC; margin: 0 0.2em; } /* ==== resource spots ==== */ .rinsp-spots-main { position: fixed; left: calc(50vw - 490px); transform: translateX(-100%); top: calc(49vh - 30px); } .rinsp-highlight-spots > dl { max-height: calc(50vh - 40px); overflow: auto; } .rinsp-highlight-spots > b { display: inline-block; color: #FFF8; box-sizing: border-box; font-size: 1.3em; font-weight: normal; } .rinsp-highlight-spots > dl { background: #FFF1; border-top: 3px solid #FFF8; font-size: 1.1em; margin-top: 0; padding: 0.2em 0.5em; scrollbar-gutter: stable; scrollbar-width: thin; } .rinsp-highlight-spots > dl > dt > a { display: flex; color: #CCC; } .rinsp-spot-label { flex: 1; max-width: 10em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rinsp-highlight-spots > dl > dt > a .rinsp-spot-floor { color: #EEE; font-weight: bold; font-family: monospace; margin-right: 0.3em; min-width: 2.5em; display: inline-block; } .rinsp-thread-header { display: flex; } .rinsp-spots-bar { display: flex; flex: 1; white-space: nowrap; order: 2; overflow: hidden; text-overflow: ellipsis; } .rinsp-spots-bar .rinsp-spot-floor { font-weight: bold; font-size: 0.8em; } .rinsp-spots-bar > a { margin-left: 5px; } .rinsp-spots-bar > a + a { border-left: 1px solid #AAA; padding-left: 5px; } .rinsp-thread-header > .fl { flex: 0; order: 1; } .rinsp-thread-header > .fl:not(:empty) { padding-right: 1em; } .rinsp-thread-header > .fr { flex: 0; order: 3; padding-left: 1.5em; } @media (max-width: 1278px) { .rinsp-spots-main .rinsp-spot-label { display: none; } } @media (max-width: 1150px) { .rinsp-highlight-spots { font-size: 0.8em; } } /* ==== infinite scroll ==== */ .rinsp-infscroll-armed .rinsp-infscroll-page-endtrigger { --rinsp-infscroll-height: 70px; /* add 15px */ } .rinsp-infscroll-switch.rinsp-active { background: var(--rinsp-cell-background-active); } div.rinsp-infscroll-divider, tbody.rinsp-infscroll-divider > tr > td { color: var(--rinsp-infscroll-divider-text-color); border-top: 1px solid var(--rinsp-infscroll-divider-border-color); background: var(--rinsp-infscroll-divider-background); padding: 0.2em 0 0.2em 1em; text-align: center; clear: both; } div.rinsp-infscroll-divider { border-bottom: 1px dotted var(--rinsp-infscroll-divider-border-color); margin-bottom: 8px; } .rinsp-infscroll-enabled .rinsp-infscroll-page-endtrigger { width: 1px; height: var(--rinsp-infscroll-height); } .rinsp-infscroll-enabled .rinsp-infscroll-page-endtrigger:after { content: ""; position: absolute; top: 100vh; width: 1px; height: var(--rinsp-infscroll-height); z-index: 10000; } .rinsp-infscroll-armed .rinsp-infscroll-loader-bar { position: fixed; left: 50%; transform: translatex(-50%); bottom: 0; width: 940px; border-top: 1px dotted var(--rinsp-infscroll-loader-border-color); height: 15px; background: var(--rinsp-infscroll-loader-background); overflow: hidden; } .rinsp-infscroll-firing .rinsp-infscroll-loader-bar:before { content: ""; display: block; position: relative; top: 0; left: 0; height: 100%; background: linear-gradient(to right, var(--rinsp-infscroll-loader-progress) 90%, transparent 100%); animation: count-down 2s; } .rinsp-infscroll-armed:not(.rinsp-infscroll-firing) .rinsp-infscroll-loader-bar:after { content: ""; display: block; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAANCAYAAACtpZ5jAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMi1jMDAwIDc5LjU2NmViYzViNCwgMjAyMi8wNS8wOS0wODoyNTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjZEQTE2QTZENzJDOTExRURCMUZDQTc0M0NBRkM3OUY0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjZEQTE2QTZFNzJDOTExRURCMUZDQTc0M0NBRkM3OUY0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NkRBMTZBNkI3MkM5MTFFREIxRkNBNzQzQ0FGQzc5RjQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NkRBMTZBNkM3MkM5MTFFREIxRkNBNzQzQ0FGQzc5RjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6KmXcOAAABXElEQVR42rTUzSsEcRzHcTutcvIHKHHmJO2FJCkPB5GaiMJFcfEQUeIiSdRykAOxDuugkZI2iaKUQnKQkvJQ5ODCwUkx3r98pjZNQ7S/erX1nf195vt72A25rpuWihF2HMficwY5aMPzP/Ia0WnbdqmlwhkiuEDLHwIzsYKo15gJ/kAMWVjFBHaR+8vQZlyiDK2o84KTRy9q9JJD9AUEZmMbU1jTnB3voSXLuNV2nCIPswo221TwLbQHRwo3HXapblZ77wWbazGPK2xgWl8aRxFekcAgCnGCAcypgWPVz3U+C8nBZtmVmtCAR1ThDiUYQTc28abamBqIqv6kRkb99jiuLvaxpJPOwKIm9aMY16jAg67YEMrVyNc99jmUFzShWoE3WklcE9P1UhN0gA6/ux8OOPUtHY5Z6iRqsYdhPW/X3vsO64c7+q69rUe+Qtf1K00ETQyl6r/iU4ABAH2/U1xZevwpAAAAAElFTkSuQmCC) top center no-repeat; } @keyframes count-down { 0% { width: 0%; } 100% { width: 120%; } } /* ==== lazy image loading ==== */ img[src][data-rinsp-defer-src] { width: auto; animation: fade-in 2s alternate infinite; } @keyframes fade-in { 0% { opacity: 0.2; } 100% { opacity: 1; } } /* ==== reply refresh free anim ==== */ .rinsp-reply-refresh-free { position: relative; display: block; } .rinsp-reply-refresh-free.rinsp-refresh-free-submitting::after { content: "提交中 ..."; display: flex; align-items: center; justify-content: center; position: absolute; top: 0; height: 100%; left: 0; width: 100%; z-index: 1; background: rgba(255,255,255,0.7); } .rinsp-reply-refresh-free.rinsp-refresh-free-submitting::before { content: "⌛"; font-size: 16px; display: inline-block; animation-name: rinsp-watch-checking-rotate-anim; animation-duration: 2s; animation-iteration-count: infinite; z-index: 2; position: absolute; top: calc(50% - 0.8em); left: calc(50% - 3em); } /* ==== get sp animation ==== */ #guide a[href="plugin.php?H_name-tasks.html"][rinsp-sp-get]::before { content: "+ " attr(rinsp-sp-get) "SP"; position: absolute; transform: translate(0, 20%); opacity: 0; color: #000; background: linear-gradient(to bottom, #fff7a0 0%, #ffc033 100%); padding: 2px 5px; border: 1px solid #ee9905; border-radius: 16px; box-shadow: inset 0 0 2px #fff, inset 0 0 2px #fff, inset 0 0 2px #fff, inset 0 0 2px #fff; line-height: normal; z-index: 1; font-weight: normal; animation-name: sp-get; animation-duration: 5s; animation-iteration-count: 1; animation-timing-function: ease-out; } @keyframes sp-get { 0% { transform: translate(0, 20%); opacity: 1; } 20% { transform: translate(0, -140%); opacity: 1; } 80% { transform: translate(0, -140%); opacity: 1; } 100% { transform: translate(0, -140%); opacity: 0; } } /* ==== batch ping/sel functions ==== */ .rinsp-batchsel-form { max-height: calc(90vh - 150px); overflow-x: hidden; overflow-y: auto; } .rinsp-batchsel-form-op-cell { text-align: left; padding-left: 20px; } .rinsp-batchsel-form-op-cell.rinsp-batchsel-form-op-known::before { content: "🔖 "; margin-left: -20px; } .rinsp-batchsel-form-item-unchecked { opacity: 0.8; background: #EEE; } .rinsp-batchsel-form-item-unavailable { opacity: 0.3; } .rinsp-batchsel-form-item-unavailable > td:first-child > * { display: none; } .rinsp-batchsel-form-item-checked { background: linear-gradient(to right, #EFE 0%, transparent 50%); } .rinsp-batchping-form-item-unscore { background: linear-gradient(to right, #FEE 0%, transparent 50%); } .rinsp-batchping-form-item-unscore .rinsp-batchping-form-ping-cell::before, .rinsp-batchping-form-item-unchecked .rinsp-batchping-form-ping-cell::before { content: "---"; color: #CCC; display: block; text-align: center; } .rinsp-batchping-form-item-unchecked .rinsp-batchping-form-ping-cell > *, .rinsp-batchping-form-item-unchecked .rinsp-batchping-form-ping-cell + td > * { display: none; } .rinsp-batchping-form-item-unscore .rinsp-batchping-form-sp-input, .rinsp-batchping-form-item-unscore .rinsp-batchping-form-sp-input + span { display: none; } .rinsp-batchping-progress-list { padding: 0 1em 1em 1em !important; } .rinsp-batchping-progress-list > li { padding: 0.3em 0 !important; } .rinsp-batchping-progress-list > li:first-child { font-weight: bold; } /* ==== ping functions ==== */ .rinsp-quickping-form { width: 500px; padding: 0.2em; display: flex; flex-wrap: wrap; } .rinsp-quickping-form > dt { padding: 0.5em 0.5em 0.5em 2em; flex-basis: 4em; flex-shrink: 0; flex-grow: 0; text-align: right; } .rinsp-quickping-form > dd { padding: 0.5em 0.5em; flex-grow: 1; } .rinsp-quickping-form > div { flex-grow: 1; flex-basis: 100%; border-top: 1px dotted #CCC; } .rinsp-quickping-attr-ownbought { color: #0000FF; } .rinsp-quickping-attr-owntranslate { color: #FF0000; } .rinsp-quickping-attr-compilation { color: #FF00FF; } .rinsp-quickping-attr-ownbought, .rinsp-quickping-attr-owntranslate, .rinsp-quickping-attr-compilation, .rinsp-quickping-attr-extra { font-weight: bold; display: flex; align-items: center; } .rinsp-quickping-status-error { color: #990000; } .h2 a.rinsp-quickping-button { font-weight: bold; color: #ce0940; } .h2 a.rinsp-instantping-button { font-weight: bold; color: #0928ce; } .h2 a.rinsp-quickping-button.rinsp-quickping-grey, .h2 a.rinsp-instantping-button.rinsp-instantping-grey { color: #808080; font-weight: normal; } .h2 a.rinsp-noping-button, .h2 a.rinsp-badping-button { font-weight: bold; color: #cc0000; } .h2 a.rinsp-quickping-status { font-weight: bold; color: #112591; } .rinsp-quickping-form > .rinsp-quickping-preview-section { padding-top: 0.3em; border-top: none; text-align: center; font-size: 18px; } .rinsp-quickping-subject { padding: 0.5em 1em; word-break: break-all; font-size: 14px; max-width: 40em; background: #EEE; } .rinsp-quickping-reason-cell { display: flex; } .rinsp-quickping-reason-cell > textarea { flex: 1; } .rinsp-post-illegal-sell .tpc_content h6.quote.jumbotron > .s3.f12.fn { color: red; font-weight: bold; } .readbot .rinsp-markcorrect-switch, .readbot .rinsp-punishrequest-switch, .readbot .rinsp-punishclassify-switch { width: 5em; } .rinsp-ping-preset-switch::before { content: "🔘"; } .rinsp-ping-preset-switch { margin-left: 0.5em; color: #333; } form[action="job.php?action=endreward"] table > tbody > tr > th:first-child { text-align: right; padding: .3em .6em; line-height: 25px; } /* ==== admin show user name ==== */ .rinsp-user-name-reveal { font-family: monospace; padding: 0px 2px; display: inline-block; outline: 1px solid #CCC; margin-left: 0.5em; } /* ==== admin punish status ==== */ .rinsp-profile-punish-tag-cell { padding-right: 16px; text-align: right; } .rinsp-profile-punish-tag-cell .rinsp-profile-punish-tags { font-size: 13px; } .rinsp-profile-punish-tag { font-weight: bold; font-family: monospace; margin-left: 0.4em; border: 1px solid #CCC; padding: 1px 5px; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag::before { font-weight: normal; margin-right: 1px; font-size: 0.9em; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-blood { border-color: #C00; color: #FFF; background-color: #C00; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-blood::before { content: "血"; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-ban { border-color: #C00; color: #C00; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-ban::before { content: "禁"; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-del { border-color: #D80; color: #D80; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-del::before { content: "删"; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-misc { border-color: #059; color: #059; } .rinsp-profile-punish-tags .rinsp-profile-punish-tag-misc::before { content: "他"; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tags { font-size: 11px; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tags::before { content: "🚩"; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tag { border-width: 0; margin-left: 0; padding: 0; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tag:first-child::before { content: ""; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tag::before { content: "/"; color: #CCC; } .rinsp-userframe-udata-punish-tags .rinsp-profile-punish-tag-blood { background: none; color: #000; } .rinsp-punish-history-table .rinsp-profile-punish-tag-blood > th { color: #C00; border-left: 3px solid #C00; } .rinsp-punish-history-table .rinsp-profile-punish-tag-ban > th { border-left: 3px solid #C00; } .rinsp-punish-history-table .rinsp-profile-punish-tag-del > th { border-left: 3px solid #D80; } .rinsp-punish-history-table .rinsp-profile-punish-tag-misc > th { border-left: 3px solid #059; } .rinsp-punish-history-table .rinsp-profile-punish-tag-none > th { border-left: 3px solid #CCC; } /* ==== admin punish log ==== */ input[name="ifdel"][value="0"]:checked + .rinsp-record-delatc-msg { display: none; } input[name="step"][value="2"]:checked + .rinsp-record-showping-option { display: none; } .rinsp-punish-history-table { width: 100%; } .rinsp-punish-history-table > tbody > tr > th { width: 6em; } .rinsp-punish-history-table > tbody > tr > th > div { font-weight: bold; } .rinsp-punish-history-table .rinsp-punish-history-summary-cell { color: var(--rinsp-darkest-text-color); cursor: default; } .rinsp-punish-history-table .rinsp-punish-history-summary-cell summary::marker { color: #999; } .rinsp-punish-history-table .rinsp-punish-history-summary-cell summary > div { margin-left: 10px; white-space: pre-line; color: #AAA; } .rinsp-punish-history-data { margin-left: 2px; border-left: 1px solid #ccc; padding-left: 9px; } .rinsp-punish-history-data label { color: #4e83a8; margin-right: 0.5em; } .rinsp-punish-history-data label + span { color: #666; } .rinsp-punish-history-table > tbody > tr:hover { outline: 1px solid #DDD; } .rinsp-punish-history-table .rinsp-punish-history-del-cell { width: 5em; text-align: right; } .rinsp-punish-history-del-cell > a { cursor: pointer; } /* ==== safe mode ==== */ .rinsp-safe-mode #cate_thread #tab_1 img:not([data-rinsp-defer-src]):not(:hover), .rinsp-safe-mode .tpc_content img:not([data-rinsp-defer-src]):not([src^="images/post/smile/"]):not(:hover) { filter: var(--rinsp-image-masking-filter); transform: scale(0.99); } .rinsp-safe-mode .rinsp-pinuser-item-face img:not([data-rinsp-defer-src]):not(:hover), .rinsp-safe-mode:not(.rinsp-safe-mode-allow-myself) #user_info a[href="u.php"] img:not([data-rinsp-defer-src]):not(:hover), .rinsp-safe-mode.rinsp-safe-mode-allow-myself #u-portrait img.pic:not(.rinsp-myavatar):not(:hover), .rinsp-safe-mode:not(.rinsp-safe-mode-allow-myself) #u-portrait img.pic:not(:hover), .rinsp-safe-mode .js-post:not(.rinsp-my-post) a[href^="u.php?action-show-uid-"]:not(:hover) > img:not([data-rinsp-defer-src]), .rinsp-safe-mode:not(.rinsp-safe-mode-allow-myself) .js-post.rinsp-my-post a[href^="u.php?action-show-uid-"]:not(:hover) > img:not([data-rinsp-defer-src]) { content: url(/images/face/none.gif); } .rinsp-safe-mode .rinsp-quick-action.rinsp-safe-mode-toggle { display: block; } /* ==== dark mode toggle ==== */ .rinsp-quick-action.rinsp-dark-mode-toggle::before { content: "🌒开启暗黑模式"; } .rinsp-dark-mode-set .rinsp-quick-action.rinsp-dark-mode-toggle::before { content: "☀️关闭暗黑模式"; } /* ==== safe mode toggle ==== */ .rinsp-quick-action.rinsp-safe-mode-toggle::before { content: "😇开启贤者模式"; } .rinsp-safe-mode .rinsp-quick-action.rinsp-safe-mode-toggle::before { content: "😈关闭贤者模式"; } /* ==== patch forum default styles ==== */ /* prevent category column to become too narrow due to user name being too long */ #u-contentmain > form[action="u.php?action=favor&"] > table > tbody > tr > td:nth-child(3) { min-width: 2em; } .rinsp-user-link { max-width: 7em; display: inline-block; font-size: 12px } .rinsp-uphp-post #u-contentmain > table > tbody > tr > th + td { font-size: 10px; } .rinsp-uphp-post #u-contentmain > table > tbody > tr > th + td > .rinsp-user-link { display: block; } .rinsp-hide-mobileswitch #main > center.gray3 > div > a[href^="/simple/"] { display: none; } /* text size overrides */ html.rinsp-textsize-step-1 { font-size: 62.5%; } html.rinsp-textsize-step-1 .set-table2 td { padding: 0.5em 0.5em; } html.rinsp-textsize-step-1 #set-menu, html.rinsp-textsize-step-1 #info_base td > a[href^="message.php?"], html.rinsp-textsize-step-1 th.y-style > a { font-size: 1.4rem; } html.rinsp-textsize-step-1 .t_one > td > h3 { font-size: 13px; } html.rinsp-textsize-step-1 #u-fav-cate form[action="u.php?action=favor&"] { font-size: 1.3rem; } html.rinsp-textsize-step-2 #main, html.rinsp-textsize-step-2 #rinsp-watcher-menu { font-size: 1.4rem; } html.rinsp-textsize-step-2 h1 { font-size: 1.8rem; } html.rinsp-textsize-step-2 .small { font-size: 1.4rem; } html.rinsp-textsize-step-2 .f14, html.rinsp-textsize-step-2 .middle { font-size: 1.7rem; } html.rinsp-textsize-step-2 .big { font-size: 2rem; } html.rinsp-textsize-step-3 #main, html.rinsp-textsize-step-3 #rinsp-watcher-menu { font-size: 1.5rem; } html.rinsp-textsize-step-3 .tiptop { font-size: 1.3rem; } html.rinsp-textsize-step-3 h1 { font-size: 2rem; } html.rinsp-textsize-step-3 .small { font-size: 1.4rem; } html.rinsp-textsize-step-3 .f14, html.rinsp-textsize-step-3 .middle { font-size: 1.8rem; } html.rinsp-textsize-step-3 .big { font-size: 2.1rem; } html.rinsp-textsize-step-3 .t_one > td > h3 { font-size: 14px; } /* fix textarea too wide in firefox */ form[action="message.php"] textarea#atc_content { width: calc(100% - 0.5em); } form[action="mawhole.php?"] textarea[name="atc_content"] { width: calc(100% - 300px); } #info_base .td1 { min-width: 4em; } /* 短消息设置 width bug */ textarea[name="banidinfo"] { width: 100%; } /* handy highlights */ .rinsp-last-clicked { outline: 2px solid #dbbc60; outline-offset; } `; const darkThemeCss = ` :root { --rinsp-dm-invert-filter: invert(1) hue-rotate(180deg); --rinsp-dm-invert-filter-blk: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.81); --rinsp-dm-invert-filter-darkbg1: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.76) brightness(1.1); --rinsp-dm-hgt-text-filter: contrast(0.8) brightness(1.2); --rinsp-dm-color-black: #0d0d0d; --rinsp-dm-color-darkbg1: #141414; --rinsp-dm-color-darkbg2: #2d2d2d; --rinsp-dm-color-darkbg-hgt: #2f2f2f; --rinsp-dm-color-darkbg-hover: #36311f; --rinsp-dm-color-border: #666; --rinsp-dm-color-light-border: #AAA; --rinsp-dm-color-text: #CCC; --rinsp-dm-color-link: #BBB; --rinsp-dm-color-content-link: #6388A8; --rinsp-dm-color-content-link-hover: #8FB6D7; --rinsp-dm-color-heading: #eaeaea; --rinsp-dm-color-label: #eaeaea; --rinsp-dm-color-ctrl: #eaeaea; --rinsp-dm-color-grey: #AAA; --rinsp-dm-button-bg: #4b0707; } @media (prefers-color-scheme: light) { html.rinsp-dark-mode input[type="checkbox"], html.rinsp-dark-mode input[type="radio"] { filter: invert(1) hue-rotate(180deg) brightness(0.8); } } html.rinsp-dark-mode { opacity: 1; transition: opacity 0.08s ease-in; --rinsp-active-toggler-bg-color: #2b2b2b; --rinsp-active-toggler-fg-color: #CCC; --rinsp-visited-link-color: #AAA; --rinsp-visited-link-hover-color: #CCC; --rinsp-visited-update-color: #dba4f9; --rinsp-visited-update-border-color: #843cab; --rinsp-blocked-row-bg: #222; --rinsp-blocked-row-label-color: #999; --rinsp-avatar-replace-default-text-color: #999; --rinsp-avatar-replace-default-border-color: #202224; --rinsp-avatar-replace-default-bg: #303234; --rinsp-cell-background-grey-out: #444; --rinsp-cell-background-hgt-new: #44430e; --rinsp-cell-background-hgt-err: #491616; --rinsp-cell-background-active: #283d44; --rinsp-lightest-bg-color: #111; --rinsp-darkest-text-color: #CCC; --rinsp-text-color-orange: #bf9342; --rinsp-text-color-violet: #aa4ec4; --rinsp-text-color-green: #2fa847; --rinsp-text-color-red: #a63232; --rinsp-text-color-blue: #4ca0d3; --rinsp-text-color-grey: #999; --rinsp-infscroll-divider-text-color: #556a71; --rinsp-infscroll-divider-border-color: #535c5c; --rinsp-infscroll-divider-background: #1e2d30; --rinsp-infscroll-loader-border-color: #555; --rinsp-infscroll-loader-background: #272727; --rinsp-infscroll-loader-progress: #444; } html.rinsp-dark-mode.rinsp-dark-theme-darkbeige { --rinsp-dm-color-black: #181818; --rinsp-dm-color-darkbg1: #1c1c19; --rinsp-dm-color-darkbg2: #26292b; --rinsp-dm-color-darkbg-hgt: #242d31; --rinsp-dm-color-darkbg-hover: #413d2c; --rinsp-dm-color-border: #666; --rinsp-dm-color-light-border: #AAA; --rinsp-dm-color-text: #BBB; --rinsp-dm-color-link: #BBB; --rinsp-dm-color-content-link: #6388A8; --rinsp-dm-color-content-link-hover: #8FB6D7; --rinsp-dm-color-heading: #a08c6c; --rinsp-dm-color-label: #BBB; --rinsp-dm-color-ctrl: #958568; --rinsp-dm-color-grey: #AAA; --rinsp-dm-button-bg: #4b0707; } html.rinsp-dark-mode.rinsp-dark-theme-darkblue { --rinsp-dm-color-black: #161b1f; --rinsp-dm-color-darkbg1: #0d0e14; --rinsp-dm-color-darkbg2: #0e1622; --rinsp-dm-color-darkbg-hgt: #151f25; --rinsp-dm-color-darkbg-hover: #363019; --rinsp-dm-color-border: #383d3e; --rinsp-dm-color-light-border: #666; --rinsp-dm-color-text: #BBB; --rinsp-dm-color-link: #CCC; --rinsp-dm-color-heading: #668184; --rinsp-dm-color-label: #999; --rinsp-dm-color-ctrl: #8faab1; --rinsp-dm-color-grey: #9a9a9a; --rinsp-dm-button-bg: #4b0707; } html.rinsp-dark-mode.rinsp-dark-theme-hcdarkbeige { --rinsp-dm-invert-filter-blk: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.91); --rinsp-dm-invert-filter-darkbg1: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.88) brightness(1.1); --rinsp-dm-hgt-text-filter: none; --rinsp-darkest-text-color: #EEE; --rinsp-dm-color-black: #000; --rinsp-dm-color-darkbg1: #111; --rinsp-dm-color-darkbg2: #161616; --rinsp-dm-color-darkbg-hgt: #242d31; --rinsp-dm-color-darkbg-hover: #413d2c; --rinsp-dm-color-border: #666; --rinsp-dm-color-light-border: #AAA; --rinsp-dm-color-text: #EEE; --rinsp-dm-color-link: #DDD; --rinsp-dm-color-heading: #e6b466; --rinsp-dm-color-label: #CCC; --rinsp-dm-color-ctrl: #d7aa5b; --rinsp-dm-color-grey: #AAA; --rinsp-dm-button-bg: #4b0707; --rinsp-dm-color-content-link: #64b7ff; --rinsp-dm-color-content-link-hover: #8FB6D7; } html.rinsp-dark-mode.rinsp-dark-theme-hcdarkblue { --rinsp-dm-invert-filter-blk: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.91); --rinsp-dm-invert-filter-darkbg1: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.88) brightness(1.1); --rinsp-dm-hgt-text-filter: none; --rinsp-dm-color-black: #000; --rinsp-dm-color-darkbg1: #111; --rinsp-dm-color-darkbg2: #161616; --rinsp-dm-color-darkbg-hgt: #242d31; --rinsp-dm-color-darkbg-hover: #363019; --rinsp-dm-color-border: #666; --rinsp-dm-color-light-border: #666; --rinsp-dm-color-text: #EEE; --rinsp-dm-color-link: #DDD; --rinsp-dm-color-heading: #9bd8e6; --rinsp-dm-color-label: #CCC; --rinsp-dm-color-ctrl: #88b5d7; --rinsp-dm-color-grey: #AAA; --rinsp-dm-button-bg: #4b0707; --rinsp-dm-color-content-link: #64b7ff; --rinsp-dm-color-content-link-hover: #8FB6D7; } html.rinsp-dark-mode.rinsp-dark-theme-darkgrey { --rinsp-dm-invert-filter-blk: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.91); --rinsp-dm-invert-filter-darkbg1: invert(1) hue-rotate(180deg) saturate(1.2) contrast(0.88) brightness(1.1); --rinsp-dm-color-black: #000; --rinsp-dm-color-darkbg1: #111; --rinsp-dm-color-darkbg2: #222; --rinsp-dm-color-darkbg-hgt: #333; --rinsp-dm-color-darkbg-hover: #444; --rinsp-dm-color-border: #444; --rinsp-dm-color-light-border: #666; --rinsp-dm-color-text: #BBB; --rinsp-dm-color-link: #999; --rinsp-dm-color-heading: #CCC; --rinsp-dm-color-label: #AAA; --rinsp-dm-color-ctrl: #999; --rinsp-dm-color-grey: #999; --rinsp-dm-button-bg: #440303; } html.rinsp-dark-mode.rinsp-dark-theme-hcdarkbeige body, html.rinsp-dark-mode.rinsp-dark-theme-hcdarkblue body, html.rinsp-dark-mode.rinsp-dark-theme-darkgrey body { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAD6AAAAAFCAMAAABmOOOEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADBQTFRFGxsbLi4uMTExHR0dKysrREREICAgOjo6JiYmIyMjNzc3NTU1WlpaLCwsDAwMGRkZMVEcwAAAAK5JREFUeNrs28ERwyAMBEAZjLEdJ+m/26gNor3HFqAHww0o3t8MAODv2SKOa+5tjo+ILJFntv28j97DGQYANQgzAIAaRPTjPrOgP2qPyBoZWdDnlQV9c4YBQJH7mhkAQBEiXlcW9McLusg6BX2cWdA39zUAKEKYAQAUIdqRBX344i6yTkEfexb0Fu5rAFDlPcUMAKDKEnrPgj7soIusU9CflgX91XtYQgeAGvwEGADVjGYq2FVMyAAAAABJRU5ErkJggg==); } html.rinsp-dark-mode .menu .bor { border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .rinsp-thread-filter-menu-button:after { filter: brightness(2); } html.rinsp-dark-mode .rinsp-config-item-tip { color: #668ca2; } @media (max-width: 1278px) { html.rinsp-dark-mode .rinsp-pinuser-main:not(:empty) { background-image: url(data:image/gif;base64,R0lGODlhEAAFAIAAADMzMyMjIyH5BAAAAAAALAAAAAAQAAUAAAIOjAOHmtf2lmN0WljxRQUAOw==); } } html.rinsp-dark-mode body { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAD6AAAAAFCAMAAABmOOOEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADBQTFRFJSUlNTU1ODg4JiYmR0dHKCgoLS0tKysrMjIyPj4+VlZWYWFhQkJCMzMzGBgYIyMjHMR+EwAAAK1JREFUeNrs28kRxCAMBEAZ8IG9R/7ZrtJg1fPoAPSgmALF+5sBAPw9W8R+vq42x0dElsgz23XMvfdwhgFADcIMAKAGEX2fRxb0R+0RWSMjC/rrzIK+OcMAoMh9zQwAoAgR7cyCfntBF1mnoI8jC/rmvgYARQgzAIAiRNuzoA9f3EXWKejjyoLewn0NAKq8p5gBAFRZQu9Z0IcddJF1CvrdsqC33sMSOgDU4CfAAHHlZhZqfHuaAAAAAElFTkSuQmCC); } html.rinsp-dark-mode { --rinsp-image-masking-filter: contrast(0%) brightness(30%) drop-shadow(0px 0px 1px #FFF); } html.rinsp-dark-mode #fav-fid li { filter: grayscale(); } html.rinsp-dark-mode .guide li.current a { background: #822d2d8f; } html.rinsp-dark-mode .rinsp-quick-action-overlay:focus { background: #111A; } html.rinsp-dark-mode h1, html.rinsp-dark-mode h2, html.rinsp-dark-mode h3, html.rinsp-dark-mode h4, html.rinsp-dark-mode h5, html.rinsp-dark-mode h6 { color: var(--rinsp-dm-color-heading); } html.rinsp-dark-mode body, html.rinsp-dark-mode .gray2, html.rinsp-dark-mode .gray3, html.rinsp-dark-mode #honor { color: var(--rinsp-dm-color-ctrl); } html.rinsp-dark-mode td, html.rinsp-dark-mode th, html.rinsp-dark-mode #set-menu li a { color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode #u-top-nav li a { background: transparent; color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode #u-top-nav li:not(.current) { box-shadow: 0 1px 0 var(--rinsp-dm-color-border); } html.rinsp-dark-mode .gray, html.rinsp-dark-mode .threadlist li a { color: var(--rinsp-dm-color-grey); } /* code block */ html.rinsp-dark-mode .blockquote2 ol li { background-color: unset; } /* thread list typical color headings */ html.rinsp-dark-mode .stream li .section-title a font[color="#0000FF"], html.rinsp-dark-mode .t_one h3 > a > b > font[color="#0000FF"] { color: #0af; } html.rinsp-dark-mode .stream li .section-title a font[color="#FF0000"], html.rinsp-dark-mode .t_one h3 > a > b > font[color="#FF0000"] { color: #f33; } html.rinsp-dark-mode .stream li .section-title a font[color="#FF00FF"], html.rinsp-dark-mode .t_one h3 > a > b > font[color="#FF00FF"] { color: #e5f; } html.rinsp-dark-mode .tpc_content span[style="color:#000000 "] { color: unset !important; } html.rinsp-dark-mode .tpc_content span[style="background-color:#ffffff "] { background-color: unset !important; } html.rinsp-dark-mode .tpc_content span[style="color:#333333 "] { color: color: var(--rinsp-dm-color-grey) !important; } /* mobile version switcher / redirect landing page */ html.rinsp-dark-mode div[style^="padding:15px 60px;border:1px solid #eeeeee;background:#ffffff"], html.rinsp-dark-mode #main > center.gray3 > div > a[href^="/simple/"] > div[style] { background-color: var(--rinsp-dm-color-black) !important; border-color: var(--rinsp-dm-color-black) !important; color: var(--rinsp-dm-color-border) !important; } html.rinsp-dark-mode #header .banner, html.rinsp-dark-mode .rinsp-input-max-hint { filter: var(--rinsp-dm-invert-filter-blk); } html.rinsp-dark-mode #footer { background-color: var(--rinsp-dm-color-black); border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .gonggao { background-color: var(--rinsp-dm-color-black); border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .gongul { border-color: transparent; } html.rinsp-dark-mode .gonggao .gongul li { background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .gonggao .gongul .lin { border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode body, html.rinsp-dark-mode button, html.rinsp-dark-mode input, html.rinsp-dark-mode select, html.rinsp-dark-mode textarea, html.rinsp-dark-mode .tips, html.rinsp-dark-mode .btn, html.rinsp-dark-mode .abtn, html.rinsp-dark-mode .set-h2 { color: var(--rinsp-dm-color-grey); background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode th[bgcolor="#ffffff"], html.rinsp-dark-mode .t_one, html.rinsp-dark-mode .f_one, html.rinsp-dark-mode .r_one { background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .card, html.rinsp-dark-mode .f_two { background: var(--rinsp-dm-color-darkbg2); } html.rinsp-dark-mode .stream li .section-text > span { filter: invert(0.6); } html.rinsp-dark-mode .stream li .section-intro { filter: brightness(0.7); } html.rinsp-dark-mode .stream li { background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .stream li .section-title a { color: var(--rinsp-dm-color-link); } html.rinsp-dark-mode .t table { border-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .tipad .fr a { color: var(--rinsp-dm-color-ctrl); } html.rinsp-dark-mode #breadcrumbs, html.rinsp-dark-mode #set-side-wrap, html.rinsp-dark-mode #u-wrap, html.rinsp-dark-mode #u-wrap2, html.rinsp-dark-mode #u-portrait, html.rinsp-dark-mode .bgA { color: var(--rinsp-dm-color-label); background: var(--rinsp-dm-color-darkbg1); border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .tr2, html.rinsp-dark-mode .h2, html.rinsp-dark-mode .menu, html.rinsp-dark-mode .user-infoWrap { color: var(--rinsp-dm-color-label); background: var(--rinsp-dm-color-darkbg2); border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .user-infoWrap { color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .user-info .co { filter: invert(0.5); } html.rinsp-dark-mode .h, html.rinsp-dark-mode .t { color: var(--rinsp-dm-color-heading); background: var(--rinsp-dm-color-darkbg1); border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .u-h5 .r, html.rinsp-dark-mode .u-h5 span { background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .r_two { background-color: var(--rinsp-dm-color-darkbg1); border-right: 1px dotted var(--rinsp-dm-color-border); } html.rinsp-dark-mode th.r_two > .user-pic.rinsp-userpic-sticky { background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .tr4 { background-color: var(--rinsp-dm-color-darkbg2); } html.rinsp-dark-mode .user-infoWraptwo { background-color: var(--rinsp-dm-color-darkbg2) !important; } html.rinsp-dark-mode .blockquote { background-color: var(--rinsp-dm-color-darkbg2); } html.rinsp-dark-mode .blockquote2, html.rinsp-dark-mode .blockquote3 { background-color: var(--rinsp-dm-color-darkbg1); border-color: var(--rinsp-dm-color-border); color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode input, html.rinsp-dark-mode select, html.rinsp-dark-mode textarea, html.rinsp-dark-mode #editor-tool span { border-color: var(--rinsp-dm-color-border) } html.rinsp-dark-mode h1#subject_tpc { color: var(--rinsp-dm-color-heading) !important; } html.rinsp-dark-mode .rinsp-userframe-userinfo { background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode img[src="images/colorImagination/post.png"]:not(:hover), html.rinsp-dark-mode img[src="images/colorImagination/reply.png"]:not(:hover) { filter: brightness(0.7); } html.rinsp-dark-mode .pages, html.rinsp-dark-mode .pages ul li, html.rinsp-dark-mode .set-h2 { border-color: var(--rinsp-dm-color-border) !important; } html.rinsp-dark-mode .pages ul li b { background: var(--rinsp-dm-color-darkbg-hgt); color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .pages ul li a:hover { background-color: var(--rinsp-dm-color-darkbg2); } html.rinsp-dark-mode .rinsp-message-selection-panel, html.rinsp-dark-mode #set-content { background: var(--rinsp-dm-color-darkbg); border-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .menu #showface { background: var(--rinsp-dm-color-darkbg1) !important; } html.rinsp-dark-mode .menu #buttons.face { filter: var(--rinsp-dm-invert-filter); } html.rinsp-dark-mode #smiliebox { background: var(--rinsp-dm-color-darkbg1) !important; border-color: var(--rinsp-dm-color-darkbg1) !important; } html.rinsp-dark-mode #infobox { background: var(--rinsp-dm-color-black); border-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .btn { background: var(--rinsp-dm-button-bg); } html.rinsp-dark-mode .t5, html.rinsp-dark-mode .tips, html.rinsp-dark-mode th, html.rinsp-dark-mode td, html.rinsp-dark-mode .btn, html.rinsp-dark-mode .abtn, html.rinsp-dark-mode #set-content-wrap { border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .blockquote { color: var(--rinsp-dm-color-text); border-color: var(--rinsp-dm-color-light-border); background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode #editor-tab span, html.rinsp-dark-mode #editor-button { color: #000; filter: var(--rinsp-dm-invert-filter-blk); } html.rinsp-dark-mode a { color: var(--rinsp-dm-color-link); } html.rinsp-dark-mode .tpc_content a { color: var(--rinsp-dm-color-content-link); } html.rinsp-dark-mode .tpc_content a:hover { color: var(--rinsp-dm-color-content-link-hover); } html.rinsp-dark-mode img[src="images/post/c_editor/del.gif"], html.rinsp-dark-mode img[src="images/colorImagination/old.gif"], html.rinsp-dark-mode img[src="images/colorImagination/new.gif"], html.rinsp-dark-mode img[src="images/colorImagination/lock.gif"], html.rinsp-dark-mode img[src^="images/post/editor/"], html.rinsp-dark-mode img[src^="/images/post/smile/"], html.rinsp-dark-mode img[src^="images/post/smile/"] { filter: var(--rinsp-dm-invert-filter-darkbg1); } html.rinsp-dark-mode .set-tab-table { background: transparent; border-bottom: 1px solid var(--rinsp-dm-color-border); } html.rinsp-dark-mode .set-tab-table td.current { background-color: var(--rinsp-dm-color-darkbg1); border-color: var(--rinsp-dm-color-light-border); color: var(--rinsp-dm-color-text); font-weight: normal; border-bottom: 1px solid var(--rinsp-dm-color-darkbg1); position: relative; top: 1px; } html.rinsp-dark-mode .set-tab-table td:not(.current) { border-color: transparent; position: relative; top: 1px; } html.rinsp-dark-mode .set-tab-table td.current a { color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .set-tab-box { border-color: transparent; background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .tt2 { border-color: var(--rinsp-dm-color-border); } html.rinsp-dark-mode .tt2 table { background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode form[action="message.php"] .set-table2 > tbody > tr:not(.gray3):nth-child(odd) { background-color: var(--rinsp-dm-color-darkbg-hgt); } html.rinsp-dark-mode form[action="message.php"]:not([onsubmit="return checkCnt();"]) .set-table2 > tbody > tr:NOT(.gray3):hover { background-color: var(--rinsp-dm-color-darkbg-hover); } html.rinsp-dark-mode .set-table2 td { padding: .5em 1em; border-bottom: 1px dotted var(--rinsp-dm-color-light-border); } html.rinsp-dark-mode form[action="message.php"] a[href^="message.php?"]:not(.b) { color: var(--rinsp-dm-color-ctrl); } html.rinsp-dark-mode .z .tr3:hover, html.rinsp-dark-mode .threadlist li a:hover { background-color: var(--rinsp-dm-color-black); } html.rinsp-dark-mode .menu .ul2 li a:hover, html.rinsp-dark-mode .threadlist li.current a, html.rinsp-dark-mode .threadlist li.current a:hover { background-color: var(--rinsp-dm-color-darkbg1); color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .rinsp-notification-item { background-color: var(--rinsp-dm-color-darkbg); } html.rinsp-dark-mode .rinsp-notification-item { color: var(--rinsp-dm-color-grey); } html.rinsp-dark-mode #peacemakerconfig .blackListPlan { filter: var(--rinsp-dm-invert-filter); border-color: transparent; } html.rinsp-dark-mode .rinsp-pinuser-item-face > div { background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .rinsp-pinuser-item:hover .rinsp-pinuser-item-face > div { background-color: var(--rinsp-dm-color-darkbg-hover); } html.rinsp-dark-mode .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike) { background: linear-gradient(to right, var(--rinsp-text-color-green) 2px, transparent 3px); } html.rinsp-dark-mode .rinsp-thread-filter-like:not(.rinsp-thread-filter-dislike):hover { background: linear-gradient(to right, var(--rinsp-text-color-green) 2px, #142707 3px, transparent 50%); } html.rinsp-dark-mode .rinsp-sharetype-filter { filter: var(--rinsp-dm-invert-filter-blk); } html.rinsp-dark-mode .rinsp-sharetype-filter > .rinsp-sharetype-item { color: #333; } html.rinsp-dark-mode .rinsp-visited-thread-view-mode .rinsp-thread-visited:not(:hover) { opacity: 0.7; } html.rinsp-dark-mode img[src="/images/noimageavailble_icon.png"] { filter: var(--rinsp-dm-invert-filter-blk); opacity: 0.5; } html.rinsp-dark-mode .u-table .fav.forum { background-image: url(data:image/gif;base64,R0lGODlhDgAMALMPAPPz87e3t7S0tPx6G8PDw+Tk5PDw8MrKyl285lKdzVaDrn9/f/r6+vxhAP///wAAACH5BAEAAA8ALAAAAAAOAAwAAARO8KlJ6bsS6b0VVkkojqGyMGiqossCHDBcAMZROMJiEDwxD4MCsLAoGI2MQaMxcCSLgWjAsaxSi4KsgFqt6gxgA4NbvbTOC4JDOcC43+4IADs=); } html.rinsp-dark-mode .u-table .fav.current { background-image: url(data:image/gif;base64,R0lGODlhDgAMAKIHAL29DcvGGKSrBZ6jA5CWANTLMlWAAAAAACH5BAEAAAcALAAAAAAOAAwAAAM0eGds+tCUWRxUkmjd2gpDKI4DEwhd2gQS5U5mts0EUxVCruu2BPxAYK8QKBqNw5fLoVIdEgA7); } html.rinsp-dark-mode .u-table .fav { background-image: url(data:image/gif;base64,R0lGODlhDgAMAKIGAMjIyLi4uK2traSkpNPT02pqagAAAAAAACH5BAEAAAYALAAAAAAOAAwAAAMvaFZc+rCQSRxUcmjd2iJCKI4CQwRd2lRUS5nZJg/wGdz4XQN839euFjA4cahUhgQAOw==); } html.rinsp-dark-mode .rinsp-reply-refresh-free.rinsp-refresh-free-submitting::after { background: #00000099; } html.rinsp-dark-mode .rinsp-thread-filter-pinned .tr1 .r_two, html.rinsp-dark-mode .rinsp-thread-filter-pinned th.r_two > .rinsp-userpic-sticky { background-color: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .rinsp-highlight-spots > dl { opacity: 0.8; } html.rinsp-dark-mode .t_one h3 > a > b > font[color] { filter: var(--rinsp-dm-hgt-text-filter) !important; opacity: 1 !important; } html.rinsp-dark-mode .rinsp-userframe-userinfo, html.rinsp-dark-mode .rinsp-filter-ignored .r_one { background: transparent; } html.rinsp-dark-mode .rinsp-dialog-modal-mask { background: #0008; } html.rinsp-dark-mode .t_one td > a[title="打开新窗口"] { filter: var(--rinsp-dm-invert-filter-darkbg1); } html.rinsp-dark-mode .rinsp-answered-request-ignore-toggler, html.rinsp-dark-mode .rinsp-unanswered-request-ignore-toggler, html.rinsp-dark-mode .rinsp-expired-request-ignore-toggler { background: var(--rinsp-active-toggler-bg-color); } html.rinsp-dark-mode .jumbotron.rinsp-sell-free .btn-danger { background: #0f1e2d; } html.rinsp-dark-mode .jumbotron.rinsp-sell-5 .btn-danger { background: #312705; } html.rinsp-dark-mode .jumbotron.rinsp-sell-100 .btn-danger { background: #490909; } html.rinsp-dark-mode .jumbotron.rinsp-sell-high .s3 { background: transparent; outline-color: transparent; } html.rinsp-dark-mode .jumbotron.rinsp-sell-high .btn-danger { background: #950000; } html.rinsp-dark-mode .jumbotron.rinsp-sell-99999 .btn-danger { background: transparent; color: #fff; font-weight: normal; } html.rinsp-dark-mode .rinsp-pinuser-item-face img { filter: brightness(0.8) contrast(1.1); } html.rinsp-dark-mode .user-pic img { filter: brightness(0.8) contrast(1.1); border-color: var(--rinsp-dm-color-darkbg1); background: var(--rinsp-dm-color-darkbg1); } html.rinsp-dark-mode .rinsp-quickping-subject { background: var(--rinsp-dm-color-darkbg1); color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .rinsp-quickping-preview-section, html.rinsp-dark-mode .rinsp-quickping-form dd { color: var(--rinsp-dm-color-text); } html.rinsp-dark-mode .h2 a.rinsp-quickping-status { filter: var(--rinsp-dm-invert-filter); } html.rinsp-dark-mode .h2 a.rinsp-instantping-button { color: #0ac679; } `; const devCss = ` body .rinsp-dev-toggle { opacity: 1; cursor: default; } *:focus-visible { outline: none; } #header a[href][onclick^="ga("] { display: none; } `; const RESOURCE_AREA_IDS = new Set([14,128,4,73,5,6,201,135,142]); const PAYWALL_AREA_IDS = new Set([171,172,173,174]); const DEV_MODE = checkDevMode(); const DEBUG_MODE = localStorage.getItem('rinsp-debug-mode') === '1'; const TEMP_INCREMENT = 10000; const MIN_REQUEST_DELAY = 5000; const MAX_ACC_REQUEST_RATE_COUNT = 20; const MAX_ACC_REQUEST_RATE_BASE = 60000 * 5; const MIN_CHECK_INTERVAL = 'MTA'; const MAX_CHECK_INTERVAL = 'ODAw'; const MIN_CHECK_INTERVAL_OLDEST_ITEM = 'NQ'; const MAX_CHECK_INTERVAL_OLDEST_ITEM = 'NjAw'; const MAX_WATCH_ITEM_COUNT = DEV_MODE&&'NTA'||'MjA'; const OLD_ITEM_THRESHOLD_DAYS = 14; const OLD_ITEM_SCALING = 1.3; const MAX_IGNORE_CONTENT_HTML_LENGTH = 2048; const MAX_IGNORE_CONTENT_TEXT_LENGTH = 512; const MAX_RECENT_ACCESS_LOG = DEV_MODE ? 5000 : 2000; const WATCHER_POPUP_MENU_ID = 'rinsp-watcher-menu'; const IGNORE_LIST_POPUP_MENU_ID = 'rinsp-ignorelist-menu'; const THREAD_FILTER_POPUP_MENU_ID = 'rinsp-threadfilter-menu'; const THREAD_LIKE_POPUP_MENU_ID = 'rinsp-threadlike-menu'; const THREAD_FILTER_ACTION_POPUP_MENU_ID = 'rinsp-threadfilter-action-menu'; const USER_BOOKMARK_ACTION_POPUP_MENU_ID = 'rinsp-userbookmark-action-menu'; const SCORING_DIALOG_POPUP_MENU_ID = 'rinsp-scoring-dialog'; const LOADING_DIALOG_POPUP_MENU_ID = 'rinsp-loading-dialog'; const SCORING_REASON_TEMPLATE_POPUP_MENU_ID = 'rinsp-scoring-reasonlist-menu'; const darkModeEnabledProperty = createLocalStorageProperty('rinsp-dark-mode'); const darkModeThemeProperty = createLocalStorageProperty('rinsp-dark-theme', 'darkbeige'); const darkModeFollowsSystemProperty = createLocalStorageProperty('rinsp-dark-mode-auto'); const textEncoder = (function() { try { return new TextEncoder(); } catch (e) { return null; } })(); async function getNumMapValue(key) { const strValue = await GM.getValue(key); if (strValue) { try { const items = strValue.split(' ') .map(item => { const pair = item.split(':'); return [ pair[0] * 1, (pair[1] * 1)||0 ]; }) .filter(pair => !Number.isNaN(pair[0])); return new Map(items); } catch (ex) {} } return new Map(); } async function setNumMapValue(key, mapEntries) { const strValue = Array.from(mapEntries).map(entry => entry[1] > 0 ? `${entry[0]}:${entry[1]}` : `${entry[0]}`).join(' '); if (strValue) { await GM.setValue(key, strValue); } else { await GM.deleteValue(key).catch(ex => null); } } function createIdStore(namespace, bucketSize) { const indexKey = `${namespace}:index`; function getBucketKey(bucketId) { return `${namespace}:#${bucketId}`; } async function adjustCount(bucketId, adjustment) { const bucketSizeMap = await getNumMapValue(indexKey); const current = bucketSizeMap.get(bucketId) || 0; const newCount = current + adjustment; if (newCount > 0) { bucketSizeMap.set(bucketId, newCount); } else { bucketSizeMap.delete(bucketId); } await setNumMapValue(indexKey, bucketSizeMap.entries()); } return { async set(id, numValue) { const bucketId = Math.floor(id / bucketSize); const localId = id % bucketSize; const localMap = await getNumMapValue(getBucketKey(bucketId)); const current = localMap.get(localId); const newValue = numValue||0; if (current == null || current !== newValue) { localMap.set(localId, newValue); await setNumMapValue(getBucketKey(bucketId), localMap.entries()); } if (current == null) { await adjustCount(bucketId, 1); } }, async remove(id) { const bucketId = Math.floor(id / bucketSize); const localId = id % bucketSize; const localMap = await getNumMapValue(getBucketKey(bucketId)); if (localMap.get(localId) != null) { localMap.delete(localId); await setNumMapValue(getBucketKey(bucketId), localMap.entries()); await adjustCount(bucketId, -1); } }, async get(id) { const bucketId = Math.floor(id / bucketSize); const localId = id % bucketSize; const localMap = await getNumMapValue(getBucketKey(bucketId)); return localMap.get(localId); }, async getBatch(ids) { if (ids.length === 0) return []; if (ids.length === 1) return [this.get(ids[0])]; const buckets = new Map(); const answer = ids.map(id => null); ids.forEach((id, i) => { const bucketId = Math.floor(id / bucketSize); const localId = id % bucketSize; const entry = [localId, i]; let localEntries = buckets.get(bucketId); if (localEntries == null) { localEntries = [entry]; buckets.set(bucketId, localEntries); } else { localEntries.push(entry); } }); async function loadBucket(bucketId, localEntries) { const localMap = await getNumMapValue(getBucketKey(bucketId)); localEntries.forEach(localEntry => { const value = localMap.get(localEntry[0]); answer[localEntry[1]] = value >= 0 ? value : null; }); } // console.info('buckets', buckets); const promises = []; Array.from(buckets.entries()).forEach(entry => { promises.push(loadBucket(entry[0], entry[1])); }); await Promise.allSettled(promises); return answer; }, async size() { const bucketSizeMap = await getNumMapValue(indexKey); let sum = 0; for (let count of bucketSizeMap.values()) { sum += count; } return sum; }, async clear() { const keys = await GM.listValues(); const results = []; keys.filter(key => key.startsWith(namespace + ':')).forEach(key => { results.push(GM.deleteValue(key)); }); await Promise.allSettled(results).catch(ex => null); } }; } function createRecordStore(namespace, maxSize) { const indexKey = maxSize >= 0 ? `${namespace}:recent` : null; function getItemKey(id) { return `${namespace}:#${id}`; } return { async put(id, data) { if (indexKey) { const now = Date.now(); const recordMap = await getNumMapValue(indexKey); const baseValueOffset = recordMap.get(0)||0; // special value recordMap.delete(0); const records = []; records.push([id, now]); recordMap.forEach((value, key) => { if (key !== id) { records.push([key, value + baseValueOffset]); } }); records.sort(comparator(1, true)); // bigger number first const removedRecords = []; while (records.length > maxSize) { removedRecords.push(records.pop()); } let minValue = records[records.length - 1][1]; records.forEach(record => { record[1] = record[1] - minValue; }); records.push([0, minValue]); await setNumMapValue(indexKey, records); for (let removedRecord of removedRecords) { await GM.deleteValue(getItemKey(removedRecord[0])).catch(ex => null); } } await GM.setValue(getItemKey(id), JSON.stringify(data)); }, async get(id) { const value = await GM.getValue(getItemKey(id)); return value == null ? null : JSON.parse(value); }, async remove(id) { await GM.deleteValue(getItemKey(id)).catch(ex => null); if (indexKey) { const recordMap = await getNumMapValue(indexKey); recordMap.remove(id); await setNumMapValue(indexKey, recordMap.entries()); } }, async list() { const keys = await GM.listValues(); const queue = []; keys.filter(key => key.startsWith(namespace + ':#')).forEach(key => { queue.push(GM.getValue(key)); }); const values = await Promise.all(queue); return values.map(value => JSON.parse(value)); }, async clear() { const keys = await GM.listValues(); const results = []; keys.filter(key => key.startsWith(namespace + ':')).forEach(key => { results.push(GM.deleteValue(key)); }); await Promise.allSettled(results).catch(ex => null); } }; } function createUserHashLookupStore() { // uhashStore <uhash-base10: uid> return createIdStore('uhash_uid_mapping', 1000); // bucket size = 1000, DO NOT CHANGE } function createThreadHistoryStore(myUserId, userConfig) { if (!userConfig.keepVisitPostHistory) { return null; } // historyStore <tid: maxFloor> const historyStore = createIdStore(`tid_visit_history#${myUserId}`, 1000); // bucket size = 1000, DO NOT CHANGE const recentAccessStore = createRecordStore(`tid_recent#${myUserId}`, MAX_RECENT_ACCESS_LOG); return { historyStore, recentAccessStore }; } function findMyUserId() { const userWrap = document.querySelector('#user_info #showface .user-infoWraptwo'); if (userWrap != null) { const userMatch = userWrap.textContent.match(/\sUID: +(\d+)\s/); if (userMatch != null) { return userMatch[1] * 1; } } const selfInfo = document.querySelector('#menu_profile .ul2 a[href^="u.php?action-show-uid-"]'); if (selfInfo != null && selfInfo.textContent === '查看个人资料') { return Number.parseInt(selfInfo.getAttribute('href').substring(22))||null; } return null; } function hasMentionedMe(title, myUserId, userConfig) { if (title.indexOf('@' + userConfig.myUserHashId) !== -1) // e.g. @a89f389e return true; if (new RegExp('(^|[^A-Za-z0-9])' + escapeRegExp(userConfig.myUserHashId) + '([^A-Za-z0-9]|$)').exec(title) != null) // e.g. a89f389e return true; if (new RegExp('(^|[^0-9])' + myUserId + '([^0-9]|$)').exec(title) != null) { if (title.indexOf('#' + myUserId) !== -1) // e.g. #1234567 return true; if (title.match(/(^|[^a-z])u?id([^a-z]|$)/i) != null) // e.g. uid 1234567 return true; } const nickNameNorm = (userConfig.myNickName||'').trim().replace(/\s+/, ' '); if (nickNameNorm) { const titleNorm = title.replace(/\s+/, ' '); if (new RegExp('@' + escapeRegExp(nickNameNorm)).exec(titleNorm) != null) return true; if (new RegExp('(^|请|给|用户|结算|\s|,|。|,|:)' + escapeRegExp(nickNameNorm) + '(\s|大佬|老哥|兄弟|用户|结算|请|,|。|,|$)').exec(titleNorm) != null) return true; } } function getDefaultRating(postTitle) { const ownBought = postTitle.match(/自购|自購/) != null; const ownTranslate = postTitle.match(/[个個]人(汉化|漢化|翻译|翻譯)/) != null; const sizeMatches = Array.from(postTitle.toUpperCase().replace(/\b(R18G)\b/g, '').matchAll(/(?<![0-9.])((?:\d\d?,?)?\d{1,3}(?:\.\d{1,3})?) *([GMKT](?:I?B)?)(?![A-Z])/g)); // 10M = 1SP let baseTotalScore = 0; let resourceSizeTotalMB = null; let resourceSizeMBs = []; let resourceSizeTexts = []; if (sizeMatches.length > 0) { resourceSizeTotalMB = 0; sizeMatches.forEach(sizeMatch => { let matchSizeMB = sizeMatch[1].replace(/,/g, '') * 1; if (sizeMatch[2] === 'K' && (sizeMatch[1] === '2' || sizeMatch[1] === '4')) { // ignore 2K , 4K return; } switch (sizeMatch[2][0]) { case 'K': matchSizeMB /= 1000; break; case 'G': matchSizeMB *= 1000; break; case 'T': matchSizeMB *= 1000000; break; case 'M': default: break; } resourceSizeTexts.push(sizeMatch[0]); resourceSizeMBs.push(matchSizeMB); resourceSizeTotalMB += matchSizeMB; }); if (resourceSizeMBs.length === 1) { baseTotalScore = computeBaseScoreText(resourceSizeTotalMB) * 1; } else { baseTotalScore = 0; } } return { resourceSizeTexts, resourceSizeMBs, resourceSizeTotalMB, baseTotalScore, ownBought, ownTranslate }; } function computeBaseScoreText(resourceSizeMB) { return Math.max(1, resourceSizeMB / 10).toFixed(1).replace(/\.0+$/, ''); } const PAGE_TITLE_RULES = { u_php(queryParams, keyword, myUserId, userConfig, adminRole) { const myself = queryParams.uid == null || queryParams.uid * 1 === myUserId; let suffix = ''; if (!myself && queryParams.uid) { const userId = queryParams.uid * 1; const knownUser = userConfig.customUserHashIdMappings['#' + userId]; if (knownUser) { suffix += '| ' + knownUser[2]; } else { suffix += '| #' + queryParams.uid; } } if (queryParams.action == null) { return queryParams.uid == null || myself ? '我的个人首页' : '个人首页' + suffix; } else if (queryParams.action === 'feed') { return myself ? '我的好友近况' : '好友近况' + suffix; } else if (queryParams.action === 'show') { return myself ? '我的资料' : '用户资料' + suffix; } else if (queryParams.action === 'topic') { return myself ? '我的主题' : '主题列表' + suffix; } else if (queryParams.action === 'post') { return myself ? '我的回复' : '回复列表' + suffix; } else if (queryParams.action === 'favor') { return myself ? '我的收藏' : '收藏列表' + suffix; } else if (queryParams.action === 'friend') { return myself ? '我的好友' : '好友列表' + suffix; } else if (queryParams.action === 'trade') { if (myself) { if (adminRole && queryParams.view === 'admin') { return '我的管理记录'; } else { return '我的浏览记录'; } } else if (adminRole) { return '不良记录' + suffix; } else { return '商品列表' + suffix; } } }, profile_php(queryParams, keyword, myUserId, userConfig) { return '用户中心'; }, message_php(queryParams, keyword, myUserId, userConfig) { function withSubject(title) { const subjectCell = document.querySelector('#info_base .set-table2 > tbody > tr:nth-child(2) > td:nth-child(2)'); if (subjectCell) { return title + '| ' + subjectCell.textContent; } else { return title; } } if (queryParams.action == null || queryParams.action === 'receivebox') { return '📩收件箱'; } else if (queryParams.action == 'sendbox') { return '✉️发件箱'; } else if (queryParams.action == 'readsnd') { return withSubject('✉️发件箱'); } else if (queryParams.action == 'write') { if (queryParams.remid) { return '✉️' + keyword; } else { return '✳️发短信'; } } else if (queryParams.action == 'read') { return withSubject('📩读短信'); } else if (queryParams.action == 'scout') { return '✉️消息跟踪'; } else if (queryParams.action == 'readscout') { return withSubject('✉️消息跟踪'); } else if (queryParams.action == 'chatlog') { return withSubject('✉️通信记录'); } }, post_php(queryParams, keyword, myUserId, userConfig) { if (queryParams.action == null) { const currentArea = document.querySelector('#breadcrumbs > .crumbs-item.current > strong > a'); return '✏️发新帖' + (currentArea ? '| ' + currentArea.textContent : ''); } else if (queryParams.action == 'modify') { return '✏️编辑| ' + keyword; } else if (queryParams.action == 'reply') { return '↩️回帖| ' + keyword; } }, search_php(queryParams, keyword, myUserId, userConfig) { const message = document.querySelector('#main .t .f_one td center'); const prefix = keyword ? keyword + '| ' : ''; if (message && message.textContent.trim() === '没有查找匹配的内容') { return prefix + '🚫没搜索结果'; } const totalElem = document.querySelector('#main .bdbA + .t3 ~ .fr'); if (totalElem) { const total = (totalElem.textContent.match(/共搜索到了(\d+)条信息/)||[])[1] * 1; if (total > 0) { return prefix + `🔍搜索到${total}条`; } } return prefix + '🔍搜索'; }, mawhole_php(queryParams, keyword, myUserId, userConfig) { switch (queryParams.action||'') { case 'headtopic': return '置顶| 帖子管理'; case 'digest': return '精华| 帖子管理'; case 'lock': return '锁定| 帖子管理'; case 'pushtopic': return '提前| 帖子管理'; case 'downtopic': return '压贴| 帖子管理'; case 'edit': return '加亮| 帖子管理'; case 'move': return '移动| 帖子管理'; case 'copy': return '复制| 帖子管理'; case 'unite': return '合并| 帖子管理'; case 'del': return '删除| 帖子管理'; default: return '帖子管理'; } }, masingle_php(queryParams, keyword, myUserId, userConfig) { switch (queryParams.action||'') { case 'banuser': return '禁言| 管理'; case 'shield': return '屏蔽| 管理'; case 'remind': return '提醒| 管理'; case 'delatc': return '删除回复| 管理'; default: return '管理'; } }, operate_php(queryParams, keyword, myUserId, userConfig) { switch (queryParams.action||new URLSearchParams(document.location.search).get('action')||'') { case 'showping': return '评分| 管理'; default: return '管理'; } }, job_php(queryParams, keyword, myUserId, userConfig) { switch (queryParams.action||new URLSearchParams(document.location.search).get('action')||'') { case 'endreward': return '悬赏帖结案 | 管理'; default: return '管理'; } }, sendemail_php(queryParams, keyword, myUserId, userConfig) { return '发送邮件'; }, member_php(queryParams, keyword, myUserId, userConfig) { return '会员列表'; }, sort_php(queryParams, keyword, myUserId, userConfig) { switch (queryParams.action||'') { case '': return '基本统计| 统计排行'; case 'ipstate': return '到访IP统计| 统计排行'; case 'team': return '管理团队| 统计排行'; case 'admin': return '管理统计| 统计排行'; case 'online': return '在线统计| 统计排行'; case 'member': return '会员排行| 统计排行'; case 'forum': return '版块排行| 统计排行'; case 'article': return '帖子排行| 统计排行'; default: return '统计排行'; } }, show_php(queryParams, keyword, myUserId, userConfig) { return '展区'; // dead area }, push_php(queryParams, keyword, myUserId, userConfig) { return '推荐主题'; // dead area }, forumcp_php(queryParams, keyword, myUserId, userConfig) { return '版块管理'; }, plugin_php(queryParams, keyword, myUserId, userConfig) { if (queryParams.H_name == 'tasks') { switch (queryParams.actions||'') { case '': return '新任务选择| 社区论坛任务'; case 'newtasks': return '进行中任务| 社区论坛任务'; case 'endtasks': return '已完成任务| 社区论坛任务'; case 'errotasks': return '已失败任务| 社区论坛任务'; default: return '社区论坛任务'; } } return '附加功能'; }, }; function enhancePageTitle(queryParams, myUserId, userConfig, adminRole) { const originalTitle = document.title; const parts = originalTitle.match(/^(?:(.+) )?([^ -]+\+[^+-]+) - powered by Pu!mdHd$/); if (parts == null) { return; } const keyTitle = (parts[1]||'').replace(/ ? - ?$/, ''); const siteName = parts[2]; const key = document.location.pathname.replace(/^\//, '').replace(/\./g, '_'); const handler = PAGE_TITLE_RULES[key]; let keyword = null; if (handler) { keyword = handler(queryParams, keyTitle||'', myUserId, userConfig, adminRole); } if (keyword == null) { keyword = keyTitle||''; } if (keyword) { document.title = keyword.trim() + ' - ' + siteName; } else { document.title = siteName; } } function initSiteMainMenu(hasManagementRole) { const mainGuideBar = document.querySelector('#guide'); if (hasManagementRole) { // add link to 系统设置 if absent if (document.querySelector('#guide > li > a[href="admin.php"]') == null) { const adminItem = addElem(mainGuideBar, 'li'); addElem(adminItem, 'a', null, { href: 'admin.php' }).textContent = '系统设置'; } } } function addModalMask() { return addElem(document.body, 'div', 'rinsp-dialog-modal-mask'); } function createPopupMenu(popupId, anchor, rightAligned, verticallyInverted) { function computeStyle(hShift) { if (anchor) { const bounds = anchor.getClientRects()[0]; const left = bounds.x + hShift; if (verticallyInverted) { return 'opacity: 0.95; left: ' + left.toFixed(0) + 'px; z-index: 3000; visibility: visible; top: ' + (bounds.top + document.documentElement.scrollTop).toFixed(0) + 'px; transform: translateY(-100%);'; } else { return 'opacity: 0.95; left: ' + left.toFixed(0) + 'px; z-index: 3000; visibility: visible; top: ' + (bounds.height + bounds.top + document.documentElement.scrollTop).toFixed(0) + 'px;'; } } else { return 'position: fixed; opacity: 0.95; left: calc(50vw - 400px); z-index: 3000; visibility: visible; top: 10vh'; } } const menuElem = addElem(document.body, 'div', 'menu rinsp-common-popup-menu', { id: popupId, style: computeStyle(0) }); // just copy-n-paste from site, lol menuElem.innerHTML = `<div class="bor" style="padding:13px 30px"><img src="images/loading.gif" align="absbottom"> 正在加载数据...</div>`; return { renderContent(renderer) { menuElem.innerHTML = ''; const borElem = addElem(menuElem, 'div', 'bor'); renderer(borElem); if (rightAligned) { menuElem.setAttribute('style', computeStyle(-menuElem.getClientRects()[0].width)); } } }; } async function setupPopupMenu(config) { const menuConfig = { title: '', width: 120, popupMenuId: '', anchor: null, rightAligned: false, verticallyInverted: false, onClose: () => {}, items: [] // [ { label, class, action } ] }; Object.assign(menuConfig, config); const modelMask = addModalMask(); const popupMenu = createPopupMenu(menuConfig.popupMenuId, menuConfig.anchor, menuConfig.rightAligned, menuConfig.verticallyInverted); function close() { modelMask.remove(); closePopupMenu(menuConfig.popupMenuId); menuConfig.onClose(); } modelMask.addEventListener('click', () => close()); popupMenu.renderContent(async function(borElem) { const tableElem = addElem(borElem, 'table', null, { width: `${menuConfig.width}`, cellspacing: '0', cellpadding: '0' }); const tbodyElem = addElem(tableElem, 'tbody'); const trElem1 = addElem(tbodyElem, 'tr'); const thElem1_1 = addElem(trElem1, 'th', 'h'); const frElem1 = addElem(thElem1_1, 'span', 'fr', { style: 'margin-top:2px;cursor:pointer' }); frElem1.addEventListener('click', () => close()); addElem(frElem1, 'img', null, { src: 'images/close.gif' }); thElem1_1.appendChild(document.createTextNode(menuConfig.title)); function addButton(item) { const trElem = addElem(tbodyElem, 'tr'); const tdElem = addElem(trElem, 'td', item.cellClass||null); const button = addElem(tdElem, 'a', item.class||null); button.textContent = item.label; if (item.action == null) { // nothing } else if (typeof item.action === 'string') { button.setAttribute('href', item.action); if (item.target) { button.setAttribute('target', item.target); } } else { button.setAttribute('href', 'javascript:'); button.addEventListener('click', () => { close(); setTimeout(() => item.action()); return false; }); } return button; } menuConfig.items.forEach(item => { addButton(item); }); }); } function closePopupMenu(popupId) { const menu = document.getElementById(popupId); if (menu) { menu.remove(); } } async function showMessagePopup(message, anchor, autoCloseTimeout) { return runWithProgressPopup(async () => message, '', anchor, autoCloseTimeout); } async function runWithProgressPopup(action, loadingMessage, anchor, feedbackTimeout) { return new Promise((resolve, reject) => { const modelMask = addModalMask(); const popupMenu = createPopupMenu(LOADING_DIALOG_POPUP_MENU_ID, anchor); popupMenu.renderContent(async function(borElem) { // not happy about copy-n-paste ... borElem.innerHTML = ''; const loadingContent = addElem(borElem, 'div', null, { style: 'padding: 16px 30px' }); addElem(loadingContent, 'img', null, { src: 'images/loading.gif', align: 'absbottom' }); loadingContent.appendChild(document.createTextNode(loadingMessage + ' ')); const customContent = addElem(borElem, 'div'); action(customContent) .then(async status => { borElem.innerHTML = ''; const loadingContent = addElem(borElem, 'div', null, { style: 'padding: 16px 30px; font-size: 1.5em' }); loadingContent.appendChild(document.createTextNode(status||'✔️已完成')); await sleep(feedbackTimeout); closePopupMenu(LOADING_DIALOG_POPUP_MENU_ID); resolve(); }) .catch(ex => { reject(ex); }) .finally(() => modelMask.remove()); }); }); } async function openTextListEditor(popupMenuId, config, callback) { const modelMask = addModalMask(); const popupMenu = createPopupMenu(popupMenuId, null); const initContent = await config.read(); popupMenu.renderContent(async function(borElem) { const tableElem = addElem(borElem, 'table', 'rinsp-textlist-popup-table', { width: '700', cellspacing: '0', cellpadding: '0' }); const tbodyElem = addElem(tableElem, 'tbody'); const trElem1 = addElem(tbodyElem, 'tr'); const thElem1_1 = addElem(trElem1, 'th', 'h'); const frElem1 = addElem(thElem1_1, 'span', 'fr', { style: 'margin-top:2px;cursor:pointer' }); frElem1.addEventListener('click', function() { modelMask.remove(); closePopupMenu(popupMenuId); }); addElem(frElem1, 'img', null, { src: 'images/close.gif' }); thElem1_1.appendChild(document.createTextNode(config.title)); const trElem2 = addElem(tbodyElem, 'tr', 'tr2 tac'); const tdElem2 = addElem(trElem2, 'td'); if (config.description) { addElem(tdElem2, 'div', 'rinsp-textlist-description').textContent = config.description; } const editorTextArea = addElem(tdElem2, 'textarea', 'rinsp-textlist-editor'); editorTextArea.value = initContent; const footer = addElem(borElem, 'ul', null, { style: 'text-align:center;padding:4px 0;' }); const submitButton = addElem(footer, 'input', 'btn', { type: 'button', value: '保存' }); submitButton.addEventListener('click', async function() { tableElem.classList.remove('rinsp-config-saving'); await config.save(editorTextArea.value) .then(function() { modelMask.remove(); closePopupMenu(popupMenuId); if (callback) callback(); }) .finally(function() { tableElem.classList.remove('rinsp-config-saving'); }); }); }); } function initConfigAccess(myUserId, configKey, defaultValue) { return { async read() { return readUserConfig(myUserId, configKey, defaultValue); }, async write(newValue) { await GM.setValue(configKey + '#' + myUserId, JSON.stringify(newValue)); }, async update(updater) { return updateUserConfig(myUserId, updater, configKey, defaultValue); } }; } async function readUserConfig(myUserId, configKey, defaultValue) { try { let savedData = await GM.getValue(configKey + '#' + myUserId); return Object.assign({}, defaultValue, JSON.parse(savedData)); } catch (e) {} return Object.assign({}, defaultValue); } async function updateUserConfig(myUserId, updater, configKey, defaultValue) { let oldConfig = await readUserConfig(myUserId, configKey, defaultValue); let newConfig = await updater(oldConfig); if (newConfig != null) { await GM.setValue(configKey + '#' + myUserId, JSON.stringify(newConfig)); } return newConfig; } async function populateThreadAccess(tid, myUserId, threadHistoryAccess) { const doc = await fetchGetPage(`${document.location.origin}/read.php?tid-${tid}.html`); const pageError = findErrorMessage(doc); if (pageError) { if (pageError.indexOf('数据已被删除') !== -1) { await recordThreadDeleted(tid, myUserId, threadHistoryAccess); } else { throw new Error(pageError); } } else { const posts = getPosts(doc); await recordThreadAccess(doc, posts, myUserId, threadHistoryAccess, {}); } } async function recordThreadDeleted(tid, myUserId, threadHistoryAccess) { const lastAccessRecord = await threadHistoryAccess.recentAccessStore.get(tid); if (lastAccessRecord && !lastAccessRecord.deleted) { lastAccessRecord.deleted = true; await threadHistoryAccess.recentAccessStore.put(tid, lastAccessRecord); } } async function recordThreadAccess(doc, posts, myUserId, threadHistoryAccess, memoryObj) { const currentPostLink = doc.querySelector('#breadcrumbs .crumbs-item.current strong > a[href^="read.php?tid-"]'); if (currentPostLink == null) { return; } const areaLink = currentPostLink.closest('.crumbs-item').previousElementSibling; if (!areaLink.matches('a[href^="thread.php?fid-"]')) { return; } const fid = Number.parseInt(areaLink.getAttribute('href').substring(15)); const threadTitle = currentPostLink.textContent.trim(); const tid = Number.parseInt(currentPostLink.getAttribute('href').substring(13)); const areaName = areaLink.textContent.trim(); const uid = posts[0].floor === 0 ? posts[0].postUid : 0; const lastPost = posts[posts.length - 1]; const lastFloor = lastPost.floor; if (memoryObj.lastFloor !== lastFloor) { if (DEBUG_MODE) console.info('recordThreadAccess', tid, lastFloor); await threadHistoryAccess.historyStore.set(tid, lastFloor); memoryObj.lastFloor = lastFloor; } const bountyStatus = getBountyStatus(doc); if (memoryObj.lastAccessRecord == null) { memoryObj.lastAccessRecord = await threadHistoryAccess.recentAccessStore.get(tid); } function getNewRecord(lastAccessRecord) { let hasBoughtAny = false; let lastReplied = 0; posts.forEach(post => { if (post.postUid === myUserId) { if (post.floor > lastReplied) { lastReplied = post.floor; } } else { if (post.contentElem.querySelector('h6.quote.jumbotron + blockquote.jumbotron') != null) { hasBoughtAny = true; } } }); const newRecord = { tid, uid, fid, area: areaName, title: threadTitle, time: Date.now(), bought: hasBoughtAny, replied: lastReplied, bounty: bountyStatus }; if (lastAccessRecord == null) { return newRecord; } if ((lastAccessRecord.replied||0) > lastReplied) { newRecord.replied = lastAccessRecord.replied; } if (bountyStatus == null && lastAccessRecord.bounty != null) { newRecord.bounty = lastAccessRecord.bounty; } let savingRecord = null; if (lastAccessRecord.initialTitle) { newRecord.initialTitle = lastAccessRecord.initialTitle; } if (lastAccessRecord.title !== threadTitle) { newRecord.bought = newRecord.bought || lastAccessRecord.bought; if (!newRecord.initialTitle) { newRecord.initialTitle = lastAccessRecord.title; } savingRecord = newRecord; } if (!lastAccessRecord.bought && newRecord.bought) { savingRecord = newRecord; } if (!lastAccessRecord.uid && newRecord.uid > 0) { savingRecord = newRecord; } if (lastReplied > (lastAccessRecord.replied||0)) { savingRecord = newRecord; } if (bountyStatus != null) { if (lastAccessRecord.bounty == null || lastAccessRecord.bounty.ended != bountyStatus.ended) { savingRecord = newRecord; } } return savingRecord; } const newRecord = getNewRecord(memoryObj.lastAccessRecord); if (newRecord) { if (DEBUG_MODE) console.info('recordThreadAccess', tid, newRecord); await threadHistoryAccess.recentAccessStore.put(tid, newRecord); memoryObj.lastAccessRecord = newRecord; } } function checkManagementRole(myUserId, userConfig, mainConfigAccess) { if (document.querySelector('#guide > li > a[href="admin.php"]') != null) { return true; } let hasManageRole = false; const userNavTopicLink = document.querySelector('#user_info .fl > .link5[href="u.php?action-topic.html"]'); if (userNavTopicLink) { let rank = (userNavTopicLink.parentElement.textContent.match(/等级:([^,]+),/)||[])[1]; hasManageRole = ['管理员', '总版主', '论坛版主'].includes(rank); if (hasManageRole !== userConfig.hasSeenAdminRole) { userConfig.hasSeenAdminRole = hasManageRole; mainConfigAccess.update(updatingUserConfig => { updatingUserConfig.hasSeenAdminRole = hasManageRole; return updatingUserConfig; }); } } else { // some pages do not have rank information visible, use cached state instead hasManageRole = !!userConfig.hasSeenAdminRole; } return hasManageRole; } function showNativeWarningPopup(msg) { try { unsafeWindow.ajax.request = unsafeWindow.Object(); unsafeWindow.ajax.request.responseText = msg; unsafeWindow.ajax.guide(); } catch (ignore) { alert(msg); } } function showNativeSpinner(anchor) { try { unsafeWindow.read.obj = anchor; unsafeWindow.read.guide(); } catch (ignore) {} } function closeNativeSpinner() { try { unsafeWindow.closep(); } catch (ignore) {} } function addFloatingFavorButton(topControlContainer, tid, favorThreadsCacheAccess) { const storeButton = addElem(topControlContainer, 'a', 'rinsp-excontrol-item rinsp-excontrol-item-ticker'); storeButton.classList.add('rinsp-excontrol-item-favor'); storeButton.classList.add('rinsp-running'); storeButton.textContent = '✨\n收\n藏'; storeButton.addEventListener('click', async () => { showNativeSpinner(); let favId = storeButton.dataset.favId * 1; if (favId) { let data; if (favId === -1) { data = await readFavorRecords(favorThreadsCacheAccess, true, false); if (Object_hasOwn(data, tid)) { favId = data[tid].id; } else { favId = null; } } else { data = await readFavorRecords(favorThreadsCacheAccess, false, false); } delete data[tid]; await favorThreadsCacheAccess.write(data); if (favId != null) { const postData = new FormData(); postData.set('verify', verifyhash()); postData.set('selid[]', favId); postData.set('type', 0); postData.set('job', 'clear'); await fetch(`${document.location.origin}/u.php?action=favor`, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', body: postData }) .then(resp => { if (!resp.ok) throw Error(); }) .then(() => { storeButton.dataset.favId = 0; storeButton.classList.remove('rinsp-active'); }) .catch(ex => { showNativeWarningPopup('操作出错'); }) .finally(() => closeNativeSpinner()); } } else { fetchGetPage(`${document.location.origin}/pw_ajax.php?action=favor&tid=${tid}&type=0&nowtime=${Date.now()}&verify=${verifyhash()}`, 'text/xml') .then(async xml => { const msg = xml.childNodes[0].textContent; if (['您已经收藏了该主题', '帖子收藏成功!'].includes(msg)) { await favorThreadsCacheAccess.update(cache => { if (cache && cache.data) { if (Object_hasOwn(cache.data, tid)) { return null; } cache.data[tid] = { tid: tid }; return cache; } else { return { data: {}, time: 0 }; } }); } else { throw msg; } }) .then(() => { storeButton.dataset.favId = -1; storeButton.classList.add('rinsp-active'); }) .catch(msg => { showNativeWarningPopup('' + msg); }) .finally(() => closeNativeSpinner()); } }); readFavorRecords(favorThreadsCacheAccess) .catch(ex => null) .then(data => { storeButton.classList.remove('rinsp-running'); const favId = data && Object_hasOwn(data, tid) ? data[tid].id || -1 : 0; storeButton.dataset.favId = favId; if (favId) { storeButton.classList.add('rinsp-active'); } }); } function readFavorList(doc) { const items = {}; doc.querySelectorAll('#u-contentmain .u-table tr > th > a[href^="read.php?tid-"]').forEach(a => { const id = a.closest('tr').querySelector('input[name="selid[]"]').value * 1; const tid = Number.parseInt(a.getAttribute('href').substring(13)); const opLink = a.closest('tr').querySelector('a[href^="u.php?uid-"]'); const opId = Number.parseInt(opLink.getAttribute('href').substring(10)); const category = opLink.closest('td').nextElementSibling.textContent.trim(); items[tid] = { id, tid, op: opId, category }; }); return items; } function readCurrentAndMaxPage(doc) { let currentPage = 1; let maxPage = 1; const pagesElem = doc.querySelector('.pages li.pagesone'); if (pagesElem != null) { let match = pagesElem.textContent.replace(/\u00a0/g, ' ').match(/Pages: *(\d+)[^\/]*\/ *(\d+) */); if (match != null) { currentPage = Number.parseInt(match[1]); maxPage = Number.parseInt(match[2]); } } return [currentPage, maxPage]; } function isCurrentThreadMatchingFid(fid) { if (document.location.pathname === '/thread.php') { return document.location.search.startsWith(`?fid-${fid}-`) || document.location.search.startsWith(`?fid-${fid}.`) || document.location.search.startsWith(`?fid=${fid}&`) || document.location.search.endsWith(`?fid=${fid}`) || document.location.search.indexOf(`&fid=${fid}&`); } return false; } function addBackToTopButton(container) { const button = addElem(container, 'a', 'rinsp-excontrol-item rinsp-excontrol-totop rinsp-opacity-0'); button.addEventListener('click', () => { document.body.querySelector('#toptool').scrollIntoView({ behavior: 'smooth', block: 'start' }); }); function checkScroll() { if (document.documentElement.scrollTop > 30) { button.classList.remove('rinsp-opacity-0'); } else { button.classList.add('rinsp-opacity-0'); } } document.addEventListener('scroll', () => { checkScroll(); }); setTimeout(checkScroll); } async function addSearchBar(queryParams, searchConfigAccess, userConfig) { const guide = document.querySelector('#guide'); if (guide == null) { return; } const searchPref = await searchConfigAccess.read(); guide.classList.add('rinsp-quicksearch-added'); const quicksearchForm = newElem('form', 'rinsp-quicksearch', { method: 'post', action: '/search.php?' }); function setFormTarget(forceNewWindow) { quicksearchForm.setAttribute('target', forceNewWindow || document.location.pathname !== '/search.php' ? '_blank' : '_self'); } function addHiddenField(k, v) { return addElem(quicksearchForm, 'input', null, { type: 'hidden', name: k, value: v }); } const keywordDataField = addHiddenField('keyword', ''); addHiddenField('step', '2'); addHiddenField('method', searchPref.defaultSearchAll ? 'AND' : 'OR'); addHiddenField('sch_time', searchPref.defaultTimeRange||'all'); addHiddenField('pwuser', ''); addHiddenField('sch_area', '0'); addHiddenField('f_fid', 'all'); addHiddenField('orderway', 'postdate'); addHiddenField('asc', 'DESC'); if (userConfig.showSearchBar$align === 'menu-first') { guide.insertBefore(quicksearchForm, guide.firstChild); } else if (userConfig.showSearchBar$align === 'bar-center') { let gap = guide.getBoundingClientRect().width - 250; guide.classList.add('rinsp-quicksearch-align-center'); guide.insertBefore(newElem('li', 'rinsp-spacer'), guide.firstChild); guide.insertBefore(quicksearchForm, guide.firstChild); guide.insertBefore(newElem('li', 'rinsp-spacer'), guide.firstChild); guide.insertBefore(newElem('li', null, { style: `flex: 0 1 ${gap.toFixed(0)}px` }), guide.firstChild); } else { guide.appendChild(quicksearchForm); } const searchField = addElem(quicksearchForm, 'input', 'rinsp-quicksearch-field', { required: '', value: queryParams.keyword||'' }); const searchButton = addElem(quicksearchForm, 'a', 'rinsp-quicksearch-button'); searchButton.addEventListener('click', evt => { if (beforeSubmit(evt.shiftKey)) { quicksearchForm.submit(); } else { searchField.focus(); } }); quicksearchForm.addEventListener('submit', evt => { evt.stopPropagation(); if (!beforeSubmit()) { evt.preventDefault(); return false; } }); function beforeSubmit(forceNewWindow) { keywordDataField.value = ''; let keyword = searchField.value.trim(); if (keyword.length === 0) { return false; } if (keyword.match(/^[\x00-\x7F]+$/)) { switch (keyword.replace(/\s/g, '').length) { case 0: return false; case 1: keyword = keyword + ' ' + keyword + ' ' + keyword; break; case 2: keyword = keyword + ' ' + keyword; break; } } keyword = keyword.replace(/\s+/g, ' '); keywordDataField.value = keyword; quicksearchForm.setAttribute('action', `/search.php?keyword-${encodeURIComponent(keyword).replace(/-/g, '%2D')}.html`); setFormTarget(forceNewWindow); return true; } } async function init() { // improve hover popup closing mechanism to avoid closing still active popups const scpt = document.createElement('script'); scpt.textContent = ` if (window['PwMenu']) { PwMenu.prototype.close = function() { read.t = setTimeout(() => {if (!read.menu.matches(':hover')) {closep()}}, 100); }; } `; document.head.appendChild(scpt); // start main script const myUserId = findMyUserId(); if (myUserId == null) { if (DEV_MODE) console.info('(southplus-watcher) cannot find user id'); return; } let domainRedirectTarget = await getDomainRedirectTarget(); if (domainRedirectTarget != null && domainRedirectTarget !== document.location.origin) { setDomainRedirectEnabled(true); // update target domain domainRedirectTarget = document.location.origin; } const mainConfigAccess = initConfigAccess(myUserId, MAIN_CONFIG_KEY, DEFAULT_MAIN_CONFIG); const searchConfigAccess = initConfigAccess(myUserId, SEARCH_CONFIG_KEY, DEFAULT_SEARCH_CONFIG); const contentIgnoreListConfigAccess = initConfigAccess(myUserId, CONTENT_IGNORE_LIST_CONFIG_KEY, DEFAULT_CONTENT_IGNORE_LIST_CONFIG); const threadFilterConfigAccess = initConfigAccess(myUserId, THREAD_FILTER_CONFIG_KEY, DEFAULT_THREAD_FILTER_CONFIG); const threadCategoryConfigAccess = initConfigAccess(myUserId, THREAD_CUSTOM_CATEGORY_CONFIG_KEY, DEFAULT_THREAD_CUSTOM_CATEGORY_CONFIG); const userFilterConfigAccess = initConfigAccess(myUserId, USER_FILTER_CONFIG_KEY, DEFAULT_USER_FILTER_CONFIG); const pinnedUsersConfigAccess = initConfigAccess(myUserId, PINNED_USERS_CONFIG_KEY, DEFAULT_PINNED_USERS_CONFIG); const sharetypeFilterConfigAccess = initConfigAccess(myUserId, SHARETYPE_FILTER_CONFIG_KEY, DEFAULT_SHARETYPE_FILTER_CONFIG); const favorThreadsCacheAccess = initConfigAccess(myUserId, FAVOR_THREADS_CACHE_CONFIG_KEY, DEFAULT_FAVOR_THREADS_CACHE_CONFIG); let userConfig = await mainConfigAccess.read(); if (!userConfig.v15migrationApplied) { let migratedUserConfig = await mainConfigAccess.update(function(updatingUserConfig) { if (updatingUserConfig.v15migrationApplied) { return null; } const spuUserIds = new Set(); Object.keys(updatingUserConfig.customUserHashIdMappings) .forEach(key => { if (key[0] === '#') { spuUserIds.add(key); } }); Object.entries(updatingUserConfig.customUserHashIdMappings) .forEach(entry => { if (entry[0][0] === '@') { spuUserIds.delete('#' + entry[1][0]); } }); for (let spuUserId of spuUserIds) { delete updatingUserConfig.customUserHashIdMappings[spuUserId]; } updatingUserConfig.v15migrationApplied = true; return updatingUserConfig; }); if (migratedUserConfig != null) { userConfig = migratedUserConfig; } } if (!userConfig.v42migrationApplied) { let migratedUserConfig = await mainConfigAccess.update(function(updatingUserConfig) { if (updatingUserConfig.v42migrationApplied) { return null; } const newMappings = {}; Object.entries(updatingUserConfig.customUserHashIdMappings) .forEach(entry => { if (entry[0][0] === '#') { let userHashId = entry[1][1]; if (typeof userHashId === 'object' && typeof userHashId.hashId === 'string') { userHashId = userHashId.hashId; } let newRecord = [entry[1][0] * 1, userHashId, entry[1][2]]; newMappings['#' + newRecord[0]] = newRecord; newMappings['@' + newRecord[1]] = newRecord; } }); updatingUserConfig.customUserHashIdMappings = newMappings; updatingUserConfig.v42migrationApplied = true; return updatingUserConfig; }); if (migratedUserConfig != null) { userConfig = migratedUserConfig; } } for (let i = userConfig.textSize * 1; i > 0; i--) { document.documentElement.classList.add('rinsp-textsize-step-' + i); } if (userConfig.siteThemeDarkerSubjectLine) { document.body.classList.add('rinsp-theme-darksubject'); } if (userConfig.hideMobileVerSwitch) { document.body.classList.add('rinsp-hide-mobileswitch'); } let showResourceSpots = null; Object.keys(DEFAULT_MAIN_CONFIG).filter(k => k.startsWith('showResourceSpots$')).forEach(k => { if (userConfig[k]) { showResourceSpots = showResourceSpots || {}; showResourceSpots[k.substring(18)] = true; } }); // 所在地是免空区 const inResourceArea = document.querySelector('#breadcrumbs .crumbs-item[href="thread.php?fid-13.html"] + .crumbs-item') != null; // 所在地是事务受理 const inOfficeArea = document.querySelector('#breadcrumbs .crumbs-item[href="thread.php?fid-2.html"] + .crumbs-item') != null; // 所在地是网赚区 const inPaywallArea = document.querySelector('#breadcrumbs .crumbs-item[href="thread.php?fid-170.html"] + .crumbs-item') != null; const hasManagementRole = checkManagementRole(myUserId, userConfig, mainConfigAccess); initSiteMainMenu(hasManagementRole); const pendingWatchItemChecks = new Set(); if (userConfig.floatingShortcut !== 'off') { const quickActionOverlay = addElem(document.body, 'div', 'rinsp-quick-action-overlay', { tabindex: "1" }); quickActionOverlay.addEventListener('click', evt => { evt.stopPropagation(); }); document.body.addEventListener('click', () => { if (document.activeElement === quickActionOverlay) { quickActionOverlay.blur(); } }); addElem(quickActionOverlay, 'label', null, { version: VERSION_TEXT }).textContent = '⚙️'; addElem(quickActionOverlay, 'div', 'rinsp-quick-action', { label: '🚫屏蔽关键词' }) .addEventListener('mousedown', () => { openIgnoreThreadEditor(() => { userPinArea.fireUpdateListeners(); }); }); addElem(quickActionOverlay, 'div', 'rinsp-quick-action', { label: '💚喜欢关键词' }) .addEventListener('mousedown', () => { openLikeThreadEditor(() => { userPinArea.fireUpdateListeners(); }); }); addElem(quickActionOverlay, 'div', 'rinsp-quick-action-divider'); addElem(quickActionOverlay, 'div', 'rinsp-quick-action rinsp-dark-mode-toggle') .addEventListener('mousedown', () => { darkModeEnabledProperty.set(darkModeEnabledProperty.get() ? null : '1'); applyDarkTheme(); }); addElem(quickActionOverlay, 'div', 'rinsp-quick-action rinsp-safe-mode-toggle') .addEventListener('mousedown', () => { setSafeMode(!isSafeMode()); }); addElem(quickActionOverlay, 'div', 'rinsp-quick-action-divider'); addElem(quickActionOverlay, 'div', 'rinsp-quick-action', { label: '🔧设置页' }) .addEventListener('mousedown', () => { document.location.href = '/u.php'; }); if (userConfig.floatingShortcut === 'tl') { quickActionOverlay.classList.add('rinsp-quick-action-overlay-pos-tl'); } else { quickActionOverlay.classList.add('rinsp-quick-action-overlay-pos-tr'); } } const notificationContainer = addElem(document.body, 'div', 'rinsp-notification-container'); const extraControlContainer = addElem(document.body, 'div', 'rinsp-excontrol-container'); const topControlContainer = addElem(document.body, 'div', 'rinsp-tpcontrol-container'); const bottomControlContainer = addElem(document.body, 'div', 'rinsp-bmcontrol-container'); const pmNotificationElem = addElem(notificationContainer, 'a', 'rinsp-notification-item rinsp-notification-item-type-pm'); const watchNotificationElem = addElem(notificationContainer, 'div', 'rinsp-notification-item rinsp-notification-item-type-watch'); if (userConfig.showBackToTopButton) { addBackToTopButton(bottomControlContainer); } function addIgnoreToggler(togglerClass, modeActiveClass, modeInactiveClass) { const lastStateKey = 'rinplus.mode.' + togglerClass + '.laststate'; const togglerElem = addElem(extraControlContainer, 'a', 'rinsp-excontrol-item rinsp-excontrol-item-hidden'); togglerElem.classList.add(togglerClass); togglerElem.addEventListener('click', function() { let newValue = !isActive(); setActive(newValue); if (newValue) { localStorage.setItem(lastStateKey, '1'); } else { localStorage.setItem(lastStateKey, '0'); } }); function restoreLastState(defaultActive) { const savedValue = localStorage.getItem(lastStateKey); if (defaultActive && savedValue !== '0') { setActive(true); } else { setActive(savedValue === '1'); } } function isActive() { return document.body.classList.contains(modeActiveClass); } function setActive(active) { if (active) { document.body.classList.add(modeActiveClass); if (modeInactiveClass) { document.body.classList.remove(modeInactiveClass); } } else { document.body.classList.remove(modeActiveClass); if (modeInactiveClass) { document.body.classList.add(modeInactiveClass); } } } function setCount(count) { togglerElem.innerHTML = ''; if (count !== 0) { togglerElem.classList.remove('rinsp-excontrol-item-hidden'); addElem(togglerElem, 'span', `rinsp-excontrol-item-count rinsp-excontrol-item-count-${(''+count).length}d`).textContent = String(count); } else { togglerElem.classList.add('rinsp-excontrol-item-hidden'); } } return { isActive, setActive, restoreLastState, setCount }; } document.body.addEventListener('click', evt => { if (!evt.target) { return; } if (evt.target.classList.contains('r_two')) { const row = evt.target.closest('table'); if (!row || !row.classList.contains('rinsp-filter-ignored')) { return; } document.body.classList.add('rinsp-filter-peek-mode'); evt.target.scrollIntoView({ behavior: 'auto', block: 'center' }); } if (evt.target.tagName === 'TD' && (evt.target.getAttribute('id')||'').startsWith('td_')) { if (evt.target.parentNode.classList.contains('rinsp-thread-filter-dislike') || evt.target.parentNode.classList.contains('rinsp-thread-filter-masked-bysharetype')) { document.body.classList.add('rinsp-dislike-thread-peek-mode'); evt.target.parentNode.scrollIntoView({ behavior: 'auto', block: 'center' }); } else if (evt.target.parentNode.classList.contains('rinsp-request-settlement-thread') && !evt.target.parentNode.classList.contains('rinsp-request-settlement-bypass')) { document.body.classList.add('rinsp-settlement-peek-mode'); evt.target.scrollIntoView({ behavior: 'auto', block: 'center' }); } } }); const userHashLookupStore = createUserHashLookupStore(); const threadHistoryAccess = createThreadHistoryStore(myUserId, userConfig); const adminFunctions = (() => { if (!hasManagementRole) { return null; } const userPunishRecordStore = createRecordStore(`user_punish#${myUserId}`); return initAdminFunctions(myUserId, userConfig, userPunishRecordStore); })(); const ignoreContentToggler = addIgnoreToggler('rinsp-content-ignore-toggler', 'rinsp-filter-peek-mode'); const ignoreSettlementToggler = addIgnoreToggler('rinsp-settlement-ignore-toggler', 'rinsp-settlement-peek-mode'); const ignorePaywellToggler = addIgnoreToggler('rinsp-paywall-ignore-toggler', 'rinsp-paywall-peek-mode'); ignorePaywellToggler.restoreLastState(true); const ignoreDislikeThreadToggler = addIgnoreToggler('rinsp-dislike-thread-ignore-toggler', 'rinsp-dislike-thread-peek-mode'); let hideAnsweredRequestToggler = null; let hideUnansweredRequestToggler = null; let hideExpiredRequestToggler = null; if (userConfig.showRequestThreadFilters) { if (isCurrentThreadMatchingFid(QUESTION_AND_REQUEST_AREA_ID)) { hideAnsweredRequestToggler = addIgnoreToggler('rinsp-answered-request-ignore-toggler', 'rinsp-answered-request-hide-mode'); hideUnansweredRequestToggler = addIgnoreToggler('rinsp-unanswered-request-ignore-toggler', 'rinsp-unanswered-request-hide-mode'); hideExpiredRequestToggler = addIgnoreToggler('rinsp-expired-request-ignore-toggler', 'rinsp-expired-request-hide-mode'); hideAnsweredRequestToggler.restoreLastState(); hideUnansweredRequestToggler.restoreLastState(); hideExpiredRequestToggler.restoreLastState(); } } let hideVisitedThreadToggler = null; if (threadHistoryAccess) { hideVisitedThreadToggler = addIgnoreToggler('rinsp-visited-thread-toggler', 'rinsp-visited-thread-view-mode', 'rinsp-visited-thread-mask-mode'); hideVisitedThreadToggler.restoreLastState(true); const profilePopupMenu = document.querySelector('#menu_u > ul'); if (profilePopupMenu) { const historyItem = addElem(profilePopupMenu, 'li'); const historyLink = addElem(historyItem, 'a', null, { href: 'u.php?action-trade.html' }); historyLink.textContent = '浏览记录'; } } let scoringModeToggler = null; if (hasManagementRole && inResourceArea) { scoringModeToggler = addIgnoreToggler('rinsp-highlight-unscored-thread-toggler', 'rinsp-highlight-unscored-thread-mode'); scoringModeToggler.setCount('+'); scoringModeToggler.restoreLastState(); } if (hasManagementRole && inOfficeArea) { document.addEventListener('mousedown', e => { if (e.target && e.target.tagName === 'A' && e.target.href) { if (!e.target.href.startsWith('javascript:')) { document.querySelectorAll('.rinsp-last-clicked').forEach(el => el.classList.remove('rinsp-last-clicked')); e.target.classList.add('rinsp-last-clicked'); } } }); } let hideClosedThreadToggler = addIgnoreToggler('rinsp-closed-thread-toggler', 'rinsp-closed-thread-view-mode', 'rinsp-closed-thread-mask-mode'); hideClosedThreadToggler.restoreLastState(true); // auto-discover nick name the first time if (userConfig.myNickName == null) { const doc = await fetchGetPage(`${document.location.origin}/u.php?action-show-uid-${myUserId}.html`); let nickName = ''; for (let td of doc.querySelectorAll('#u-profile .u-table > tbody > tr > td')) { if (td.textContent.trim() === '昵称') { nickName = td.parentNode.querySelector('th').textContent.trim(); break; } } userConfig = await mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.myNickName = nickName; return updatingUserConfig; }); } if (userConfig.openIntroAfterUpdate) { tryOpenUpdateSummary(); } if (userConfig.showFloatingMessageIndicator) { pmNotificationElem.setAttribute('href', 'message.php'); addPrivateMessageNotifier(); } if (userConfig.hideWatchButtonIfEmpty) { document.body.classList.add('rinsp-watchemnu-autohide'); } if (userConfig.heightReductionMode) { const main = document.querySelector('#main'); if (main) { main.classList.add('rinsp-compact-mode'); if (userConfig.heightReductionMode$LV2) { main.classList.add('rinsp-compact-mode-smaller'); } } } if (userConfig.hideFilteredThread) { document.body.classList.add('rinsp-threadfilter-hide-mode'); } if (userConfig.requestThreadHighlightEnded) { document.body.classList.add('rinsp-request-highlight-ended-mode'); } if (userConfig.requestThreadShowExtraBounty) { document.body.classList.add('rinsp-request-show-extra-bounty'); } if (userConfig.requestThreadUseHistoryData) { document.body.classList.add('rinsp-request-show-bounty-history'); } if (userConfig.hideSettlementPost) { if (userConfig.hideSettlementPost$GreyoutOnly) { document.body.classList.add('rinsp-request-settlement-greyout-mode'); } else { document.body.classList.add('rinsp-request-settlement-hide-mode'); } } if (userConfig.hideIgnoreContentPost) { document.body.classList.add('rinsp-filter-hide-mode'); } if (userConfig.hideZeroReply) { document.body.classList.add('rinsp-byop-noreply-hide-mode'); } const userPinArea = await addUserPinArea(pinnedUsersConfigAccess, userConfig.hideInactivePinnedUsers, userConfig.showResourceSpotsFloating); let shareTypeFilter = null; if (userConfig.showShareTypeFilter) { if (inResourceArea || inPaywallArea) { const headrow = document.querySelector('#ajaxtable .hthread'); if (headrow) { shareTypeFilter = await addShareTypeFilterArea(headrow, sharetypeFilterConfigAccess); } } else { const headrow = document.querySelector('#main .t > table > tbody > tr > .h[colspan]'); if (headrow && headrow.textContent.trim() === '主题列表') { shareTypeFilter = await addShareTypeFilterArea(headrow, sharetypeFilterConfigAccess); } } } const queryParams = {}; const queryParamMatch = document.location.search.match(/^\?([A-Za-z_]+)-([^-]+)(?:-(.*))?\.html.*$/); if (queryParamMatch) { queryParams[queryParamMatch[1]] = decodeURIComponent(queryParamMatch[2]); if (queryParamMatch[3]) { const rest = queryParamMatch[3].split('-'); while (rest.length > 0) { const key = rest.shift(); const value = rest.shift(); queryParams[key] = decodeURIComponent(value); } } } else if (document.location.search && !document.location.search.endsWith('.html')) { (new URLSearchParams(document.location.search)).forEach((value, key) => { queryParams[key] = value; }); } if (userConfig.showSearchBar) { addSearchBar(queryParams, searchConfigAccess, userConfig); } let action = queryParams.action||''; if (document.location.pathname === '/' || document.location.pathname === '/index.php') { enhanceFrontPage(userConfig, mainConfigAccess); } if (document.location.pathname === '/u.php') { if (document.location.search === '' || queryParams.action == null && queryParams.uid * 1 === myUserId) { document.body.classList.add('rinsp-uphp-self'); await initConfigPanel(); rewriteUserRecentPostLinks(userConfig, userPinArea); await enhancePostListUserDisplay(queryParams.action||'', myUserId, myUserId, userConfig, userPinArea); } else if (['friend', 'favor', 'feed', ''].includes(action)) { const uid = queryParams.uid * 1 || myUserId; await enhancePostListUserDisplay(queryParams.action||'', uid, myUserId, userConfig, userPinArea); if (action === 'feed') { rewriteUserRecentPostLinks(userConfig, userPinArea); } else if (action === 'favor') { if (uid === myUserId) { updateFavorRecords(favorThreadsCacheAccess); } enhanceFavorPageDisplay(uid, myUserId, userConfig, threadHistoryAccess); } } else if (action === 'trade') { if ((queryParams.uid * 1 || myUserId) === myUserId) { if (adminFunctions && queryParams.view === 'admin') { adminFunctions.renderAdminHistoryPage(myUserId, userConfig); } else { renderVisitHistoryPage(myUserId, userConfig, threadHistoryAccess); } } else if (adminFunctions) { adminFunctions.renderPunishHistoryPage(queryParams.uid * 1, userConfig); } } if (queryParams.uid) { enhanceUserProfilePages(queryParams.uid * 1, myUserId, userConfig, adminFunctions); } else { enhanceUserProfilePages(myUserId, myUserId, userConfig, adminFunctions); } if (action === 'topic') { enhanceTopicPostListDisplay(queryParams.uid * 1 || myUserId, myUserId, userConfig, threadHistoryAccess, threadCategoryConfigAccess, hideVisitedThreadToggler); setupInfiniteScroll_userTopics(userConfig, mainConfigAccess); } else if (action === 'post') { await enhanceReplyPostListDisplay(queryParams.uid * 1 || myUserId, myUserId, userConfig, userPinArea, mainConfigAccess, threadHistoryAccess, hideVisitedThreadToggler); setupInfiniteScroll_userReplies(userConfig, mainConfigAccess); } enhanceUserDetailPage(queryParams.action||'', queryParams.uid * 1, userConfig, userPinArea, adminFunctions); replaceTradeWithHistoryTab(myUserId, queryParams); if (adminFunctions) { replaceTradeWithPunishHistoryTab(myUserId, queryParams); } if (action) { document.body.classList.add('rinsp-uphp-' + action); } } else { if (userConfig.myUserHashId == null) { document.location.href = '/u.php'; } } if (document.location.pathname === '/message.php') { if (action === '' || action === 'receivebox') { enhanceMessagingList(userConfig, userPinArea, true); setupInfiniteScroll_msgInbox(userConfig, mainConfigAccess); } else if (action === 'scout' || action === 'sendbox') { enhanceMessagingList(userConfig, userPinArea, false); setupInfiniteScroll_msgSent(userConfig, mainConfigAccess); } else if (action === 'chatlog') { setupInfiniteScroll_msgChatLog(userConfig, mainConfigAccess); } else if (action === 'write') { enhanceMessagingForm(); } else if (action === 'read' || action === 'readsnd' || action === 'readscout') { enhanceMessageReadDisplay(userConfig); if (action === 'read' && threadHistoryAccess) { testRecordBestAnswerNotification(userConfig, threadHistoryAccess); } } } if (document.location.pathname === '/search.php') { enhanceSearchPage(); setupInfiniteScroll_search(userConfig, mainConfigAccess); if (adminFunctions) { adminFunctions.addBatchDeleteFunction(); } } if (document.location.pathname === '/read.php') { addSearchShortcut(); setupInfiniteScroll_thread(userConfig, mainConfigAccess); addTitleLengthLimitIndicator(); } if (document.location.pathname === '/plugin.php' || document.location.pathname === '/hack.php') { showAutoSpTaskStatus(myUserId, userConfig, mainConfigAccess); } else if (userConfig.autoSpTasks) { autoCompleteSpTasks(myUserId); } if (document.location.pathname === '/post.php') { addTitleLengthLimitIndicator(); } if (document.location.pathname === '/thread.php' || document.location.pathname === '/thread_new.php') { enableForumAnnouncementFolding(); addSearchShortcut(); if (userConfig.showDefaultingPicWallOption) { addPicWallDefaultOption(queryParams.fid * 1); } if (document.location.pathname === '/thread.php') { setupInfiniteScroll_threadList(userConfig, mainConfigAccess); } else { setupInfiniteScroll_threadPicWall(userConfig, mainConfigAccess); } } if (userConfig.enhancePageTitle) { enhancePageTitle(queryParams, myUserId, userConfig, adminFunctions != null); } if (adminFunctions) { if (document.location.pathname === '/read.php') { if (inResourceArea) { adminFunctions.enableQuickPingFunction(); } adminFunctions.addDeleteFormMetadata(); } else if (document.location.pathname === '/masingle.php') { if (action === 'banuser') { adminFunctions.enhanceBanUserActionPage(); } else { adminFunctions.enhanceReasonSelector(); if (action === 'shield') { adminFunctions.enhanceShieldActionPage(); } else if (action === 'delatc') { adminFunctions.enhanceDeleteReplyActionPage(); } } } else if (document.location.pathname === '/mawhole.php') { adminFunctions.enhanceReasonSelector(); adminFunctions.enhanceMawholeActionPage(); } else if (document.location.pathname === '/operate.php') { adminFunctions.enhanceReasonSelector(); if (action === 'showping') { if (queryParams.pid === 'tpc') { adminFunctions.autoFillShowPingPage(); } adminFunctions.enhancePingActionPage(); } } else if (document.location.pathname === '/job.php') { if (action === 'endreward') { adminFunctions.enhanceEndRewardActionPage(); } } if (document.location.pathname === '/thread.php') { const fid = queryParams.fid * 1; if (inResourceArea && fid > 0) { adminFunctions.addBatchPingFunction(fid); } adminFunctions.addBulkDeleteFormMetadata(); } } if (userConfig.subCategoryInheritImageWallMode) { if (document.location.pathname === '/thread_new.php') { changeToImageWallThreadLinks(); } } function tryRemoveCorruptDeployCookie() { try { const currentCookieString = document.cookie; if (!currentCookieString) { return; } const modified = currentCookieString .split(' ') .map(cookie => { if (cookie.startsWith('deploy=') && cookie.length > 256) { return 'deploy=\t\t\n'; } return cookie; }) .join(' '); if (currentCookieString !== modified) { if (DEV_MODE) { console.info('dropped "deploy" cookie'); } document.cookie = modified; } } catch (e) { if (DEV_MODE) { console.error(e); } } } tryRemoveCorruptDeployCookie(); if (userConfig.siteNoticeSectionDefaultFolded) { const foldIcon = document.querySelector('.gongul #img_thread[src$="/cate_fold.gif"]'); if (foldIcon) { if (DEV_MODE) { console.info('set notice default folded'); } foldIcon.click(); } } const watcherApi = initTopMenu(); let ignoreList = await contentIgnoreListConfigAccess.read(); let userFilter = await userFilterConfigAccess.read(); initCurrentPost(); function setupStickyUserInfoPopup() { let userInfoPopup = document.querySelector('body > .user-info'); function bindUpdate() { const observer = new MutationObserver(() => { update(); }); observer.observe(userInfoPopup, { childList: false, subtree: false, attributes: true }); } function update() { const top = Number.parseInt(userInfoPopup.style.top); if (document.documentElement.scrollTop > top && userInfoPopup.style.display !== 'none') { document.body.classList.add('rinsp-user-info-sticky-mode'); } else { document.body.classList.remove('rinsp-user-info-sticky-mode'); } } if (userInfoPopup == null) { const observer = new MutationObserver(() => { userInfoPopup = document.querySelector('body > .user-info'); if (userInfoPopup != null) { observer.disconnect(); observer.takeRecords(); bindUpdate(); update(); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false }); } else { bindUpdate(); } } if (userConfig.stickyUserInfo) { setupStickyUserInfoPopup(); } document.addEventListener('mousedown', function(e) { if (!e.target) { return; } const enclosingPost = e.target.closest('.rinsp-post'); if (enclosingPost) { enclosingPost.focus(); } if (document.querySelector('.rinsp-dialog-modal-mask') != null) { return; } const withinMenu = e.target.closest('.rinsp-common-popup-menu'); let withinMenuId = '-n/a-'; if (withinMenu != null) { withinMenuId = withinMenu.getAttribute('id'); } document.querySelectorAll('.rinsp-common-popup-menu').forEach(function(menu) { if (menu.getAttribute('id') !== withinMenuId) { menu.remove(); } }); }); if (userConfig.autoCheckReplyOption) { autoCheckReply(); } const stickyUserpicController = (function() { let posts = []; let schedule = null; return { setPosts(newPosts) { posts = newPosts; }, update() { let activeFound = false; for (let post of posts) { post.userPicElem.classList.remove('rinsp-userpic-sticky'); post.userPicElem.style.top = ''; if (!activeFound) { const postRect = post.rootElem.getBoundingClientRect(); const visibleHeight = postRect.top + postRect.height; if (postRect.top < 0 && visibleHeight > 0) { const userPicElemStaticRect = post.userPicElem.getBoundingClientRect(); if (postRect.height - userPicElemStaticRect.height >= MIN_OVER_HEIGHT_STICKY_MODE_TRIGGER) { const dummy = post.userPicElem.parentNode.querySelector('.rinsp-user-pic-dummy'); dummy.style.height = userPicElemStaticRect.height.toFixed(0) + 'px'; if (userConfig.stickyUserInfo) { post.userPicElem.classList.add('rinsp-userpic-sticky'); } const userPicElemStickyRect = post.userPicElem.getBoundingClientRect(); const overflowHeight = userPicElemStickyRect.height - visibleHeight; if (overflowHeight > 0) { post.userPicElem.style.top = '-' + overflowHeight.toFixed(0) + 'px'; } else { post.userPicElem.style.top = ''; } } activeFound = true; continue; } } } }, scheduleUpdate() { if (schedule != null) return; schedule = setTimeout(() => { this.update(); schedule = null; }, 0); } }; })(); let forceCleanUpdate = false; let enhancementMemoryObj = {}; let postEnhancementActions = []; let lastCacheKey = null; const observer = createMutationObserver(applyEnhancement); await applyEnhancement(true); async function applyEnhancement(firstTime) { if (DEBUG_MODE) console.info('applyEnhancement'); function disableSystemPostCollapse(post) { if (userConfig.disablePostCollapse) { return true; } if (userConfig.customUserpicBypassIgnoreList && !post.defaultUserPic) { return true; } if (post.postUid === myUserId && userConfig.disablePostCollapse$Self) { return true; } if (userConfig.customUserBypassIgnoreList && userConfig.customUserHashIdMappings['#' + post.postUid]) { return true; } return false; } function handleCollapseStateClasses(post) { post.rootElem.classList.remove('rinsp-sys-collapse-override'); post.rootElem.classList.remove('rinsp-post-sys-collapsed'); if (!post.contentDefaultIgnorable) { return; } if (disableSystemPostCollapse(post)) { post.rootElem.classList.add('rinsp-sys-collapse-override'); const expandButton = post.rootElem.querySelector('.js-puremark-content ~ .bianji > a[onclick="return expandpuremark(this)"]'); if (expandButton) { expandButton.click(); } } else { post.rootElem.classList.add('rinsp-post-sys-collapsed'); } } let threadFilter = await threadFilterConfigAccess.read(); function onConfigUpdate(updatedConfigs) { forceCleanUpdate = true; if (updatedConfigs) { if (updatedConfigs.threadFilter != null) { threadFilter = updatedConfigs.threadFilter; } if (updatedConfigs.userFilter != null) { userFilter = updatedConfigs.userFilter; } if (updatedConfigs.ignoreList != null) { ignoreList = updatedConfigs.ignoreList; } } } function contentFilter_onConfigUpdate(updatedConfigs) { onConfigUpdate(updatedConfigs); observer.trigger(); } function threadTitleFilter_onConfigUpdate(updatedConfigs) { onConfigUpdate(updatedConfigs); observer.trigger(); } if (userConfig.stickyUserInfo) { document.addEventListener('scroll', () => { stickyUserpicController.scheduleUpdate(); }); } async function enhanceThreadView(tid, posts, userMap, userFilter, userPinArea, adminFunctions) { let gfPost = null; if (userConfig.stickyUserInfo) { stickyUserpicController.setPosts(posts); } userPinArea.clearState(); const postCountsByUid = new Map(); if (userConfig.showActiveRepliers) { const minReplyCount = userConfig.showActiveRepliers$min * 1 || 2; posts.forEach(post => { if (post.floor > 0) { const count = postCountsByUid.get(post.postUid); postCountsByUid.set(post.postUid, (count||0) + 1); } }); postCountsByUid.forEach((count, uid) => { if (count >= minReplyCount) { if (!isUserBlacklisted(userFilter, uid)) { const userInfo = userMap.get(uid); userPinArea.watchUserLocally(uid, userInfo.nickName, userInfo.img); } } }); } posts.forEach(post => { post.rootElem.classList.add('rinsp-post'); if (post.floor === 0) { gfPost = post; post.rootElem.classList.add('rinsp-post-gf'); } handleCollapseStateClasses(post); }); if (inResourceArea && gfPost) { // add scored marker const markElem = gfPost.rootElem.querySelector('#mark_tpc'); if (markElem && markElem.textContent.indexOf('评分记录') !== -1) { gfPost.rootElem.classList.add('rinsp-post-scored'); } else { gfPost.rootElem.classList.add('rinsp-post-unscored'); } } if (gfPost && gfPost.areaId === QUESTION_AND_REQUEST_AREA_ID) { const tac = document.querySelector('.tpc_content > .tips:not(.rinsp-uid-inspected) > .tac:last-child'); if (tac) { const winnerMatch = tac.textContent.match(/最佳答案获得者: ([a-z0-9]{8})/); if (winnerMatch) { const tips = tac.parentNode; tips.classList.add('rinsp-uid-inspected'); const uhash = winnerMatch[1]; let replace = null; if (userConfig.highlightMyself && uhash === userConfig.myUserHashId) { (replace = newElem('span', 'rinsp-nickname-byme')).textContent = MY_NAME_DISPLAY; } else { // userPinArea const fullName = userConfig.customUserHashIdMappings['@' + uhash]; if (fullName != null) { (replace = newElem('span', 'rinsp-nickname-byother')).textContent = fullName[2]; } } if (replace) { tac.hidden = true; const tac2 = addElem(tips, 'div', 'tac'); tac2.appendChild(document.createTextNode('最佳答案获得者: ')); tac2.appendChild(replace); } } } } const punishRecordAccess = adminFunctions ? adminFunctions.createPunishRecordAccess() : null; annotateUsers(posts, myUserId, userConfig, userMap, { punishRecordAccess, changeCustomUserMapping }); if (userConfig.enhanceSellFrame || userConfig.buyRefreshFree) { enhanceBuyButtons(userConfig, () => { forceCleanUpdate = true; observer.trigger(); }); } if (userConfig.replyRefreshFree) { const lastPost = posts.at(-1); applyReplyRefreshFree(tid, lastPost, myUserId); } applySubjectLineEnhancement(posts, userConfig); applyHookDirectives(posts, myUserId, userConfig); await applyContentFilter(posts, myUserId, userConfig, threadFilter, userFilter, ignoreList, contentFilter_onConfigUpdate); if (showResourceSpots) { let likePatternMatcher = null; if (threadFilter && threadFilter.likes.length > 0) { likePatternMatcher = createKeywordMatcherFactory(threadFilter.likes); } posts.forEach(post => { if (!post.rootElem.classList.contains('rinsp-filter-ignored')) { userPinArea.offerPostContent(post, showResourceSpots, likePatternMatcher); } }); } await userPinArea.update(); posts.forEach(post => { post.rootElem.parentNode.classList.remove('rinsp-thread-filter-pinned'); if (userPinArea.isWatching(post.postUid)) { if (userPinArea.isPinned(post.postUid)) { post.rootElem.parentNode.classList.add('rinsp-thread-filter-pinned'); } else { post.rootElem.parentNode.classList.add('rinsp-thread-filter-active-reply'); } const bannedText = post.rootElem.querySelector('.tpc_content div[id^="read_"] > span[style="color:black;background-color:#ffff66"]'); const banned = bannedText && bannedText.textContent.trim() === '用户被禁言,该主题自动屏蔽!'; const contentIgnored = post.rootElem.classList.contains('rinsp-filter-ignored'); const locClass = post.floor === 0 ? 'gf' : banned ? 'banned' : contentIgnored ? 'ignoredmark' : null; userPinArea.addLocation(post.postUid, post.floor === 0 ? 'GF' : `${post.floor}F`, locClass, () => { if (contentIgnored && !ignoreContentToggler.isActive()) { ignoreContentToggler.setActive(true); } post.rootElem.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } }); await userPinArea.update(); if (adminFunctions) { adminFunctions.addPostAdminControl(posts, queryParams.page||1, userMap); } } if (document.location.pathname === '/read.php') { if (adminFunctions) { adminFunctions.addBatchPostSelectionControl(); } const posts = getPosts(document); const cacheKey = 'posts:' + posts.length; if (forceCleanUpdate || cacheKey !== lastCacheKey) { lastCacheKey = cacheKey; if (posts.length > 0) { const userMap = readEnhanceUserInfoMap(myUserId, userConfig); const myself = userMap.get(myUserId); if (myself) { // auto update my nick name if changed if (userConfig.myNickName == null || myself.nickName !== userConfig.myNickName) { userConfig = await mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.myNickName = myself.nickName; return updatingUserConfig; }); } } await enhanceThreadView(queryParams.tid * 1, posts, userMap, userFilter, userPinArea, adminFunctions); if (threadHistoryAccess) { recordThreadAccess(document, posts, myUserId, threadHistoryAccess, enhancementMemoryObj) // no need to wait .catch(ex => console.error('recordThreadAccess FAILED', ex)); } if (firstTime) { const targetFloor = (document.location.hash.match(/^#fl-(\d+)$/)||[])[1] * 1; if (targetFloor > 0) { const targetElem = posts.filter(post => post.floor === targetFloor).map(post => post.rootElem)[0]; if (targetElem) { postEnhancementActions.push(() => { targetElem.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); } } } } else { const pageError = findErrorMessage(document); if (pageError && pageError.indexOf('数据已被删除') !== -1 && (queryParams.tid * 1) > 0) { recordThreadDeleted(queryParams.tid * 1, myUserId, threadHistoryAccess); // no need to wait } } } else { if (DEBUG_MODE) console.info('skipped update', cacheKey); } } else { const threadList = getThreadList(); if (threadList == null) { lastCacheKey = null; } else { const cacheKey = 'threads:' + threadList.length; if (forceCleanUpdate || cacheKey !== lastCacheKey) { lastCacheKey = cacheKey; for (let thread of threadList) { if (thread.opHashId) { await userHashLookupStore.set(Number.parseInt(thread.opHashId, 16), thread.op).catch(ex => null); } } await showUserNames(userConfig, userHashLookupStore, myUserId); if (userConfig.threadListDefaultOpenNewPage) { threadList.forEach(thread => { thread.row.querySelectorAll('a[href]:not([target])').forEach(el => el.setAttribute('target', '_blank')); }); } // unused: reserved for later use /* const pagesElem = document.querySelector('#main .t3 .pages'); if (pagesElem && document.querySelector('#main .rinsp-threadlist-options') == null) { const t3 = pagesElem.closest('.t3'); const c = Array.from(t3.children).find(el => el.classList.contains('c')); const optionsElem = newElem('span', 'fr rinsp-threadlist-options'); t3.insertBefore(optionsElem, c); } */ let settlementPostMatcher = null; if (userConfig.hideSettlementPost) { if (document.location.href.match(/\/thread(_new)?\.php\?fid-48[-.]/)) { const settlementDetectionRules = threadFilter.settlementKeywords||[]; if (settlementDetectionRules.length > 0) { settlementPostMatcher = createKeywordMatcherFactory(settlementDetectionRules); } } } await applyThreadListEnhancement(myUserId, userConfig, threadList, threadFilter, userFilter, userPinArea, shareTypeFilter, settlementPostMatcher, threadHistoryAccess, threadTitleFilter_onConfigUpdate, forceCleanUpdate); } else { if (DEBUG_MODE) console.info('skipped update', cacheKey); } } } forceCleanUpdate = false; const actions = postEnhancementActions; postEnhancementActions = []; actions.forEach(action => { action(); }); } function getThreadList() { switch (document.location.pathname) { case '/thread_new.php': return readPicWallThreadList(); case '/thread.php': return readThreadList(); case '/search.php': if (document.querySelector('form[action="search.php?"]')) { return null; } else { return readSearchThreadList(); } default: return null; } } userPinArea.addUpdateListener(() => { forceCleanUpdate = true; observer.trigger(); }); if (shareTypeFilter) { shareTypeFilter.addUpdateListener(() => { forceCleanUpdate = true; observer.trigger(); }); } observer.init(document.getElementById('main'), { childList: true, subtree: true, attributes: false }); let hiddenStateChangeObserver = null; const postListForm = document.querySelector('#main > form[name="delatc"]'); if (postListForm) { let lastHiddenPostCount = 0; hiddenStateChangeObserver = createMutationObserver(async () => { const hiddenPostCount = document.querySelectorAll('.t5.t2[hidden]').length; if (hiddenPostCount != lastHiddenPostCount) { forceCleanUpdate = true; lastHiddenPostCount = hiddenPostCount; postEnhancementActions.push(() => { document.querySelectorAll('#main > form[name="delatc"] > .t5.t2[hidden] > .js-post').forEach(defaultHiddenPost => { if (defaultHiddenPost.classList.contains('rinsp-filter-bypass')) { return; } const postContainer = defaultHiddenPost.parentElement; if (postContainer.hidden) { defaultHiddenPost.classList.remove('rinsp-filter-bypass'); } else { defaultHiddenPost.classList.add('rinsp-filter-bypass'); } }); }); observer.trigger(); } }); hiddenStateChangeObserver.init(postListForm, { childList: true, subtree: true, attributes: true }); } if (threadHistoryAccess) { // if history based styling is active, need to recheck on page activation document.addEventListener('visibilitychange', () => { if (!document.hidden) { forceCleanUpdate = true; observer.trigger(); } }); } if (DEV_MODE) console.info('(southplus-watcher) loaded'); function getUserProfileHashId() { const elem = document.querySelector('#main #u-wrap #u-content #u-top h1'); if (elem != null) { const userHashId = elem.textContent.trim(); if (userHashId.match(/[0-9a-f]{8}/)) { return userHashId; } } return null; } async function enhanceUserProfilePages(userId, myUserId, userConfig, adminFunctions) { if (userId !== myUserId) { // add more action shortcuts const actionShortcutGrid = document.querySelector('#u-portrait ~ .bdbA > table > tbody'); if (actionShortcutGrid) { const actionRow = addElem(actionShortcutGrid, 'tr'); const cell = addElem(actionRow, 'td', null, { align: 'center' }); addElem(cell, 'span', null, { style: 'display: inline-block; width: 14px' }); cell.appendChild(document.createTextNode(' ')); addElem(cell, 'a', null, { href: `message.php?action-chatlog-withuid-${userId}.html` }).textContent = '通信记录'; addElem(actionRow, 'td'); if (adminFunctions) { const punishRecordAccess = adminFunctions.createPunishRecordAccess(); const record = await punishRecordAccess.getPunishRecord(userId); if (record && record.logs.length > 0) { const punishRecordRow = addElem(actionShortcutGrid, 'tr'); const punishRecordCell = addElem(punishRecordRow, 'td', 'rinsp-profile-punish-tag-cell', { colspan: '2' }); punishRecordCell.appendChild(renderPunishTags(userId, record)); } } } } else { document.querySelectorAll('#u-portrait img.pic').forEach(el => el.classList.add('rinsp-myavatar')); } } async function enhanceUserDetailPage(action, userId, userConfig, userPinArea, adminFunctions) { // add user memory action buttons let nickName = null; let uidCell = null; if (action === 'show') { for (let td of document.querySelectorAll('#u-profile table > tbody > tr > td')) { if (td.textContent === 'UID') { uidCell = td.nextElementSibling; if (!userId) { userId = uidCell.textContent * 1; } } if (td.textContent === '昵称') { nickName = td.nextElementSibling.textContent.trim(); break; } } } const userHashId = getUserProfileHashId(); if (userHashId != null) { if (uidCell) { const pinOper = addElem(uidCell, 'div', 'rinsp-user-action-pinuser-button'); const update = () => { pinOper.innerHTML = ''; delete pinOper.dataset.pinned; if (userPinArea.isPinned(userId)) { pinOper.dataset.pinned = '1'; addElem(pinOper, 'a', 'rinsp-user-action-pinuser-icon', { href: 'javascript:'}) .addEventListener('click', () => { userPinArea.unwatchUser(userId); }); } else { addElem(pinOper, 'a', 'rinsp-user-action-pinuser-icon', { href: 'javascript:'}) .addEventListener('click', () => { const img = document.querySelector('#u-portrait img.pic'); userPinArea.watchUser(userId, nickName, getImgSrc(img)); }); } }; userPinArea.addUpdateListener(() => update()); update(); if (adminFunctions) { uidCell.appendChild(document.createTextNode(' / ')); const showUserNameButton = addElem(uidCell, 'a', null, { href: 'javascript:'}); showUserNameButton.textContent = '显示用户名'; showUserNameButton.addEventListener('click', async () => { const userNameDisplay = addElem(uidCell, 'span'); userNameDisplay.textContent = '⌛'; showUserNameButton.remove(); adminFunctions.findUserName(userId) .then(userName => { userNameDisplay.classList.add('rinsp-user-name-reveal'); userNameDisplay.textContent = userName; }) .catch(ex => userNameDisplay.textContent = '⚠️' + ex); }); } } let customNameEntry = userConfig.customUserHashIdMappings['@' + userHashId]; const parentElem = document.querySelector('#main #u-wrap #u-content #u-top h1'); const button = addElem(parentElem, 'span', 'rinsp-user-map-icon'); if (customNameEntry) { button.setAttribute('rinsp-nickname', customNameEntry[2]); button.classList.add('rinsp-user-mapped'); } button.addEventListener('click', async () => { button.classList.add('rinsp-config-saving'); changeCustomUserMapping(userId, userHashId, customNameEntry&&customNameEntry[2]||nickName||'') .then(function(newName) { if (newName == null) { return; } if (newName) { button.classList.add('rinsp-user-mapped'); button.setAttribute('rinsp-nickname', newName); } else { button.classList.remove('rinsp-user-mapped'); button.removeAttribute('rinsp-nickname'); } }) .finally(() => { button.classList.remove('rinsp-config-saving'); }); }); } } async function changeCustomUserMapping(userId, userHashId, nickName, bypassPrompt) { let chosenNickName = bypassPrompt ? nickName : prompt('记住昵称 - 输入空白以撤销记录', nickName); if (chosenNickName == null) { // cancelled return null; } chosenNickName = chosenNickName.trim(); await mainConfigAccess.update(function(updatingUserConfig) { const currentMetadata = updatingUserConfig.customUserHashIdMappings['@' + userHashId]; if (!chosenNickName && !currentMetadata) { return null; } if (chosenNickName) { const newMetadata = [userId * 1, userHashId, chosenNickName]; updatingUserConfig.customUserHashIdMappings['#' + userId] = newMetadata; updatingUserConfig.customUserHashIdMappings['@' + userHashId] = newMetadata; return updatingUserConfig; } else { delete updatingUserConfig.customUserHashIdMappings['#' + currentMetadata[0]]; delete updatingUserConfig.customUserHashIdMappings['@' + userHashId]; return updatingUserConfig; } }); return chosenNickName; } async function openClearHistoryMenu(anchor, onUpdate) { const items = []; function generateAction(task, confirmMessage, successMessage) { return async () => { if (!confirm('请确认 ' + confirmMessage)) return; const action = async () => { await task(); return '✔️' + successMessage; }; runWithProgressPopup(action, '清空中...', anchor, 1500) .catch((ex) => alert(String(ex))) .finally(() => onUpdate()); }; } items.push({ label: '清空最近浏览列表', action: generateAction(async () => { await threadHistoryAccess.recentAccessStore.clear(); }, '清空最近浏览列表', '已清空最近浏览列表') }); items.push({ label: '清空全部记录', action: generateAction(async () => { await threadHistoryAccess.historyStore.clear(); await threadHistoryAccess.recentAccessStore.clear(); }, '清空全部记录', '已清空全部记录') }); setupPopupMenu({ title: '清空帖子浏览记录', popupMenuId: 'CLEAR_HISTORY_MENU', width: 150, anchor, items }); } async function initConfigPanel() { if (userConfig.myUserHashId == null) { const userHashId = getUserProfileHashId(); if (userHashId != null) { userConfig = await mainConfigAccess.update(function(freshUserConfig) { freshUserConfig.myUserHashId = userHashId; return freshUserConfig; }); } } const mainPanel = document.querySelector('#u-contentmain'); const configPanel = addElem(mainPanel, 'div', 'rinsp-config-panel'); const configHeading = addElem(configPanel, 'h5', 'u-h5'); addElem(configHeading, 'span').textContent = `插件设置 (凛+ v${VERSION_TEXT})`; if (DEV_MODE) { const devToggler = addElem(configHeading, 'a', 'rinsp-dev-toggle r gray3'); devToggler.textContent = '开发者模式'; devToggler.addEventListener('click', () => { if (confirm('关闭开发者模式?')) { localStorage.removeItem('RIN_PLUS_DEV_MODE'); window.location.reload(); } }); } else { const devToggler = addElem(configHeading, 'input', 'rinsp-dev-toggle r gray3', { size: 4 }); devToggler.addEventListener('change', () => { if (devToggler.value === 'devmode') { devToggler.value = ''; alert('开发者模式已打开'); localStorage.setItem('RIN_PLUS_DEV_MODE', '1'); window.location.reload(); } }); } const configList = addElem(configPanel, 'dl', 'rinsp-config-list'); const introItem = addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked'); addElem(introItem, 'a', 'rinsp-intro-link', { href: INTRO_POST, target: '_blank' }).textContent = '🔗 捷径: 功能简介帖(茶館)'; putCheckBoxItem('新功能更新后 自动打开简介帖', 'openIntroAfterUpdate', 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1').textContent = '绑域名设置 (只同域名内适用)'; putCheckBoxItem('暗黑模式', { get: () => darkModeEnabledProperty.get() != null, async update(enabled) { darkModeEnabledProperty.set(enabled ? '1' : null); applyDarkTheme(); } }); const darkModeOption = addElem(configList, 'dt', 'rinsp-config-item-lv2'); addElem(darkModeOption, 'span').textContent = '🎨 样式 '; const darkModeSelect = addElem(darkModeOption, 'select'); addElem(darkModeSelect, 'option', null, { value: 'darkbeige' }).textContent = '暗棕色'; addElem(darkModeSelect, 'option', null, { value: 'hcdarkbeige' }).textContent = '暗棕色 (高反差)'; addElem(darkModeSelect, 'option', null, { value: 'darkblue' }).textContent = '暗蓝色'; addElem(darkModeSelect, 'option', null, { value: 'hcdarkblue' }).textContent = '暗蓝色 (高反差)'; addElem(darkModeSelect, 'option', null, { value: 'darkred' }).textContent = '暗红色'; addElem(darkModeSelect, 'option', null, { value: 'darkgrey' }).textContent = '暗灰色'; darkModeSelect.value = darkModeThemeProperty.get(); darkModeSelect.addEventListener('change', () => { darkModeThemeProperty.set(darkModeSelect.value); applyDarkTheme(); }); putCheckBoxItem('跟随系统偏好设置', { get: () => darkModeFollowsSystemProperty.get() === '1', async update(enabled) { darkModeFollowsSystemProperty.set(enabled ? '1' : null); applyDarkTheme(); } }, 2); putCheckBoxItem('贤者模式 隐藏NSFW图片', { get: () => isSafeMode(), update: async (enabled) => setSafeMode(enabled) }); putCheckBoxItem('贤者模式中不隐藏自己头像', { get: () => isSafeModeShowMyAvater(), update: async (enabled) => setSafeModeShowMyAvater(enabled) }, 2); putCheckBoxItem('提高页面加载速度 (日后论坛更新取代功能后移除)', { get: () => isFastLoadMode(), update: async (enabled) => setFastLoadMode(enabled) }); putCheckBoxItem('延迟加载屏幕外的图片', { get: () => isFastLoadLazyImageMode(), update: async (enabled) => setFastLoadLazyImageMode(enabled) }, 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1').textContent = '论坛设置'; putCheckBoxItem('自动签到 (社区论坛任务)', 'autoSpTasks'); if (userConfig.autoSpTasks) { const taskRecordAccess = initConfigAccess(myUserId, 'autosptask', {}); const autoSpTasksSummary = addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2'); taskRecordAccess.read().then(taskRecord => { if (taskRecord.lastCompleteSp > 0) { const hr = (Date.now() - taskRecord.lastComplete) / 3600000; autoSpTasksSummary.textContent = `💡 ${getAgeString(hr * 60)} 已领取 ${taskRecord.lastCompleteSp||'?'} SP | 记录中总共领取 ${taskRecord.totalSp||'?'} SP`; } }); } putCheckBoxItem('自动域名跳转', { get: () => domainRedirectTarget != null, update: async (enabled) => setDomainRedirectEnabled(enabled) }); putCheckBoxItem('增強网页标题 (令其更有意义)', 'enhancePageTitle'); const searchBarItem = putCheckBoxItem('主菜单上添加快速搜索栏', 'showSearchBar'); addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2').textContent = '💡 为了腾出空间 "社区论坛任务" 缩写为 "任务"'; addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2').textContent = '💡 快速搜索使用已保存的默认设置 (发表主题时间、部分或完全匹配)'; addElem(searchBarItem, 'span').textContent = '🔹位置 '; const searchBarAlignSelect = addElem(searchBarItem, 'select'); addElem(searchBarAlignSelect, 'option', null, { value: 'default' }).textContent = '主菜单 最右边'; addElem(searchBarAlignSelect, 'option', null, { value: 'menu-first' }).textContent = '主菜单 右边第一项'; addElem(searchBarAlignSelect, 'option', null, { value: 'bar-center' }).textContent = '主菜单 空间置中'; searchBarAlignSelect.value = userConfig.showSearchBar$align; searchBarAlignSelect.value = searchBarAlignSelect.value || 'default'; searchBarAlignSelect.addEventListener('change', () => { configPanel.classList.add('rinsp-config-saving'); mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.showSearchBar$align = searchBarAlignSelect.value; return updatingUserConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); window.location.reload(); }); }); const fontSizeOption = addElem(configList, 'dt', 'rinsp-config-item-lv1'); addElem(fontSizeOption, 'span').textContent = '🗚 页面字体大小 '; const fontSizeSelect = addElem(fontSizeOption, 'select'); addElem(fontSizeSelect, 'option', null, { value: '0' }).textContent = '论坛默认大小'; addElem(fontSizeSelect, 'option', null, { value: '1' }).textContent = '列表标题大小 +1'; addElem(fontSizeSelect, 'option', null, { value: '2' }).textContent = '列表标题及内容大小 +1'; addElem(fontSizeSelect, 'option', null, { value: '3' }).textContent = '列表标题及内容大小 +2'; fontSizeSelect.value = userConfig.textSize; fontSizeSelect.value = fontSizeSelect.value || '0'; fontSizeSelect.addEventListener('change', () => { configPanel.classList.add('rinsp-config-saving'); mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.textSize = fontSizeSelect.value||0; return updatingUserConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); window.location.reload(); }); }); const floatShortcutOption = addElem(configList, 'dt', 'rinsp-config-item-lv1'); addElem(floatShortcutOption, 'span').textContent = '⚙️ 浮动式捷径窗口 '; const floatShortcutSelect = addElem(floatShortcutOption, 'select'); addElem(floatShortcutSelect, 'option', null, { value: 'off' }).textContent = '不显示'; addElem(floatShortcutSelect, 'option', null, { value: 'tr' }).textContent = '右上角'; addElem(floatShortcutSelect, 'option', null, { value: 'tl' }).textContent = '左上角'; floatShortcutSelect.value = userConfig.floatingShortcut; floatShortcutSelect.value = floatShortcutSelect.value || 'tr'; floatShortcutSelect.addEventListener('change', () => { configPanel.classList.add('rinsp-config-saving'); mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.floatingShortcut = floatShortcutSelect.value; return updatingUserConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); window.location.reload(); }); }); addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '浮动功能捷径'; putCheckBoxItem('浮动私信通知', 'showFloatingMessageIndicator', 2); putCheckBoxItem('浮动收藏按钮 (坛上内置功能)', 'showFloatingStoreThreadButton', 2); putCheckBoxItem('浮动关注按钮 (类似MARK++)', 'showFloatingWatchIndicator', 2); putCheckBoxItem('回到顶部按钮', 'showBackToTopButton', 2); addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '自动无缝加载翻页'; putCheckBoxItem('主题帖 - 列表', 'infiniteScroll$threads', 2); putCheckBoxItem('主题帖 - 图墙', 'infiniteScroll$threads_picwall', 2); putCheckBoxItem('用户主题', 'infiniteScroll$usertopics', 2); putCheckBoxItem('用户回复', 'infiniteScroll$userposts', 2); putCheckBoxItem('主题帖内容', 'infiniteScroll$thread_posts', 2); putCheckBoxItem('搜索结果', 'infiniteScroll$search', 2); putCheckBoxItem('收件箱', 'infiniteScroll$msg_inbox', 2); putCheckBoxItem('消息跟踪', 'infiniteScroll$msg_sent', 2); putCheckBoxItem('通信记录', 'infiniteScroll$msg_chatlog', 2); putCheckBoxItem('加载新页后取代地址 (即手动刷新时会停留在新的页数)', 'infiniteScrollReplaceURL', 2).classList.add('rinsp-config-item-sep'); putCheckBoxItem('加载新页后滚动到新页顶部', 'infiniteScrollScrollToNewPage', 2); addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '帖内标题'; putCheckBoxItem('不灰暗化 (改为黑色)', 'siteThemeDarkerSubjectLine'); putCheckBoxItem('隐藏多余的Re标题', 'hideRedundantReSubjectLine'); putCheckBoxItem('增加可点击跳转回复原楼链接', 'addQuickJumpReSubjectLine'); putCheckBoxItem('在列表上显示及加亮自己的主题及最后回复', 'highlightMyself').classList.add('rinsp-config-item-sep'); putCheckBoxItem('列表内链接默认在新窗口打开', 'threadListDefaultOpenNewPage'); putCheckBoxItem('发新帖吋自动选择 新回复站内通知', 'autoCheckReplyOption'); putCheckBoxItem('色标出售框 (以及5sp以上警告)', 'enhanceSellFrame'); putCheckBoxItem('购买免刷新', 'buyRefreshFree'); putCheckBoxItem('回复免刷新', 'replyRefreshFree'); putCheckBoxItem('显示网盘筛选栏', 'showShareTypeFilter'); putCheckBoxItem('关注列表为空时不在主菜单上显示', 'hideWatchButtonIfEmpty'); putCheckBoxItem('显示私信长度限制', 'showInputLimit'); putCheckBoxItem('子分区承继当前图墙模式', 'subCategoryInheritImageWallMode'); putCheckBoxItem('默认折叠版块公告', 'siteNoticeSectionDefaultFolded'); putCheckBoxItem('显示默认图墙模式按钮', 'showDefaultingPicWallOption'); putCheckBoxItem('隐藏 [-- 查看移动版 --]', 'hideMobileVerSwitch'); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1').textContent = '隐藏用户头像 (鼠标放在头像上才显示)'; putCheckBoxItem('隐藏系统默认头像', 'hideDefaultUserpic'); putCheckBoxItem('隐藏其他头像', 'hideOtherUserpic'); putCheckBoxItem('收藏夹内用户除外', 'customUserBypassHideUserpic'); putCheckBoxItem('自己头像除外', 'selfBypassHideUserpic'); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1').textContent = '其他用户头像及资料显示选项'; putCheckBoxItem('替代弹出信息窗口内的快捷功能', 'useCustomUserInfoPopup'); putCheckBoxItem('当前楼层用户头像常时可见', 'stickyUserInfo'); putCheckBoxItem('显示更多用户信息 (头像下)', 'showExtendedUserInfo'); putCheckBoxItem('显示及高亮负HP', 'showExtendedUserInfo$HP', 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '显示焦点楼层快捷跳转栏'; putCheckBoxItem('出售框', 'showResourceSpots$sells', 2); putCheckBoxItem('附件', 'showResourceSpots$attachments', 2); putCheckBoxItem('分享连接', 'showResourceSpots$shares', 2); putCheckBoxItem('图片', 'showResourceSpots$images', 2); putCheckBoxItem('其他连接', 'showResourceSpots$links', 2); putCheckBoxItem('喜欢关键词', 'showResourceSpots$likes', 2); putCheckBoxItem('浮动焦点列表 (页面左边)', 'showResourceSpotsFloating', 2).classList.add('rinsp-config-item-sep'); putCheckBoxItem('减低高度占用量模式', 'heightReductionMode').classList.add('rinsp-config-item-sep'); putCheckBoxItem('减低更多', 'heightReductionMode$LV2', 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '调整论坛默认楼层折叠功能 (恢复论坛旧版模式)'; putCheckBoxItem('完全不折叠', 'disablePostCollapse', 1, true); putCheckBoxItem('不折叠自己的楼层', 'disablePostCollapse$Self', 2); addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '屏蔽内容/折叠 例外'; putCheckBoxItem('不折叠及屏蔽收藏夹内用户', 'customUserBypassIgnoreList', 2); putCheckBoxItem('不折叠及屏蔽自订头像用户', 'customUserpicBypassIgnoreList', 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1').textContent = '询问&求物区设置'; putCheckBoxItem('加亮已有答案帖子', 'requestThreadHighlightEnded'); putCheckBoxItem('显示额外悬赏', 'requestThreadShowExtraBounty'); putCheckBoxItem('使用浏览记录显示更多资讯', 'requestThreadUseHistoryData'); putCheckBoxItem('隐藏结算帖', 'hideSettlementPost'); putCheckBoxItem('只灰暗化 不完全隐藏', 'hideSettlementPost$GreyoutOnly', 2); // putCheckBoxItem('使用默认规则', 'hideSettlementPost$UseDefaultKeywords', 2); putCheckBoxItem('针对自己的结算帖除外', 'hideSettlementPost$HighlightMyself', 2); putCheckBoxItem('显示状态筛选操作 (在列表右边)', 'showRequestThreadFilters'); putCheckBoxItem('求物区内不屏蔽用户回复', 'dontFilterRequestReplyByUser'); /* const ignoreSettlementItem = addElem(configList, 'dt', 'rinsp-config-item-lv2 rinsp-config-item-checked'); const ignoreSettlementButton = addElem(ignoreSettlementItem, 'a'); ignoreSettlementButton.textContent = '✏️编辑关键词列表'; ignoreSettlementButton.addEventListener('click', function() { openIgnoreSettlementEditor(); }); */ addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '改善帖列表上by乱码'; putCheckBoxItem('改显示 by楼主 **如有记录时', 'replyShownAsByOp', 2); putCheckBoxItem('新帖无回复时不显示 by〷', 'hideZeroReply', 2); addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '🗞️记录已读帖子 - ⚠️占用资源较高'; addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2').textContent = '💡 可搜索,主题及回复列表上也会显示记录中的完整标题'; putCheckBoxItem('启用记录功能', 'keepVisitPostHistory', 2); putCheckBoxItem('主题及回复列表上显示原始标题', 'showInitialRememberedTitle', 2); const historySummaryItem = addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2'); const historyActionItem = addElem(configList, 'dt', 'rinsp-config-item-lv2'); const clearHistoryMenuButton = addElem(historyActionItem, 'a', 'rinsp-config-item-lv3'); clearHistoryMenuButton.textContent = '🗑️ 打开清空菜单'; clearHistoryMenuButton.addEventListener('click', async () => { openClearHistoryMenu(clearHistoryMenuButton, updateHistoryCount); }); async function updateHistoryCount() { historySummaryItem.textContent = '💡 统计中...'; const size = await threadHistoryAccess.historyStore.size(); if (size > 0) { historySummaryItem.textContent = `💡 共大约 ${size} 条浏览记录`; } else { historySummaryItem.textContent = '💡 没有浏览记录'; } } updateHistoryCount(); const ignoreControlItem = addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked'); const ignoreControlButton = addElem(ignoreControlItem, 'a'); ignoreControlButton.textContent = '✏️ 编辑: 🚫屏蔽内容列表'; ignoreControlButton.addEventListener('click', () => { openIgnoreListEditor(); }); addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2').textContent = '💡 南+内置的 (屏蔽此人) 会合并处理'; addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv2').textContent = '💡 自己的楼层不屏蔽'; putCheckBoxItem('表情符号不予区分 (不同的表情都会一起屏蔽)', 'treatAllEmojiTheSameWay', 2); putCheckBoxItem('当有文字内容时不管其他表情 (例: 插眼)', 'ignoreContentUseTextOnly', 2); putCheckBoxItem('完全隐藏屏蔽内容的楼层', 'hideIgnoreContentPost', 2); addElem(configList, 'dt', 'rinsp-config-item-tip rinsp-config-item-lv3').textContent = '💡 不推荐 求帖有机会要选屏蔽内容为最佳以结帖'; putCheckBoxItem('关注时屏蔽/折叠内容不予通知', 'watchSkipIgnoreContent', 2); putCheckBoxItem('收藏夹内用户的回复除外', 'customUserBypassWatchSkip', 3); const ignoreThreadItem = addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked'); const ignoreThreadButton = addElem(ignoreThreadItem, 'a'); ignoreThreadButton.textContent = '✏️ 编辑: 🚫屏蔽标题关键词列表'; ignoreThreadButton.addEventListener('click', () => { openIgnoreThreadEditor(); }); putCheckBoxItem('完全隐藏被屏蔽的帖子', 'hideFilteredThread', 2); putCheckBoxItem('收藏夹内用户的帖子除外', 'customUserBypassThreadFilter', 2); const likeThreadItem = addElem(configList, 'dt', 'rinsp-config-item-lv1 rinsp-config-item-checked'); const likeThreadButton = addElem(likeThreadItem, 'a'); likeThreadButton.textContent = '✏️ 编辑: 💚喜欢帖子标题关键词列表 (屏蔽占优先权)'; likeThreadButton.addEventListener('click', function() { openLikeThreadEditor(); }); putCheckBoxItem('浮动喜欢帖子列表', 'showFavThreadFloatingList', 2); putCheckBoxItem('显示标题前段文字', 'showFavThreadFloatingList$withTitle', 3); if (hasManagementRole) { addElem(configList, 'dt', 'rinsp-config-item-sep rinsp-config-item-lv1 rinsp-config-item-checked').textContent = '💯版主评分设置'; putCheckBoxItem('免空区评分默认不发送通知', 'adminNoScoreNotifByDefault', 2); putCheckBoxItem('不显示 "格式不正" 快捷评分 (固定有通知)', 'adminHideMarkUnscoreButton', 2); putCheckBoxItem('不显示 "标记不评分" 快捷评分 (固定不通知)', 'adminHideMarkBadFormatButton', 2); } renderUserBookmarks(); renderPinnedUsers(); let userFilterConfig = await userFilterConfigAccess.read(); renderUserBlacklist('屏蔽用户列表 - 屏蔽所有内容', entry => entry[1].hideReplies); renderUserBlacklist('屏蔽用户列表 - 只屏蔽主题帖', entry => entry[1].hideThreads && !entry[1].hideReplies); renderConfigManagement(); function renderUserBookmarks() { const nickNameListHeading = addElem(configPanel, 'h5', 'u-h5'); addElem(nickNameListHeading, 'span').textContent = '用户收藏夹'; addElem(nickNameListHeading, 'span', null, { style: 'font-weight:normal'}).textContent = ' (亦记忆其昵称) 点击🔖打开菜单'; const nickNameListHeadingControls = addElem(nickNameListHeading, 'span', 'r gray3'); nickNameListHeadingControls.appendChild(document.createTextNode('顺序 ')); const sortOrderSelect = addElem(nickNameListHeadingControls, 'select'); addElem(sortOrderSelect, 'option', null, { value: 'uid' }).textContent = 'UID'; addElem(sortOrderSelect, 'option', null, { value: 'nickname' }).textContent = '昵称'; sortOrderSelect.value = userConfig.customUserOrderBy; sortOrderSelect.value = sortOrderSelect.value || 'uid'; sortOrderSelect.addEventListener('change', () => { configPanel.classList.add('rinsp-config-saving'); mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.customUserOrderBy = sortOrderSelect.value; return updatingUserConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); window.location.reload(); }); }); const nickNameMappingBlock = addElem(configPanel, 'dt', 'rinsp-nickname-list'); let userItemComparator; if (userConfig.customUserOrderBy === 'nickname') { userItemComparator = comparator(2); } else { userItemComparator = comparator(item => item[0] * 1); } Object.entries(userConfig.customUserHashIdMappings) .filter(entry => entry[0][0]==='@') .map(entry => entry[1]) .sort(userItemComparator) .forEach(function(entry) { const userId = entry[0]; const userHashId = entry[1]; const userNickName = entry[2]; const userTag = addElem(nickNameMappingBlock, 'span', 'rinsp-user-tag', { title: `${userNickName}\n${userHashId}` }); nickNameMappingBlock.appendChild(document.createTextNode(' ')); const userTagIcon = addElem(userTag, 'span', 'rinsp-user-tag-icon'); const userLink = addElem(userTag, 'a', null, { href: 'u.php?action-show-uid-' + userId + '.html', target: '_blank' }); userLink.textContent = userNickName; userTagIcon.addEventListener('click', function() { const deleteAction = () => { if (confirm('取消收藏(' + userNickName + ')?')) { configPanel.classList.add('rinsp-config-saving'); changeCustomUserMapping(userId, userHashId, '', true) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); userTag.remove(); }); } }; openBookmarkUserMenu(userTag, userId, userHashId, userNickName, deleteAction); }); }); } async function renderPinnedUsers() { const pinListHeading = addElem(configPanel, 'h5', 'u-h5'); addElem(pinListHeading, 'span').textContent = '焦点人物'; configPanel.appendChild(putCheckBoxItem('焦点人物出现时才显示', 'hideInactivePinnedUsers')); const showActiveRepliersControl = putCheckBoxItem('自动显示当前页活跃回复者', 'showActiveRepliers'); const showActiveRepliersMinControl = addElem(showActiveRepliersControl, 'a', null, { href: 'javascript:' }); showActiveRepliersMinControl.addEventListener('click', async () => { let minCount = prompt('最低回复次数', '') * 1; if (minCount > 1) { configPanel.classList.add('rinsp-config-saving'); mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.showActiveRepliers$min = minCount; userConfig = updatingUserConfig; return updatingUserConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); updateMinReplyDisplay(); }); } }); function updateMinReplyDisplay() { const minReplyCount = userConfig.showActiveRepliers$min > 1 ? userConfig.showActiveRepliers$min : 2; showActiveRepliersMinControl.textContent = ` [最低回复次数: ${minReplyCount}]`; } updateMinReplyDisplay(); configPanel.appendChild(showActiveRepliersControl); const recordListBlock = addElem(configPanel, 'dt', 'rinsp-pinned-user-list'); const pinnedUsersConfig = await pinnedUsersConfigAccess.read(); Object.values(pinnedUsersConfig.users).forEach(record => { const item = addElem(recordListBlock, 'div', 'rinsp-pinuser-item'); const face = addElem(item, 'div', 'rinsp-pinuser-item-face'); addElem(face, 'img', null, { src: record.img }); const label = addElem(face, 'div'); addElem(label, 'a', null, { href: `u.php?action-show-uid-${record.uid}.html`, target: '_blank' }).textContent = record.nickName||`#${record.uid}`; const unpin = addElem(label, 'div', 'rinsp-pinuser-unpin-icon'); unpin.addEventListener('click', async () => { await pinnedUsersConfigAccess.update(function(updatingPinnedUsersConfig) { delete updatingPinnedUsersConfig.users['#' + record.uid]; // clean up corrupt data ... delete updatingPinnedUsersConfig.users['#NaN']; delete updatingPinnedUsersConfig.users['#null']; return updatingPinnedUsersConfig; }); item.remove(); }); }); } function renderUserBlacklist(heading, filter) { const blockedUserListHeading = addElem(configPanel, 'h5', 'u-h5'); addElem(blockedUserListHeading, 'span').textContent = heading; addElem(blockedUserListHeading, 'span', null, { style: 'font-weight:normal'}).textContent = ' 点击🚫移除'; let userItemComparator; if (userConfig.customUserOrderBy === 'nickname') { userItemComparator = comparator(item => item[1].name); } else { userItemComparator = comparator(item => item[0].substring(1) * 1); } const blockedUserListBlock = addElem(configPanel, 'dt', 'rinsp-user-blacklist'); Object.entries(userFilterConfig.users) .filter(entry => entry[0][0]==='#') .sort(userItemComparator) .filter(filter) .forEach(function(entry) { const userId = entry[0].substring(1) * 1; const userNickName = entry[1].name; const hideThreads = entry[1].hideThreads; const hideReplies = entry[1].hideReplies; if (!hideThreads && !hideReplies) { return; } const userTag = addElem(blockedUserListBlock, 'span', 'rinsp-user-tag', { title: userNickName + '\n' + (hideReplies ? '屏蔽所有内容' : '只屏蔽主题帖') }); blockedUserListBlock.appendChild(document.createTextNode(' ')); const userTagDel = addElem(userTag, 'span', 'rinsp-user-tag-icon'); const userLink = addElem(userTag, 'a', null, { href: 'u.php?action-show-uid-' + userId + '.html', target: '_blank' }); userLink.textContent = userNickName; userTagDel.addEventListener('click', function() { if (confirm('取消屏蔽(' + userNickName + ')?')) { configPanel.classList.add('rinsp-config-saving'); userFilterConfigAccess .update(function(userFilterConfig) { delete userFilterConfig.users[entry[0]]; return userFilterConfig; }) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); userTag.remove(); }); } }); }); } function putCheckBoxItem(label, propertyNameOrAccessor, level, inverted) { let propertyAccess; if (typeof propertyNameOrAccessor === 'string') { propertyAccess = { get() { return !!userConfig[propertyNameOrAccessor]; }, update(newValue) { return mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig[propertyNameOrAccessor] = newValue; return updatingUserConfig; }); } }; } else { propertyAccess = propertyNameOrAccessor; } const configItem = addElem(configList, 'dt', `rinsp-config-item-lv${level||1}`); const checkboxLabel = addElem(configItem, 'label'); const checkbox = addElem(checkboxLabel, 'input', null, { type: 'checkbox' }); checkboxLabel.appendChild(document.createTextNode(' ' + label)); checkbox.checked = propertyAccess.get(); updateStyle(); function updateStyle() { const checkedValue = !inverted; // when inverted false is considered checked if (checkbox.checked === checkedValue) { configItem.classList.add('rinsp-config-item-checked'); } else { configItem.classList.remove('rinsp-config-item-checked'); } } checkbox.addEventListener('change', async function() { configPanel.classList.add('rinsp-config-saving'); const newValue = checkbox.checked; propertyAccess.update(newValue) .finally(function() { configPanel.classList.remove('rinsp-config-saving'); updateStyle(); }); }); return configItem; } function renderConfigManagement() { const nickNameListHeading = addElem(configPanel, 'h5', 'u-h5'); addElem(nickNameListHeading, 'span').textContent = '管理设置 (仅当前用户)'; addElem(nickNameListHeading, 'span', null, { style: 'font-weight:normal'}).textContent = ' ⚠️进阶操作'; const panel = addElem(configPanel, 'div', 'rinsp-config-manage-panel'); const exportButton = addElem(panel, 'button', null, { type: 'button' }); exportButton.textContent = '导出设置'; exportButton.addEventListener('click', () => downloadUserSettings()); addElem(panel, 'span').textContent = ' / '; const importButton = addElem(panel, 'button', null, { type: 'button' }); importButton.textContent = '导入设置'; addElem(panel, 'span').textContent = ' '; const uploadInput = addElem(panel, 'input', null, { type: 'file', accept: '.rpcfg', style: 'max-width: 18em' }); importButton.addEventListener('click', () => importSettings(uploadInput)); } } function rewriteUserRecentPostLinks(userConfig, userPinArea) { document.querySelectorAll('#minifeed .feed-list dt > a[href^="//"]').forEach(el => { const href = el.getAttribute('href'); const tid = (href.match(/\/read.php\?tid-(\d+)\.html$/)||[])[1]; if (tid) { el.setAttribute('href', `/read.php\?tid-${tid}.html`); } }); } async function importSettings(input) { if (!input.value) { showMessagePopup('💡请选择文件', input, 1500); return; } async function readData() { const dataStream = new Blob([input.files[0]]).stream().pipeThrough(new DecompressionStream('gzip')); const rawData = await (await new Response(dataStream).blob()).text(); return JSON.parse(rawData); } async function init() { const data = await readData(); const userDataMap = new Map(); Object.keys(data).forEach(key => { if (key[0] === '#') { const id = key.substring(1) * 1; if (id > 0) { userDataMap.set(id, data[key]); } } }); if (userDataMap.size !== 1) { showMessagePopup('⚠️数据错误', null, 3000); return; } const importedUserId = Array.from(userDataMap.keys())[0]; if (importedUserId !== myUserId) { if (!confirm('⚠️导入数据与用户ID不同,继续操作?')) { return; } } setupPopupMenu({ title: '已读取数据,请选择操作', width: 260, popupMenuId: 'CONFIG_IMPORT_ACTION_MENU', anchor: input, verticallyInverted: true, items: [ { label: '❗操作前请先关闭其他页面❗', cellClass: 'rinsp-alert-menu-message' }, { label: '⚠️注意,请先备份', cellClass: 'rinsp-alert-menu-message' }, { label: '合并设置', class: 'rinsp-alert-menu-button', action: () => { if (confirm('现有设置及资料会被改写!!确认执行?')) { const action = async cnt => { const mergedUserData = await mergeUserData(myUserId, userDataMap.get(importedUserId)); if (!confirm('合并准备完成,最终确认置换!?')) { return; } await importUserSettings(myUserId, mergedUserData); return '✔️导入完成,自动刷新窗口'; }; runWithProgressPopup(action, '导入中...', null, 5000) .then(() => document.location.reload()) .catch(ex => { console.error(ex); alert('' + ex); }); } } }, { label: '完全覆盖置换', class: 'rinsp-alert-menu-button', action: () => { if (confirm('现有设置及资料会被覆盖!!确认执行?')) { const action = async cnt => { await importUserSettings(myUserId, userDataMap.get(importedUserId)); return '✔️导入完成,自动刷新窗口'; }; runWithProgressPopup(action, '导入中...', null, 5000) .then(() => document.location.reload()) .catch(ex => { console.error(ex); alert('' + ex); }); } } }, { label: '取消操作', action: () => input.value = null } ] }); } init().catch(ex => showMessagePopup('⚠️' + ex, input, 3000)); } function mergeArrayData(newData, currentData, keyFunc) { const set = new Set(); newData.forEach(item => { set.add(keyFunc(item)); }); return currentData.filter(item => !set.has(keyFunc(item))).concat(newData); } async function importUserSettings(userId, userData) { const userKey = '#' + userId; const allKeys = await GM.listValues(); const myKeys = allKeys.filter(key => key.split(':', 1)[0].endsWith(userKey)); for (let key of myKeys) { await GM.deleteValue(key).catch(ex => null); } const entries = Object.entries(userData); for (let entry of entries) { let dbKey; if (entry[0].indexOf(':') === -1) { dbKey = entry[0] + userKey; } else { dbKey = entry[0].replace(':', userKey + ':'); } await GM.setValue(dbKey, entry[1]); } } async function mergeUserData(userId, userData) { let currentUserData = await exportUserSettings(userId); function merger(configKey) { const newData = JSON.parse(userData[configKey]||'{}'); const baseData = JSON.parse(currentUserData[configKey]||'{}'); return { mergeArrayEntry(key, itemKeyFunc) { newData[key] = mergeArrayData(newData[key]||[], baseData[key]||[], itemKeyFunc||(x=>x)); return this; }, mergeKeyValueEntry(key) { newData[key] = Object.assign({}, baseData[key]||{}, newData[key]||{}); return this; }, getBaseData() { return baseData; }, getData() { return newData; }, replacing(targetData) { targetData[configKey] = JSON.stringify(newData); } }; } const newUserData = Object.assign({}, userData); merger(MAIN_CONFIG_KEY) .mergeKeyValueEntry('customUserHashIdMappings') .replacing(newUserData); merger(CONTENT_IGNORE_LIST_CONFIG_KEY) .mergeArrayEntry('terms') .mergeArrayEntry('exceptions') .replacing(newUserData); merger(SEARCH_CONFIG_KEY) .mergeKeyValueEntry('pinnedTopics') .replacing(newUserData); merger(THREAD_CUSTOM_CATEGORY_CONFIG_KEY) .mergeArrayEntry('keywords') .replacing(newUserData); merger(THREAD_FILTER_CONFIG_KEY) .mergeArrayEntry('dislikes') .mergeArrayEntry('likes') .mergeArrayEntry('settlementKeywords') .replacing(newUserData); merger(USER_FILTER_CONFIG_KEY) .mergeKeyValueEntry('users') .replacing(newUserData); merger(PINNED_USERS_CONFIG_KEY) .mergeKeyValueEntry('users') .replacing(newUserData); merger(SHARETYPE_FILTER_CONFIG_KEY) .mergeArrayEntry('hides') .replacing(newUserData); const my_watchlist_merger = merger('my_watchlist'); newUserData['my_watchlist'] = JSON.stringify(Object.assign({}, my_watchlist_merger.getBaseData(), my_watchlist_merger.getData())); const tid_visit_history$map = new Map(); Object.keys(userData).filter(key => key.startsWith('tid_visit_history:#')).forEach(key => { const itemMap = new Map(); userData[key].split(' ').map(item => item.split(':')).forEach(pair => { const id = pair[0] * 1; if (id > 0) { const count = (pair[1] * 1)||0; itemMap.set(id, count); } }); tid_visit_history$map.set(key, itemMap); }); Object.keys(currentUserData).filter(key => key.startsWith('tid_visit_history:#')).forEach(key => { let itemMap; if (tid_visit_history$map.has(key)) { itemMap = tid_visit_history$map.get(key); } else { itemMap = new Map(); tid_visit_history$map.set(key, itemMap); } currentUserData[key].split(' ').map(item => item.split(':')).forEach(pair => { const id = pair[0] * 1; if (id > 0) { const count = (pair[1] * 1)||0; itemMap.set(id, Math.max(count, itemMap.get(id)||0)); } }); }); const tid_visit_history$index = Array.from(tid_visit_history$map.entries()).map(entry => entry[0].split('#', 2)[1] + ':' + entry[1].size).join(' '); Object.keys(newUserData).filter(key => key.startsWith('tid_visit_history:#')).forEach(key => delete newUserData[key]); tid_visit_history$map.forEach((itemMap, key) => { newUserData[key] = Array.from(itemMap.entries()).map(entry => entry[1] > 0 ? `${entry[0]}:${entry[1]}` : `${entry[0]}`).join(' '); }); newUserData['tid_visit_history:index'] = tid_visit_history$index; const tid_recent$map = new Map(); Object.keys(userData).filter(key => key.startsWith('tid_recent:#')).forEach(key => { const record = JSON.parse(userData[key]); tid_recent$map.set(record.tid, record); }); Object.keys(currentUserData).filter(key => key.startsWith('tid_recent:#')).forEach(key => { const record = JSON.parse(currentUserData[key]); const dup = tid_recent$map.get(record.tid); if (dup == null || record.time > dup.time) { tid_recent$map.set(record.tid, record); } }); const sortByVisitTime = comparator('time', true); const tid_recent$list = Array.from(tid_recent$map.values()).sort(sortByVisitTime).slice(0, MAX_RECENT_ACCESS_LOG); let tid_recent$index = ''; if (tid_recent$list.length > 0) { let minTime = (tid_recent$list[0]||{}).time||0; tid_recent$list.forEach(record => { if (minTime > record.time) minTime = record.time; }); tid_recent$index = tid_recent$list.map(record => record.tid + ':' + (record.time - minTime)).join(' ') + ' 0:' + minTime; } tid_recent$list.forEach(record => { newUserData['tid_recent:#' + record.tid] = JSON.stringify(record); }); newUserData['tid_recent:recent'] = tid_recent$index; Object.keys(currentUserData) .filter(key => !key.startsWith('tid_visit_history:#') && !key.startsWith('tid_recent:#') && !Object_hasOwn(newUserData, key)) .forEach(key => { newUserData[key] = currentUserData[key]; }); const user_punish$map = new Map(); Object.keys(userData).filter(key => key.startsWith('user_punish:#')).forEach(key => { const record = JSON.parse(userData[key]); record.uid = key.substring(13) * 1; user_punish$map.set(key, record); }); Object.keys(currentUserData).filter(key => key.startsWith('user_punish:#')).forEach(key => { const current = JSON.parse(currentUserData[key]); current.uid = key.substring(13) * 1; const dup = user_punish$map.get(key); if (dup == null) { user_punish$map.set(key, current); } else { dup.logs = mergeArrayData(current.logs, dup.logs, x=>x.time); } }); user_punish$map.forEach((record, key) => { newUserData[key] = JSON.stringify(record); }); return newUserData; } async function exportUserSettings(userId) { const userKey = '#' + userId; const allKeys = await GM.listValues(); const myKeys = allKeys.filter(key => key.split(':', 1)[0].endsWith(userKey)); const myData = {}; for (let key of myKeys) { const value = await GM.getValue(key); const parts = key.split(':'); parts[0] = parts[0].substring(0, parts[0].length - userKey.length); const localKey = parts.join(':'); myData[localKey] = value; } return myData; } async function downloadUserSettings() { const action = async () => { const userData = await exportUserSettings(myUserId); const data = {}; data['#' + myUserId] = userData; const utf8Bytes = new TextEncoder().encode(JSON.stringify(data)); const chunks = []; const reader = new Blob([utf8Bytes]).stream().pipeThrough(new CompressionStream('gzip')).getReader(); while (true) { const status = await reader.read(); if (status.done) break; chunks.push(status.value); } const blob = new Blob(chunks, { type: 'application/octet-stream' }); //const blob = new Blob([base64data], { type: 'text/plain;charset=utf-8' }); const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(blob); downloadLink.download = `rinplus_${myUserId}_${new Date().toISOString().replace(/:/g, '').replace(/\.\d{3}Z$/, '')}.rpcfg`; downloadLink.click(); URL.revokeObjectURL(downloadLink.href); return '✔️已触发下载'; }; await runWithProgressPopup(action, '导出中...', null, 1500) .catch((ex) => { console.error(ex); alert(String(ex)); }); } async function openIgnoreListEditor(callback) { openTextListEditor(IGNORE_LIST_POPUP_MENU_ID, { title: '🚫屏蔽内容列表', async read() { const ignoreList = await contentIgnoreListConfigAccess.read(); return ignoreList.terms.sort().join('\n'); }, async save(textData) { return await contentIgnoreListConfigAccess.update(function(ignoreList) { const newTerms = textData.split('\n').map(itm=>itm.trim()).filter(itm=>!!itm); ignoreList.terms = newTerms; return ignoreList; }); } }, callback); } async function openIgnoreThreadEditor(callback) { openTextListEditor(THREAD_FILTER_POPUP_MENU_ID, { title: '🚫屏蔽标题关键词列表', async read() { const filterConfig = await threadFilterConfigAccess.read(); return filterConfig.dislikes.sort().join('\n'); }, async save(textData) { return await threadFilterConfigAccess.update(function(filterConfig) { const newKeywords = textData.toLowerCase().split('\n').map(itm=>itm.replace(/\s+/g, ' ').trim()).filter(itm=>!!itm); filterConfig.dislikes = newKeywords; return filterConfig; }); } }, callback); } async function openLikeThreadEditor(callback) { openTextListEditor(THREAD_LIKE_POPUP_MENU_ID, { title: '💚喜欢帖子标题关键词列表', description: `每行一个关键字 正则表达式高级设置 (懂的都懂 不懂可无视) 格式: regex:/正则表达式语法/ 例子: regex:/[东南西北]\+/`, async read() { const filterConfig = await threadFilterConfigAccess.read(); return filterConfig.likes.sort().join('\n'); }, async save(textData) { return await threadFilterConfigAccess.update(function(filterConfig) { const newKeywords = textData.toLowerCase().split('\n').map(itm=>itm.replace(/\s+/g, ' ').trim()).filter(itm=>!!itm); filterConfig.likes = newKeywords; return filterConfig; }); } }, callback); } function addPrivateMessageNotifier() { const pmElem = document.querySelector('#td_msg'); const observer = new MutationObserver(function() { updatePrivateMessageStatus(); }); updatePrivateMessageStatus(); observer.observe(pmElem, { childList: true, subtree: true, attributes: true }); function updatePrivateMessageStatus() { const hasMessage = pmElem.querySelector('img[src="images/colorImagination/shortmail.gif"]') != null; pmNotificationElem.textContent = ''; if (hasMessage) { pmNotificationElem.classList.add('rinsp-status-new'); addElem(pmNotificationElem, 'img', null, { src: 'images/colorImagination/shortmail.gif', align: 'absmiddle' }); pmNotificationElem.appendChild(document.createTextNode('\n未\n读\n私\n信')); } else { pmNotificationElem.classList.remove('rinsp-status-new'); } } } function addTitleLengthLimitIndicator() { const titleField = document.querySelector('form[action="post.php?"] input[name="atc_title"]'); if (titleField) { addLengthLimit(titleField, 300, 100, true, true); } } function addWatchMenuButton() { const userLogin = document.querySelector('#user-login #td_msg'); if (userLogin != null) { const watchMenuElem = newElem('a', 'rinsp-watch-menuitem', { href: 'javascript:void(0)' }); watchMenuElem.textContent = '关注'; userLogin.after(watchMenuElem); return watchMenuElem; } return null; } function initTopMenu() { const watchMenuElem = addWatchMenuButton(); if (watchMenuElem == null) { return; } watchNotificationElem.addEventListener('click', () => { document.body.scrollIntoView(); watchMenuElem.click(); }); watchMenuElem.addEventListener('click', function() { showWatchMenu(); return false; }); async function tryCheck(override) { if (DEBUG_MODE) console.info('tryCheck (override=' + (override||null) + ')'); let pending = []; let skipUntil = Date.now() + TEMP_INCREMENT; await updateWatchList(function(watchList) { const now = Date.now(); const watchItems = Object.values(watchList).sort(comparator('lastChecked')); if (watchItems.length === 0) { return null; } let checkableOldestItem = null; for (let watchItem of watchItems) { if (isWatchExpired(watchItem)) { continue; } if (typeof override === 'number') { if (watchItem.id !== override) { continue; } } else if (override !== true) { let outdatedBy = now - watchItem.lastChecked; if (watchItem.skipUntil > now) { if (DEBUG_MODE) console.info('skip-for-now', watchItem.id); continue; } if (outdatedBy < getScaledValue(watchItem, num(MIN_CHECK_INTERVAL), num(MAX_CHECK_INTERVAL)) * 60000) { if (checkableOldestItem == null) { if (outdatedBy >= getScaledValue(watchItem, num(MIN_CHECK_INTERVAL_OLDEST_ITEM), num(MAX_CHECK_INTERVAL_OLDEST_ITEM)) * 60000) { checkableOldestItem = watchItem; } } continue; } } pending.push(watchItem.id); watchItem.skipUntil = skipUntil; } if (pending.length === 0 && checkableOldestItem != null) { if (DEBUG_MODE) console.info('tryCheck - picked one oldest item: ' + checkableOldestItem.id); pending.push(checkableOldestItem.id); checkableOldestItem.skipUntil = skipUntil; } if (pending.length > 0) { return watchList; } else { return null; } }); if (pending.length > 0) { if (DEBUG_MODE) console.info('tryCheck - ' + pending.length + ' items'); watchMenuElem.classList.add('rinsp-checking'); await runCheck(pending, skipUntil) .finally(function() { watchMenuElem.classList.remove('rinsp-checking'); }); } else { if (DEBUG_MODE) console.info('tryCheck - nothing to do'); } await refreshWatchStatus(); } async function runCheck(postIds, mySkipUntil) { let first = true; const ignoreList = await contentIgnoreListConfigAccess.read(); async function checkWatchPost(watchList, watchPostId) { const watchItem = watchList['#' + watchPostId]; if (watchItem == null) { return null; } const now = Date.now(); if (isWatchExpired(watchItem)) { return null; } if (watchItem.skipUntil !== mySkipUntil && watchItem.skipUntil > now) { return null; } watchItem.lastChecked = now; // enforce request rate control while (true) { const rateInterval = Math.floor(now / MAX_ACC_REQUEST_RATE_BASE); const lastRateRecord = (localStorage.getItem(REQUEST_RATE_RECORD_KEY)||'').split(':').map(item=>item*1); if (rateInterval === lastRateRecord[0]) { const accessCount = (lastRateRecord[1]*1)||0; if (accessCount >= MAX_ACC_REQUEST_RATE_COUNT) { if (DEBUG_MODE) console.info('HITTING RATE LIMIT - WAITING'); await sleep(MIN_REQUEST_DELAY); continue; } localStorage.setItem(REQUEST_RATE_RECORD_KEY, `${rateInterval}:${accessCount + 1}`); } else { localStorage.setItem(REQUEST_RATE_RECORD_KEY, `${rateInterval}:1`); } break; } const lastPost = await fetchLastPost(watchPostId, ignoreList); if (lastPost.error) { if (lastPost.errorMessage.indexOf('刷新不要') != -1) { watchItem.skipUntil = Date.now() + MIN_REQUEST_DELAY; return watchList; } else { watchItem.skipUntil = Number.MAX_SAFE_INTEGER; watchItem.lastError = lastPost.errorMessage; return watchList; } } let bountyEnded = null; if (watchItem.bountyUntil > 0) { if (lastPost.page > 1) { await sleep(1500); bountyEnded = !!(await fetchCheckBountyEnded(watchPostId)); } else { bountyEnded = lastPost.bountyEnded; } } if (lastPost.postUid === myUserId && lastPost.page == lastPost.maxPage && (lastPost.floor - watchItem.maxFloor) <= 1) { // note: only can trust my own last post if the floor differs by one only watchItem.lastVisitedPage = lastPost.page; watchItem.lastVisitedFloor = lastPost.floor; watchItem.lastVisitedPostId = lastPost.postId; } let lastUpdateIgnorable = null; if (userConfig.watchSkipIgnoreContent) { const lastSeenPost = lastPost.postsByFloor[watchItem.lastVisitedFloor]; if (lastSeenPost != null && lastSeenPost.postId === watchItem.lastVisitedPostId) { // last seen post is still valid and at the same floor if (DEBUG_MODE) console.info('check floors', watchItem.lastVisitedFloor + 1, lastPost.floor); for (let floor = watchItem.lastVisitedFloor + 1; floor <= lastPost.floor; floor++) { if (lastUpdateIgnorable == null) { lastUpdateIgnorable = true; } const newPost = lastPost.postsByFloor[floor]; if (!newPost.ignorable) { lastUpdateIgnorable = false; break; } if (userConfig.customUserBypassWatchSkip) { if (userConfig.customUserHashIdMappings['#' + newPost.postUid] != null) { lastUpdateIgnorable = false; break; } } } } } if (DEBUG_MODE) console.info('ignorable', watchItem, lastUpdateIgnorable); watchItem.lastUpdateIgnorable = !!lastUpdateIgnorable; watchItem.maxPage = lastPost.maxPage; watchItem.maxFloor = lastPost.floor; watchItem.maxFloorApprox = lastPost.maxPage !== lastPost.page; watchItem.lastPostId = lastPost.postId; watchItem.lastPostUid = lastPost.postUid; watchItem.lastPostTime = lastPost.postTime; watchItem.skipUntil = 0; watchItem.lastError = null; if (bountyEnded) { watchItem.bountyUntil = -1; } return watchList; } async function checkAndUpdateWatchPost(watchPostId) { if (DEBUG_MODE) console.info('checking ... ' + watchPostId); watchMenuElem.classList.add('rinsp-checking'); await updateWatchList( async function(watchList) { const updatedWatchList = await checkWatchPost(watchList, watchPostId); if (updatedWatchList != null) { refreshWatchStatus(updatedWatchList); } return updatedWatchList; }) .catch(e => { console.warn(`error when checking post ${watchPostId}:`, e); return true; }) .finally(() => { pendingWatchItemChecks.delete(watchPostId); if (pendingWatchItemChecks.size === 0) { watchMenuElem.classList.remove('rinsp-checking'); } if (document.getElementById(WATCHER_POPUP_MENU_ID)) { showWatchMenu(); } }); if (DEBUG_MODE) console.info('done ... ' + watchPostId); } const checkablePostIds = []; postIds.forEach(postId => { if (!pendingWatchItemChecks.has(postId)) { pendingWatchItemChecks.add(postId); checkablePostIds.push(postId); } }); if (document.getElementById(WATCHER_POPUP_MENU_ID)) { showWatchMenu(); } for (let watchPostId of checkablePostIds) { if (first) { //console.info('wait'); await sleep(MIN_REQUEST_DELAY); } else { first = false; } await checkAndUpdateWatchPost(watchPostId); } } async function refreshWatchStatus(initWatchList) { const watchList = initWatchList ? initWatchList : await readWatchList(); let errorCount = 0; let updateCount = 0; let totalCount = 0; for (let watchItem of Object.values(watchList)) { const itemStatus = getWatchItemStatus(watchItem); if (itemStatus === 'new') { updateCount++; } else if (itemStatus === 'error') { errorCount++; } totalCount++; } if (totalCount > 0) { watchMenuElem.classList.add('rinsp-status-enabled'); } else { watchMenuElem.classList.remove('rinsp-status-enabled'); } watchNotificationElem.textContent = ''; if (updateCount > 0) { watchMenuElem.classList.add('rinsp-status-new'); watchMenuElem.dataset.newCount = updateCount; watchNotificationElem.classList.add('rinsp-status-new'); addElem(watchNotificationElem, 'div', 'rinsp-notification-item-count').textContent = String(updateCount); watchNotificationElem.appendChild(document.createTextNode('关\n注\n更\n新')); } else { watchMenuElem.dataset.newCount = ''; watchMenuElem.classList.remove('rinsp-status-new'); watchNotificationElem.classList.remove('rinsp-status-new'); } if (errorCount > 0) { watchMenuElem.classList.add('rinsp-status-error'); watchNotificationElem.classList.add('rinsp-status-error'); watchNotificationElem.textContent = '⚠️\n关\n注\n出\n错'; } else { watchMenuElem.classList.remove('rinsp-status-error'); watchNotificationElem.classList.remove('rinsp-status-error'); } } document.addEventListener('visibilitychange', function() { if (document.visibilityState === 'visible') { setTimeout(function() { if (document.visibilityState === 'visible') { tryCheck(); } }, 3000); } }); refreshWatchStatus().then(() => { setTimeout(function() { tryCheck(); }, 3000); }); setInterval(function() { if (document.visibilityState === 'visible') { tryCheck(); } }, num(MIN_CHECK_INTERVAL_OLDEST_ITEM) * 60000 + 1000); const currentThreadWatchControls = []; let currentThreadWatching = false; const currentThread = { register(ctrl) { currentThreadWatchControls.push(ctrl); }, isWatching() { return currentThreadWatching; }, setWatching(watching) { currentThreadWatching = watching; currentThreadWatchControls.forEach(ctrl => ctrl.setWatching(watching)); }, setBusy(busy) { currentThreadWatchControls.forEach(ctrl => ctrl.setBusy(busy)); } }; return { currentThread, tryCheck }; } function getWatchItemStatus(watchItem) { if (watchItem.lastError) { return 'error'; } if (watchItem.lastUpdateIgnorable) { return 'ignorable'; } if (watchItem.lastPostUid !== myUserId && watchItem.maxFloor != watchItem.lastVisitedFloor) { return 'new'; } } async function showWatchMenu() { closePopupMenu(WATCHER_POPUP_MENU_ID); const popupMenu = createPopupMenu(WATCHER_POPUP_MENU_ID, document.querySelector('#user-login')); const watchList = await readWatchList(); popupMenu.renderContent(async function(borElem) { const watchItems = Object.values(watchList); if (watchItems.length === 0) { borElem.setAttribute('style', 'padding:13px 30px'); borElem.textContent = '关注列表为空'; return; } watchItems.sort(function(x, y) { const xNew = x.lastVisitedFloor != x.maxFloor; const yNew = y.lastVisitedFloor != y.maxFloor; if (xNew !== yNew) { return xNew ? -1 : 1; } return x.timeAdded - x.timeAdded; }); const tableElem = addElem(borElem, 'table', null, { width: '800', cellspacing: '0', cellpadding: '0', style: 'table-layout:fixed' }); const colgroup = addElem(tableElem, 'colgroup'); addElem(colgroup, 'col', null, { style: 'width: 100px' }); addElem(colgroup, 'col'); addElem(colgroup, 'col', null, { style: 'width: 100px' }); addElem(colgroup, 'col', null, { style: 'width: 120px' }); addElem(colgroup, 'col', null, { style: 'width: 120px' }); addElem(colgroup, 'col', null, { style: 'width: 50px' }); addElem(colgroup, 'col', null, { style: 'width: 20px' }); // allow for scrollbar const tbodyElem = addElem(tableElem, 'tbody'); const trElem1 = addElem(tbodyElem, 'tr'); const thElem1_1 = addElem(trElem1, 'th', 'h', { colspan: '6' }); addElem(trElem1, 'th', 'h'); const frElem1 = addElem(thElem1_1, 'span', 'fr', { style: 'margin-top:2px;cursor:pointer' }); frElem1.addEventListener('click', function() { closePopupMenu(WATCHER_POPUP_MENU_ID); }); addElem(frElem1, 'img', null, { src: 'images/close.gif' }); thElem1_1.appendChild(document.createTextNode('关注列表')); const trElemAct = addElem(tbodyElem, 'tr', 'tr2 tac'); const actCell = addElem(trElemAct, 'td', 'rinsp-action-bar', { colspan: '6' }); addElem(trElemAct, 'td'); const checkNowButton = addElem(actCell, 'span', 'rinsp-check-now'); checkNowButton.textContent = '🔃立即检查'; checkNowButton.addEventListener('click', function() { closePopupMenu(WATCHER_POPUP_MENU_ID); watcherApi.tryCheck(true); }); let maxReachedWarnElem = null; if (watchItems.length >= num(MAX_WATCH_ITEM_COUNT)) { maxReachedWarnElem = addElem(actCell, 'span', 'rinsp-limit-warning').textContent = '关注数量已达上限'; } const trElem2 = addElem(tbodyElem, 'tr', 'tr2 tac'); addElem(trElem2, 'td').textContent = '版块名称'; addElem(trElem2, 'td').textContent = '标题'; addElem(trElem2, 'td').textContent = '悬赏剩时'; addElem(trElem2, 'td').textContent = '最近回复'; addElem(trElem2, 'td').textContent = '最近检查'; addElem(trElem2, 'td').textContent = '删'; addElem(trElem2, 'td'); const now = Date.now(); watchItems.forEach(function(watchItem) { const trElem3 = addElem(tbodyElem, 'tr', 'tr3 tac'); addElem(trElem3, 'td').textContent = watchItem.areaName; const titleCell = addElem(trElem3, 'td', 'tal'); let postUrl = `read.php?tid-${watchItem.id}.html`; if (watchItem.lastVisitedPage > 1) { postUrl = `read.php?tid-${watchItem.id}-fpage-0-toread--page-${watchItem.lastVisitedPage}.html`; } addElem(titleCell, 'a', null, { href: postUrl, target: '_blank' }).textContent = watchItem.postTitle; const bountyCell = addElem(trElem3, 'td', 'rinsp-bounty-cell'); const expired = isWatchExpired(watchItem); if (expired) { trElem3.classList.add('rinsp-expired'); bountyCell.textContent = '关注已过期'; } else if (watchItem.areaName === REQUEST_ZONE_NAME) { if (watchItem.bountyUntil === -1) { bountyCell.classList.add('rinsp-bounty-answered'); bountyCell.textContent = '已有答案'; } else { const hourLeft = ((watchItem.bountyUntil||0) - now) / 3600000; if (hourLeft <= 0) { bountyCell.classList.add('rinsp-bounty-ended'); if (watchItem.bountyUntil != null) { const ageString = getAgeString((now - watchItem.bountyUntil) / 60000); bountyCell.textContent = `已过期 (${ageString})`; } else { bountyCell.textContent = '已过期'; } } else { bountyCell.textContent = `${Math.floor(hourLeft.toFixed(0))}小时`; } } } const lastReplyCell = addElem(trElem3, 'td', 'rinsp-lastreply-cell'); const floorElem = newElem('span', 'rinsp-lastreply-floor'); if (watchItem.lastPostUid === myUserId) { floorElem.textContent = MY_NAME_DISPLAY; lastReplyCell.classList.add('rinsp-reply-self'); } else if (watchItem.maxFloor > 0) { if (watchItem.maxFloorApprox) { floorElem.textContent = `${watchItem.maxFloor}楼+`; } else { floorElem.textContent = `${watchItem.maxFloor}楼`; } } else { floorElem.textContent = '无回复'; } if (watchItem.lastPostTime > 0) { lastReplyCell.appendChild(document.createTextNode(`${getAgeString((now - watchItem.lastPostTime) / 60000)} / `)); } lastReplyCell.appendChild(floorElem); const statusCell = addElem(trElem3, 'td', 'rinsp-status-cell'); statusCell.textContent = getStatusText(watchItem); if (expired) { statusCell.setAttribute('title', '下次检查: 已关闭'); } else { const checkMin = getScaledValue(watchItem, num(MIN_CHECK_INTERVAL), num(MAX_CHECK_INTERVAL)); const checkOneMin = getScaledValue(watchItem, num(MIN_CHECK_INTERVAL_OLDEST_ITEM), num(MAX_CHECK_INTERVAL_OLDEST_ITEM)); if (pendingWatchItemChecks.has(watchItem.id)) { trElem3.classList.add('rinsp-checking'); statusCell.setAttribute('title', '正在检查'); } else { if (checkMin >= 60) { if (checkOneMin >= 60) { statusCell.setAttribute('title', '下次检查: ' + formatRangeText((checkOneMin / 60).toFixed(0), (checkMin / 60).toFixed(0)) + ' 小时'); } else { statusCell.setAttribute('title', '下次检查: ' + checkOneMin.toFixed(0) + ' 分钟 - ' + (checkMin / 60).toFixed(0) + ' 小时'); } } else { statusCell.setAttribute('title', '下次检查: ' + formatRangeText(checkOneMin.toFixed(0), checkMin.toFixed(0)) + ' 分钟'); } statusCell.addEventListener('click', function() { watcherApi.tryCheck(watchItem.id); }); } } const delCell = addElem(trElem3, 'td'); const delButton = addElem(delCell, 'img', null, { src: 'images/post/c_editor/del.gif', style: 'cursor:pointer' }); delButton.addEventListener('click', function() { if (watchItem.lastError == null && !expired) { if (!confirm('从关注列表中移除?以后不会收到更多回复通知')) { return; } } showNativeSpinner(); updateWatchList(watchList => { const postIdKey = '#' + watchItem.id; if (!Object_hasOwn(watchList, postIdKey)) { return null; } delete watchList[postIdKey]; return watchList; }) .then(function() { trElem3.remove(); if (maxReachedWarnElem != null) { maxReachedWarnElem.remove(); maxReachedWarnElem = null; } if (tableElem.querySelectorAll('tr.tr3.tac').length === 0) { closePopupMenu(WATCHER_POPUP_MENU_ID); } }) .finally(() => closeNativeSpinner()); }); addElem(trElem3, 'td'); const status = getWatchItemStatus(watchItem); if (status != null) { trElem3.classList.add('rinsp-' + status); } }); }); function formatRangeText(minText, maxText) { if (minText === maxText) { return minText; } else { return minText + ' - ' + maxText; } } } function initCurrentPost() { const currentPostLink = document.querySelector('#breadcrumbs .crumbs-item.current strong > a[href^="read.php?tid-"]'); if (currentPostLink == null) { return; } const areaLink = currentPostLink.closest('.crumbs-item').previousElementSibling; const fid = Number.parseInt(areaLink.getAttribute('href').split('?fid-', 2)[1]); const areaName = areaLink.textContent.trim(); const postTitle = currentPostLink.textContent.trim(); const postId = Number.parseInt(currentPostLink.getAttribute('href').substring(13)); if (Number.isNaN(postId)) { return; } const postIdKey = '#' + postId; const pageArr = readCurrentAndMaxPage(document); const currentPage = pageArr[0]; let maxPage = pageArr[1]; if (userConfig.showFloatingStoreThreadButton) { addFloatingFavorButton(topControlContainer, postId, favorThreadsCacheAccess); } if (userConfig.showFloatingWatchIndicator) { const watchButton = addElem(topControlContainer, 'a', 'rinsp-excontrol-item rinsp-excontrol-item-watch rinsp-excontrol-item-ticker'); watchButton.textContent = '🔥\n关\n注'; watchButton.addEventListener('click', () => { if (watcherApi.currentThread.isWatching()) { showNativeSpinner(); removeFromWatchList() .finally(() => closeNativeSpinner()); } else { showNativeSpinner(); addToWatchList() .finally(() => closeNativeSpinner()); } }); watcherApi.currentThread.register({ setWatching(watching) { if (watching) { watchButton.classList.add('rinsp-active'); } else { watchButton.classList.remove('rinsp-active'); } }, setBusy(busy) { if (busy) { watchButton.classList.add('rinsp-running'); } else { watchButton.classList.remove('rinsp-running'); } } }); } function initIfFirstPage() { const shouElem = document.querySelector('.tr1.r_one .tipad .readbot .shou'); if (shouElem == null) { return; } const watchElem = document.createElement('li'); watchElem.classList.add('rinsp-watch'); watchElem.textContent = '关注: '; shouElem.parentNode.appendChild(watchElem); watcherApi.currentThread.register({ setWatching(watching) { if (watching) { watchElem.classList.add('rinsp-active'); } else { watchElem.classList.remove('rinsp-active'); } }, setBusy(busy) { if (busy) { watchElem.classList.add('rinsp-running'); } else { watchElem.classList.remove('rinsp-running'); } } }); watchElem.addEventListener('click', function() { if (watcherApi.currentThread.isWatching()) { showNativeSpinner(); removeFromWatchList() .finally(() => closeNativeSpinner()); } else { showNativeSpinner(); addToWatchList() .finally(() => closeNativeSpinner()); } }); } async function removeFromWatchList() { watcherApi.currentThread.setBusy(true); await updateWatchList(function(watchList) { if (!Object_hasOwn(watchList, postIdKey)) { return null; } delete watchList[postIdKey]; return watchList; }); watcherApi.currentThread.setWatching(false); watcherApi.currentThread.setBusy(false); } async function addToWatchList() { watcherApi.currentThread.setBusy(true); let fp = null; if (currentPage === 1) { const onlyGfLink = document.querySelector('#td_tpc a[href^="read.php?tid-' + postId + '-uid-"]'); if (onlyGfLink != null) { fp = document; } } if (fp == null) { fp = await fetchGetPage(`${document.location.origin}/read.php?tid-${postId}.html`); } const onlyGfLink = fp.querySelector('#td_tpc a[href^="read.php?tid-' + postId + '-uid-"]'); if (onlyGfLink == null) { return; } const gfUid = Number.parseInt(onlyGfLink.getAttribute('href').replace(/.*-uid-/, '')); const opDate = new Date(fp.querySelector('#td_tpc > .tiptop > .fl.gray[title^="发表于:"]').textContent.trim()) * 1; let bountyUntil = null; if (fid === QUESTION_AND_REQUEST_AREA_ID) { const bountyStatus = getBountyStatus(fp); bountyUntil = bountyStatus ? bountyStatus.bountyUntil : null; } const lastPost = await getLastPost(); let added = false; await updateWatchList(watchList => { if (Object.keys(watchList).length >= num(MAX_WATCH_ITEM_COUNT)) { return null; } watchList[postIdKey] = { id: postId, owner: gfUid, areaName: areaName, postTitle: postTitle, maxPage: lastPost.page, maxFloor: lastPost.floor, maxFloorApprox: false, lastVisitedPage: lastPost.page, lastVisitedFloor: lastPost.floor, lastVisitedPostId: lastPost.postId, lastPostId: lastPost.postId, lastPostUid: lastPost.postUid, lastPostTime: lastPost.postTime, lastUpdateIgnorable: false, lastChecked: Date.now(), skipUntil: 0, lastError: null, bountyUntil: bountyUntil, timeOpened: opDate, timeAdded: Date.now() }; added = true; return watchList; }); watcherApi.currentThread.setBusy(false); watcherApi.currentThread.setWatching(added); if (!added) { showWatchMenu(); } } if (currentPage === 1) { initIfFirstPage(); } async function update(alsoUpdateSavedData) { const watchList = await readWatchList(); const watchItem = watchList[postIdKey]; watcherApi.currentThread.setWatching(watchItem != null); if (watchItem != null && alsoUpdateSavedData) { if (DEBUG_MODE) console.info('mark read', postId); // only use the current page if applicable to update the current watch item let bountyUntil = null; if (watchItem.bountyUntil > 0) { if (currentPage === 1) { const bountyStatus = getBountyStatus(document); if (bountyStatus != null) { bountyUntil = bountyStatus.bountyUntil; } } } if (currentPage === maxPage) { const lastPost = await getLastPost(); watchItem.postTitle = postTitle; watchItem.maxPage = maxPage; watchItem.maxFloor = lastPost.floor; watchItem.maxFloorApprox = false; watchItem.lastVisitedPage = lastPost.page; watchItem.lastVisitedFloor = lastPost.floor; watchItem.lastVisitedPostId = lastPost.postId; watchItem.lastPostId = lastPost.postId; watchItem.lastPostUid = lastPost.postUid; watchItem.lastPostTime = lastPost.postTime; watchItem.lastUpdateIgnorable = false; watchItem.lastChecked = Date.now(); watchItem.skipUntil = 0; watchItem.lastError = null; if (bountyUntil != null) { watchItem.bountyUntil = bountyUntil; } await writeWatchList(watchList); } else if (bountyUntil != null && watchItem.bountyUntil != bountyUntil) { watchItem.bountyUntil = bountyUntil; await writeWatchList(watchList); } } } update(true); async function getLastPost() { //console.info('getLastPost', currentPage, maxPage); if (currentPage === maxPage) { return findLastPost(document, null, null); // ignore filters not needed } else { return fetchLastPost(postId); } } } async function fetchCheckBountyEnded(postId) { const doc = await fetchGetPage(`${document.location.origin}/read.php?tid-${postId}.html`); const bountyStatus = getBountyStatus(doc); return bountyStatus == null ? null : bountyStatus.ended === 2; } async function fetchLastPost(postId, ignoreList) { const url = `${document.location.origin}/read.php?tid-${postId}-page-e-fpage-1.html`; const doc = await fetchGetPage(url); const lastPost = findLastPost(doc, userFilter, ignoreList); if (DEBUG_MODE) console.info('fetchLastPost', url, lastPost); if (lastPost == null) { return { error: 'PAGE_GONE', errorMessage: findErrorMessage(doc) }; } else { return lastPost; } } function getStatusText(watchItem) { //console.info('getStatusText', watchItem); if (watchItem.lastError) { return watchItem.lastError; } const unreadFloor = watchItem.maxFloor - watchItem.lastVisitedFloor; if (unreadFloor === 0) { const ageMins = (Date.now() - watchItem.lastChecked) / 60000; let ageString = getAgeString(ageMins); return `${ageString}`; } if (unreadFloor < 0) { return '有新回复'; } if (watchItem.lastUpdateIgnorable) { return '屏蔽 ' + unreadFloor + ' 回复'; } else { return '有 ' + unreadFloor + ' 新回复'; } } function findLastPost(doc, userFilter, ignoreList) { const posts = getPosts(doc); if (posts.length === 0) { return null; } const postsByFloor = Object.create(null); let highestFloor = 0; posts.forEach(post => { if (post.floor > highestFloor) { highestFloor = post.floor; } postsByFloor[post.floor] = { floor: post.floor, postId: post.postId, postUid: post.postUid, postUname: post.postUname, postTime: post.postTime, ignorable: false }; }); const lastPost = postsByFloor[highestFloor]; const pageArr = readCurrentAndMaxPage(doc); if (ignoreList != null) { const ignoreContentMatcher = createIgnoreContentMatcher(ignoreList); posts.forEach(post => { if (post.floor > 0) { const [ignoreState] = annotateIgnorablePost(post, myUserId, userConfig, userFilter, ignoreContentMatcher); if (ignoreState) { postsByFloor[post.floor].ignorable = true; } } }); } let bountyEnded = null; if (pageArr[0] === 1) { const bountyStatus = getBountyStatus(doc); bountyEnded = bountyStatus == null ? null : bountyStatus.ended === 2; } return { page: pageArr[0], maxPage: pageArr[1], floor: lastPost.floor, postsByFloor: postsByFloor, postId: lastPost.postId, postUid: lastPost.postUid, postTime: lastPost.postTime, bountyEnded: bountyEnded }; } async function readWatchList() { try { let sSavedData = await GM.getValue('my_watchlist#' + myUserId); let watchList = JSON.parse(sSavedData); Object.values(watchList).forEach(watchItem => { if (watchItem.timeOpened == null) { watchItem.timeOpened = Date.now(); // backward-compatibility } }); return watchList; } catch (e) {} return {}; } async function writeWatchList(watchList) { await GM.setValue('my_watchlist#' + myUserId, JSON.stringify(watchList)); return true; } async function updateWatchList(updater) { async function execute() { const watchList = await readWatchList(); const updatedWatchList = await updater(watchList); if (updatedWatchList != null) { await writeWatchList(updatedWatchList); } } await runWithLock('watchlist-update', 1500, execute); } async function testRecordBestAnswerNotification(userConfig, threadHistoryAccess) { const canditateThreadLink = document.querySelector('#info_base .set-table2 > tbody > tr > td > b + a[href^="//"][href$=".html"]'); if (canditateThreadLink == null) { return; } const tid = (canditateThreadLink.getAttribute('href').match(/.*\/read\.php\?tid-(\d+)\.html$/)||[])[1] * 1; if (Number.isNaN(tid)) { return; } if (!canditateThreadLink.closest('td').textContent.startsWith('您的回复被设为最佳答案!')) { return; } const lastAccessRecord = await threadHistoryAccess.recentAccessStore.get(tid); if (lastAccessRecord && lastAccessRecord.bounty && lastAccessRecord.bounty.winner == null) { lastAccessRecord.bounty.winner = userConfig.myUserHashId; lastAccessRecord.bounty.ended = 2; await threadHistoryAccess.recentAccessStore.put(tid, lastAccessRecord); if (DEBUG_MODE) { console.info(`bounty winner recorded (tid=${tid})`); } } } function enhanceMessageReadDisplay(userConfig) { const userLink = document.querySelector('#info_base .set-table2 > tbody > tr:first-child > td > a[href^="u.php?action-show-uid-"]'); if (userLink == null) { return; } const userId = Number.parseInt(userLink.getAttribute('href').substring(22)); if (userId === 0) { userLink.textContent = SYSTEM_SENDER_DISPLAY_NAME; userLink.closest('table').classList.add('rinsp-system-message-view'); } else { const userRecord = userConfig.customUserHashIdMappings['#' + userId]; if (userRecord != null) { userLink.closest('td').classList.add('rinsp-message-user-mapped'); const currentName = userLink.textContent; userLink.textContent = userRecord[2]; userLink.setAttribute('title', '现在昵称: ' + currentName); } } } function enhanceMessagingList(userConfig, userPinArea, inboxMode) { document.querySelectorAll('#set-content .set-tab-box #info_base .set-table2').forEach(list => { let messages = []; const selectionPanel = newElem('div', 'rinsp-message-selection-panel'); list.parentNode.insertBefore(selectionPanel, list); if (inboxMode) { const selectSysMsgButton = addElem(selectionPanel, 'div', 'rinsp-selection-button'); selectSysMsgButton.textContent = '系统信息'; selectSysMsgButton.addEventListener('click', () => { messages.filter(message=>!!message.checkbox).forEach(m => m.checkbox.checked = m.userId === 0 || SYSTEM_MESSAGE_TITLES.has(m.title)); }); addElem(selectionPanel, 'div', 'rinsp-selection-sep').textContent = '|'; } const selectAllButton = addElem(selectionPanel, 'div', 'rinsp-selection-button'); selectAllButton.textContent = '全选'; selectAllButton.addEventListener('click', () => { messages.filter(message=>!!message.checkbox).forEach(m => m.checkbox.checked = true); }); const deselectAllButton = addElem(selectionPanel, 'div', 'rinsp-selection-button'); deselectAllButton.textContent = '清除'; deselectAllButton.addEventListener('click', () => { messages.filter(message=>!!message.checkbox).forEach(m => m.checkbox.checked = false); }); const observer = createMutationObserver(async () => { apply(); }); observer.init(list, { childList: true, subtree: false, attributes: false }); observer.trigger(); userPinArea.addUpdateListener(() => { observer.trigger(); }); function apply() { messages.length = 0; userPinArea.clearState(); list.querySelectorAll('tbody > tr > td > a[href^="u.php?action-show-uid-"]').forEach(userLink => { const row = userLink.closest('tr'); const messageLink = row.querySelector('a[href^="message.php?action-read-mid-"],a[href^="message.php?action-readscout-mid-"],a[href^="message.php?action-readsnd-mid-"]'); if (messageLink == null) { return; } const checkbox = row.querySelector('td > input[name="delid[]"]'); const userId = Number.parseInt(userLink.getAttribute('href').substring(22)); if (userId === 0) { userLink.textContent = SYSTEM_SENDER_DISPLAY_NAME; row.classList.add('rinsp-system-message-row'); } else { const userCell = userLink.closest('td'); userCell.classList.remove('rinsp-message-user-pinned'); userCell.classList.remove('rinsp-message-user-mapped'); let pinnedUser = userPinArea.getPinned(userId); if (pinnedUser) { userCell.classList.add('rinsp-message-user-pinned'); userPinArea.addLocation(userId, '⚪', null, () => { userLink.closest('tr').scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } let userRecord = userConfig.customUserHashIdMappings['#' + userId]; if (userRecord != null) { if (!pinnedUser) { userCell.classList.add('rinsp-message-user-mapped'); } const currentName = userLink.textContent; userLink.textContent = userRecord[2]; userLink.setAttribute('title', '现在昵称: ' + currentName); } } const title = messageLink.textContent.trim(); messages.push({ row, checkbox, userLink, userId, title }); }); selectionPanel.hidden = messages.length === 0; userPinArea.update(); } }); } function enhanceMessagingForm() { const form = document.querySelector('#main form[name="FORM"][action="message.php"]'); if (form == null) { return null; } const touidmsg = form.querySelector('input[name="touidmsg"]'); const touid = touidmsg.value * 1; if (touid > 0) { const nicknameCell = touidmsg.closest('tr').nextElementSibling.querySelector('td.td1 + td'); const homeButton = newElem('a', 'abtn', { href: `u.php?action-show-uid-${touid}.html`, target: '_blank' }); homeButton.textContent = '用户资料'; nicknameCell.appendChild(homeButton); nicknameCell.appendChild(document.createTextNode(' ')); const historyButton = newElem('a', 'abtn', { href: `message.php?action-chatlog-withuid-${touid}.html`, target: '_blank' }); historyButton.textContent = '通信记录'; nicknameCell.appendChild(historyButton); } if (userConfig.showInputLimit) { const input = form.querySelector('input[name="msg_title"]'); if (input) { input.value = truncateByByteLength(input.value, 75, '...'); addLengthLimit(input, 75); } const textarea = form.querySelector('textarea[name="atc_content"]'); if (textarea) { addLengthLimit(textarea, 1500); } form.addEventListener('submit', evt => { if (form.querySelector('.rinsp-input-max-exceeded')) { evt.preventDefault(); return false; } }); } } function addLengthLimit(input, byteLimit, charLimit, warnOnly, skipRe) { const hint = newElem('span', 'rinsp-input-max-hint'); input.after(hint); input.addEventListener('input', () => update()); input.addEventListener('change', () => update()); function update() { hint.classList.remove('rinsp-input-max-exceeded'); input.classList.remove('rinsp-input-max-exceeded'); hint.textContent = ''; let valueTrim = input.value.trim(); if (skipRe && valueTrim.startsWith('Re:')) { valueTrim = valueTrim.substring(3); } if (charLimit > 0) { if (valueTrim.length > charLimit) { hint.textContent = `${valueTrim.length} (过长) / ${charLimit}`; hint.classList.add('rinsp-input-max-exceeded'); input.classList.add('rinsp-input-max-exceeded'); return; } else if (!warnOnly) { hint.textContent = `${valueTrim.length} / ${charLimit}`; } } if (byteLimit > 0) { const curLength = getByteLength(valueTrim); if (curLength > byteLimit) { hint.textContent = `${curLength} (过长) / ${byteLimit}字节`; hint.classList.add('rinsp-input-max-exceeded'); input.classList.add('rinsp-input-max-exceeded'); return; } else if (!warnOnly) { if (charLimit === 0) { hint.textContent = `${curLength} / ${byteLimit}字节`; } } } } update(); } function changeToImageWallThreadLinks() { document.querySelectorAll('ul.threadlist li a[href^="thread.php?"]').forEach(link => { link.setAttribute('href', 'thread_new' + link.getAttribute('href').substring(6)); }); } async function addPicWallDefaultOption(fid) { const toggleButton = document.querySelector('#breadcrumbs > .fr > a[href^="thread"]'); if (toggleButton == null || !['[点击进入图墙模式]', '[点击进入列表模式]'].includes(toggleButton.textContent.trim())) { return; } const toggle = newElem('span', 'fr'); toggleButton.parentElement.parentElement.insertBefore(toggle, toggleButton.parentElement.nextElementSibling); const labelElem = addElem(toggle, 'label'); const checkbox = addElem(labelElem, 'input', null, { type: 'checkbox' }); checkbox.checked = readPinnedPicWallFids().includes(fid); addElem(labelElem, 'span', null, { style: 'vertical-align: middle; margin-right: 0.5em' }).textContent = '默认图墙模式'; checkbox.addEventListener('change', () => { const fids = readPinnedPicWallFids().filter(x=>x!==fid); if (checkbox.checked) { fids.push(fid); } localStorage.setItem(PIC_WALL_PREF_KEY, fids.join(' ')); update(); }); update(); function update() { if (checkbox.checked && toggleButton.getAttribute('href').startsWith('thread.php?')) { toggleButton.parentElement.setAttribute('style', 'opacity: 0.1; pointer-events:none'); } else { toggleButton.parentElement.setAttribute('style', ''); } } } async function enableForumAnnouncementFolding() { const anchor = document.querySelector('td.tac > img[src="images/colorImagination/thread/anc.gif"]'); if (anchor == null) { return; } const th = anchor.parentNode.nextElementSibling; if (th == null || !th.textContent.trim().startsWith('论坛公告:')) { return; } const row = anchor.closest('tr.tr3'); row.classList.add('rinsp-top-announcement-row'); let announcementRows = []; const normalTopicMarker = document.querySelector('.rinsp-top-announcement-row ~ tr.tr2 > .tac'); if (normalTopicMarker && normalTopicMarker.textContent.trim() === '普通主题') { const endMarkerRow = normalTopicMarker.closest('.tr2'); let testRow = row; while ((testRow = testRow.nextElementSibling) !== endMarkerRow) { announcementRows.push(testRow); } } else { let testRow = row; while ((testRow = testRow.nextElementSibling) != null) { if (testRow.querySelector('td + td > img[src="images/colorImagination/file/headtopic_3.gif"]') == null) { break; } announcementRows.push(testRow); } } if (announcementRows.length === 0) { return; } const toggler = newElem('a', 'rinsp-announcement-toggler'); const img = newElem('img'); toggler.appendChild(img); th.insertBefore(toggler, th.childNodes[0]); function updateFoldingState(folded) { if (folded) { row.classList.add('rinsp-announcement-folded'); announcementRows.forEach(r => r.classList.add('rinsp-announcement-row-folded')); img.setAttribute('src', 'images/colorImagination/index/cate_open.gif'); } else { row.classList.remove('rinsp-announcement-folded'); announcementRows.forEach(r => r.classList.remove('rinsp-announcement-row-folded')); img.setAttribute('src', 'images/colorImagination/index/cate_fold.gif'); } } updateFoldingState(userConfig.siteAnnouncementSectionDefaultFolded); toggler.addEventListener('click', async () => { const newFoldState = !row.classList.contains('rinsp-announcement-folded'); updateFoldingState(newFoldState); await mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.siteAnnouncementSectionDefaultFolded = newFoldState; return updatingUserConfig; }); }); } async function enhanceSearchPage() { const searchForm = document.querySelector('#main form[action="search.php?"]'); if (searchForm == null) { return; } const searchPref = await searchConfigAccess.read(); const keywordInput = searchForm.querySelector('input[name="keyword"]'); if (keywordInput != null) { keywordInput.classList.add('rinsp-search-keyword-input'); enhanceKeywordInput(keywordInput, searchPref); setTimeout(function () { keywordInput.focus(); }, 0); } const dateRangeSelect = searchForm.querySelector('select[name="sch_time"]'); if (dateRangeSelect != null) { enhanceSearchRangeSelect(dateRangeSelect, searchPref); } const topicSelect = searchForm.querySelector('select[name="f_fid"]'); if (topicSelect != null) { enhanceSearchTopicSelect(topicSelect, searchPref); } const prefillParamString = (document.location.hash.match(/^#prefill\((.*)\)$/)||[])[1]; if (prefillParamString) { const prefillData = {}; try { const array = JSON.parse(`[${decodeURIComponent(prefillParamString)}]`); for (let i = 0; i < array.length - 1; i += 2) { prefillData[array[i]] = array[i + 1]; } } catch (ignore) {} if (prefillData.fid > 0) { topicSelect.value = String(prefillData.fid); } if (prefillData.keyword) { keywordInput.value = prefillData.keyword; } if (prefillData.pwuser) { searchForm.querySelector('input[name="pwuser"]').value = prefillData.pwuser; } } } async function enhanceKeywordInput(keywordInput, searchPref) { let defaultSearchAll = !!searchPref.defaultSearchAll; const parentCell = keywordInput.closest('th'); const setDefaultButton = document.createElement('span'); setDefaultButton.classList.add('rinsp-fav-search-setdefault-mode'); setDefaultButton.classList.add('rinsp-hide'); setDefaultButton.textContent = '➕设为默认'; setDefaultButton.addEventListener('click', async function () { await searchConfigAccess.update(function(updatingSearchPref) { updatingSearchPref.defaultSearchAll = searchAllRadioOption.checked; return updatingSearchPref; }); defaultSearchAll = searchAllRadioOption.checked; redraw(); }); parentCell.appendChild(setDefaultButton); const searchAllRadioOption = parentCell.querySelector('input[name="method"][value="AND"]'); parentCell.querySelectorAll('input[name="method"]').forEach(function (methodRadioOption) { methodRadioOption.addEventListener('change', function () { redraw(); }); }); function redraw() { if (defaultSearchAll === searchAllRadioOption.checked) { setDefaultButton.classList.add('rinsp-hide'); } else { setDefaultButton.classList.remove('rinsp-hide'); } } if (defaultSearchAll) { searchAllRadioOption.checked = true; } setTimeout(function () { redraw(); }, 0); // account for browser auto-fill } async function enhanceSearchRangeSelect(rangeSelect, searchPref) { const parentCell = rangeSelect.closest('th'); const setDefaultButton = addElem(parentCell, 'span', 'rinsp-fav-search-setdefault-range rinsp-hide'); setDefaultButton.textContent = '➕设为默认'; let currentDefaultRange = searchPref.defaultTimeRange||'all'; function update() { if (rangeSelect.value === currentDefaultRange) { setDefaultButton.classList.add('rinsp-hide'); } else { setDefaultButton.classList.remove('rinsp-hide'); } } setDefaultButton.addEventListener('click', function () { parentCell.classList.add('rinsp-temp-disabled'); async function execute() { await searchConfigAccess.update(function(updatingSearchPref) { updatingSearchPref.defaultTimeRange = rangeSelect.value; currentDefaultRange = rangeSelect.value; return updatingSearchPref; }); } execute().finally(function () { parentCell.classList.remove('rinsp-temp-disabled'); update(); }); }); rangeSelect.addEventListener('change', function () { update(); }); rangeSelect.value = currentDefaultRange; } async function enhanceSearchTopicSelect(topicSelect, searchPref) { const parentCell = topicSelect.closest('th'); const pin = addElem(parentCell, 'span', 'rinsp-fav-search-area-pin'); const originalContent = topicSelect.innerHTML; await update(searchPref.pinnedTopics || {}); async function update(topicMappings) { let entries = Object.entries(topicMappings); let favOptGroup = null; const fullOptGroup = newElem('optgroup', null, { label: '目录树' }); const allOptionValues = new Set(); topicSelect.querySelectorAll('option').forEach(function (option, i) { if (i > 0) { allOptionValues.add(option.getAttribute('value')); fullOptGroup.appendChild(option); } else { option.classList.add('rinsp-fav-search-area-all'); } }); if (entries.length > 0) { favOptGroup = newElem('optgroup', 'rinsp-fav-search-area-group', { label: '版块捷径' }); entries.forEach(function (entry) { const fid = entry[0].substring(1); const favOption = addElem(favOptGroup, 'option', null, { value: fid, title: `fid: ${fid}` }); if (allOptionValues.has(fid)) { favOption.textContent = `🔖${entry[1]}`; } else { favOption.textContent = `⚠️${entry[1]} (此区不存在)`; } }); } if (favOptGroup != null) { topicSelect.appendChild(favOptGroup); } topicSelect.appendChild(fullOptGroup); } pin.addEventListener('click', function () { parentCell.classList.add('rinsp-temp-disabled'); async function execute() { const current = getCurrentItem(); let newSearchPref = await searchConfigAccess.update(function(updatingSearchPref) { const updatingMappings = updatingSearchPref.pinnedTopics || {}; if (current.fav) { delete updatingMappings['$' + current.value]; } else { let chosenLabel = prompt('显示名称', current.label.trim().replace(/^.*? /, '')); chosenLabel = chosenLabel ? chosenLabel.trim() : ''; if (!chosenLabel) { return; } updatingMappings['$' + current.value] = chosenLabel; } updatingSearchPref.pinnedTopics = updatingMappings; return updatingSearchPref; }); if (newSearchPref != null) { topicSelect.innerHTML = originalContent; update(newSearchPref.pinnedTopics||{}); topicSelect.value = current.value; if (!topicSelect.value) { topicSelect.value = 'all'; } updatePin(); } } execute().finally(function () { parentCell.classList.remove('rinsp-temp-disabled'); }); }); function updatePin() { const current = getCurrentItem(); if (current == null || current.value === 'all') { pin.textContent = ''; } else { pin.textContent = current.fav ? '❌删除捷径' : '➕存为捷径'; } } function getCurrentItem() { let options = topicSelect.querySelectorAll('option[value="' + topicSelect.value + '"]'); if (options.length === 0) { return null; } return { fav: options[0].closest('.rinsp-fav-search-area-group') != null, label: options[options.length - 1].textContent, value: topicSelect.value }; } updatePin(); topicSelect.addEventListener('change', function () { updatePin(); }); } function annotateIgnorablePost(post, myUserId, userConfig, userFilter, ignoreContentMatcher) { post.rootElem.classList.remove('rinsp-filter-default-ignorable'); post.rootElem.classList.remove('rinsp-filter-ignored-bykeyword'); post.rootElem.classList.remove('rinsp-filter-ignored-byuid'); post.rootElem.classList.remove('rinsp-my-post'); post.rootElem.classList.remove('rinsp-filter-bypass'); post.rootElem.classList.add('rinsp-filter-added'); if (post.postUid === myUserId) { post.rootElem.classList.add('rinsp-my-post'); post.rootElem.classList.add('rinsp-filter-bypass'); } let ignoreState = false; let userFilterRule; if (userConfig.dontFilterRequestReplyByUser && post.areaId === QUESTION_AND_REQUEST_AREA_ID) { userFilterRule = null; } else { userFilterRule = userFilter.users['#' + post.postUid]; } let userBlacklisted = userFilterRule && userFilterRule.hideReplies; if (post.postDefaultHidden) { ignoreState = true; } else if (userBlacklisted) { post.rootElem.classList.add('rinsp-filter-ignored-byuid'); ignoreState = true; } else { if (userConfig.customUserpicBypassIgnoreList && !post.defaultUserPic) { post.rootElem.classList.add('rinsp-filter-bypass'); } else if (userConfig.customUserBypassIgnoreList) { // user blacklist still wins over bookmarked users if (userConfig.customUserHashIdMappings['#' + post.postUid] != null) { post.rootElem.classList.add('rinsp-filter-bypass'); } } } let effectiveContent = null; if (ignoreContentMatcher != null) { if (post.contentDefaultIgnorable) { effectiveContent = DEFAULT_IGNORABLE_MARKER_TAG; } else { effectiveContent = getEffectiveTextContent(post.contentElem, !!userConfig.ignoreContentUseTextOnly, !!userConfig.treatAllEmojiTheSameWay); } //if (DEBUG_MODE) console.info(item, 'effectiveContent: `' + effectiveContent + '`'); if (!ignoreState && effectiveContent != null && ignoreContentMatcher.matches(effectiveContent)) { ignoreState = true; post.rootElem.classList.add('rinsp-filter-ignored-bykeyword'); } } if (ignoreState) { post.rootElem.classList.add('rinsp-filter-ignored'); } else { post.rootElem.classList.remove('rinsp-filter-ignored'); } return [ignoreState, effectiveContent]; } async function applyContentFilter(allPosts, myUserId, userConfig, threadFilter, userFilter, ignoreList, updateCallback) { const posts = allPosts.slice(); if (posts.length === 0) { return; } function onUpdate(updatedConfigs) { // reset annotated state posts.forEach(post => { post.rootElem.parentNode.removeAttribute('rinsp-filter-ignored-cont'); post.rootElem.classList.remove('rinsp-filter-added'); }); updateCallback(updatedConfigs); } posts.forEach(post => { // add block user const contentTh = post.contentElem.closest('th'); contentTh.querySelectorAll('.rinsp-filter-block-menu-item,.rinsp-filter-block-menu-sep').forEach(el => el.remove()); const blockMenu = contentTh.querySelector('.tiptop .fr > .dropdown > .dropdown-content'); if (blockMenu) { const menuItem = blockMenu.previousElementSibling; menuItem.classList.remove('rinsp-filter-block-menu-item-active'); const dislikeUserRule = userFilter.users['#' + post.postUid]; const hideThreads = dislikeUserRule && dislikeUserRule.hideThreads; const hideReplies = dislikeUserRule && dislikeUserRule.hideReplies; addElem(blockMenu, 'div', 'rinsp-filter-block-menu-sep'); const blockThreadButton = addElem(blockMenu, 'a', 'rinsp-filter-block-menu-item'); if (hideThreads && !hideReplies) { menuItem.classList.add('rinsp-filter-block-menu-item-active'); blockThreadButton.classList.add('rinsp-filter-block-menu-item-active'); blockThreadButton.textContent = '🚫只主题帖'; blockThreadButton.addEventListener('click', () => { promptRemoveUserBlock(post.postUid, post.postUname, onUpdate); }); } else { blockThreadButton.textContent = '只主题帖'; blockThreadButton.addEventListener('click', () => { promptBlockOp(post.postUid, post.postUname, onUpdate); }); } const blockReplyButton = addElem(blockMenu, 'a', 'rinsp-filter-block-menu-item'); if (hideReplies) { menuItem.classList.add('rinsp-filter-block-menu-item-active'); blockReplyButton.classList.add('rinsp-filter-block-menu-item-active'); blockReplyButton.textContent = '🚫所有内容'; blockReplyButton.addEventListener('click', () => { promptRemoveUserBlock(post.postUid, post.postUname, onUpdate); }); } else { blockReplyButton.textContent = '所有内容'; blockReplyButton.addEventListener('click', () => { promptBlockComplete(post.postUid, post.postUname, onUpdate); }); } } }); // GF: add keyword filter if (posts[0].floor === 0) { const opPost = posts[0]; const threadFilterExecutor = initThreadFilterExecutor(myUserId, threadFilter, userFilter, onUpdate, true); const subjectElem = opPost.rootElem.querySelector('#subject_tpc'); const threadLikeModel = { tid: opPost.tid, row: opPost.rootElem, op: opPost.postUid, opName: opPost.postUname, opElem: null, titleCell: subjectElem, title: getEffectiveThreadTitle(subjectElem.textContent) }; threadFilterExecutor.clear(threadLikeModel); threadFilterExecutor.run(threadLikeModel); } function ignoreButtonDef() { return { create(post) { const ignoreElem = document.createElement('li'); const actionBar = post.rootElem.querySelector('.tipad .fl.readbot'); actionBar.appendChild(ignoreElem); return ignoreElem; }, restoreLabel: '已屏蔽', hideLabel: '屏蔽内容' }; } function defaultIgnorableButtonDef() { return { create(post) { const ignoreElem = document.createElement('span'); const actionBar = post.rootElem.querySelector('.tiptop > .fr'); const insertBeforeNode = actionBar.childNodes[0]; actionBar.insertBefore(ignoreElem, insertBeforeNode); return ignoreElem; }, restoreLabel: '已屏蔽', hideLabel: '屏蔽内容' }; } const ignoreContentMatcher = createIgnoreContentMatcher(ignoreList); posts.forEach(post => { if (post.floor === 0) { annotateIgnorablePost(post, myUserId, userConfig, userFilter, null); return; } const [ignoreState, effectiveContent] = annotateIgnorablePost(post, myUserId, userConfig, userFilter, ignoreContentMatcher); if (post.contentDefaultIgnorable) { post.rootElem.classList.add('rinsp-filter-default-ignorable'); } if (post.postDefaultHidden) { return; } // add block content post.rootElem.querySelectorAll('.rinsp-ignore-switch').forEach(el => el.remove()); setupIgnoreButton(ignoreButtonDef()); if (post.postCollapsed) { setupIgnoreButton(defaultIgnorableButtonDef()); } function setupIgnoreButton(buttonDef) { if (effectiveContent != null && !post.rootElem.classList.contains('rinsp-filter-ignored-byuid')) { let ignoreElem = buttonDef.create(post); ignoreElem.classList.add('rinsp-ignore-switch'); const ignoreLabel = addElem(ignoreElem, 'a', null, { href: 'javascript:void(0)', title: '关键内容: \n' + effectiveContent }); ignoreLabel.addEventListener('click', async function() { if (DEBUG_MODE) console.info('effective content = ' + effectiveContent); ignoreElem.classList.add('rinsp-config-saving'); const newIgnoredState = !ignoreState; const updatedIgnoreList = await contentIgnoreListConfigAccess.update(function(ignoreList) { const newTerms = ignoreList.terms.filter(term => term !== effectiveContent); if (newIgnoredState) { newTerms.push(effectiveContent); } if (ignoreList.terms.length === newTerms.length) { // no change return; } ignoreList.terms = newTerms; return ignoreList; }) .finally(function() { ignoreElem.classList.remove('rinsp-config-saving'); }); if (updatedIgnoreList != null) { onUpdate({ ignoreList: updatedIgnoreList }); } return false; }); ignoreLabel.textContent = ignoreState ? buttonDef.restoreLabel : buttonDef.hideLabel; } } }); const ignoredPosts = posts.filter(post => post.rootElem.classList.contains('rinsp-filter-ignored')); const ignoreCount = ignoredPosts.length; document.querySelectorAll('.r_two[rinsp-filter-group-size]').forEach(el=>el.removeAttribute('rinsp-filter-group-size')); document.querySelectorAll('.rinsp-filter-ignored-range-end-summary').forEach(el=>el.remove()); if (ignoreCount > 0) { const groups = []; ignoredPosts.filter(post => !post.rootElem.classList.contains('rinsp-filter-bypass')).forEach(function(post) { const floor = post.floor; const forcedSingleton = post.rootElem.parentNode.previousElementSibling.matches('div'); const thisSingleGroup = { elems: [{ row: post.rootElem, floor: post.floorElem }], floorMin: floor, floorMax: floor, singleton: forcedSingleton }; if (groups.length === 0) { groups.push(thisSingleGroup); } else { const last = groups[groups.length - 1]; if (floor == last.floorMax + 1 && !forcedSingleton && !last.singleton) { last.elems.push({ row: post.rootElem, floor: post.floorElem }); last.floorMax = floor; } else { groups.push(thisSingleGroup); } } }); groups.forEach(group => { group.elems[0].row.parentNode.removeAttribute('rinsp-filter-ignored-cont'); group.elems[0].floor.closest('tr').querySelector('.r_two').removeAttribute('rinsp-filter-group-size'); group.elems[0].floor.querySelectorAll('.rinsp-filter-ignored-range-end-summary').forEach(el => el.remove()); if (group.floorMin === group.floorMax) { return; } group.elems[0].floor.closest('tr').querySelector('.r_two').setAttribute('rinsp-filter-group-size', String(group.elems.length)); const rangeEnd = newElem('span', 'rinsp-filter-ignored-range-end-summary'); rangeEnd.appendChild(document.createTextNode(' - ')); rangeEnd.appendChild(group.elems[group.elems.length - 1].floor.cloneNode(true)); group.elems[0].floor.after(rangeEnd); const rangeEnd2 = newElem('span', 'rinsp-filter-ignored-range-end-summary'); rangeEnd2.appendChild(document.createTextNode(' - ')); const dateEndElem = group.elems[group.elems.length - 1].floor.closest('.fl').nextElementSibling.cloneNode(true); dateEndElem.classList.remove('fl'); dateEndElem.classList.remove('gray'); rangeEnd2.appendChild(dateEndElem); group.elems[0].floor.closest('.fl').nextElementSibling.appendChild(rangeEnd2); group.elems[0].floor.closest('tr').querySelector('.r_two').setAttribute('rinsp-filter-group-size', String(group.elems.length)); group.elems.slice(1).forEach(el=>el.row.parentNode.setAttribute('rinsp-filter-ignored-cont', '1')); }); } ignoreContentToggler.setCount(ignoreCount); if (ignoreCount === 0) { document.body.classList.remove('rinsp-filter-peek-mode'); } } function createIgnoreContentMatcher(ignoreList) { const ignoreSet = new Set(); ignoreList.terms.forEach(term => { const matchableText = term .split(/(<[^>]+>)/) .map(chunk => { if (chunk.length === 0 || chunk[0] === '<') { return chunk; } else { return simplifyText(chunk); } }) .join(''); ignoreSet.add(matchableText); }); return { matches(effectiveContent) { return ignoreSet.has(effectiveContent); } }; } async function updateUserBlockRule(opId, opName, data, callback) { const updatedUserFilterConfig = await userFilterConfigAccess.update(function(userFilterConfig) { const userIdKey = '#' + opId; if (data) { const updateData = Object.assign({ name: opName }, data); if (userFilterConfig.users[userIdKey] == null) { userFilterConfig.users[userIdKey] = updateData; } else { userFilterConfig.users[userIdKey] = Object.assign(userFilterConfig.users[userIdKey], updateData); } } else { delete userFilterConfig.users[userIdKey]; } return userFilterConfig; }); callback({ userFilter: updatedUserFilterConfig }); } async function promptBlockOp(opId, opName, callback) { if (confirm(`屏蔽此用户(${opName}) 所有主题帖?`)) { updateUserBlockRule(opId, opName, { hideThreads: true, hideReplies: false }, callback); } } async function promptRemoveUserBlock(opId, opName, callback) { if (confirm(`取消屏蔽此用户(${opName})?`)) { updateUserBlockRule(opId, opName, null, callback); } } async function promptBlockComplete(opId, opName, callback) { if (confirm(`屏蔽此用户(${opName}) 所有主题及回复?`)) { updateUserBlockRule(opId, opName, { hideThreads: true, hideReplies: true }, callback); } } async function promptEditKeywords(message, initValues, initText, attr, callback) { const input = prompt(message, initText == null ? initValues.slice(0).sort().join(' ') : initText); if (input == null) { return; } const keywords = input.toLowerCase().trim().split(' ').filter(s=>!!s); const updatedFilterConfig = await threadFilterConfigAccess.update(function(filterConfig) { const set = new Set(filterConfig[attr]); initValues.forEach(kw => set.delete(kw)); keywords.forEach(kw => set.add(kw)); filterConfig[attr] = Array.from(set); return filterConfig; }); callback({ threadFilter: updatedFilterConfig }); } async function removeBlockThread(tid, callback) { const updatedFilterConfig = await threadFilterConfigAccess.update(function(filterConfig) { const set = new Set(filterConfig.dislikes); set.delete(getTidMatchDirective(tid)); filterConfig.dislikes = Array.from(set); return filterConfig; }); callback({ threadFilter: updatedFilterConfig }); } async function promptBlockThread(tid, callback) { if (confirm('屏蔽此帖?')) { const updatedFilterConfig = await threadFilterConfigAccess.update(function(filterConfig) { const set = new Set(filterConfig.dislikes); set.add(getTidMatchDirective(tid)); filterConfig.dislikes = Array.from(set); return filterConfig; }); callback({ threadFilter: updatedFilterConfig }); } } async function removeLikeThread(tid, callback) { const updatedFilterConfig = await threadFilterConfigAccess.update(function(filterConfig) { const set = new Set(filterConfig.likes); set.delete(getTidMatchDirective(tid)); filterConfig.likes = Array.from(set); return filterConfig; }); callback({ threadFilter: updatedFilterConfig }); } async function promptLikeThread(tid, callback) { if (confirm('标记此帖?')) { const updatedFilterConfig = await threadFilterConfigAccess.update(function(filterConfig) { const set = new Set(filterConfig.likes); set.add(getTidMatchDirective(tid)); filterConfig.likes = Array.from(set); return filterConfig; }); callback({ threadFilter: updatedFilterConfig }); } } function initRequestThreadEnhancementExecutor(myUserId, userConfig, settlementPostMatcher) { const bountySteps = [10000, 5000, 2000, 1000, 500, 200, 100]; function isSettlementPost(title) { if (userConfig.hideSettlementPost$UseDefaultKeywords) { if (title.match(DEFAULT_SETTLEMENT_BLACKLIST_PATTERN) != null) { return false; } let titleReduced = title.replace(/@[^@ ]+/, ''); const hasTarget = titleReduced.length < title.length; titleReduced = titleReduced.replace(DEFAULT_SETTLEMENT_STOPWORD_PATTERN, ''); const beforeLength = titleReduced.length; titleReduced = titleReduced.replace(DEFAULT_SETTLEMENT_KEYWORD_PATTERN, ''); const keywordLength = beforeLength - titleReduced.length; titleReduced = titleReduced.trim(); if (hasTarget && titleReduced.length === 0) return true; if (titleReduced.length <= DEFAULT_SETTLEMENT_TITLE_MAX_OTHER_TEXT_AMOUNT) { if (hasTarget || keywordLength > 0) { return true; } } } if (settlementPostMatcher != null) { const matches = settlementPostMatcher.match(title.toLowerCase()); if (matches.size > 0) { return true; } } return false; } return { async run(thread) { this.clear(thread); const areaTag = thread.titleCell.querySelector(`.s8[href^="thread.php?fid-${QUESTION_AND_REQUEST_AREA_ID}-type-"]`); if (areaTag == null) { return; } thread.row.classList.add('rinsp-request-thread'); const title = thread.titleCell.querySelector('h3 > a').textContent; const bountyElem = thread.titleCell.querySelector('h3 > .s1'); if (bountyElem) { const betterBountyDisplay = newElem('span', 'rinsp-request-bounty'); bountyElem.parentNode.insertBefore(betterBountyDisplay, bountyElem); const opDate = new Date((thread.row.querySelector('td[id^="td_"] + td > .f10.gray2')||{}).textContent||'') * 1; const nowRoundDay = Math.floor(Date.now() / 86400000) * 86400000; const expired = nowRoundDay - opDate >= 30 * 86400000; // 30 days request expiry const statusText = expired ? '已超时' : '悬赏金额'; if (expired) { thread.row.classList.add('rinsp-request-thread-expired'); } else { thread.row.classList.add('rinsp-request-thread-ongoing'); } const baseBounty = (bountyElem.textContent.match(/ *— 悬赏金额:*(\d+) *— */)||[])[1]*1||0; const extraBounty = parseSpAmount(title); addElem(betterBountyDisplay, 'span').textContent = `— ${statusText}:`; addElem(betterBountyDisplay, 'span', 'rinsp-base-bounty').textContent = String(baseBounty); if (extraBounty > baseBounty) { addElem(betterBountyDisplay, 'span', 'rinsp-extra-bounty').textContent = '+' + extraBounty; } addElem(betterBountyDisplay, 'span').textContent = ' —'; const totalBounty = extraBounty > baseBounty ? extraBounty : baseBounty; thread.row.setAttribute('rinsp-request-bounty-steps', bountySteps.filter(amt => totalBounty > amt).join(' ') + ' min'); } else { if (userConfig.requestThreadUseHistoryData && threadHistoryAccess) { const lastAccessRecord = await threadHistoryAccess.recentAccessStore.get(thread.tid); if (lastAccessRecord && lastAccessRecord.bounty && lastAccessRecord.bounty.winner === userConfig.myUserHashId) { thread.row.classList.add('rinsp-request-thread-won'); } } thread.row.classList.add('rinsp-request-thread-ended'); } if (hasMentionedMe(title, myUserId, userConfig)) { if (userConfig.hideSettlementPost$HighlightMyself) { thread.row.classList.add('rinsp-request-settlement-bypass'); } thread.row.classList.add('rinsp-thread-mention-me'); } if (thread.op === myUserId) { thread.row.classList.add('rinsp-thread-byme'); } if (userConfig.hideSettlementPost) { if (isSettlementPost(thread.title)) { if (thread.op === myUserId) { thread.row.classList.add('rinsp-request-settlement-bypass'); } thread.row.classList.add('rinsp-request-settlement-thread'); } } }, clear(thread) { thread.row.removeAttribute('rinsp-request-bounty-steps'); thread.row.classList.remove('rinsp-request-thread'); thread.row.classList.remove('rinsp-request-thread-ongoing'); thread.row.classList.remove('rinsp-request-thread-ended'); thread.row.querySelectorAll('.rinsp-request-bounty').forEach(el => el.remove()); thread.row.classList.remove('rinsp-thread-mention-me'); thread.row.classList.remove('rinsp-thread-byme'); thread.row.classList.remove('rinsp-request-settlement-bypass'); thread.row.classList.remove('rinsp-request-settlement-thread'); } }; } function isScoreMismatched(score, defaultRating, bold) { if (score <= 1) { return false; } if (defaultRating.baseTotalScore === 0) { return false; } const fid = getCurrentPageFid(); const diffAllowance = SCORE_DIFF_ALLOWANCE[`fid=${fid}`] || SCORE_DIFF_ALLOWANCE['fid=*']; if (defaultRating.ownBought || defaultRating.ownTranslate || bold) { const diff = score - defaultRating.baseTotalScore * 10; if (diff < defaultRating.baseTotalScore + 10) { return false; } if (score <= diffAllowance[1]) { return false; } } const diff = score - defaultRating.baseTotalScore; if (diff < defaultRating.baseTotalScore / 2 + diffAllowance[0]) { return false; } return true; } function initThreadHistoryExecutor(threadHistoryAccess) { return { async runBatch(threads) { const threadsById = new Map(); threads.forEach(thread => { threadsById.set(thread.tid, thread); }); const tids = Array.from(threadsById.keys()); const lastSeenRecords = await threadHistoryAccess.historyStore.getBatch(tids); lastSeenRecords.forEach((record, i) => { const thread = threadsById.get(tids[i]); this.clear(thread); if (record != null) { thread.row.classList.add('rinsp-thread-visited'); if (thread.replyCount > record) { thread.row.classList.add('rinsp-thread-visited-update'); } } }); }, async run(thread) { await this.runBatch([thread]); }, clear(thread) { thread.row.classList.remove('rinsp-thread-visited'); thread.row.classList.remove('rinsp-thread-visited-update'); } }; } function initMarkPinnedUserThreadExecutor() { return { async runBatch(threads) { threads.forEach(thread => { this.clear(thread); if (userPinArea.isWatching(thread.op)) { thread.row.classList.add('rinsp-thread-user-pinned'); userPinArea.addLocation(thread.op, '⚪', null, () => { thread.row.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } }); }, clear(thread) { thread.row.classList.remove('rinsp-thread-user-pinned'); } }; } function initMarkPaywellThreadExecutor() { return { run(thread) { this.clear(thread); if (thread.areaId != null) { if (PAYWALL_AREA_IDS.has(thread.areaId)) { thread.row.classList.add('rinsp-thread-paywall'); } } }, clear(thread) { thread.row.classList.remove('rinsp-thread-paywall'); } }; } function initClosedThreadFilterExecutor() { return { run(thread) { this.clear(thread); if (thread.row.querySelector('td > a > img[src="images/colorImagination/thread/topicclose.gif"]')) { thread.row.classList.add('rinsp-thread-filter-closed'); } }, clear(thread) { thread.row.classList.remove('rinsp-thread-filter-closed'); } }; } function initScoredThreadFilterExecutor() { return { run(thread) { this.clear(thread); let scoreElem = thread.titleCell.querySelector('h3 + .gray.tpage'); if (scoreElem) { const score = (scoreElem.textContent.match(/ *( [+-]\d+ ) */)||[])[1] * 1; const defaultRating = getDefaultRating(thread.titleCell.querySelector('h3 > a').textContent); if (isScoreMismatched(score, defaultRating, thread.titleCell.querySelector('h3 > a > b') != null)) { thread.row.classList.add('rinsp-thread-filter-miscored'); } else { thread.row.classList.add('rinsp-thread-filter-scored'); } } else { thread.row.classList.add('rinsp-thread-filter-unscored'); } }, clear(thread) { thread.row.classList.remove('rinsp-thread-filter-scored'); thread.row.classList.remove('rinsp-thread-filter-unscored'); } }; } function initCategorizeShareTypeThreadExecutor(shareTypeFilter) { return { async runBatch(threads) { shareTypeFilter.clearState(); let rejectedChainSize = 0; let lastRejected = null; threads.slice().reverse().forEach(thread => { if (thread.areaId > 0 && !RESOURCE_AREA_IDS.has(thread.areaId) && !PAYWALL_AREA_IDS.has(thread.areaId)) { rejectedChainSize = 0; lastRejected = null; return; } this.clear(thread); const accepted = shareTypeFilter.addAndAcceptThread(thread.tid, thread.title); if (accepted) { rejectedChainSize = 0; lastRejected = null; } else { rejectedChainSize++; thread.row.classList.add('rinsp-thread-filter-masked-bysharetype'); if (lastRejected) { thread.row.setAttribute('rinsp-sharetype-chain', rejectedChainSize); thread.titleCell.setAttribute('rinsp-sharetype-chain', rejectedChainSize); lastRejected.titleCell.removeAttribute('rinsp-sharetype-chain'); lastRejected.row.classList.add('rinsp-thread-filter-masked-bysharetype-collapse'); } lastRejected = thread; } }); await shareTypeFilter.update(); }, clear(thread) { thread.row.removeAttribute('rinsp-sharetype-chain'); thread.titleCell.removeAttribute('rinsp-sharetype-chain'); thread.row.classList.remove('rinsp-thread-filter-masked-bysharetype'); thread.row.classList.remove('rinsp-thread-filter-masked-bysharetype-collapse'); } }; } async function openBookmarkUserMenu(anchor, userId, userHashId, userNickName, deleteAction) { setupPopupMenu({ title: userNickName, width: 160, popupMenuId: USER_BOOKMARK_ACTION_POPUP_MENU_ID, anchor, items: [ { label: '发短消息', class: 'rinsp-mailto-user-bookmark-button', action: `message.php?action-write-touid-${userId}.html` }, { label: '通信记录', action: `message.php?action-chatlog-withuid-${userId}.html` }, { label: '移除收藏', class: 'rinsp-alert-menu-button', action: deleteAction } ] }); } async function openFilterMenu(anchor, thread, onClose, onUpdate, unblockable) { const fid = getCurrentPageFid(); const items = []; const dlsiteMatch = thread.title.match(/[RBVrbv][Jj](?:01)?\d{6}/); if (dlsiteMatch) { const code = dlsiteMatch[0].toUpperCase(); items.push({ label: `${code} (官网)`, action: `https://www.dlsite.com/maniax/work/=/product_id/${code}.html`, // dlsite will handle redirection target: '_blank' }); } items.push({ label: '搜索 (全部版块)', action: `search.php#prefill("keyword",${JSON.stringify(thread.title)})`, target: '_blank' }); items.push({ label: '搜索 (此版块内)', action: `search.php#prefill("fid",${fid},"keyword",${JSON.stringify(thread.title)})`, target: '_blank' }); items.push({ label: '添加屏蔽词', class: 'rinsp-thread-filter-dislike-button', action: () => promptEditKeywords('请输入屏蔽关键词 用空格分开', [], thread.title, 'dislikes', onUpdate) }); items.push({ label: '添加喜欢项目', class: 'rinsp-thread-filter-like-button', action: () => promptEditKeywords('请输入喜欢的关键词 用空格分开', [], thread.title, 'likes', onUpdate) }); if (thread.row.classList.contains('rinsp-thread-filter-like-bytid')) { items.push({ label: '移除标记', class: 'rinsp-thread-filter-liketid-button', action: () => removeLikeThread(thread.tid, onUpdate) }); } else { items.push({ label: '标记此帖', class: 'rinsp-thread-filter-liketid-button', action: () => promptLikeThread(thread.tid, onUpdate) }); } if (thread.row.classList.contains('rinsp-thread-filter-dislike-bytid')) { items.push({ label: '取消屏蔽此帖', class: 'rinsp-thread-filter-hidetid-button', action: () => removeBlockThread(thread.tid, onUpdate) }); } else if (!unblockable) { items.push({ label: '🛇只屏蔽此帖', class: 'rinsp-thread-filter-hidetid-button', action: () => promptBlockThread(thread.tid, onUpdate) }); } if (thread.row.classList.contains('rinsp-thread-filter-dislike-byuid')) { items.push({ label: '取消屏蔽用户', class: 'rinsp-thread-filter-hideop-button', action: () => promptRemoveUserBlock(thread.op, thread.opName, onUpdate) }); } else if (!unblockable) { items.push({ label: '🛇屏蔽所有主题帖', class: 'rinsp-thread-filter-hideop-button', action: () => promptBlockOp(thread.op, thread.opName, onUpdate) }); } setupPopupMenu({ title: '帖子功能选择', popupMenuId: THREAD_FILTER_ACTION_POPUP_MENU_ID, width: 150, anchor, rightAligned: true, onClose, items }); } function getTidMatchDirective(tid) { return 'tid:<' + tid + '>'; } function initThreadFilterExecutor(myUserId, threadFilter, userFilter, onUpdate, isGF) { const dislikePatternMatcher = createKeywordMatcherFactory(threadFilter.dislikes); const likePatternMatcher = createKeywordMatcherFactory(threadFilter.likes); return { run(thread) { this.clear(thread); const unblockable = thread.op === myUserId || THREAD_FILTER_EXEMPTED_USERS.has(thread.op); const tidDirective = getTidMatchDirective(thread.tid); const matchableTitle = thread.title.toLowerCase() + ' ' + tidDirective; const filterMenuButton = addElem(thread.titleCell, 'div', 'rinsp-thread-filter-menu-button'); filterMenuButton.addEventListener('click', () => { thread.row.classList.add('rinsp-thread-selected'); openFilterMenu(filterMenuButton, thread, () => thread.row.classList.remove('rinsp-thread-selected'), onUpdate, unblockable); }); thread.titleCell.appendChild(document.createTextNode(' ')); // put a space to avoid copying to include button text const dislikeUserRule = userFilter.users['#' + thread.op]; if (dislikeUserRule != null) { if (dislikeUserRule.hideThreads) { thread.row.classList.add('rinsp-thread-filter-dislike'); thread.row.classList.add('rinsp-thread-filter-dislike-byuid'); } } if (dislikePatternMatcher != null) { const matches = dislikePatternMatcher.match(matchableTitle); if (matches.size > 0) { thread.row.classList.add('rinsp-thread-filter-dislike'); thread.row.classList.add('rinsp-thread-filter-dislike-bytitle'); const filteredWords = addElem(thread.titleCell, 'div', 'rinsp-thread-filter-dislike-words'); if (matches.has(tidDirective)) { thread.row.classList.add('rinsp-thread-filter-dislike-bytid'); filteredWords.textContent = '🚫此帖已屏蔽'; filteredWords.addEventListener('click', async () => { if (confirm('取消屏蔽此帖?')) { removeBlockThread(thread.tid, onUpdate); } }); } else { filteredWords.textContent = '🚫' + Array.from(matches).sort().join(' ').replace(/^(.{20}).+/, '$1...'); filteredWords.addEventListener('click', async () => { const staticKeywords = Array.from(matches).filter(w => w !== tidDirective && dislikePatternMatcher.isStaticKeyword(w)); promptEditKeywords('请编辑屏蔽关键词 用空格分开', Array.from(staticKeywords), null, 'dislikes', onUpdate); }); } if (unblockable) { thread.row.classList.add('rinsp-thread-filter-bypass'); } else if (userConfig.customUserBypassThreadFilter) { if (userConfig.customUserHashIdMappings['#' + thread.op] != null) { thread.row.classList.add('rinsp-thread-filter-bypass'); } } } } if (likePatternMatcher != null) { const matches = likePatternMatcher.match(matchableTitle); if (matches.size > 0) { thread.row.classList.add('rinsp-thread-filter-like'); const filteredWords = addElem(thread.titleCell, 'div', 'rinsp-thread-filter-like-words'); if (matches.has(tidDirective)) { thread.row.classList.add('rinsp-thread-filter-like-bytid'); filteredWords.textContent = '💚标记帖'; filteredWords.addEventListener('click', async () => { if (confirm('移除标记?')) { removeLikeThread(thread.tid, onUpdate); } }); if (userConfig.showFavThreadFloatingList) { if (isGF) { userPinArea.addLocationHighlight(0, '💚:标记帖'); } else { let label = userConfig.showFavThreadFloatingList$withTitle ? '标记帖 - ' + thread.title : '标记帖'; userPinArea.addLocationHighlight(null, '💚:' + label, thread.row, `/read.php?tid-${thread.tid}.html`); } } } else { const matchList = Array.from(matches).sort(); filteredWords.textContent = '💚' + matchList.join(' ').replace(/^(.{20}).+/, '$1...'); filteredWords.addEventListener('click', async () => { const staticKeywords = matchList.filter(w => likePatternMatcher.isStaticKeyword(w)); promptEditKeywords('请编辑喜欢的关键词 用空格分开', staticKeywords, null, 'likes', onUpdate); }); if (userConfig.showFavThreadFloatingList) { if (isGF) { userPinArea.addLocationHighlight(0, '💚:' + matchList[0]); } else { let label = userConfig.showFavThreadFloatingList$withTitle ? matchList[0] + ' - ' + thread.title : matchList[0]; userPinArea.addLocationHighlight(null, '💚:' + label, thread.row, `/read.php?tid-${thread.tid}.html`); } } } } } if (thread.opElem) { if (thread.row.classList.contains('rinsp-thread-filter-dislike-byuid')) { const removeFilterOpButton = newElem('div', 'rinsp-thread-filter-unhideop-button'); removeFilterOpButton.textContent = '🚫'; thread.opElem.after(removeFilterOpButton); removeFilterOpButton.addEventListener('click', async () => { promptRemoveUserBlock(thread.op, thread.opElem.textContent.trim(), onUpdate); }); } } }, clear(thread) { thread.row.classList.remove('rinsp-thread-filter-like'); thread.row.classList.remove('rinsp-thread-filter-like-bytid'); thread.row.classList.remove('rinsp-thread-filter-dislike'); thread.row.classList.remove('rinsp-thread-filter-dislike-bytitle'); thread.row.classList.remove('rinsp-thread-filter-dislike-bytid'); thread.row.classList.remove('rinsp-thread-filter-dislike-byuid'); thread.row.querySelectorAll('.rinsp-thread-filter-like-words,.rinsp-thread-filter-dislike-words,.rinsp-thread-filter-menu-button,.rinsp-thread-filter-unhideop-button').forEach(el => el.remove()); } }; } async function applyThreadListEnhancement(myUserId, userConfig, threadList, threadFilter, userFilter, userPinArea, shareTypeFilter, settlementPostMatcher, threadHistoryAccess, updateCallback, fresh) { if (DEBUG_MODE) console.info('thread-list', threadList); const executors = [ initScoredThreadFilterExecutor(), initClosedThreadFilterExecutor(), initThreadFilterExecutor(myUserId, threadFilter, userFilter, onUpdate), initRequestThreadEnhancementExecutor(myUserId, userConfig, settlementPostMatcher), initMarkPaywellThreadExecutor(), initMarkPinnedUserThreadExecutor(userPinArea), ]; if (shareTypeFilter) { executors.push(initCategorizeShareTypeThreadExecutor(shareTypeFilter)); } if (threadHistoryAccess) { executors.push(initThreadHistoryExecutor(threadHistoryAccess)); } function onUpdate(updatedConfigs) { updateCallback(updatedConfigs); } let newThreadList; if (fresh || threadList.some(thread => !thread.row.classList.contains('rinsp-thread-inspected'))) { threadList.forEach(thread => { thread.row.classList.remove('rinsp-thread-inspected'); }); newThreadList = threadList; } else { return; } userPinArea.clearState(); for (let executor of executors) { if (typeof executor.runBatch === 'function') { await executor.runBatch(newThreadList); } else { const awaits = []; for (let thread of newThreadList) { const returnValue = executor.run(thread); if (returnValue instanceof Promise) { awaits.push(returnValue); } } if (awaits.length > 0) { await Promise.allSettled(awaits); } } } await userPinArea.update(); newThreadList.forEach(thread => { thread.row.classList.add('rinsp-thread-inspected'); }); const dislikeThreadCount = document.querySelectorAll('.rinsp-thread-filter-dislike').length; ignoreDislikeThreadToggler.setCount(dislikeThreadCount); const settlementCount = document.querySelectorAll('.rinsp-request-settlement-thread:not(.rinsp-request-settlement-bypass)').length; ignoreSettlementToggler.setCount(settlementCount); ignorePaywellToggler.setCount(document.querySelectorAll('.rinsp-thread-paywall').length); if (hideAnsweredRequestToggler) { hideAnsweredRequestToggler.setCount(document.querySelectorAll('.rinsp-request-thread.rinsp-request-thread-ended').length); } if (hideUnansweredRequestToggler) { hideUnansweredRequestToggler.setCount(document.querySelectorAll('.rinsp-request-thread.rinsp-request-thread-ongoing').length); } if (hideExpiredRequestToggler) { hideExpiredRequestToggler.setCount(document.querySelectorAll('.rinsp-request-thread.rinsp-request-thread-expired').length); } if (hideVisitedThreadToggler) { hideVisitedThreadToggler.setCount(document.querySelectorAll('.rinsp-thread-visited').length); } hideClosedThreadToggler.setCount(document.querySelectorAll('.rinsp-thread-filter-closed').length); } } // END: init() let forumUserBlacklistCache = { data: { banPosts: new Set(), banAvatars: new Set(), }, key: '' }; function getForumUserBlacklist() { if (forumUserBlacklistCache.key === localStorage.config||'') { return forumUserBlacklistCache.data; } const banPosts = new Set(); const banAvatars = new Set(); try { const data = JSON.parse(localStorage.config||'{}'); for (let entry of Object.entries(data.blist||{})) { for (let mode of entry[1].level||[]) { if (mode === 'topic') { banPosts.add(entry[0] * 1); } else if (mode === 'avatar') { banAvatars.add(entry[0] * 1); } } } } catch (ex) {} const data = { banPosts, banAvatars }; forumUserBlacklistCache = { data: data, key: localStorage.config||'' }; return data; } function isUserBlacklisted(userFilter, uid) { const userFilterRule = userFilter.users['#' + uid]; if (userFilterRule && userFilterRule.hideReplies) { return true; } return getForumUserBlacklist().banPosts.has(uid * 1); } function getPosts(doc, max) { const tidRefLink = doc.querySelector('#td_tpc .tiptop .fr a[href^="read.php?tid-"], th[id^="td_"] .tiptop .fr a[href^="read.php?tid-"]'); if (tidRefLink == null) { return []; } const tid = Number.parseInt(tidRefLink.getAttribute('href').substring(13)); const currentLevel = doc.querySelector('#breadcrumbs > a.crumbs-item[href^="thread.php?fid-"] + .crumbs-item.current'); let areaId = null; if (currentLevel) { areaId = Number.parseInt(currentLevel.previousElementSibling.getAttribute('href').substring(15)); } function getAuthor(mainCell) { const nameElem = mainCell.closest('tr').querySelector('.user-pic ~ div a[href^="u.php?action-show-uid-"] > strong'); return [Number.parseInt(nameElem.parentNode.getAttribute('href').substring(22)), nameElem.textContent.trim(), nameElem]; } function getFloorElem(mainCell) { return mainCell.querySelector('.tiptop > .fl > .s3'); } function getFloor(floorElem) { const flText = floorElem.textContent.trim(); return flText === 'GF' ? 0 : Number.parseInt(flText.substring(1)); } function getDate(mainCell) { return new Date(mainCell.querySelector('.tiptop > .fl + .fl.gray[title^="发表于"]').textContent.trim()) * 1; } const posts = []; doc.querySelectorAll('th.r_one[id^=td_] .tpc_content > div[id^="read_"]').forEach(function(contentElem) { if (max != null && posts.length >= max) { return; } const rootElem = contentElem.closest('.t2.t5 > table'); if (rootElem == null) { return; } const mainCell = contentElem.closest('th.r_one[id^=td_]'); const floorElem = getFloorElem(mainCell); const [postUid, postUname, nameElem] = getAuthor(mainCell); const userPicImg = rootElem.querySelector('.user-pic a[href^="u.php?action-show-uid-"] > img'); const userPic = userPicImg.closest('.user-pic'); const userPicImgSrc = getImgSrc(userPicImg); let defaultUserPic = false; if (userPicImgSrc.startsWith('images/face/')) { if (userPicImgSrc === 'images/face/none.gif') { defaultUserPic = 'none'; } else { defaultUserPic = 'preset'; } } const postDefaultHidden = rootElem.parentNode.getAttribute('hidden') != null; const puremarkContent = rootElem.querySelector('.tiptop > .js-puremark-content'); const postCollapsed = puremarkContent != null && (puremarkContent.getAttribute('style')||'').match(/\bdisplay *: *none(?:;|\b)/) == null; const postEid = contentElem.getAttribute('id'); const postId = postEid.split('_')[1] * 1; const post = { tid, areaId, postId, postUid, postUname, postTime: getDate(mainCell), floor: getFloor(floorElem), floorElem: getFloorElem(mainCell), rootElem, mainCell, contentElem, userNameElem: nameElem, userPicElem: userPic, defaultUserPic, postDefaultHidden, contentDefaultIgnorable: puremarkContent != null, postCollapsed }; posts.push(post); }); return posts; } function readThreadList() { const threadList = []; let divider = document.querySelector('tr.tr2 > .tac[colspan="6"]'); let prefixSelector = ''; if (divider && divider.textContent.trim() === '普通主题') { divider = divider.closest('tr'); divider.classList.add('rinsp-thread-section-divider'); prefixSelector = '.rinsp-thread-section-divider ~ '; let row = divider.previousElementSibling; while (row != null && row.classList.contains('t_one')) { const elem = row.querySelector('td[id^="td_"] > h3 > a[href^="read.php?tid-"]'); if (elem && elem.closest('td').querySelector('a.s8[href^="thread.php?fid-"]') != null) { collect(elem); } row = row.previousElementSibling; } threadList.reverse(); } else { divider = null; } document.querySelectorAll(prefixSelector + '.t_one > td[id^="td_"] > h3 > a[href^="read.php?tid-"], .rinsp-infscroll-divider ~ tbody .t_one > td[id^="td_"] > h3 > a[href^="read.php?tid-"], .spp-infinite-scroll-divider ~ tbody .t_one > td[id^="td_"] > h3 > a[href^="read.php?tid-"]').forEach(elem => { collect(elem); }); function collect(link) { const tid = Number.parseInt(link.getAttribute('href').substring(13)); const row = link.closest('tr'); const opElem = row.querySelector('td > a.bl[href^="u.php?action-show-uid-"]'); const countElem = row.querySelector('td.f10 > .s8'); let replyCount = 0; let hitCount = 0; if (countElem) { const match = countElem.parentElement.textContent.trim().match(/^(\d+)[\u00A0 ]*\/[\u00A0 ]*(\d+)$/); if (match) { replyCount = match[1] * 1; hitCount = match[2] * 1; } } const byCell = row.querySelector('td.tal.y-style > .f10 + br + .gray2'); const replyHashId = byCell ? (byCell.textContent.trim().match(/^by: ([a-f0-9]{8})$/)||[])[1]||null : null; threadList.push({ tid, row: row, title: getEffectiveThreadTitle(link.textContent), titleCell: link.closest('td'), opElem: opElem, opName: opElem ? opElem.textContent.trim() : null, op: opElem ? Number.parseInt(opElem.getAttribute('href').substring(22)) : null, opHashId: replyCount === 0 ? replyHashId : null, replyCount, hitCount }); } return threadList; } function readPicWallThreadList() { const threadList = []; document.querySelectorAll('#wall ul.stream > li > .inner').forEach(elem => { const row = elem.parentNode; const link = elem.querySelector('.section-title > a[href^="./read.php?tid-"]'); const tid = Number.parseInt(link.getAttribute('href').substring(15)); const countElem = elem.querySelector('.section-text > span + span'); let replyCount = 0; let hitCount = 0; if (countElem) { const match = countElem.textContent.trim().match(/:[\u00A0 ]*(\d+)[\u00A0 ]*\/[\u00A0 ]*(\d+)$/); if (match) { replyCount = match[1] * 1; hitCount = match[2] * 1; } } const opElem = elem.querySelector('.section-intro a.bl[href^="u.php?action-show-uid-"]'); threadList.push({ tid, row, title: getEffectiveThreadTitle(link.textContent), titleCell: link.parentNode, opElem: opElem, opName: opElem ? opElem.textContent.trim() : null, op: opElem ? Number.parseInt(opElem.getAttribute('href').substring(22)) : null, replyCount, hitCount }); }); return threadList; } function readSearchThreadList() { const threadList = []; document.querySelectorAll('table > tbody > tr.tr3.tac > th > a[href^="read.php?tid-"]').forEach(link => { const tid = Number.parseInt(link.getAttribute('href').substring(13)); const row = link.closest('tr'); const areaElem = row.querySelector('th + td > a[href^="thread.php?fid-"]'); const areaId = areaElem ? Number.parseInt(areaElem.getAttribute('href').substring(15)) : null; const areaName = areaElem ? areaElem.textContent.trim() : null; const opElem = row.querySelector('td.smalltxt > a[href^="u.php?action-show-uid-"]'); const replyCountElem = opElem.parentElement.nextElementSibling; const hitCountElem = replyCountElem.nextElementSibling; threadList.push({ tid, row: row, titleCell: link.closest('th'), title: getEffectiveThreadTitle(link.textContent), opElem: opElem, opName: opElem ? opElem.textContent.trim() : null, op: opElem ? Number.parseInt(opElem.getAttribute('href').substring(22)) : null, opHashId: opElem ? opElem.textContent.trim() : null, replyCount: replyCountElem.textContent.trim() * 1, hitCount: hitCountElem.textContent.trim() * 1, areaId: areaId, areaName: areaName }); }); return threadList; } function getEffectiveThreadTitle(rawTitle) { return rawTitle.replace(/^(\[(?:[三二]次元R18相关|全年龄正常向)\] *)+/g, '').replace(/\s+/g, ' ').trim(); } function getEffectiveTextContent(rootNode, preferTextOnly, treatAllEmojiTheSameWay) { if (rootNode.textContent.length > MAX_IGNORE_CONTENT_TEXT_LENGTH || rootNode.innerHTML.length > MAX_IGNORE_CONTENT_HTML_LENGTH) { return null; } const contentList = []; let externalImg = false; let complexData = false; function visit(node) { let endContent = null; if (node.classList.contains('jumbotron')) { contentList.push('<SELL>'); complexData = true; return; } switch (node.tagName) { case 'HR': return; case 'BR': contentList.push(' '); return; case 'SCRIPT': case 'STYLE': case 'BUTTON': case 'INPUT': case 'SELECT': case 'FORM': case 'VIDEO': case 'OBJECT': case 'FRAME': case 'IFRAME': contentList.push('<' + node.tagName + '>'); complexData = true; return; case 'IMG': const imgSrc = getImgSrc(node)||''; if (imgSrc.startsWith('images/post/smile/')) { let token; if (treatAllEmojiTheSameWay) { token = `<EMOJI>`; } else { const emojiId = imgSrc.substring(18).replace(/\//g, ':').replace(/\.[a-z]+$/, '').toUpperCase(); token = `<EMOJI:${emojiId}>`; } if (contentList.length === 0 || contentList[contentList.length - 1] !== token) { contentList.push(token); // skip consecutive emoji } } else { contentList.push('<EXT_IMG>'); externalImg = true; } return; case 'FONT': case 'CENTER': case 'I': case 'U': case 'B': case 'SUP': case 'SUB': case 'CODE': case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6': case 'H7': case 'H8': case 'H9': case 'P': case 'SPAN': case 'DIV': break; case 'BLOCKQUOTE': if (node.classList.contains('blockquote3')) { contentList.push('<QUOTE>'); complexData = true; return; } break; case 'A': default: contentList.push(`<${node.tagName}>`); endContent = `<${node.tagName}_END>`; break; } for (const childNode of node.childNodes) { if (childNode.nodeType === 3) { const text = childNode.textContent.toLowerCase(); if (text.length > 0) { const normText = simplifyText(text); if (normText) { contentList.push(normText); } } } else if (childNode.nodeType === 1) { visit(childNode); if (complexData) { return; } } } if (endContent != null) { contentList.push(endContent); } } visit(rootNode); if (complexData) { return null; } if (preferTextOnly) { const textOnly = contentList.filter(s=>s[0]!=='<').join('').replace(/\s+/g, ' ').trim(); if (textOnly) { const textWithoutSymbols = textOnly.replace(/!/g, ' ').replace(/ +/g, ' ').trim(); if (textWithoutSymbols) { return textWithoutSymbols; } return textOnly; } else if (externalImg) { return null; } } return contentList.join('') .replace(/\s+/g, ' ') .trim() || '<EMPTY>'; } function simplifyText(text) { const chars = Array.from(text); const normText = chars .reduce((arr, itm) => { if (arr.length === 0 || arr[arr.length - 1] !== itm) { arr.push(itm); } return arr; }, []) .join(''); return normText.trim().replace(/\s+/g, ' ').replace(/[~⁉`‘’'~!!??。,、,.<>]+/g, '!'); } function num(v) { return atob(v)*1; } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async function tryOpenUpdateSummary() { const lastVersion = await GM.getValue('last_version') * 1||0; if (VERSION_MAJOR > lastVersion) { await GM.setValue('last_version', String(VERSION_MAJOR)); GM.notification({ title: `南家功能强化(凛+) 已更新 ver.${VERSION_MAJOR}`, text: "请参阅功能简介帖" }); if (confirm(`凛+ 已更新 ${VERSION_MAJOR},打开功能简介帖?(新页面)`)) { GM.openInTab(INTRO_POST, false); } } } const allowingHookPosters = [1597333055699695, 501998265301213]; function applyHookDirectives(posts, myUserId, userConfig) { function handleDirective_if_not_installed(hook) { const minVersion = (hook.getAttribute('sparg-min-ver')||'0') * 1; if (VERSION_FULL >= minVersion) { hook.setAttribute('hidden', ''); hook.textContent = ''; } } function handleDirective_if_installed(hook) { const minVersion = (hook.getAttribute('sparg-min-ver')||'0') * 1; if (VERSION_FULL >= minVersion) { hook.removeAttribute('hidden'); } } function handleDirective_value_userhash(hook) { const prefix = hook.getAttribute('sparg-prefix')||''; const length = hook.getAttribute('sparg-length') * 1 || 6; const hashNum = cyrb53(prefix + myUserId); let hashStr = btoa(hashNum).replace(/[\+\/=]/g, '').split('').reverse().join(''); while (hashStr.length < length) { hashStr = hashStr + hashStr; } hook.textContent = hashStr.substring(0, length).toUpperCase(); } function handleDirective_value_version(hook) { hook.textContent = VERSION_TEXT; } function handleDirective(hook, directive) { switch (directive) { case 'when-not-installed': return handleDirective_if_not_installed(hook); case 'when-installed': return handleDirective_if_installed(hook); case 'value-userhash': return handleDirective_value_userhash(hook); case 'value-version': return handleDirective_value_version(hook); } } posts .filter(post => allowingHookPosters.indexOf(cyrb53(''+post.postUid, 233)) !== -1) .forEach(post => { post.contentElem.querySelectorAll('.rinsp-sp-hook').forEach(hook => { hook.classList.remove('rinsp-sp-hook'); const directive = hook.getAttribute('sp-directive'); hook.removeAttribute('sp-directive'); if (directive) { handleDirective(hook, directive); } }); }); } function applyReplyRefreshFree(tid, lastPost, myUserId) { const pagination = document.querySelector('.pages .pagesone'); let onLastPage = true; if (pagination) { const match = pagination.textContent.match(/Pages: (\d+)\/(\d+)/); if (match && match[1] !== match[2]) { onLastPage = false; } } const replyForm = document.FORM; if (replyForm == null || replyForm.getAttribute('action') !== 'post.php?') { return; } if (replyForm.getAttribute('onsubmit') == null) { return; } const lastPageUrl = `${document.location.origin}/read.php?tid=${tid}&page=e&#a`; replyForm.addEventListener('submit', evt => { let ok = 1; try { ok = unsafeWindow.checkpost(replyForm); } catch (ignore) { console.warn('fail to execute unsafeWindow.checkpost', ignore); } if (!ok) { evt.preventDefault(); return false; } executeSubmit() .catch(err => { console.error(err); document.location.href = lastPageUrl; }) .finally(() => { replyForm.classList.remove('rinsp-refresh-free-submitting'); // unlock and reset form for resubmission replyForm.Submit.disabled = false; replyForm.encoding = 'multipart/form-data'; if (ok === true) { // meaning checkpost was executed successfully unsafeWindow.cnt = 0; } }); evt.preventDefault(); return false; }); replyForm.classList.add('rinsp-reply-refresh-free'); replyForm.removeAttribute('onsubmit'); let lastPostId = lastPost.postId; async function executeSubmit() { replyForm.classList.add('rinsp-refresh-free-submitting'); const resp = await fetch(`${document.location.origin}/post.php`, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', body: new FormData(replyForm) }); if (!resp.ok) { throw Error('request failed'); } const postFeedback = await resp.text(); const parser = new DOMParser(); const postFeedbackDoc = parser.parseFromString(postFeedback, 'text/html'); const err = findErrorMessage(postFeedbackDoc); if (err) { alert(err.replace(/\s+/g, ' ')); return; } if (onLastPage) { const newDoc = await fetchGetPage(lastPageUrl); const knownLastPost = newDoc.querySelector(`#td_${lastPostId||'tpc'}`); if (knownLastPost == null) { document.location.href = lastPageUrl; return; } const newRows = []; let myNewPostIdAttr = null; let row = knownLastPost.closest('.t5.t2'); if (row.nextElementSibling && row.nextElementSibling.classList.contains('menu')) { row = row.nextElementSibling; if (row.nextElementSibling && row.nextElementSibling.matches('a[name="a"]')) { row = row.nextElementSibling; } } while ((row = row.nextElementSibling) != null) { newRows.push(row.outerHTML); if (row.querySelector(`.user-pic a[href="u.php?action-show-uid-${myUserId}.html"]`)) { myNewPostIdAttr = row.querySelector('th[id^="td_"]').getAttribute('id'); } } if (myNewPostIdAttr == null) { // can't find new post, just load last page instead document.location.href = lastPageUrl; } const postList = lastPost.rootElem.closest('form'); const tmpElem = newElem('div'); tmpElem.innerHTML = newRows.join('\n'); Array.from(tmpElem.children).forEach(node => postList.appendChild(node)); tmpElem.remove(); lastPostId = Number.parseInt(Array.from(postList.querySelectorAll('.t5.t2 th[id^="td_"]')).at(-1).getAttribute('id').substring(3)); setTimeout(() => { document.querySelector(`#${myNewPostIdAttr}`).closest('.t5.t2').scrollIntoView({ behavior: 'smooth', block: 'center' }); }); document.FORM.atc_content.value = ''; document.FORM.querySelector('#attach').innerHTML = ''; try { if (unsafeWindow.newAtt) { // attachment might be disabled, check if handler exists first unsafeWindow.newAtt.create(); } } catch (ignore) {} } else { document.location.href = lastPageUrl; } } } function applySubjectLineEnhancement(posts, userConfig) { let threadTitle = null; if (userConfig.hideRedundantReSubjectLine) { threadTitle = (document.querySelector('#breadcrumbs .crumbs-item.current > strong')||{}).textContent; if (threadTitle) { threadTitle = threadTitle.replace(/\s|\u00A0|&[a-z;]{0,5}$/g, '').trim(); } } const pageArr = readCurrentAndMaxPage(document); const currentPage = pageArr[0]; const maxPage = pageArr[1]; const form = document.querySelector('form[name="delatc"]'); let pageSize = 0; if (currentPage === 1) { if (maxPage > currentPage) { for (let child of form.children) { if (child.matches('.rinsp-infscroll-divider')) { break; } if (child.matches('.t5.t2')) { pageSize++; } } } } else { pageSize = posts[0].floor / (currentPage - 1); } function getFloorTarget(targetFloor) { let targetElem = posts.filter(post => post.floor === targetFloor).map(post => post.rootElem)[0]; let targetPage = pageSize === 0 ? 1 : Math.floor(targetFloor / pageSize)+1; let hash = targetFloor > 0 ? `#fl-${targetFloor}` : ''; if (targetPage > 1) { return { url: `/read.php?tid-${posts[0].tid}-fpage-0-toread--page-${targetPage}.html${hash}`, targetElem }; } else { return { url: `/read.php?tid-${posts[0].tid}.html${hash}`, targetElem }; } } posts.forEach(post => { if (post.rootElem.classList.contains('rinsp-subject-inspected')) { return; } post.rootElem.classList.add('rinsp-subject-inspected'); const subjectLine = post.mainCell.querySelector('.h1.fl > h1[id^="subject_"]'); if (subjectLine) { if (post.floor === 0) { const textNode = subjectLine.childNodes[0]; if (textNode) { const prefix = textNode.textContent.match(/^(\[(?:二次元R18相关|三次元R18相关|全年龄正常向)\])/); if (prefix) { textNode.textContent = textNode.textContent.substring(prefix[1].length); const classTag = newElem('span', 'rinsp-subject-class'); classTag.textContent = prefix[1]; subjectLine.insertBefore(classTag, textNode); } } } if (threadTitle && post.floor > 0) { const subject = subjectLine.textContent.replace(/\s|\u00A0|&[a-z;]{0,5}$/g, '').trim(); if (subject.startsWith('Re:') && threadTitle.startsWith(subject.substring(3))) { subjectLine.parentNode.classList.add('rinsp-subject-redundant'); } } } function addClickableLine(targetLine, floor, match) { const target = getFloorTarget(floor); addElem(targetLine, 'span').textContent = match[1]; const link = addElem(targetLine, 'a', 'rinsp-subject-floor-link', { href: target.url, target: '_blank' }); link.textContent = match[2]; addElem(targetLine, 'span').textContent = match[3]; if (target.targetElem) { link.addEventListener('click', evt => { evt.preventDefault(); target.targetElem.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); } } if (userConfig.addQuickJumpReSubjectLine) { if (subjectLine && subjectLine.childNodes.length === 1 && subjectLine.childNodes[0].nodeType === 3) { const match = subjectLine.textContent.match(/^((?:Re:)?回 )(\d+楼|楼主)(\(.+ 的帖子)$/); if (match) { subjectLine.textContent = ''; const floor = Number.parseInt(match[2])||0; addClickableLine(subjectLine, floor, match); if (userConfig.hideRedundantReSubjectLine && floor === 0) { subjectLine.classList.add('rinsp-subject-redundant'); } } } post.mainCell.querySelectorAll('.blockquote3 > .quote2 + div').forEach(quote => { if (quote.childNodes.length > 1 && quote.childNodes[1].nodeType === 1 && quote.childNodes[1].tagName === 'BR') { const textNode = quote.childNodes[0]; if (textNode.nodeType === 3) { const match = textNode.textContent.match(/^(引用 *)(第\d+楼|楼主)(.*)$/); if (match) { const floor = Number.parseInt(match[2].substring(1))||0; const quoteLine = newElem('span'); addClickableLine(quoteLine, floor, match); quote.childNodes[0].replaceWith(quoteLine); } } } }); } }); } function getPostMetadata(postContent) { const row = postContent.closest('form > .t5.t2'); if (row == null) return null; const link = row.querySelector('.tiptop a[href^="read.php?tid-"]'); const match = link.getAttribute('href').match(/\?tid-(\d+)(?:-uid-(\d+))?\.html/); let tid = match[1] * 1; let uid; if (match[2] == null) { uid = Number.parseInt(link.closest('tr').querySelector('.user-pic a[href^="u.php?action-show-uid-"]').getAttribute('href').substring(22)); } else { uid = match[2] * 1; } const pid = row.querySelector('.tpc_content > div[id^="read_"]').getAttribute('id').substring(5); return { tid, uid, pid }; } function enhanceBuyButtons(userConfig, triggerRefresh) { document.querySelectorAll('span[id^="att_"]:not(.rinsp-att-enhanced) > a[href^="job.php?action-download-"]').forEach(function(attLink) { const root = attLink.parentNode; root.classList.add('rinsp-att-enhanced'); const price = (root.textContent.match(/ 售价:(\d+)SP币/)||[])[1] * 1; if (Number.isNaN(price) || price <= 0) { return; } const href = attLink.getAttribute('href'); attLink.dataset.price = String(price); attLink.removeAttribute('href'); attLink.setAttribute('buyhref', href); root.classList.add('rinsp-att-sell'); if (userConfig.enhanceSellFrame && price > 5) { root.classList.add('rinsp-att-sell-high'); } attLink.addEventListener('click', () => { if (attLink.getAttribute('href')) return; if (price > 5) { let msg = `警告: 高额附件 ${price} SP 请确认购买?`; if (!confirm(msg)) { return false; } } attLink.setAttribute('href', href); }); }); document.querySelectorAll('.quote.jumbotron > .s3 + .btn-danger:not(.rinsp-sell-relay-button):not(.rinsp-sell-enhanced)').forEach(function(buyButton) { const tpc = buyButton.closest('.tpc_content'); const sellFrame = buyButton.closest('h6.jumbotron'); buyButton.classList.add('rinsp-sell-enhanced'); sellFrame.querySelectorAll('.rinsp-sell-relay-button').forEach(btn => btn.remove()); // remove any existing relay button and recreate again const relayButton = newElem('input', 'btn btn-danger rinsp-sell-relay-button', { type: 'button' }); buyButton.parentNode.insertBefore(relayButton, buyButton); let priceMatch = sellFrame.querySelector('.s3').textContent.match(/此帖售价 (\d+) SP币,已有 (\d+) 人购买/); let price = priceMatch[1] * 1; if (userConfig.enhanceSellFrame) { if (price === 0) { relayButton.setAttribute('value', '免费'); sellFrame.classList.add('rinsp-sell-free'); } else if (price <= 5) { relayButton.setAttribute('value', `愿意购买, 售价 ${price} SP`); sellFrame.classList.add('rinsp-sell-5'); } else if (price <= 100) { relayButton.setAttribute('value', `警告: 高额 ${price} SP 出售 (欢迎举报)`); sellFrame.classList.add('rinsp-sell-100'); } else if (price >= 99999) { relayButton.setAttribute('value', '只楼主可见'); sellFrame.classList.add('rinsp-sell-99999'); } else { relayButton.setAttribute('value', `警告: 高额 ${price} SP 出售 (欢迎举报)`); sellFrame.classList.add('rinsp-sell-high'); } } else { relayButton.setAttribute('value', buyButton.getAttribute('value')); } relayButton.addEventListener('click', evt => { evt.stopPropagation(); if (price >= 99999) { return false; } else if (price > 5) { if (!confirm('高额出售 确认购买?')) { return false; } } if (userConfig.buyRefreshFree) { relayButton.classList.add('rinsp-sell-buying'); try { const handled = executeBackgroundBuy(tpc, triggerRefresh); if (handled) { return false; } } catch (err) { console.error('exception when handling buy', err); } } // if failed, fallback to execute original buy action buyButton.click(); return false; }); }); } function executeBackgroundBuy(tpc, triggerRefresh) { const pm = getPostMetadata(tpc); if (pm == null) { return false; } const {tid, uid, pid} = pm; const contentHref = `${document.location.origin}/read.php?tid-${tid}-uid-${uid}.html`; // one page should be enough to find the bought post const sellframes = []; tpc.querySelectorAll('.quote.jumbotron + blockquote').forEach(el => { el.innerHTML = '正在购买 ...'; sellframes.push(el); }); tpc.querySelectorAll('.quote.jumbotron').forEach(el => { el.innerHTML = ''; }); async function run() { let feedbackPage; while (true) { feedbackPage = await fetch(`${document.location.origin}/job.php?action=buytopic&tid=${tid}&pid=${pid}&verify=${verifyhash()}`, { method: 'GET', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) .then(resp => resp.text()); if (feedbackPage.indexOf('刷新不要快于') === -1) { break; } await sleep(1000); } if (feedbackPage.match(/>\s*操作完成\s*</)) { let html; while (true) { await sleep(1000); html = await fetch(contentHref, { method: 'GET', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) .then(resp => resp.text()); if (html.indexOf('刷新不要快于') === -1) { break; } } const doc = new DOMParser().parseFromString(html, 'text/html'); let purchasedContent = doc.querySelector('#read_' + pid); if (purchasedContent) { const oldContent = document.querySelector('#read_' + pid); oldContent.parentNode.replaceChild(purchasedContent, oldContent); } else { throw Error('content outdated'); } } else { const title = feedbackPage.match(/<title>([^-<]+)/); if (title) { const reason = title[1].trim(); sellframes.forEach(el => { el.classList.add('rinsp-buy-failed'); el.textContent = reason; }); } } triggerRefresh(); } run().catch(e => { console.info('buy-refresh-free ERR', e); window.location.reload(); }); return true; } function autoCheckReply() { let cb = document.querySelector('form[action="post.php?"] input[name="atc_newrp"]'); if (cb) { cb.checked = true; } } async function addUserPinArea(pinnedUsersConfigAccess, hideInactivePinnedUsers, showResourceSpotsFloating) { document.addEventListener('click', evt => { if (evt.target && evt.target.classList.contains('rinsp-button-placeholder-pinuser')) { const uid = evt.target.dataset.uid * 1; const nickName = evt.target.dataset.nickname; const img = evt.target.dataset.img; if (pinnedUsers.has(uid)) { unwatchUser(uid); } else { watchUser(uid, nickName, img); } } }); const pinMain = addElem(document.body, 'div', 'rinsp-pinuser-main'); let spotsMain = null; if (showResourceSpotsFloating) { spotsMain = addElem(document.body, 'div', 'rinsp-spots-main'); } let spotsBar = null; const threadHeaderAnchor = document.querySelector('#main > .h2 > .fr.w'); if (threadHeaderAnchor) { const targetControlBar = threadHeaderAnchor.parentNode; targetControlBar.classList.add('rinsp-thread-header'); spotsBar = newElem('div', 'rinsp-spots-bar'); targetControlBar.insertBefore(spotsBar, targetControlBar.childNodes[0]); } let updateListeners = []; const pinnedUsers = new Map([[0, null]]); // 0 entry marks it out of date const localWatchUsers = new Map(); const locationHighlights = []; async function update(notifyListeners) { if (pinnedUsers.has(0)) { const pinnedUsersConfig = await pinnedUsersConfigAccess.read(); pinnedUsers.clear(); Object.values(pinnedUsersConfig.users).forEach(record => { pinnedUsers.set(record.uid, record); }); } pinMain.innerHTML = ''; if (spotsMain) { spotsMain.innerHTML = ''; } if (spotsBar) { spotsBar.innerHTML = ''; } function createSpotButton(spot) { let locButton = newElem('a', null, { href: spot.href||'javascript:', target: '_blank' }); if (spot.floor != null) { const flootLabel = spot.floor === 0 ? 'GF' : '' + spot.floor + 'F'; addElem(locButton, 'span', 'rinsp-spot-floor').textContent = flootLabel; } const pair = spot.type.split(':', 2); addElem(locButton, 'span', 'rinsp-spot-icon').textContent = pair[0]; addElem(locButton, 'span', 'rinsp-spot-label').textContent = pair[1]; locButton.addEventListener('click', evt => { evt.preventDefault(); let alignment = spot.alignment||'center'; if (alignment === 'center') { let diff = spot.elem.getBoundingClientRect().height - window.screen.availHeight; if (diff > window.screen.availHeight / 3) { // if target is larger than 2/3 of the screen, align top alignment = 'start'; } } if (alignment === 'start') { window.scrollTo({ behavior: 'smooth', top: spot.elem.getBoundingClientRect().top - document.body.getBoundingClientRect().top - 50 }); } else { spot.elem.scrollIntoView({ behavior: 'smooth', block: alignment }); } }); return locButton; } if (locationHighlights.length > 0) { if (spotsMain) { const list = addElem(spotsMain, 'div', 'rinsp-highlight-spots'); addElem(list, 'b').textContent = '焦点'; const dl = addElem(list, 'dl'); locationHighlights.forEach(spot => { let locItem = addElem(dl, 'dt'); locItem.appendChild(createSpotButton(spot)); }); } if (spotsBar) { locationHighlights.forEach(spot => { spotsBar.appendChild(createSpotButton(spot)); }); } } const timeComparator = comparator('time'); const pinnedRecords = Array.from(pinnedUsers.values()).filter(record => !hideInactivePinnedUsers || record.locations && record.locations.length > 0).sort(timeComparator); if (pinnedRecords.length + localWatchUsers.size > 0) { const list = addElem(pinMain, 'div', 'rinsp-pinuser-list'); if (pinnedRecords.length > 0) { addElem(list, 'b').textContent = '焦点人物'; const listitems = addElem(list, 'div'); pinnedRecords.forEach(record => { renderUserRecord(listitems, record); }); } if (localWatchUsers.size > 0) { addElem(list, 'b').textContent = '活跃人物'; const listitems = addElem(list, 'div'); Array.from(localWatchUsers.values()) .sort((x, y) => { const xc = (x.locations||[]).length; const yc = (y.locations||[]).length; if (xc > yc) return -1; if (xc < yc) return 1; return timeComparator(x, y); }) .forEach(record => { renderUserRecord(listitems, record); }); } } if (notifyListeners) { updateListeners.forEach(listener => { listener(); }); } function renderUserRecord(parent, record) { const item = addElem(parent, 'div', 'rinsp-pinuser-item'); const face = addElem(item, 'div', 'rinsp-pinuser-item-face'); const locs = addElem(item, 'dl', 'rinsp-pinuser-item-locs'); const img = addElem(face, 'img', null, { src: record.img }); const isDefaultImg = record.img == null || record.img.startsWith('images/face/'); const label = addElem(face, 'div'); addElem(label, 'a', null, { href: `u.php?action-show-uid-${record.uid}.html`, target: '_blank' }).textContent = record.nickName||`#${record.uid}`; const unpin = addElem(label, 'div', 'rinsp-pinuser-unpin-icon'); unpin.addEventListener('click', () => { unwatchUser(record.uid); }); const locCount = record.locations ? record.locations.length : 0; if (locCount > 0) { const locButtons = []; for (let loc of record.locations) { let locItem = addElem(locs, 'dt', loc.style ? 'rinsp-pinuser-loc-' + loc.style : null); let locButton = addElem(locItem, 'a', null, { href: 'javascript:' }); locButton.textContent = loc.label; locButton.addEventListener('click', loc.action); locButtons.push(locButton); } let lastIndex = -1; img.addEventListener('click', () => { lastIndex += 1; if (lastIndex >= locButtons.length) { lastIndex = 0; } locButtons[lastIndex].click(); }); if (isDefaultImg) { const estHeight = Math.max(locCount * 14, 32); if (estHeight < 100) { img.setAttribute('style', `max-height:${estHeight}px; object-fit:contain;background:#FFF`); } } } else { item.classList.add('rinsp-pinuser-item-absent'); } } } async function unwatchUser(uid) { await pinnedUsersConfigAccess.update(function(updatingPinnedUsersConfig) { delete updatingPinnedUsersConfig.users['#' + uid]; // clean up corrupt data ... delete updatingPinnedUsersConfig.users['#NaN']; delete updatingPinnedUsersConfig.users['#null']; return updatingPinnedUsersConfig; }); pinnedUsers.set(0, null); await update(true); } async function watchUserLocally(uid, nickName, img) { if (pinnedUsers.has(uid)) { return; } localWatchUsers.set(uid, { uid, nickName, img, time: Date.now(), locations: [] }); } async function watchUser(uid, nickName, img) { await pinnedUsersConfigAccess.update(function(updatingPinnedUsersConfig) { updatingPinnedUsersConfig.users['#' + uid*1] = { uid, nickName, img, time: Date.now(), locations: [] }; return updatingPinnedUsersConfig; }); pinnedUsers.set(0, null); await update(true); } function guessInterestingContent(text, spotsFilter) { const textLower = text.toLowerCase(); if (spotsFilter.shares) { if (textLower.indexOf('pan.baidu.com/') != -1) { return '📂:分享 (度盘)'; } if (textLower.indexOf('pan.quark.cn/') != -1) { return '📂:分享 (夸克)'; } if (textLower.indexOf('/mypikpak.com/s/') != -1) { return '📂:分享 (PP)'; } if (textLower.indexOf('://mega.nz/') != -1) { return '📂:分享 (MEGA)'; } if (textLower.indexOf('/drive.google.com/') != -1) { return '📂:分享 (谷歌)'; } if (textLower.indexOf('/1drv.ms/f/s!') != -1 || textLower.match(/:\/\/[a-zA-Z-]+\.sharepoint\.com\/:/) || textLower.indexOf('-my.sharepoint.com/:') != -1) { return '📂:分享 (OD)'; } if (textLower.indexOf('.lanzoub.com') != -1 || textLower.indexOf('/share.weiyun.com/') != -1 || textLower.indexOf('.aliyundrive.com') != -1 || textLower.indexOf('.123pan.com/') != -1 || textLower.indexOf('pan.xunlei.com/') != -1 || textLower.indexOf('cowtransfer.com/s/') != -1 || textLower.indexOf('/files.catbox.moe/') != -1 || textLower.indexOf('/litter.catbox.moe/') != -1 || textLower.indexOf('/pixeldrain.com/l/') != -1 || textLower.indexOf('/pixeldrain.com/u/') != -1 || textLower.indexOf('/gofile.io/d/') != -1 || textLower.indexOf('/uploadhaven.com/download/') != -1 || textLower.indexOf('/racaty.com/') != -1 || textLower.indexOf('.swisstransfer.com/d/') != -1 || textLower.indexOf('.gigafile.nu/') != -1 || textLower.indexOf('/sakuradrive.com/s/') != -1 || textLower.indexOf('/dogpan.com/s/') != -1) { return '📂:分享'; } if (textLower.match(/https?:\/\/([a-z0-9]+\.)?[a-z0-9]+\.(com|net|io)\/s\//)) { return '📂:分享'; } if (textLower.match(/(?:[^a-zA-Z0-9]|^)(?:bafy[a-z0-9]{55}|Qm[A-Za-z0-9]{44})(?:[^a-zA-Z0-9]|$)/)) { return '🧲:ipfs'; } if (textLower.indexOf('magnet:?') != -1) { return '🧲:磁链'; } if (textLower.indexOf('ed2k://') != -1) { return '🧲:ed2k'; } if (textLower.indexOf('thunder://') != -1) { return '🧲:迅雷'; } if (textLower.indexOf('/jmj.cc/s/') != -1 || textLower.indexOf('.feimaoyun.com/') != -1 || textLower.indexOf('/rosefile.net/') != -1 || textLower.indexOf('.xun-niu.com/file-') != -1 || textLower.indexOf('.xunniu-pan.com/file-') != -1) { return '💲:网赚盘'; } } } async function offerPostContent(post, spotsFilter, likePatternMatcher) { if (spotsFilter.likes && likePatternMatcher) { scanTextualContent(childText => { const matches = likePatternMatcher.match(childText.toLowerCase()); if (matches.size > 0) { return '💚:' + Array.from(matches)[0]; } else { return null; } }); } if (spotsFilter.sells) { const sellArea = post.contentElem.querySelector('.quote.jumbotron > .s3 + .btn-danger'); if (sellArea) { const jumbotron = sellArea.closest('.jumbotron'); let type = '💰:出售框'; if (jumbotron.classList.contains('rinsp-sell-99999')) { type = '🔒:密享'; } locationHighlights.push({ floor: post.floor, type: type, elem: jumbotron }); return; } } const found = scanTextualContent(childText => guessInterestingContent(childText, spotsFilter)); if (found) { return; } let links = post.contentElem.querySelectorAll('a[href]:not(.rinsp-subject-floor-link)'); for (let link of links) { let hit = guessInterestingContent(link.getAttribute('href'), spotsFilter); if (hit) { locationHighlights.push({ floor: post.floor, type: hit, elem: link }); return; } } if (spotsFilter.images) { for (let img of post.contentElem.closest('.tpc_content').querySelectorAll('img[src],img[data-rinsp-defer-src]')) { let src = getImgSrc(img); if (src.startsWith('images/')) { continue; // skip default emotions } if (!img.classList.contains('rinsp-img-loading') && img.naturalWidth > 0 && img.naturalHeight > 0 && img.naturalWidth < 64 && img.naturalHeight < 64) { continue; // skip tiny images } locationHighlights.push({ floor: post.floor, type: '🖼️:图片', elem: img, alignment: 'start' }); return; } for (let link of links) { let href = link.getAttribute('href'); if (href.match(/\.(png|jpg|jpeg|gif|avif|webp)$/)) { locationHighlights.push({ floor: post.floor, type: '🖼️:图片', elem: link, alignment: 'start' }); return; } } } if (spotsFilter.attachments) { const attFile = post.rootElem.querySelector('a[href^="job.php?action-download-"]'); if (attFile) { locationHighlights.push({ floor: post.floor, type: '📄:附件', elem: attFile }); return; } } if (spotsFilter.sells) { const sellArea = post.contentElem.querySelector('h6.quote.jumbotron + blockquote.jumbotron'); if (sellArea) { let type; if (sellArea.textContent.startsWith('若发现会员采用欺骗的方法获取财富')) { type = '💰:出售框'; } else { type = '✔️:已购买'; } locationHighlights.push({ floor: post.floor, type: type, elem: sellArea }); return; } } if (spotsFilter.links) { for (let link of links) { if (link.querySelector('img') == null || link.textContent.length > 0) { locationHighlights.push({ floor: post.floor, type: '🔗:链接', elem: link }); return; } } } function scanTextualContent(handler) { let lastNode = null; for (let childNode of post.contentElem.childNodes) { let childText; if (childNode.nodeType === 3) { childText = childNode.textContent.trim(); } else if (childNode.nodeType === 1) { childText = childNode.textContent.trim(); lastNode = childNode; } if (childText) { let hit = handler(childText, spotsFilter); if (hit) { if (lastNode) { locationHighlights.push({ floor: post.floor, type: hit, elem: lastNode }); } else { locationHighlights.push({ floor: post.floor, type: hit, elem: post.rootElem, alignment: 'start' }); } return true; } } } return false; } } await update(); return { addUpdateListener(listener) { updateListeners.push(listener); }, fireUpdateListeners() { updateListeners.forEach(listener => { listener(); }); }, getWatchCount() { return pinnedUsers.size + localWatchUsers.size; }, isPinned(uid) { return pinnedUsers.has(uid); }, getPinned(uid) { const record = pinnedUsers.get(uid); if (record) { return { uid: uid, nickName: record.nickName, img: record.img }; } else { return null; } }, isWatching(uid) { return pinnedUsers.has(uid) || localWatchUsers.has(uid); }, clearState() { for (let record of pinnedUsers.values()) { record.locations = []; } localWatchUsers.clear(); locationHighlights.length = 0; }, addLocation(uid, label, locClass, action) { let record = pinnedUsers.get(uid); if (record == null) { record = localWatchUsers.get(uid); } if (record) { record.locations.push({ label, style: locClass, action }); } }, unwatchUser, watchUser, watchUserLocally, offerPostContent, addLocationHighlight(floor, type, elem, href) { locationHighlights.push({ floor, type, elem, href }); }, update }; } const TITLE_SPLIT_PATTERN = /[\u3000-\u303F()/:,\s\[\]\{\}\(\)\\\/<>\u00A0➕\+]+|(?<![妙秒喵])[轉传]|原档|原檔|分享|下载|下載|长期|長期|直[链连連]接?|(?<![妙秒喵])[链连連]接?|[网網云雲][盘盤]|[盘盤云雲]|有效期?|(?<![0-9a-z])\d+(?:\.\d{1,2})?(?:[GMKT](?:i?B)?)|(?<![0-9a-z])\d{1,3}(?:[日天]|个?月)/i; const SHARE_TYPES = [ { name: '度盘', pattern: /(?:百?度(?:[标標][准準])?[长長短]?[盘盤云雲链连連]|百?度娘|毒[盘盤云雲链连連]|[妙秒喵][传傳链连連])+|^(?:bd|百度)$/i, }, { name: 'OD', pattern: /(?:onedrive|one drive|微软盘)|^(?:OD|微软)$/i, }, { name: 'Google盘', pattern: /(?:GoogleDrive|Google Drive)|(?:GD|Google|谷歌)$/i, }, { name: 'MEGA', pattern: /(?:mega|mg)[盘盤云雲]|^(?:mega|mg)$/i, }, { name: '樱盒', pattern: /(?:樱花?[盒盘盤云雲])|^(?:樱花)$/i, }, { name: '微云', pattern: /(?:微[云雲][盘盤]?)/, }, { name: '移动云', pattern: /(?:移动[云雲][盘盤]?)/, }, { name: '夸克', pattern: /(?:夸克|Quack|夸[盘盤云雲])/, }, { name: '阿里云', pattern: /(?:阿里|aliyun|aliyundrive)[网網]?[盘盤云雲]?|ali[网網]?[盘盤云雲]|^(?:阿里|aliyun|ali)$/i, }, { name: 'mediafire', pattern: /^(?:mediafire)$/i, }, { name: 'ipfs', pattern: /^(?:ipfs)$/i, }, { name: 'BT', pattern: /^(?:磁[力链鏈]|bt|bittorrent|torrent)$/i, }, { name: 'TERABOX', pattern: /(?:terabox)/i, }, { name: 'catbox', pattern: /(?:catbox|[猫喵][猫喵]?盒)/i, }, { name: 'PikPak', pattern: /(?:pikpak|p[网網]?[盘盤云雲])|^(?:pp)$/i, }, { name: 'pixeldrain', pattern: /(?:pixeldrain)|^(?:pd)$/i, }, { name: 'gofile', pattern: /(?:gofile)/i, }, { name: '115', pattern: /^(?:ed2k|115ed2k|115)$/i, }, { name: 'RF', pattern: /(?:rosefile)|^(?:RF)$/i, }, { name: '飛猫', pattern: /^(?:飛[猫喵]|FM)$/i, }, { name: '迅牛', pattern: /^(?:迅牛|XN)$/i, }, { name: '不明', pattern: /^(?:多空|多[盘盤])$/, }, ]; const SHARE_TYPES_MIXED_SPLIT_PATTERNS = /(bd|od|gd|mg)/i; const SHARE_UNKNOWN_NAME = '不明'; async function addShareTypeFilterArea(target, sharetypeFilterConfigAccess) { let updateListeners = []; const controlBlock = addElem(target, 'div', 'rinsp-sharetype-filter'); const states = new Map(); const sharetypeFilterConfig = await sharetypeFilterConfigAccess.read(); SHARE_TYPES.map(shareTypeDef => shareTypeDef.name).forEach(shareType => { const toggler = addElem(controlBlock, 'div', 'rinsp-sharetype-item'); toggler.textContent = shareType; const model = { count: 0, toggler, disabled: sharetypeFilterConfig.hides.indexOf(shareType) !== -1 }; states.set(shareType, model); toggler.addEventListener('click', async () => { model.disabled = !model.disabled; sharetypeFilterConfigAccess.update(function(updatingSharetypeFilterConfigAccess) { const set = new Set(updatingSharetypeFilterConfigAccess.hides||[]); if (model.disabled) { set.add(shareType); } else { set.delete(shareType); } updatingSharetypeFilterConfigAccess.hides = Array.from(set); return updatingSharetypeFilterConfigAccess; }); update(true); }); }); const clearButton = addElem(controlBlock, 'img', 'rinsp-sharetype-filter-clear', { src: '/images/close.gif'}); clearButton.addEventListener('click', async () => { states.forEach(model => { model.disabled = false; }); sharetypeFilterConfigAccess.update(function(updatingSharetypeFilterConfigAccess) { updatingSharetypeFilterConfigAccess.hides = []; return updatingSharetypeFilterConfigAccess; }); update(true); }); async function update(notifyListeners) { let disabledCount = 0; states.forEach(state => { state.toggler.setAttribute('count', state.count); state.toggler.setAttribute('disabled', state.disabled ? '1' : '0'); if (state.disabled) { disabledCount++; } }); if (notifyListeners) { updateListeners.forEach(listener => { listener(); }); } clearButton.setAttribute('disabled', disabledCount === 0 ? '1' : '0'); } await update(); return { addUpdateListener(listener) { updateListeners.push(listener); }, clearState() { states.forEach(entry => entry.count = 0); }, addAndAcceptThread(tid, title) { const shares = []; const parts = title.split(TITLE_SPLIT_PATTERN); parts.push(title); nextpart: for (let part of parts) { for (let shareTypeDef of SHARE_TYPES) { if (part.match(shareTypeDef.pattern)) { shares.push(shareTypeDef.name); continue nextpart; } } if (title.length > part.length) { const splits = part.split(SHARE_TYPES_MIXED_SPLIT_PATTERNS); if (splits.length > 1) { for (let i = 0; i < splits.length; i += 2) { if (splits[i]) { // any additional content between known share names invalidates the whole part continue nextpart; } } nextsubpart: for (let i = 1; i < splits.length; i += 2) { for (let shareTypeDef of SHARE_TYPES) { if (splits[i].match(shareTypeDef.pattern)) { shares.push(shareTypeDef.name); continue nextsubpart; } } } } } } if (shares.length === 0) { shares.push(SHARE_UNKNOWN_NAME); } let accepted = false; shares.forEach(shareType => { const entry = states.get(shareType); entry.count++; if (!entry.disabled) { accepted = true; } }); return accepted; }, update }; } function readEnhanceUserInfoMap(myUserId, userConfig) { let map = new Map(); document.querySelectorAll('.r_two .user-info > .user-infoWrap > .c + .f12').forEach(function(uidCell) { let uid = uidCell.textContent.trim() * 1; const img = uidCell.closest('.r_two').querySelector(`.user-pic a[href="u.php?action-show-uid-${uid}.html"] > img`); let imgSrc = null; if (img) { imgSrc = getImgSrc(img); } let infoWrap = uidCell.closest('.user-infoWrap'); let infoText = infoWrap.textContent; let nickName = ((infoText.match(/\s昵称: *([^\r\n]+)在线时间/)||[])[1]||'').trim()||''; let unnamed = !!infoText.match(/昵称:\s*在线时间/); let m = infoText.match(/\s发帖: (\d+)\s+HP: (-?\d+) 点\s+SP币: (-?\d+) G\s+.*\s+在线时间: (\d+)\(小时\)\s+注册时间: (\d{4}-\d{2}-\d{2})\s*最后登录: (\d{4}-\d{2}-\d{2})\s/); let data = { post: m[1] * 1, hp: m[2] * 1, sp: m[3] * 1, online: m[4] * 1, register: m[5], login: m[6], img: imgSrc, nickName, unnamed }; map.set(uid, data); if (userConfig.useCustomUserInfoPopup && infoWrap.querySelector('.listread > .rinsp-user-popup-action-list') == null) { const actionList = infoWrap.querySelector('.listread > ul'); const actionItems = actionList.querySelectorAll('li'); const newActionList = addElem(actionList.parentElement, 'ul', 'rinsp-user-popup-action-list'); // read profile actionItems[0].querySelector('a').setAttribute('target', '_blank'); newActionList.appendChild(actionItems[0]); if (myUserId !== uid) { // send mail const mailToItem = addElem(newActionList, 'li', 'rinsp-user-popup-action-mailto', { title: '发短消息' }); addElem(mailToItem, 'a', null, { target: '_blank', href: `message.php?action-write-touid-${uid}.html` }).innerHTML = '<div></div>'; } // show topics const showTopicItem = addElem(newActionList, 'li', 'rinsp-user-popup-action-topics', { title: '主题列表' }); addElem(showTopicItem, 'a', null, { target: '_blank', href: `u.php?action-topic-uid-${uid}.html` }).innerHTML = '<div></div>'; if (myUserId !== uid) { // add friend (and add confirmation) const addFriendAction = actionItems[2].querySelector('a'); addFriendAction.setAttribute('onclick', "if (confirm('加为好友 ?')) " + addFriendAction.getAttribute('onclick')); newActionList.appendChild(actionItems[2]); } // show topics const pinUserItem = addElem(newActionList, 'li', 'rinsp-user-popup-action-pinuser', { title: '设为焦点' }); const pinButton = addElem(pinUserItem, 'a', null, { href: 'javascript:' }); const dataNode = addElem(pinButton, 'div', 'rinsp-button-placeholder-pinuser'); dataNode.dataset.uid = uid; dataNode.dataset.nickname = nickName; const img = document.querySelector(`.user-pic a[href="u.php?action-show-uid-${uid}.html"] > img`); if (img) { dataNode.dataset.img = getImgSrc(img); } actionList.remove(); } }); return map; } function getCurrentPageFid() { const item = document.querySelector('#breadcrumbs .crumbs-item.current > strong > a[href^="thread.php?fid-"]') || Array.from(document.querySelectorAll('#breadcrumbs .crumbs-item[href^="thread.php?fid-"]')).pop() || null; if (item) { return Number.parseInt(item.getAttribute('href').substring(15)); } return null; } function showAutoSpTaskStatus(myUserId, userConfig, mainConfigAccess) { const headCell = Array.from(document.querySelectorAll('td > .t > table td.h[colspan="4"]')).filter(el => el.textContent === '社区论坛任务')[0]; if (headCell == null) { return; } const block = headCell.closest('.t').closest('td'); const autoSpOptionBlock = addElem(block, 'div', null, { style: 'padding: 0.5em' }); const labelElem = addElem(autoSpOptionBlock, 'label'); const checkbox = addElem(labelElem, 'input', null, { type: 'checkbox' }); checkbox.checked = !!userConfig.autoSpTasks; addElem(labelElem, 'span', null, { style: 'vertical-align: middle; margin-right: 0.5em' }).textContent = '自动签到 (社区论坛任务)'; checkbox.addEventListener('change', () => { mainConfigAccess .update(updatingUserConfig => { updatingUserConfig.autoSpTasks = checkbox.checked; return updatingUserConfig; }) .catch(ex => alert('' + ex)) .then(() => document.location.reload()); }); if (userConfig.autoSpTasks) { const taskRecordAccess = initConfigAccess(myUserId, 'autosptask', {}); const autoSpTasksSummary = addElem(autoSpOptionBlock, 'p'); taskRecordAccess.read().then(taskRecord => { if (taskRecord.lastCompleteSp > 0) { const hr = (Date.now() - taskRecord.lastComplete) / 3600000; autoSpTasksSummary.textContent = `💡 ${getAgeString(hr * 60)} 已领取 ${taskRecord.lastCompleteSp||'?'} SP | 记录中总共领取 ${taskRecord.totalSp||'?'} SP`; } }); } } function autoCompleteSpTasks(myUserId) { const taskMenuButton = document.querySelector('#guide a[href="plugin.php?H_name-tasks.html"]'); const taskRecordAccess = initConfigAccess(myUserId, 'autosptask', {}); async function execute() { const now = Date.now(); let taskRecord = await taskRecordAccess.read(); if ((Math.max(taskRecord.lastChecked, taskRecord.lastComplete, taskRecord.runningUntil) - now) / 3600000 > 23) { // if any time record is ahead of time for more than 23 hour, reset taskRecord = await taskRecordAccess.update(newRecord => { delete newRecord.lastChecked; delete newRecord.lastComplete; delete newRecord.lastCompleteSp; delete newRecord.runningUntil; return newRecord; }); } if (taskRecord.lastComplete && now > taskRecord.lastComplete) { const hr = (now - taskRecord.lastComplete) / 3600000; if (taskMenuButton && taskRecord.lastCompleteSp > 0) { taskMenuButton.setAttribute('title', `${getAgeString(hr * 60)} 已领取 ${taskRecord.lastCompleteSp} SP`); } if (hr < 11) { // min. check interval from last completed task is 11 hour return; } } if (taskRecord.lastChecked && now > taskRecord.lastChecked && (now - taskRecord.lastChecked) < 3600000) { // min. check interval is 1 hour return; } if (taskRecord.runningUntil > now) { return; } await taskRecordAccess.update(newRecord => { newRecord.runningUntil = Date.now() + 10000; // pause other pages for 10 seconds return newRecord; }); const newTaskDoc = await fetchGetPage(`${document.location.origin}/plugin.php?H_name-tasks.html`); const newTasks = await scanAndRunAllTasks(newTaskDoc, 'a[onclick^="startjob(\'"][title=按这申请此任务]', 'job'); await taskRecordAccess.update(newRecord => { newRecord.runningUntil = Date.now() + 10000; return newRecord; }); if (newTasks.length > 0) { await sleep(1500); } const currentTaskDoc = await fetchGetPage(`${document.location.origin}/plugin.php?H_name-tasks-actions-newtasks.html.html`); const completedTasks = await scanAndRunAllTasks(currentTaskDoc, 'a[onclick^="startjob(\'"][title=领取此奖励]', 'job2'); const sp = completedTasks.reduce((acc, tk) => acc + (tk.sp||0), 0); await taskRecordAccess.update(newRecord => { const now = Date.now(); newRecord.totalSp = (newRecord.totalSp || 0) + sp; newRecord.lastChecked = now; // if just complete a task if (completedTasks.length > 0) { newRecord.lastComplete = now; newRecord.lastCompleteSp = sp; } delete newRecord.runningUntil; return newRecord; }); if (sp > 0) { if (taskMenuButton) { taskMenuButton.setAttribute('rinsp-sp-get', '' + sp); } } setTimeout(execute, 3600000); // 1 hour } async function scanAndRunAllTasks(doc, selector, actionType) { let completed = []; for (let taskButton of doc.querySelectorAll(selector)) { const rewardMatch = taskButton.closest('td').previousElementSibling.textContent.match(/奖励 : SP币 (\d+) G/); const sp = rewardMatch ? rewardMatch[1] * 1 : 0; const jobId = Number.parseInt(taskButton.getAttribute('onclick').substring(10)); const taskURL = `${document.location.origin}/plugin.php?H_name=tasks&action=ajax&actions=${actionType}&cid=${jobId}&nowtime=${Date.now()}&verify=${verifyhash()}`; await sleep(100); const resp = await fetch(taskURL, { method: 'GET', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'text/html' }, cache: 'no-cache' }); if (resp.ok) { const xml = await resp.text(); if (xml.indexOf('success\t')) { // success completed.push({sp}); } } } return completed; } setTimeout(execute, 0); } function addSearchShortcut() { function createSearchButton(fid) { const button = newElem('a', 'rinsp-fid-search-button', { href: `search.php#prefill("fid",${fid})` }); return button; } document.querySelectorAll('#breadcrumbs').forEach(breadcrumbs => { // how comes the same id used by multiple elements ... const currentThreadLink = breadcrumbs.querySelector('.crumbs-item.current > strong > a[href^="thread.php?fid-"]'); if (currentThreadLink) { const fid = Number.parseInt(currentThreadLink.getAttribute('href').substring(15)); currentThreadLink.closest('.crumbs-item').prepend(createSearchButton(fid)); } else { const deepestThreadLink = Array.from(breadcrumbs.querySelectorAll('.crumbs-item[href^="thread.php?fid-"]')).pop(); if (deepestThreadLink) { const fid = Number.parseInt(deepestThreadLink.getAttribute('href').substring(15)); deepestThreadLink.prepend(createSearchButton(fid)); } } }); } function setAreaScoped(target, fid) { target.classList.add('rinsp-area-scoped-item'); target.classList.add(`rinsp-area-scoped-item-${fid}`); } const specialAreaNames = { 171: 'CG资源 (网赚)', 172: '实用动画 (网赚)', 173: '实用漫画 (网赚)', 174: '游戏资源 (网赚)', }; const PIC_WALL_PREF_KEY = 'rinsp:pic-wall-fids'; const LAST_AREA_FILTER_MEMORY_KEY = 'rinsp:last-filter-fid'; const REQUEST_RATE_RECORD_KEY = 'rinsp:request-rate'; function defaultAreaFilterHandler(fid) { const targetClass = `rinsp-area-scoped-item-${fid}`; document.querySelectorAll('.rinsp-area-scoped-item').forEach(item => { if (fid === 0 || item.classList.contains(targetClass)) { item.classList.remove('rinsp-area-scoped-item-hidden'); } else { item.classList.add('rinsp-area-scoped-item-hidden'); } }); } function addAreaFilter(baseAreaMap, userId, filterHandler) { return addSectionFilter(baseAreaMap, userId, filterHandler, { nameMappings: specialAreaNames, heading: '版块分类', showAllLabel: '全部版块' }); } function addSectionFilter(baseAreaMap, userId, filterHandler, options) { const areaMap = new Map(baseAreaMap); let basePageKey = document.location.href.replace(/(-page-\d+)?\.html$/, ''); if (basePageKey.indexOf('-uid-') === -1) { basePageKey = basePageKey + '-uid-' + userId; } const specialNameMappings = options.nameMappings||{}; Object.keys(specialNameMappings).forEach(spFid => { if (areaMap.get(spFid * 1)) { areaMap.set(spFid * 1, specialNameMappings[spFid]); } }); let defaultFid = 0; let defaultFilterItem = null; try { const lastState = JSON.parse(localStorage.getItem(LAST_AREA_FILTER_MEMORY_KEY) || 'null'); if (lastState != null && lastState.key === basePageKey) { areaMap.set(lastState.fid, lastState.areaName); defaultFid = lastState.fid; } } catch (ex) {} const sidePanel = document.querySelector('#u-contentside'); sidePanel.querySelectorAll('.rinsp-area-filter-panel').forEach(el => el.remove()); const section = addElem(sidePanel, 'div', 'rinsp-area-filter-panel'); const heading = addElem(section, 'h5', 'u-h5'); addElem(heading, 'span').textContent = options.heading||'分类'; const table = addElem(section, 'table', 'u-table', { width: "100%", cellspacing: "0", cellpadding: "0", border: "0" }); const tbody = addElem(table, 'tbody'); const filterItems = []; function addFilterItem(fid, areaName) { const tr = addElem(tbody, 'tr'); const td = addElem(tr, 'td', 'fav'); const a = addElem(td, 'a', null, { href: fid > 0 ? `thread.php?fid-${fid}.html` : 'javascript:' }); const count = addElem(td, 'span', null); const filterItem = { fid, td, areaName, setCount(n) { if (n >= 0) { count.textContent = ` (${n})`; } else { count.textContent = ''; } }, apply() { filterItems.forEach(item => item.td.classList.remove('current')); td.classList.add('current'); filterHandler(fid); preserveState(); } }; if (defaultFid > 0 && defaultFid === fid) { defaultFilterItem = filterItem; } a.addEventListener('click', evt => { evt.preventDefault(); filterItem.apply(); }); a.textContent = areaName; filterItems.push(filterItem); return filterItem; } const defaultAllItem = addFilterItem(0, options.showAllLabel||'全部'); Array.from(areaMap.keys()).sort(comparator()).forEach(fid => { addFilterItem(fid, areaMap.get(fid)); }); if (defaultFilterItem) { defaultFilterItem.td.classList.add('current'); } else { defaultAllItem.td.classList.add('current'); } document.addEventListener('visibilitychange', () => { if (!document.hidden) { preserveState(); } }); function getCurrentOption() { return filterItems.find(item => item.td.classList.contains('current')); } function preserveState() { const current = getCurrentOption(); if (current && current.fid) { const data = { key: basePageKey, fid: current.fid, areaName: current.areaName }; localStorage.setItem(LAST_AREA_FILTER_MEMORY_KEY, JSON.stringify(data)); } else { localStorage.removeItem(LAST_AREA_FILTER_MEMORY_KEY); } } return { apply() { const current = getCurrentOption(); if (current) { current.apply(); } }, getCurrentFid() { const current = getCurrentOption(); return current ? current.fid : 0; }, updateCounts(counts) { let total = 0; filterItems.forEach(filterItem => { if (filterItem.fid === 0) { return; } const n = counts.get(filterItem.fid); filterItem.setCount(n); total += n; }); defaultAllItem.setCount(total); } }; } async function enhanceFrontPage(userConfig, mainConfigAccess) { const cell = document.querySelector('#main > .t > table > tbody > .tr2 > td:last-child > .b'); if (cell == null || cell.textContent !== '提问求物') { return; } const tbody = cell.closest('tbody'); const itemRow = tbody.querySelector('.tr3'); const closeIcon = addElem(cell.parentNode, 'a', 'closeicon fr', { style: 'cursor: pointer' }); const closeImg = addElem(closeIcon, 'img', null); let collapsed = !!userConfig.hideFrontPageRecentList; function update() { if (collapsed) { closeImg.setAttribute('src', 'images/colorImagination/index/cate_open.gif'); itemRow.setAttribute('style', 'display: none'); } else { closeImg.setAttribute('src', 'images/colorImagination/index/cate_fold.gif'); itemRow.setAttribute('style', ''); } } update(); closeIcon.addEventListener('click', async () => { mainConfigAccess .update(function(updatingUserConfig) { collapsed = !collapsed; updatingUserConfig.hideFrontPageRecentList = collapsed; return updatingUserConfig; }) .finally(() => update(collapsed)); }); let targets = { '最新讨论': '/thread.php?fid=9&page=1', '最新回复': '/thread.php?fid-9.html', '同人音声': '/thread.php?fid-128.html', '提问求物': '/thread.php?fid-48.html' }; tbody.querySelectorAll('.tr2 > td > .b').forEach(el => { const label = el.textContent.trim(); let target = targets[label]; if (target) { el.textContent = ''; addElem(el, 'a', null, { href: target }).textContent = label; } }); } const cachedLastAccessRecords = new Map(); async function enrichThreadTitle(tid, tLink, myUserId, userConfig, threadHistoryAccess) { if (threadHistoryAccess == null) { return; } let prefix; if (tLink.textContent.endsWith(' ..')) { prefix = tLink.textContent.substring(0, tLink.textContent.length - 3).replace(/&?n?b?s?p?;?$/, '').trim(); } else { prefix = tLink.textContent.trim(); } function handleLastAccessRecord(lastAccessRecord, fromCache) { if (lastAccessRecord && lastAccessRecord.fid === QUESTION_AND_REQUEST_AREA_ID && userConfig.requestThreadUseHistoryData) { tLink.parentElement.querySelectorAll('.rinsp-thread-bounty-status').forEach(el => el.remove()); const bountySummary = newElem('span', 'rinsp-thread-bounty-status'); tLink.parentElement.insertBefore(bountySummary, tLink); const age = getAgeString((Date.now() - lastAccessRecord.time) / 60000); bountySummary.setAttribute('title', `记录时间: ${age}`); if (lastAccessRecord.uid === myUserId) { bountySummary.classList.add('rinsp-thread-bounty-status-own'); } if (lastAccessRecord.bounty) { if (lastAccessRecord.bounty.ended > 0) { if (lastAccessRecord.bounty.winner || lastAccessRecord.bounty.ended === 2) { if (lastAccessRecord.bounty.winner === userConfig.myUserHashId) { bountySummary.classList.add('rinsp-thread-bounty-status-won'); } else { bountySummary.classList.add('rinsp-thread-bounty-status-ended'); } } else { bountySummary.classList.add('rinsp-thread-bounty-status-expired'); } } else { bountySummary.classList.add('rinsp-thread-bounty-status-ongoing'); } const baseBounty = lastAccessRecord.bounty.sp; const extraBounty = parseSpAmount(lastAccessRecord.title); if (extraBounty > baseBounty) { addElem(bountySummary, 'span').textContent = `${baseBounty}+${extraBounty}`; } else { addElem(bountySummary, 'span').textContent = `${baseBounty}`; } } else { bountySummary.classList.add('rinsp-thread-bounty-status-unknown'); } } if (lastAccessRecord && lastAccessRecord.title.startsWith(prefix)) { tLink.textContent = lastAccessRecord.title; tLink.parentElement.querySelectorAll('.rinsp-thread-populate-button').forEach(el => el.remove()); if (lastAccessRecord.initialTitle && lastAccessRecord.initialTitle !== lastAccessRecord.title) { if (userConfig.showInitialRememberedTitle) { tLink.classList.add('rinsp-thread-title-replaced'); addElem(tLink, 'div', 'rinsp-thread-initial-title').textContent = lastAccessRecord.initialTitle; } else { tLink.setAttribute('title', '原始标题: ' + lastAccessRecord.initialTitle); } } } else if (fromCache) { // skip modification } else if (tLink.textContent.endsWith(' ..') && tLink.parentElement.querySelector('.rinsp-thread-populate-button') == null) { const expander = newElem('a', 'rinsp-thread-populate-button', { href: 'javascript:'} ); expander.textContent = '展开'; tLink.parentElement.insertBefore(expander, tLink.nextElementSibling); expander.addEventListener('click', async () => { if (expander.getAttribute('href') == null) { return; } expander.removeAttribute('href'); expander.textContent = '⌛'; try { await populateThreadAccess(tid, myUserId, threadHistoryAccess); const updatedAccessRecord = await threadHistoryAccess.recentAccessStore.get(tid); if (updatedAccessRecord) { tLink.textContent = updatedAccessRecord.title; } expander.remove(); } catch (e) { expander.textContent = '⚠️'; } }); } } let lastAccessRecord = cachedLastAccessRecords.get(tid); if (lastAccessRecord) { handleLastAccessRecord(lastAccessRecord, true); // purely to avoid flash-of-content when refreshed } lastAccessRecord = await threadHistoryAccess.recentAccessStore.get(tid); if (lastAccessRecord) { cachedLastAccessRecords.set(tid, lastAccessRecord); } handleLastAccessRecord(lastAccessRecord, false); } function isAlpha(c) { return c >= 'a' && c <= 'z'; } function isNumeric(c) { return c >= '0' && c <= '9'; } function createKeywordMatcherFactory(keywords) { if (keywords == null || keywords.length === 0) { return null; } const exp = keywords .sort(comparator('length', true)) // longest first for regex match to work correctly .filter(s => !!s) .map(s => { if (s.startsWith('ignored:')) { return null; } if (s.length > 8 && s.startsWith('regex:/') && s.endsWith('/')) { try { const exp = s.substring(7, s.length - 1); RegExp(exp); return exp; } catch (e) { console.info(`[rin+] invalid regex entry: ${s}`); return null; } } const term = s.toLowerCase(); let exp = escapeRegExp(term); if (isAlpha(term[0])) { exp = '(?<=[^a-z]|^)' + exp; } else if (isNumeric(term[0])) { exp = '(?<=[^0-9]|^)' + exp; } if (isAlpha(term[term.length - 1])) { exp = exp + '(?=[^a-z]|$)'; } else if (isNumeric(term[term.length - 1])) { exp = exp + '(?=[^0-9]|$)'; } return exp; }) .filter(s => !!s) .join('|'); try { new RegExp(exp, 'g'); } catch (ex) { if (DEV_MODE) console.warn('invalid-regexp: ' + exp); return null; } return { isStaticKeyword(word) { return keywords.indexOf(word) != -1; }, match(text) { const pattern = new RegExp(exp, 'g'); const matches = new Set(); while (true) { const match = pattern.exec(text); if (match == null) { break; } matches.add(match[0]); } return matches; } }; } async function openCustomThreadCategoryEditor(threadCategoryConfigAccess, callback) { openTextListEditor(THREAD_LIKE_POPUP_MENU_ID, { title: '自定分类词列表', description: `每行一个关键字 正则表达式高级设置 (懂的都懂 不懂可无视) 格式: regex:/正则表达式语法/ 例子: regex:/[东南西北]\+/`, async read() { const customConfig = await threadCategoryConfigAccess.read(); return customConfig.keywords.sort().join('\n'); }, async save(textData) { return await threadCategoryConfigAccess.update(function(customConfig) { const newKeywords = textData.split('\n').map(itm=>itm.replace(/\s+/g, ' ').trim()).filter(itm=>!!itm); customConfig.keywords = newKeywords; return customConfig; }); } }, callback); } function enhanceTopicPostListDisplay(userId, myUserId, userConfig, threadHistoryAccess, threadCategoryConfigAccess, hideVisitedThreadToggler) { const headerCell = document.querySelector('#u-contentmain .u-table tr:first-child > td'); headerCell.textContent = ''; headerCell.nextElementSibling.textContent = ''; const editKeywordsButton = addElem(headerCell.nextElementSibling, 'a'); editKeywordsButton.setAttribute('href', 'javascript:void(0)'); editKeywordsButton.textContent = '自定分类'; editKeywordsButton.addEventListener('click', () => { openCustomThreadCategoryEditor(threadCategoryConfigAccess, () => apply()); }); const toggleItemsArea = addElem(headerCell, 'span', 'rinsp-category-filter'); const toggleItems = []; let activeKeyword = null; const threadsById = new Map(); const threadsByKeywords = new Map(); function refreshCategoryFilter() { toggleItems.forEach(toggleItem => { if (toggleItem.keyword === activeKeyword) { toggleItem.toggler.classList.add('rinsp-category-active'); } else { toggleItem.toggler.classList.remove('rinsp-category-active'); } }); if (activeKeyword) { const activeTids = threadsByKeywords.get(activeKeyword); threadsById.forEach((row, tid) => { if (activeTids != null && activeTids.has(tid)) { row.classList.remove('rinsp-category-hide'); } else { row.classList.add('rinsp-category-hide'); } }); } else { threadsById.forEach((row, tid) => { row.classList.remove('rinsp-category-hide'); }); } } async function apply() { let keywordPatternMatcher = null; let keywordCaseMappings = new Map(); const threadCategoryConfig = await threadCategoryConfigAccess.read(); if (threadCategoryConfig.keywords.length > 0) { let keywords = threadCategoryConfig.keywords.map(kw => { let lower = kw.toLowerCase(); keywordCaseMappings.set(lower, kw); return lower; }); keywordPatternMatcher = createKeywordMatcherFactory(keywords); } threadsById.clear(); threadsByKeywords.clear(); const areaMap = new Map(); document.querySelectorAll('#u-contentmain th > a[href^="read.php?tid-"]:not(.rinsp-gf-link)').forEach(el => { const cell = el.closest('th'); const row = cell.closest('tr'); const tid = Number.parseInt(el.getAttribute('href').substring(13)); threadsById.set(tid, row); if (cell.querySelector('.rinsp-gf-link') == null) { const gfLink = newElem('a', 'rinsp-gf-link', { href: `read.php?tid-${tid}-uid-${userId}.html`, target: '_blank' }); gfLink.textContent = '只看楼主'; cell.prepend(gfLink); } const fidLink = cell.querySelector('a[href^="thread.php?fid-"]'); const fid = Number.parseInt(fidLink.getAttribute('href').substring(15)); setAreaScoped(row, fid); areaMap.set(fid, fidLink.textContent.trim()); cell.classList.remove('rinsp-thread-visited'); enrichThreadTitle(tid, el, myUserId, userConfig, threadHistoryAccess); if (keywordPatternMatcher) { const threadTitle = el.textContent; const matches = keywordPatternMatcher.match(threadTitle.toLowerCase()); for (let keyword of matches) { let threadSet = threadsByKeywords.get(keyword); if (threadSet == null) { threadSet = new Set(); threadsByKeywords.set(keyword, threadSet); } threadSet.add(tid); } } }); toggleItemsArea.textContent = ''; toggleItems.length = 0; threadsByKeywords.forEach((threadSet, keyword) => { const toggler = addElem(toggleItemsArea, 'div', 'rinsp-category-toggle-item'); toggler.setAttribute('count', '' + threadSet.size); toggler.textContent = keywordCaseMappings.get(keyword)||keyword; toggler.addEventListener('click', () => { if (activeKeyword === keyword) { activeKeyword = null; } else { activeKeyword = keyword; } refreshCategoryFilter(); }); toggleItems.push({ toggler, keyword }); }); addAreaFilter(areaMap, myUserId, defaultAreaFilterHandler).apply(); await updateHistoryCheck(); } async function updateHistoryCheck() { if (threadHistoryAccess == null || hideVisitedThreadToggler == null) return; let visitedCount = 0; const tids = Array.from(threadsById.keys()); const lastSeenRecords = await threadHistoryAccess.historyStore.getBatch(tids); lastSeenRecords.forEach((record, i) => { const row = threadsById.get(tids[i]); if (record != null) { row.classList.add('rinsp-thread-visited'); visitedCount++; } }); hideVisitedThreadToggler.setCount(visitedCount); } if (hideVisitedThreadToggler) { hideVisitedThreadToggler.restoreLastState(true); // if history based styling is active, need to recheck on page activation document.addEventListener('visibilitychange', async () => { if (!document.hidden) { await updateHistoryCheck(); } }); } // accounting for infinite scroll added by soul++ const observer = createMutationObserver(async () => { await apply(); }); observer.init(document.querySelector('#u-contentmain .u-table'), { childList: true, subtree: false, attributes: false }); observer.trigger(); document.addEventListener('visibilitychange', () => { if (!document.hidden) { observer.trigger(); } }); } async function enhanceReplyPostListDisplay(userId, myUserId, userConfig, userPinArea, mainConfigAccess, threadHistoryAccess, hideVisitedThreadToggler) { const firstItem = document.querySelector('#u-contentmain .u-table tr > th > a[href^="job.php?action-topost-tid-"]'); if (firstItem == null) { return; } let userReplyListFolded = userConfig.userReplyListFolded; let accessUpdaters = []; function registerEnrichThreadTitle(tid, elem) { if (threadHistoryAccess) { const action = () => enrichThreadTitle(tid, elem, myUserId, userConfig, threadHistoryAccess); accessUpdaters.push(action); action(); } } const baseTable = firstItem.closest('.u-table:not(.rinsp-reply-fold-table)'); const threadsById = new Map(); async function apply() { accessUpdaters.length = 0; threadsById.clear(); let table = baseTable; const areaMap = new Map(); if (userReplyListFolded) { const dataItems = []; table.querySelectorAll('tr + tr').forEach(row => { const th = row.querySelector('th'); const topostLink = th.querySelector('a[href^="job.php?action-topost-tid-"]'); const threadLink = th.querySelector('a[href^="thread.php?fid-"]'); const areaHref = threadLink.getAttribute('href'); const date = th.querySelector('span.gray.f9').textContent.replace(/^\[|\]$/g, ''); const postUidLink = row.querySelector('td a[href^="u.php?action-show-uid-"]'); const topostHref = topostLink.getAttribute('href'); const topostRef = getPostRef(topostHref); if (topostRef) { dataItems.push({ postTitle: topostLink.textContent.trim(), topostHref: topostHref, tid: topostRef.tid, pid: topostRef.pid, areaName: threadLink.textContent.trim(), areaHref: areaHref, areaFid: Number.parseInt(areaHref.substring(15)), date, postUid: Number.parseInt(postUidLink.getAttribute('href').substring(22)), postUhash: postUidLink.textContent.trim() }); } }); const groupedItems = []; const map = new Map(); dataItems.forEach(item => { let group = map.get(item.tid); if (group == null) { group = []; map.set(item.tid, group); groupedItems.push(group); } group.push(item); }); baseTable.parentElement.querySelectorAll('.rinsp-reply-fold-table').forEach(el => el.remove()); const foldTable = newElem('table', 'u-table rinsp-reply-fold-table'); const foldTbody = addElem(foldTable, 'tbody'); const foldTr1 = addElem(foldTbody, 'tr'); foldTr1.innerHTML = '<td><br></td><td><br></td>'; groupedItems.forEach(group => { const row = addElem(foldTbody, 'tr'); const th = addElem(row, 'th'); const td = addElem(row, 'td'); td.textContent = '作者:'; const firstItem = group[0]; setAreaScoped(row, firstItem.areaFid); areaMap.set(firstItem.areaFid, firstItem.areaName); threadsById.set(firstItem.tid, [row]); addElem(td, 'a', 'gray', { href: `u.php?action-show-uid-${firstItem.postUid}.html`, target: '_blank' }).textContent = firstItem.postUhash; if (group.length === 1) { const tLink = addElem(th, 'a', null, { href: firstItem.topostHref, target: '_blank' }); tLink.textContent = firstItem.postTitle; addElem(th, 'br'); registerEnrichThreadTitle(firstItem.tid, tLink); } else { const foldedItem = addElem(th, 'div', 'rinsp-reply-fold-item'); const tLink = addElem(foldedItem, 'a', null, { href: firstItem.topostHref, target: '_blank' }); tLink.textContent = firstItem.postTitle; const pages = addElem(foldedItem, 'div', 'rinsp-reply-fold-pages'); addElem(pages, 'span', 'rinsp-reply-fold-count').textContent = `${group.length}`; let range = addElem(pages, 'div', 'rinsp-reply-fold-remains'); const splitIndex = group.length > 5 ? group.length - 3 : group.length; group.slice(1).forEach((item, i) => { addElem(range, 'a', null, { href: item.topostHref, target: '_blank' }).textContent = `${group.length - i - 1}`; if (i === splitIndex) { range.classList.add('rinsp-reply-fold-remains-start'); range = addElem(pages, 'div', 'rinsp-reply-fold-remains rinsp-reply-fold-remains-end'); } }); registerEnrichThreadTitle(firstItem.tid, tLink); } const gfLink = newElem('a', 'rinsp-gf-link', { href: `read.php?tid-${firstItem.tid}-uid-${userId}.html#${firstItem.pid}`, target: '_blank' }); gfLink.textContent = '只看回复'; th.append(gfLink); addElem(th, 'a', 'gray', { href: firstItem.areaHref, target: '_blank' }).textContent = firstItem.areaName; const lastItem = group[group.length - 1]; if (firstItem.date === lastItem.date) { addElem(th, 'span', 'gray f9').textContent = ` [${firstItem.date}]`; } else { addElem(th, 'span', 'gray f9').textContent = ` [${firstItem.date} ~ ${lastItem.date}]`; } }); table.parentElement.insertBefore(foldTable, table); table = foldTable; } else { document.querySelectorAll('#u-contentmain .u-table.rinsp-reply-fold-table').forEach(el => el.remove()); table.querySelectorAll('tr > th > a[href^="job.php?action-topost-tid-"]').forEach(el => { const cell = el.closest('th'); const row = cell.closest('tr'); const postRef = getPostRef(el.getAttribute('href')); if (!postRef) { return; } registerEnrichThreadTitle(postRef.tid, el); if (cell.querySelector('.rinsp-gf-link') == null) { const gfLink = newElem('a', 'rinsp-gf-link', { href: `read.php?tid-${postRef.tid}-uid-${userId}.html#${postRef.pid}`, target: '_blank' }); gfLink.textContent = '只看回复'; cell.prepend(gfLink); } const fidLink = cell.querySelector('a[href^="thread.php?fid-"]'); const fid = Number.parseInt(fidLink.getAttribute('href').substring(15)); setAreaScoped(row, fid); areaMap.set(fid, fidLink.textContent.trim()); let rows = threadsById.get(postRef.tid); if (rows == null) { rows = []; threadsById.set(postRef.tid, rows); } rows.push(row); row.classList.remove('rinsp-thread-visited'); }); } addAreaFilter(areaMap, myUserId, defaultAreaFilterHandler).apply(); await updateHistoryCheck(); const controlContainer = table.querySelector('tr:first-child > td:first-child'); if (controlContainer) { controlContainer.querySelectorAll('br, .rinsp-reply-list-controls').forEach(el => el.remove()); const grouper = addElem(controlContainer, 'span', 'rinsp-reply-list-controls'); grouper.appendChild(document.createTextNode('显示方式: ')); const modeSelect = addElem(grouper, 'select'); addElem(modeSelect, 'option', null, { value: '' }).textContent = '一般'; addElem(modeSelect, 'option', null, { value: 'folding' }).textContent = '折叠式'; modeSelect.value = userReplyListFolded ? 'folding' : ''; modeSelect.value = modeSelect.value || ''; modeSelect.addEventListener('change', () => { modeSelect.disabled = true; mainConfigAccess.update(function(updatingUserConfig) { updatingUserConfig.userReplyListFolded = modeSelect.value === 'folding'; return updatingUserConfig; }) .finally(function() { userReplyListFolded = modeSelect.value === 'folding'; modeSelect.disabled = false; apply(); }); }); } } async function updateHistoryCheck() { if (threadHistoryAccess == null || hideVisitedThreadToggler == null) return; let visitedCount = 0; const tids = Array.from(threadsById.keys()); const lastSeenRecords = await threadHistoryAccess.historyStore.getBatch(tids); lastSeenRecords.forEach((record, i) => { if (record != null) { const rows = threadsById.get(tids[i]); rows.forEach(row => row.classList.add('rinsp-thread-visited')); visitedCount++; } }); hideVisitedThreadToggler.setCount(visitedCount); } if (hideVisitedThreadToggler) { hideVisitedThreadToggler.restoreLastState(true); // if history based styling is active, need to recheck on page activation document.addEventListener('visibilitychange', async () => { if (!document.hidden) { await updateHistoryCheck(); } }); } // accounting for infinite scroll added by soul++ (or me) const observer = createMutationObserver(async () => { await apply(); }); observer.init(baseTable, { childList: true, subtree: false, attributes: false }); observer.trigger(); document.addEventListener('visibilitychange', () => { if (!document.hidden) { accessUpdaters.forEach(accessUpdater => accessUpdater()); } }); await enhancePostListUserDisplay('post', userId, myUserId, userConfig, userPinArea); function getPostRef(postUrl) { const match = postUrl.match(/-tid-(\d+)-pid-(\d+)\.html$/); if (!match) return null; return { tid: match[1] * 1, pid: match[2] * 1, }; } } function enhanceFavorPageDisplay(userId, myUserId, userConfig, threadHistoryAccess) { if (threadHistoryAccess) { document.querySelectorAll('#u-contentmain th > a[href^="read.php?tid-"]').forEach(el => { const tid = Number.parseInt(el.getAttribute('href').substring(13)); enrichThreadTitle(tid, el, myUserId, userConfig, threadHistoryAccess); }); } } function updateFavorRecords(favorThreadsCacheAccess) { const favors = readFavorList(document); favorThreadsCacheAccess.write({ data: favors, time: Date.now() }); } async function readFavorRecords(favorThreadsCacheAccess, fresh, noupdate) { let cached = fresh ? null : await favorThreadsCacheAccess.read(); const now = Date.now(); if (cached == null || cached.time > now || (now - cached.time) > 79200000) { // older than 22 hours const doc = await fetchGetPage(`${document.location.origin}/u.php?action-favor.html`); const favors = readFavorList(doc); const cacheItem = { data: favors, time: now }; if (!noupdate) { await favorThreadsCacheAccess.write(cacheItem); } return favors; } else { return cached.data; } } async function enhancePostListUserDisplay(action, userId, myUserId, userConfig, userPinArea) { const mappings = userConfig.customUserHashIdMappings; function transformUserDisplay(el, postUid) { el.classList.add('rinsp-user-link'); if (Number.isNaN(postUid)) { return; } const myself = userConfig.highlightMyself && myUserId === postUid; const owner = userId === postUid; const userRecord = mappings ? mappings['#' + postUid] : null; const pinnedUser = userPinArea.getPinned(postUid); if (userRecord || myself || owner || pinnedUser) { if (!el.dataset.rinspRestore) { el.dataset.rinspRestore = el.textContent; } el.innerHTML = ''; if (myself) { addElem(el, 'span', 'rinsp-nickname-byme').textContent = MY_NAME_DISPLAY; } else if (owner) { addElem(el, 'span', 'rinsp-nickname-byowner').textContent = '楼主'; } else if (pinnedUser) { addElem(el, 'span', 'rinsp-nickname-bypinned').textContent = pinnedUser.nickName; userPinArea.addLocation(postUid, '⚪', null, () => { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } else { addElem(el, 'span', 'rinsp-nickname-byother').textContent = userRecord[2]; } } else { const restore = el.dataset.rinspRestore; if (restore) { el.textContent = restore; delete el.dataset.rinspRestore; } } } async function update() { userPinArea.clearState(); if (action === '' || action === 'feed') { // personal dashboard (好友近况) document.querySelectorAll('#minifeed .feed-list > dt > a.link1[href^="u.php?uid-"]').forEach(el => { const postUid = Number.parseInt(el.getAttribute('href').substring(10)); transformUserDisplay(el, postUid); }); } else if (action === 'favor') { // personal collection userPinArea.clearState(); document.querySelectorAll('#u-content td > a[href^="u.php?uid-"]').forEach(el => { const postUid = Number.parseInt(el.getAttribute('href').substring(10)); transformUserDisplay(el, postUid); }); } else if (['topic', 'post', 'friend'].includes(action)) { // reply list, friend list const table = document.querySelector('#u-content .u-table'); if (table) { table.querySelectorAll('td:nth-child(2) > a[href^="u.php?action-show-uid-"]').forEach(el => { const postUid = Number.parseInt(el.getAttribute('href').substring(22)); transformUserDisplay(el, postUid); const nextElem = el.nextElementSibling; if (nextElem && nextElem.tagName === 'B' && nextElem.textContent === '在线') { el.closest('tr').classList.add('rinsp-user-state-online'); } }); } } await userPinArea.update(); } // accounting for infinite scroll let triggerUpdate; const scope = document.querySelector('#u-contentmain'); if (scope) { const observer = createMutationObserver(update); observer.init(scope, { childList: true, subtree: true, attributes: false }); observer.trigger(); triggerUpdate = () => observer.trigger(); } else { await update(); triggerUpdate = () => update(); } userPinArea.addUpdateListener(() => { triggerUpdate(); }); } function createMutationObserver(callback) { let updateSchedule = null; let skipUpdate = 0; function onContentChange() { if (updateSchedule == null) { skipUpdate++; updateSchedule = setTimeout(function() { updateSchedule = null; callback() .finally(() => skipUpdate--); }, 0); } } const observer = new MutationObserver(function(e) { if (DEBUG_MODE) console.info(skipUpdate > 0 ? 'skipped-mutation' : 'mutation'); if (skipUpdate > 0) { return; } onContentChange(); }); return { init(watchElem, options) { observer.observe(watchElem, options); }, trigger() { onContentChange(); } }; } function replaceTradeWithHistoryTab(myUserId, queryParams) { const tradeTab = document.querySelector(`#u_trade > a[href="u.php?action-trade.html"], #u_trade > a[href="u.php?action-trade-uid-${myUserId}.html"]`); if (tradeTab) { tradeTab.textContent = '浏览记录'; } } function replaceTradeWithPunishHistoryTab(myUserId, queryParams) { const tradeTabLink = document.querySelector('#u_trade > a[href^="u.php?action-trade-uid-"]'); if (tradeTabLink) { const tradeTab = tradeTabLink.closest('li'); const uid = Number.parseInt(tradeTabLink.getAttribute('href').substring(23)); if (uid === myUserId) { const adminHistoryTab = newElem('li'); addElem(adminHistoryTab, 'a', null, { href: 'u.php?action-trade-view-admin.html' }).textContent = '管理记录'; tradeTab.closest('ul').insertBefore(adminHistoryTab, tradeTab.nextElementSibling); if (queryParams.action === 'trade' && queryParams.view === 'admin') { tradeTab.classList.remove('current'); adminHistoryTab.classList.add('current'); } } else { tradeTabLink.textContent = '不良记录'; } } } function comparator(prop, reversed) { const reverser = reversed ? -1 : 1; return (x, y) => { const xv = typeof prop === 'function' ? prop(x) : prop ? x[prop] : x; const yv = typeof prop === 'function' ? prop(y) : prop ? y[prop] : y; return xv > yv ? reverser : xv < yv ? -reverser : 0; }; } function renderVisitHistoryPage(myUserId, userConfig, threadHistoryAccess) { const mainPane = document.querySelector('#u-content > #u-contentmain'); const sidePane = document.querySelector('#u-content > #u-contentside'); sidePane.innerHTML = ''; if (threadHistoryAccess == null) { mainPane.innerHTML = '<div style="padding:16px 30px">💡 请先启用🗞️帖子记录功能</div>'; return; } mainPane.innerHTML = '<div style="padding:16px 30px"><img src="images/loading.gif" align="absbottom"> 加载中 ... (请耐心等待)</div>'; async function init() { const records = await threadHistoryAccess.recentAccessStore.list(); if (records.length === 0) { mainPane.innerHTML = '<div style="padding:16px 30px">💡 没有历史记录</div>'; return; } mainPane.innerHTML = ''; const sortByVisitTime = comparator('time', true); const sortByPostTime = comparator('tid', true); const now = Date.now(); const areaMap = new Map(); records.forEach(record => areaMap.set(record.fid, record.area)); const areaFilter = addAreaFilter(areaMap, myUserId, () => update(false)); const table = addElem(mainPane, 'table', 'u-table rinsp-thread-history-table'); const thead = addElem(table, 'thead'); const tr1 = addElem(thead, 'tr'); const filterCell = addElem(tr1, 'td', null, { colspan: '2' }); const filterBox1 = addElem(filterCell, 'div', 'rinsp-quick-filter-box'); addElem(filterBox1, 'div').textContent = '🔍︎搜索'; const filterInput = addElem(filterBox1, 'input', null); addElem(filterBox1, 'div', null, { style: 'flex: 0 1 1em' }); const controlBlock = addElem(filterBox1, 'div'); controlBlock.appendChild(document.createTextNode('排序: ')); const sortSelect = addElem(controlBlock, 'select'); addElem(sortSelect, 'option', null, { value: 'last_visit' }).textContent = '访问时间'; addElem(sortSelect, 'option', null, { value: 'thread_age' }).textContent = '发帖时间'; sortSelect.value = 'last_visit'; sortSelect.addEventListener('change', () => update(false)); controlBlock.appendChild(document.createTextNode('显示数: ')); const limitSelect = addElem(controlBlock, 'select'); addElem(limitSelect, 'option', null, { value: '100' }).textContent = '100'; addElem(limitSelect, 'option', null, { value: '200' }).textContent = '200'; addElem(limitSelect, 'option', null, { value: '500' }).textContent = '500'; addElem(limitSelect, 'option', null, { value: '1000' }).textContent = '1000'; limitSelect.value = '100'; limitSelect.addEventListener('change', () => update(false)); const filterBox2 = addElem(filterCell, 'div', 'rinsp-quick-filter-box'); addElem(filterBox2, 'div').textContent = ''; const boughtFilterCheckbox = addCheckbox(filterBox2, '已购买', 'rinsp-quick-filter-checkbox rinsp-quick-filter-bought-checkbox'); const repliedFilterCheckbox = addCheckbox(filterBox2, '已回复', 'rinsp-quick-filter-checkbox rinsp-quick-filter-replied-checkbox'); const notmineFilterCheckbox = addCheckbox(filterBox2, '隐藏我的主题', 'rinsp-quick-filter-checkbox rinsp-quick-filter-notmine-checkbox'); addElem(filterBox2, 'div', null, { style: 'flex: 1' }); const tbody = addElem(table, 'tbody'); const tfoot = addElem(table, 'tfoot'); const statusRow = addElem(tfoot, 'tr'); const statusCell = addElem(statusRow, 'td', 'grey', { colspan: '2' }); let lastFilterString = ''; function update(filterChangeOnly) { const terms = filterInput.value.toLowerCase().trim().split(/\s+/g); const thisFilterString = terms.join(' '); if (filterChangeOnly && lastFilterString === thisFilterString) return; lastFilterString = thisFilterString; statusCell.textContent = ''; const limit = limitSelect.value * 1; const sortMethod = sortSelect.value === 'thread_age' ? sortByPostTime : sortByVisitTime; const fid = areaFilter.getCurrentFid(); let selectedRecords; // apply area filter, also clone the list for sorting if (fid > 0) { selectedRecords = records.filter(record => record.fid === fid); } else { selectedRecords = records.slice(); } // apply bought filter if (boughtFilterCheckbox.checked) { selectedRecords = selectedRecords.filter(record => !!record.bought); } // apply replied filter if (repliedFilterCheckbox.checked) { selectedRecords = selectedRecords.filter(record => record.replied > 0); } // apply not self filter if (notmineFilterCheckbox.checked) { selectedRecords = selectedRecords.filter(record => record.uid !== myUserId); } // apply text filter if (terms.length === 1 && terms[0] === '') { table.classList.remove('rinsp-table-filtered'); if (selectedRecords.length > limit) { statusCell.textContent = `💡只显示前${limit}条记录 / 共${selectedRecords.length}条`; } } else { table.classList.add('rinsp-table-filtered'); selectedRecords = selectedRecords.filter(record => match(record, terms)); if (selectedRecords.length === 0) { statusCell.textContent = '💡没有搜索结果'; } else if (selectedRecords.length > limit) { statusCell.textContent = `💡只显示前${limit}条搜索结果 / 共${selectedRecords.length}条`; } } selectedRecords.sort(sortMethod); tbody.innerHTML = ''; selectedRecords.slice(0, limit).forEach(record => { const row = addElem(tbody, 'tr'); const th = addElem(row, 'th'); const td = addElem(row, 'td'); setAreaScoped(row, record.fid); const status = addElem(th, 'span', 'rinsp-thread-status-icons'); if (record.deleted) { row.classList.add('rinsp-thread-deleted'); } if (record.replied) { row.classList.add('rinsp-thread-replied'); addElem(status, 'a', 'rinsp-thread-replied-icon', { title: '已回复', href: `read.php?tid-${record.tid}-uid-${myUserId}.html`, target: '_blank' }).textContent = '↩️'; } if (record.bought) { row.classList.add('rinsp-thread-bought'); addElem(status, 'span', 'rinsp-thread-bought-icon', { title: '已购买' }).textContent = '💰'; } addElem(th, 'a', null, { href: `read.php?tid-${record.tid}.html`, target: '_blank' }).textContent = record.title; addElem(th, 'div'); if (record.initialTitle && record.initialTitle !== record.title) { addElem(th, 'div', 'rinsp-thread-initial-title').textContent = record.initialTitle; } addElem(th, 'a', 'gray', { href: `thread.php?fid-${record.fid}.html`, target: '_blank' }).textContent = record.area; addElem(th, 'span', 'gray f9').textContent = ' [ ' + getAgeString((now - record.time) / 60000) + ' ]'; const uidBlock = addElem(th, 'span', 'rinsp-thread-record-uid'); if (record.uid > 0) { if (record.uid === myUserId) { row.classList.add('rinsp-thread-byme'); const userLink = addElem(uidBlock, 'a', 'rinsp-owner-isme', { href: `u.php?uid-${record.uid}.html`, target: '_blank' }); userLink.textContent = MY_NAME_DISPLAY; } else { let customNameEntry = userConfig.customUserHashIdMappings['#' + record.uid]; if (customNameEntry) { const userLink = addElem(uidBlock, 'a', 'rinsp-owner-isknown', { href: `u.php?uid-${record.uid}.html`, target: '_blank' }); userLink.textContent = customNameEntry[2]; } } } }); } function addCheckbox(target, label, styleClass) { const labelElem = addElem(target, 'label', styleClass); const checkbox = addElem(labelElem, 'input', null, { type: 'checkbox' }); addElem(labelElem, 'span', null, { style: 'vertical-align: text-top' }).textContent = label; checkbox.addEventListener('change', () => update(false)); return checkbox; } let enqueueTimer = null; filterInput.addEventListener('keyup', () => { if (enqueueTimer) clearTimeout(enqueueTimer); enqueueTimer = setTimeout(() => update(true), 200); }); filterInput.addEventListener('change', () => { if (enqueueTimer) clearTimeout(enqueueTimer); update(true); }); update(false); } function match(record, searchTerms) { const title = record.title.toLowerCase(); const initialTitle = (record.initialTitle||'').toLowerCase(); for (let searchTerm of searchTerms) { if (title.indexOf(searchTerm) === -1 && initialTitle.indexOf(searchTerm) === -1) return false; } return true; } init(); } const todayMs = Math.floor(new Date() / 86400000) * 86400000; function annotateUsers(posts, myUserId, userConfig, userMap, services) { posts.forEach(post => { if (post.rootElem.classList.contains('rinsp-post-annotated')) { return; } post.rootElem.classList.add('rinsp-post-annotated'); let customNameEntry = userConfig.customUserHashIdMappings['#' + post.postUid]; if (!post.defaultUserPic) { post.rootElem.classList.add('rinsp-post-custom-userpic'); const img = post.userPicElem.querySelector('img'); if ((img.getAttribute('height')||'150') * 1 > 150) { post.rootElem.classList.add('rinsp-post-userpic-tall'); } } if (userConfig.selfBypassHideUserpic && post.postUid === myUserId) { // skip } else { if (!userConfig.customUserBypassHideUserpic || customNameEntry == null) { if (post.defaultUserPic) { if (userConfig.hideDefaultUserpic) { post.rootElem.classList.add('rinsp-userpic-replace'); post.rootElem.classList.add('rinsp-userpic-replace-default'); } } else { if (userConfig.hideOtherUserpic) { post.rootElem.classList.add('rinsp-userpic-replace'); post.rootElem.classList.add('rinsp-userpic-replace-custom'); } } } } const data = userMap.get(post.postUid); // basically impossible if (data == null) { return; } const userLink = post.userNameElem.closest('a'); const bookmarkElem = newElem('span', 'rinsp-userbookmark'); userLink.parentNode.insertBefore(bookmarkElem, userLink); const bookmarkActionElem = addElem(bookmarkElem, 'span', 'rinsp-userbookmark-action'); if (customNameEntry != null) { post.rootElem.classList.add('rinsp-userframe-userknown'); const renamed = customNameEntry[2] !== post.postUname; if (data.unnamed || renamed) { bookmarkElem.classList.add('rinsp-userbookmark-takeover'); if (renamed) { post.rootElem.classList.add('rinsp-userframe-userrenamed'); } const customUserLink = addElem(bookmarkElem, 'a'); customUserLink.setAttribute('href', `u.php?action-show-uid-${post.postUid}.html`); customUserLink.setAttribute('target', '_blank'); customUserLink.textContent = customNameEntry[2]; } } bookmarkActionElem.addEventListener('click', async () => { if (customNameEntry == null) { const userHashId = await fetchUserHashId(post.postUid); services.changeCustomUserMapping(post.postUid, userHashId, post.postUname) .then(value => { if (value != null) { location.reload(); } }); } else { services.changeCustomUserMapping(customNameEntry[0], customNameEntry[1], customNameEntry[2]) .then(value => { if (value != null) { location.reload(); } }); } }); let cell = post.userPicElem.closest('.r_two'); if (data.unnamed) { cell.classList.add('rinsp-userframe-unnamed'); } if (userConfig.showExtendedUserInfo && data != null) { if (userConfig.showExtendedUserInfo$HP && data.hp < 0) { let hpTag = newElem('div', 'rinsp-userframe-udata-hp'); hpTag.textContent = data.hp + 'HP'; cell.querySelector('.user-pic + div > a').appendChild(hpTag); } addElem(post.userPicElem, 'div', 'rinsp-userframe-pulldown'); let infoBlock = addElem(post.userPicElem, 'div', 'rinsp-userframe-userinfo'); const addItem = (key, value, cls) => { let item = addElem(infoBlock, 'dl', cls); let dt = addElem(item, 'dt'); dt.textContent = key; let dd = addElem(item, 'dd'); dd.textContent = value; }; addItem('SP', data.sp.toLocaleString(), 'rinsp-userframe-udata-sp'); addItem('在线', String(data.online) + '小时', 'rinsp-userframe-udata-online', { title: '在线时间' }); addElem(infoBlock, 'div', 'rinsp-userframe-udata-spacer'); let actionItem = addElem(infoBlock, 'div', 'rinsp-userframe-udata-punish-tags'); if (services.punishRecordAccess) { services.punishRecordAccess .getPunishRecord(post.postUid) .then(record => { actionItem.innerHTML = ''; if (record == null || record.logs.length === 0) { return; } actionItem.appendChild(renderPunishTags(post.postUid, record)); }); } let loginItem = addElem(infoBlock, 'div', 'rinsp-userframe-udata-login'); loginItem.textContent = '最后登录 '; let loginItemValue = addElem(loginItem, 'span'); let awayDays = (todayMs - new Date(data.login)) / 86400000; if (awayDays < 1) { loginItemValue.classList.add('rinsp-userframe-udata-login-today'); loginItemValue.textContent = '今天'; } else if (awayDays < 2) { loginItemValue.classList.add('rinsp-userframe-udata-login-yesterday'); loginItemValue.textContent = '昨天'; } else { loginItemValue.textContent = data.login; } } const nameLabel = cell.querySelector('.user-pic ~ div[align="center"]'); if (nameLabel) { nameLabel.classList.add('rinsp-userframe-username'); if (userConfig.stickyUserInfo) { const nameLabelSticky = addElem(post.userPicElem, 'div', 'rinsp-userframe-username-sticky'); nameLabelSticky.appendChild(bookmarkElem.cloneNode(true)); nameLabelSticky.appendChild(nameLabel.querySelector('.rinsp-userbookmark + a').cloneNode(true)); nameLabelSticky.querySelector('.rinsp-userbookmark > .rinsp-userbookmark-action').addEventListener('click', () => { bookmarkElem.querySelector('.rinsp-userbookmark-action').click(); }); } } if (userConfig.stickyUserInfo) { const dummyUserPic = newElem('div', 'rinsp-user-pic-dummy'); post.userPicElem.parentNode.insertBefore(dummyUserPic, post.userPicElem.nextElementSibling); } }); } async function fetchGetPage(url, docType, autoRetry) { const resp = await fetch(url, { method: 'GET', mode: 'same-origin', credentials: 'same-origin', cache: 'no-cache' }); if (!resp.ok) { throw new Error('网络或登入错误'); } const content = await resp.text(); const parser = new DOMParser(); const doc = parser.parseFromString(content, docType || 'text/html'); if (autoRetry) { if (findErrorMessage(doc) === '论坛设置:刷新不要快于 1 秒') { await sleep(1100); return await fetchGetPage(url, docType, false); } } return doc; } async function fetchUserHashId(userId) { const doc = await fetchGetPage(`${document.location.origin}/u.php?action-show-uid-${userId}.html`); const hashIdElem = doc.querySelector('#main #u-wrap #u-content #u-top h1'); const hashId = hashIdElem.textContent.trim(); return hashId; } async function showUserNames(userConfig, userHashLookupStore, myUserId) { let mappings = userConfig.customUserHashIdMappings; if (mappings == null) { mappings = {}; } function transformOwnerCells(elemList, alsoChangeName) { elemList.forEach(el => { if (el.classList.contains('rinsp-uid-inspected')) { return; } el.classList.add('rinsp-uid-inspected'); const uid = Number.parseInt(el.getAttribute('href').substring(22)); if (userConfig.highlightMyself && uid === myUserId) { el.classList.add('rinsp-owner-isme'); if (alsoChangeName) { el.textContent = MY_NAME_DISPLAY; } } else { const fullName = mappings['#' + uid]; if (fullName != null) { el.classList.add('rinsp-owner-isknown'); if (alsoChangeName) { el.textContent = fullName[2]; } } } }); } if (document.location.pathname === '/search.php') { transformOwnerCells(document.querySelectorAll('tr.tr3.tac > td.smalltxt.y-style > a[href^="u.php?action-show-uid-"]'), true); } else { transformOwnerCells(document.querySelectorAll('td[id^="td_"] ~ td.tal > a.bl[href^="u.php?action-show-uid-"]'), true); } async function transformReplyCells(itemList) { const unresolved = []; for (let item of itemList) { if (item.byElem.classList.contains('rinsp-hashid-inspected')) { continue; } item.byElem.classList.add('rinsp-hashid-inspected'); let match = item.byElem.textContent.match(/^by: ([0-9a-f]{8})$/); if (match) { const uhash = match[1]; let myself = userConfig.highlightMyself && userConfig.myUserHashId === uhash; let fullName = mappings['@' + uhash]; if (fullName || myself) { if (myself) { item.byElem.textContent = 'by ' + MY_NAME_DISPLAY; item.byElem.classList.add('rinsp-nickname-byme'); } else { item.byElem.textContent = 'by ' + fullName[2]; item.byElem.classList.add('rinsp-nickname-byother'); } } else if (userConfig.replyShownAsByOp) { if (item.replyCount === 0 || item.opHashId === uhash) { item.byElem.textContent = 'by 楼主'; item.byElem.classList.add('rinsp-nickname-byop'); if (item.replyCount === 0) { item.byElem.classList.add('rinsp-nickname-byop-only'); } } else { unresolved.push({ item, uhash, uhashNum: Number.parseInt(uhash, 16) }); } } } } if (unresolved.length > 0) { const uids = await userHashLookupStore.getBatch(unresolved.map(entry => entry.uhashNum)); unresolved.forEach((entry, i) => { if (uids[i] != null && entry.item.opUid === uids[i]) { entry.item.byElem.textContent = 'by 楼主'; entry.item.byElem.classList.add('rinsp-nickname-byop'); } }); } } let itemList; if (document.location.pathname === '/search.php') { itemList = Array.from(document.querySelectorAll('tr.tr3.tac > td.smalltxt.y-style ~ td > a[href^="read.php?tid-"] + br')) .map(br => { let byElem; let next = br.nextSibling; if (next.nodeType === 3) { byElem = document.createElement('span'); byElem.textContent = next.textContent; br.parentNode.insertBefore(byElem, next); next.remove(); } else { byElem = br.nextElementSibling; } const tr = byElem.closest('tr'); const replyCount = tr.querySelector('td.smalltxt.y-style + .y-style').textContent * 1; const op = tr.querySelector('td.smalltxt.y-style > a[href^="u.php?action-show-uid-"]'); const opUid = op ? Number.parseInt(op.getAttribute('href').substring(22)) : null; const opHashId = op ? op.textContent.trim() : null; return { opUid, opHashId, replyCount, byElem }; }); } else { itemList = Array.from(document.querySelectorAll('td[id^="td_"] ~ td.tal > a[href^="read.php?tid-"] + br + span.gray2')) .map(byElem => { const tr = byElem.closest('tr'); const replyCount = tr.querySelector('td.tal.y-style.f10 > .s8').textContent * 1; const op = tr.querySelector('td.tal.y-style > a[href^="u.php?action-show-uid-"]'); const opUid = op ? Number.parseInt(op.getAttribute('href').substring(22)) : null; return { opUid, opHashId: null, replyCount, byElem }; }); } await transformReplyCells(itemList); } function getScaledValue(watchItem, min, max) { return min + (max - min) * Math.pow(Math.min(OLD_ITEM_THRESHOLD_DAYS, Math.floor(getPostAge(watchItem))), OLD_ITEM_SCALING) / Math.pow(OLD_ITEM_THRESHOLD_DAYS, OLD_ITEM_SCALING); } function getPostAge(watchItem) { return (Date.now() - watchItem.timeOpened) / 86400000; } function getWatchExpiryDays(watchItem) { switch (watchItem.areaName) { case REQUEST_ZONE_NAME: return 45; case '茶馆': return 120; case '茶楼': return 60; default: return -1; } } function isWatchExpired(watchItem) { if (watchItem.timeOpened == null) { // impossible actually return false; } else { const expiryDays = getWatchExpiryDays(watchItem); if (expiryDays < 0) { return false; } else { return Date.now() - watchItem.timeOpened > expiryDays * 86400000; } } } function createInfiniteScrollHandler(typeId, infConfig, userConfig, mainConfigAccess) { const configKey = 'infiniteScroll$' + typeId; const enableEffect = { value: !!userConfig[configKey], listeners: [], setValue(newValue) { this.value = newValue; this.listeners.forEach(fn => fn()); if (this.value) { install(); } else { uninstall(); } } }; const cfg = Object.assign({ initPaginator(doc) { const pagesones = Array.from(doc.querySelectorAll('.pages > ul > li.pagesone')); if (pagesones.length === 0) { return null; } pagesones.forEach(el => addInfSwitch(el)); const pagesone = pagesones.pop(); const otherPageBars = pagesones.map(el => el.closest('.pages')); const match = pagesone.textContent.match(/Pages:\s*(\d+)(?: *\u2013 *(\d+))?\/(\d+)/); let curPageStart = match[1] * 1; let curPageEnd = (match[2]||match[1]) * 1; const maxPage = match[3] * 1; const curPageLabel = pagesone.closest('ul').querySelector('li > b'); const curPageElem = curPageLabel.parentNode; let morePageElems = []; let pageElem = curPageElem.nextElementSibling; while (!pageElem.classList.contains('pagesone')) { morePageElems.push(pageElem); pageElem = pageElem.nextElementSibling; } morePageElems.pop(); return { getMorePageElems() { return morePageElems; }, getCurrentStart() { return curPageStart; }, getCurrentEnd() { return curPageEnd; }, getMaxPage() { return maxPage; }, getNextPageURL() { return morePageElems.length > 0 ? morePageElems[0].querySelector('a[href]').href : null; }, setCurrentEnd(paginator) { curPageEnd = paginator.getCurrentEnd(); const cur = curPageStart === curPageEnd ? curPageStart : `${curPageStart} \u2013 ${curPageEnd}`; const input = pagesone.querySelector('input'); doc.body.appendChild(input); pagesone.textContent = `Pages: ${cur}/${maxPage}\u00A0 \u00A0 \u00A0Go `; pagesone.appendChild(input); curPageLabel.textContent = cur; while (morePageElems.length > 0) { morePageElems.pop().remove(); } const temp = doc.createElement('div'); temp.innerHTML = paginator.getMorePageElems().map(el => el.outerHTML).join(''); morePageElems = Array.from(temp.children); morePageElems.slice().reverse().forEach(el => { curPageElem.parentNode.insertBefore(el, curPageElem.nextElementSibling); }); temp.remove(); otherPageBars.forEach(el => { const extraSwitches = Array.from(el.querySelectorAll('.pagesone ~ li')); // retain added switches extraSwitches.forEach(ext => document.body.appendChild(ext)); el.innerHTML = pagesone.closest('.pages').innerHTML; el.querySelectorAll('.pagesone ~ li').forEach(el => el.remove()); extraSwitches.forEach(ext => el.querySelector('.pagesone').parentNode.appendChild(ext)); }); } }; function addInfSwitch(pagesone) { const li = addElem(pagesone.closest('ul'), 'li'); const infToggle = addElem(li, 'a', 'rinsp-infscroll-switch', { href: 'javascript:', title: '自动加载下一页' }); infToggle.textContent = '↷'; infToggle.addEventListener('click', async () => { const newUserConfig = await mainConfigAccess.update(updatingUserConfig => { updatingUserConfig[configKey] = !enableEffect.value; return updatingUserConfig; }); enableEffect.setValue(!!newUserConfig[configKey]); }); function update() { if (enableEffect.value) { infToggle.classList.add('rinsp-active'); } else { infToggle.classList.remove('rinsp-active'); } } enableEffect.listeners.push(update); update(); } }, getItemId(item) { return null; }, getContentItems(doc) { return []; }, appendContentItems(items, start, end) { }, getDividerLabel(page) { return `第 ${page} 页`; } }, infConfig); const paginator = cfg.initPaginator(document); if (paginator == null) { return; } let prepareFlipTimer = null; let triggerFlipTimer = null; const endTrigger = addElem(document.body, 'div', 'rinsp-infscroll-page-endtrigger'); addElem(endTrigger, 'div', 'rinsp-infscroll-loader-bar'); const delayBeforeFlipPage = 500; const delayBeforeAllowFlipPage = 200; const stayBottomThreshold = 15; const triggerMargin = 5; let currentState = 0; function setInfScrollState(state) { currentState = state; function clearPrepare() { if (prepareFlipTimer) { clearTimeout(prepareFlipTimer); prepareFlipTimer = null; } } function clearTrigger() { if (triggerFlipTimer) { clearTimeout(triggerFlipTimer); triggerFlipTimer = null; } } if (state === 0) { // idle clearPrepare(); clearTrigger(); document.documentElement.classList.remove('rinsp-infscroll-armed'); document.documentElement.classList.remove('rinsp-infscroll-firing'); } else if (state === 1) { // armed document.documentElement.classList.add('rinsp-infscroll-armed'); document.documentElement.classList.remove('rinsp-infscroll-firing'); clearPrepare(); clearTrigger(); } else if (state === 2) { // firing document.documentElement.classList.add('rinsp-infscroll-armed'); document.documentElement.classList.add('rinsp-infscroll-firing'); clearPrepare(); } } const scrollEventListener = () => { const bottomGap = document.body.scrollHeight - window.scrollY - window.innerHeight; if (bottomGap <= triggerMargin) { if (currentState === 1) { setInfScrollState(2); if (triggerFlipTimer == null) { triggerFlipTimer = setTimeout(async () => { setInfScrollState(0); await insertNextPageSync(); }, delayBeforeFlipPage); } } else if (currentState === 0) { if (prepareFlipTimer == null) { const nextPageURL = paginator.getNextPageURL(); if (nextPageURL != null) { fetchNextPage(nextPageURL); // prefetch } prepareFlipTimer = setTimeout(() => { setInfScrollState(1); }, delayBeforeAllowFlipPage); } } } else if (bottomGap > stayBottomThreshold) { setInfScrollState(0); } }; function install() { uninstall(); if (paginator.getNextPageURL() == null) { return; } document.documentElement.classList.add('rinsp-infscroll-enabled'); window.addEventListener('scroll', scrollEventListener); } function uninstall() { document.documentElement.classList.remove('rinsp-infscroll-enabled'); setInfScrollState(0); window.removeEventListener('scroll', scrollEventListener); } let busy = false; let nextPageCache = { expiry: Date.now(), url: null, data: null }; async function fetchNextPage(url) { if (nextPageCache.url === url && nextPageCache.expiry > Date.now()) { return nextPageCache.data; } const data = fetchGetPage(url, null, true); nextPageCache = { url, expiry: Date.now() + 10000, // 10 seconds data }; return data; } async function insertNextPageSync() { if (!busy) { busy = true; await insertNextPage() .then(obj => { if (obj && userConfig.infiniteScrollScrollToNewPage) { obj.scrollToNewPage(); } }) .finally(() => { busy = false; }); } } async function insertNextPage() { const nextPageURL = paginator.getNextPageURL(); if (nextPageURL == null) { return; } const oldScrollTop = document.documentElement.scrollTop; const divider = cfg.appendDivider(); divider.classList.add('rinsp-infscroll-loading'); divider.textContent = `第 ${paginator.getCurrentEnd() + 1} 页 - 加载中 ...`; const nextDoc = await fetchNextPage(nextPageURL); if (isFastLoadMode()) { deferImageLoading(nextDoc); } const currentIds = new Set(Array.from(cfg.getContentItems(document)).map(item => cfg.getItemId(item)).filter(item => item != null)); const newItems = Array.from(cfg.getContentItems(nextDoc)).filter(item => { const itemId = cfg.getItemId(item); return itemId == null || !currentIds.has(itemId); }); const newPaginator = cfg.initPaginator(nextDoc); if (newPaginator == null) { divider.textContent = '找不到更多记录'; uninstall(); return; } divider.textContent = cfg.getDividerLabel(newPaginator.getCurrentStart()); divider.classList.remove('rinsp-infscroll-loading'); cfg.appendContentItems(newItems); paginator.setCurrentEnd(newPaginator); if (paginator.getCurrentEnd() >= paginator.getMaxPage()) { uninstall(); } if (userConfig.infiniteScrollReplaceURL) { window.history.replaceState(null, null, nextPageURL); } if (isFastLoadMode()) { resumeImageLoading(document); } try { unsafeWindow.peacemaker.rescan(); } catch (ex) {} document.documentElement.scrollTop = oldScrollTop; return { scrollToNewPage() { divider.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; } if (enableEffect.value) { install(); } } function setupInfiniteScroll_userTopics(userConfig, mainConfigAccess) { const table = document.querySelector('#u-contentmain > .u-table'); createInfiniteScrollHandler('usertopics', { getContentItems(doc) { return doc.querySelectorAll('#u-contentmain > .u-table > tbody > tr ~ tr'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '2' }); }, appendContentItems(items) { const nextPageBody = addElem(table, 'tbody'); items.forEach(item => nextPageBody.appendChild(item)); }, getItemId(item) { const a = item.querySelector('th > a[href^="read.php?tid-"]'); return a ? a.getAttribute('href') : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_userReplies(userConfig, mainConfigAccess) { const table = document.querySelector('#u-contentmain > .u-table'); createInfiniteScrollHandler('userposts', { getContentItems(doc) { return doc.querySelectorAll('#u-contentmain > .u-table > tbody > tr ~ tr'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '2' }); }, appendContentItems(items) { const nextPageBody = addElem(table, 'tbody'); items.forEach(item => nextPageBody.appendChild(item)); }, getItemId(item) { const a = item.querySelector('th > a[href^="job.php?action-topost-tid-"]'); return a ? a.getAttribute('href') : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_thread(userConfig, mainConfigAccess) { const form = document.querySelector('form[name="delatc"]'); createInfiniteScrollHandler('thread_posts', { getContentItems(doc) { const rows = Array.from(doc.querySelector('form[name="delatc"]').children); while (rows[0].tagName === 'INPUT') { rows.shift(); } return rows; }, appendDivider() { return addElem(form, 'div', 'rinsp-infscroll-divider'); }, appendContentItems(items) { items.forEach(item => { if (item.matches('.t5.t2[style="border-top:0"]')) { item.removeAttribute('style'); } form.appendChild(item); }); }, getItemId(item) { const idCell = item.querySelector('.t5.t2 tr > th[id^="td_"], div[id^="menu_read_"]'); if (idCell) { return idCell.getAttribute('id'); } if (item.matches('a[name]')) { return 'a:' + item.getAttribute('name'); } return null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_search(userConfig, mainConfigAccess) { const table = document.querySelector('#main > .t > table'); createInfiniteScrollHandler('search', { getContentItems(doc) { return doc.querySelectorAll('#main > .t > table > tbody > .tr2, #main > .t > table > tbody > .tr3'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '7' }); }, appendContentItems(items) { const endPaddingRow = table.querySelector('tbody > tr > td.f_one[colspan]'); const nextPageBody = addElem(table, 'tbody'); items.forEach(item => nextPageBody.appendChild(item)); if (endPaddingRow) { nextPageBody.appendChild(endPaddingRow); } }, getItemId(item) { const link = item.querySelector('.tr3 > th > a[href^="read.php?tid-"]'); return link ? Number.parseInt(link.getAttribute('href').substring(13)) : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_threadList(userConfig, mainConfigAccess) { const table = document.querySelector('#ajaxtable'); createInfiniteScrollHandler('threads', { getContentItems(doc) { return doc.querySelectorAll('#ajaxtable > tbody + tbody > .tr2, #ajaxtable > tbody + tbody > tr.t_one'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '5' }); }, appendContentItems(items) { const endPaddingRow = table.querySelector('tbody > tr > td.f_one[colspan]'); const nextPageBody = addElem(table, 'tbody'); items.forEach(item => nextPageBody.appendChild(item)); if (endPaddingRow) { nextPageBody.appendChild(endPaddingRow); } }, getItemId(item) { const idCell = item.querySelector('.t_one > td[id^="td_"]'); return idCell ? idCell.getAttribute('id') : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_threadPicWall(userConfig, mainConfigAccess) { const container = document.querySelector('#thread_img > .dcsns-content'); createInfiniteScrollHandler('threads_picwall', { getContentItems(doc) { return doc.querySelectorAll('#thread_img > .dcsns-content > .stream > li'); }, appendDivider() { return addElem(container, 'div', 'rinsp-infscroll-divider'); }, appendContentItems(items) { const nextWall = addElem(container, 'ul', 'stream'); items.forEach(item => nextWall.appendChild(item)); try { unsafeWindow.jQuery('img.lazy', nextWall).lazyload({ skip_invisible : false }); } catch (ignore) {} }, getItemId(item) { const link = item.querySelector('.section-title > a[href]'); return link ? link.getAttribute('href') : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_msgInbox(userConfig, mainConfigAccess) { const table = document.querySelector('#info_base .set-table2'); createInfiniteScrollHandler('msg_inbox', { getContentItems(doc) { return doc.querySelectorAll('#info_base .set-table2 > tbody > tr'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '5' }); }, appendContentItems(items) { const nextTbody = addElem(table, 'tbody'); items.forEach(item => nextTbody.appendChild(item)); }, getItemId(item) { const link = item.querySelector('input[name="delid[]"]'); return link ? link.value : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_msgSent(userConfig, mainConfigAccess) { const table = document.querySelector('#info_base .set-table2'); createInfiniteScrollHandler('msg_sent', { getContentItems(doc) { return doc.querySelectorAll('#info_base .set-table2 > tbody > tr'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '7' }); }, appendContentItems(items) { const nextTbody = addElem(table, 'tbody'); items.forEach(item => nextTbody.appendChild(item)); }, getItemId(item) { const link = item.querySelector('input[name="delid[]"]'); return link ? link.value : null; } }, userConfig, mainConfigAccess); } function setupInfiniteScroll_msgChatLog(userConfig, mainConfigAccess) { const table = document.querySelector('#info_base .set-table2'); createInfiniteScrollHandler('msg_chatlog', { getContentItems(doc) { return doc.querySelectorAll('#info_base .set-table2 > tbody > tr'); }, appendDivider() { return addElem(addElem(addElem(table, 'tbody', 'rinsp-infscroll-divider'), 'tr'), 'td', null, { colspan: '7' }); }, appendContentItems(items) { const nextTbody = addElem(table, 'tbody'); items.forEach(item => nextTbody.appendChild(item)); }, getItemId(item) { const link = item.querySelector('input[name="delid[]"]'); return link ? link.value : null; } }, userConfig, mainConfigAccess); } function submitOriginalForm(form) { submitPostForm(form.getAttribute('action'), new FormData(form), form.getAttribute('target')||null); } function submitPostForm(action, formData, target) { const newForm = addElem(document.body, 'form', null, { action: action, method: 'post', target: target||'' }); formData.forEach((value, key) => { addElem(newForm, 'input', null, { type: 'hidden', name: key, value: value }); }); newForm.submit(); } //============= admin =============// function renderPunishTags(uid, record) { const tags = newElem('a', 'rinsp-profile-punish-tags', { href: `u.php?action-trade-uid-${uid}.html`, target: '_blank' }); let bloodCount = 0; let banCount = 0; let delCount = 0; let otherCount = 0; record.logs.forEach(log => { switch (log.type) { case PUNISH_TYPE_HP: bloodCount++; break; case PUNISH_TYPE_BAN: banCount++; break; case PUNISH_TYPE_DELETE_THREAD: case PUNISH_TYPE_DELETE_REPLY: case PUNISH_TYPE_SHIELD: delCount++; break; case PUNISH_TYPE_UNSHIELD: // skip break; default: otherCount++; } }); const putCountTag = (count, tag) => { if (count > 0) { addElem(tags, 'span', 'rinsp-profile-punish-tag rinsp-profile-punish-tag-' + tag).textContent = `${count}`; } }; putCountTag(bloodCount, 'blood'); putCountTag(banCount, 'ban'); putCountTag(delCount, 'del'); putCountTag(otherCount, 'misc'); return tags; } function initAdminFunctions(myUserId, userConfig, userPunishRecordStore) { const NO_PING_NOTIFICATION = 'no-ping-mail'; const extraPageParams = {}; if (document.location.hash) { const pairs = document.location.hash.substring(1).split('-'); while (pairs.length > 0) { const key = pairs.shift(); const value = pairs.shift(); extraPageParams[key] = value; } } const adminTemplateConfigAccess = initConfigAccess(myUserId, ADMIN_TEMPLATE_CONFIG_KEY, DEFAULT_ADMIN_TEMPLATEUSER_FILTER_CONFIG); async function recordPunishment(uid, type, summary, reason, data) { let punishRecord = await userPunishRecordStore.get(uid); if (punishRecord == null) { punishRecord = { uid, logs: [] }; } punishRecord.logs.push({ time: Date.now(), type, summary, reason, data }); await userPunishRecordStore.put(uid, punishRecord); } async function openReasonTemplateEditor(callback) { openTextListEditor(SCORING_REASON_TEMPLATE_POPUP_MENU_ID, { title: '常用评分或操作理由列表', async read() { const adminTemplate = await adminTemplateConfigAccess.read(); const text = adminTemplate.pingReasons.join('\n'); if (text.length === 0) { return DEFAULT_ADMIN_TEMPLATEUSER_FILTER_CONFIG.pingReasons.slice().join('\n'); } return text; }, async save(textData) { return await adminTemplateConfigAccess.update(function(adminTemplate) { adminTemplate.pingReasons = textData.split('\n').map(itm=>itm.trim()).filter(itm=>!!itm); return adminTemplate; }); } }, callback); } async function enableQuickPingFunction() { let gfPost = getPosts(document, 1)[0]; if (gfPost == null || gfPost.floor !== 0) { return; } const postTitleElem = document.querySelector('#subject_tpc'); if (postTitleElem == null) { return; } const postTitle = postTitleElem.textContent.trim(); const postTime = gfPost.postTime; const postUid = gfPost.postUid; const headtopicButton = document.querySelector('.fl > #headtopic'); if (headtopicButton) { // scoring allowed const markElem = document.querySelector('#mark_tpc'); if (markElem) { const pingStatus = newElem('a', 'rinsp-quickping-status'); const score = (markElem.textContent.match(/SP币:([+-]\d+)\b/)||[])[1]; if (score) { pingStatus.textContent = '已评分: SP' + score; } else { pingStatus.textContent = '已评分'; } headtopicButton.parentNode.prepend(pingStatus); } else { const currentPostLink = document.querySelector('#breadcrumbs > .crumbs-item.current > strong > a[href^="read.php?tid-"]'); if (currentPostLink == null) return; const tid = Number.parseInt(currentPostLink.getAttribute('href').substring(13)); const fidLink = currentPostLink.closest('.crumbs-item').previousElementSibling.getAttribute('href'); const fid = Number.parseInt(fidLink.substring(15)); // e.g. "thread.php?fid-4.html" const defaultRating = getDefaultRating(postTitle); let instantPingButton = null; if (defaultRating.baseTotalScore > 0 && defaultRating.baseTotalScore < 99999) { if (!defaultRating.ownBought && !defaultRating.ownTranslate) { const notif = !userConfig.adminNoScoreNotifByDefault; const score = Math.min(Math.max(1, Math.floor(defaultRating.baseTotalScore)), 99999); instantPingButton = newElem('a', 'rinsp-instantping-button'); instantPingButton.setAttribute('href', 'javascript:void(0)'); instantPingButton.textContent = notif ? `一键SP+${score}` : `一键SP+${score} (不通知)`; instantPingButton.addEventListener('click', () => { executeScoreThenReload(null, fid, tid, score, 0, {}, notif, ''); return false; }); headtopicButton.parentNode.prepend(instantPingButton); headtopicButton.parentNode.prepend(document.createTextNode(' | ')); } } const quickPingButton = newElem('a', 'rinsp-quickping-button'); quickPingButton.setAttribute('href', 'javascript:void(0)'); quickPingButton.textContent = '快捷评分'; quickPingButton.addEventListener('click', () => { let dayAge = (Date.now() - postTime) / 86400000; openScoringDialog(tid, fid, postTitle, dayAge, defaultRating); return false; }); headtopicButton.parentNode.prepend(quickPingButton); if (!userConfig.adminHideMarkBadFormatButton) { headtopicButton.parentNode.append(document.createTextNode(' | ')); const badFormatButton = newElem('a', 'rinsp-noping-button'); badFormatButton.setAttribute('href', 'javascript:void(0)'); badFormatButton.textContent = '格式不正'; badFormatButton.addEventListener('click', () => { executeScoreThenReload(null, fid, tid, -1, 0, {}, true, '请修正格式问题'); return false; }); headtopicButton.parentNode.append(badFormatButton); } if (!userConfig.adminHideMarkUnscoreButton) { headtopicButton.parentNode.append(document.createTextNode(' | ')); const noPingButton = newElem('a', 'rinsp-noping-button'); noPingButton.setAttribute('href', 'javascript:void(0)'); noPingButton.textContent = '标记不评分'; noPingButton.addEventListener('click', () => { executeScoreThenReload(null, fid, tid, 1, 0, {}, false, ''); return false; }); headtopicButton.parentNode.append(noPingButton); } const customNameEntry = userConfig.customUserHashIdMappings['#' + postUid]; if (customNameEntry && customNameEntry[2].startsWith(NEVER_SCORE_USER_PREFIX)) { quickPingButton.classList.add('rinsp-quickping-grey'); if (instantPingButton) { instantPingButton.classList.add('rinsp-instantping-grey'); } } } } } async function enhanceReasonSelector() { const reasonField = document.querySelector('form[action] textarea[name="atc_content"]'); if (reasonField == null) { return; } const reasonSelect = reasonField.nextElementSibling; if (reasonSelect == null || reasonSelect.tagName !== 'SELECT' || !!reasonSelect.getAttribute('name')) { return; } const replacement = addReasonSelector(reasonField); replacement.setAttribute('style', 'display:inline-block;vertical-align:top'); reasonField.parentNode.insertBefore(replacement, reasonSelect); reasonSelect.remove(); } function autoFillShowPingPage() { const postTitle = (document.querySelector('form[name="ping"] .tr3 > th > a[href^="read.php?tid-"]')||{}).textContent||''; const defaultRating = getDefaultRating(postTitle); const addPointField = document.querySelector('form[name="ping"] input[name="addpoint"]'); if (defaultRating.baseTotalScore > 0) { addPointField.value = String(Math.max(1, Math.floor(defaultRating.baseTotalScore))); } else { addPointField.value = ''; setTimeout(() => addPointField.focus()); } } const OPTION_SEPARATOR_VALUE = ' NONE '; const OPTION_CLEAR_ACTION_VALUE = ' CLEAR '; async function updateReasonSelect(reasonSelect, reasonField) { function addOption(value, label) { let option = addElem(reasonSelect, 'option'); option.setAttribute('value', value); option.textContent = label; } reasonSelect.innerHTML = ''; addOption('', '自定义'); addOption(OPTION_CLEAR_ACTION_VALUE, '清除'); addOption(OPTION_SEPARATOR_VALUE, '-------'); const adminTemplate = await adminTemplateConfigAccess.read(); adminTemplate.pingReasons.forEach(item => { item = item.trim(); if (item) { if (item.match(/---+/)) { addOption(OPTION_SEPARATOR_VALUE, '-------'); } else { addOption(item.replace(/\|/g, '\n'), item.substring(0, 10)); } } }); reasonSelect.value = reasonField.value.trim(); reasonSelect.value = reasonSelect.value||''; } function addReasonSelector(reasonField) { const reasonTemplateElem = newElem('div'); const reasonSelect = addElem(reasonTemplateElem, 'select'); reasonSelect.setAttribute('name', ''); reasonSelect.setAttribute('size', '6'); reasonSelect.addEventListener('change', () => { if (reasonSelect.value === '' || reasonSelect.value === OPTION_SEPARATOR_VALUE) { reasonSelect.value = ''; } else if (reasonSelect.value === OPTION_CLEAR_ACTION_VALUE) { reasonSelect.value = ''; reasonField.value = ''; } else { reasonField.value = reasonSelect.value; } }); reasonField.addEventListener('change', () => { reasonSelect.value = reasonField.value.trim(); reasonSelect.value = reasonSelect.value||''; }); addElem(reasonTemplateElem, 'div'); const editTemplateButton = addElem(reasonTemplateElem, 'a'); editTemplateButton.setAttribute('href', 'javascript:void(0)'); editTemplateButton.textContent = '📝编辑常用列表'; editTemplateButton.addEventListener('click', () => { openReasonTemplateEditor(() => updateReasonSelect(reasonSelect, reasonField)); }); updateReasonSelect(reasonSelect, reasonField); return reasonTemplateElem; } async function openScoringDialog(tid, fid, postTitle, postAgeInDays, defaultRating) { const preset = DEFAULT_SCORING_PRESETS[`fid=${fid}`] || DEFAULT_SCORING_PRESETS['fid=*']; const anchor = null; const modelMask = addModalMask(); const popupMenu = createPopupMenu(SCORING_DIALOG_POPUP_MENU_ID, anchor); popupMenu.renderContent(async function(borElem) { addElem(borElem, 'div', 'rinsp-quickping-subject').textContent = postTitle; const form = addElem(borElem, 'dl', 'rinsp-quickping-form'); addElem(form, 'dt').textContent = '发帖日期'; addElem(form, 'dd').textContent = postAgeInDays <= 1 ? '一天内' : postAgeInDays <= 2 ? '昨天' : `${Math.floor(postAgeInDays)} 天前`; addElem(form, 'div'); addElem(form, 'dt').textContent = '资源大小'; const sizeCell = addElem(form, 'dd'); addElem(form, 'div'); addElem(form, 'dt').textContent = '基础评分'; const baseScoreCell = addElem(form, 'dd'); const baseScoreInputField = addElem(baseScoreCell, 'input'); baseScoreInputField.setAttribute('size', '12'); addElem(baseScoreCell, 'span').textContent = ' \u00A0 \u00A0 可加乘部分: '; const multiBaseScoreInputField = addElem(baseScoreCell, 'input'); multiBaseScoreInputField.setAttribute('type', 'text'); multiBaseScoreInputField.setAttribute('size', '10'); multiBaseScoreInputField.setAttribute('placeholder', '(全部)'); baseScoreInputField.setAttribute('type', 'text'); const resourcePackToggles = []; if (defaultRating.baseTotalScore) { baseScoreInputField.value = defaultRating.baseTotalScore; } else { setTimeout(() => baseScoreInputField.focus()); } if (defaultRating.resourceSizeTexts.length === 0) { sizeCell.textContent = '不明'; } else if (defaultRating.resourceSizeTexts.length === 1) { sizeCell.textContent = defaultRating.resourceSizeTexts[0]; } else { const computeAutofillScore = () => { let totalSizeMB = 0; resourcePackToggles.forEach(toggle => { if (toggle.checkbox.checked) { totalSizeMB += toggle.sizeMB; } }); baseScoreInputField.value = computeBaseScoreText(totalSizeMB); updatePreview(); }; const updateSizeCheckboxes = () => { const sizeMB = Math.ceil(baseScoreInputField.value * 10); if (sizeMB > 0) { let remain = sizeMB; resourcePackToggles.forEach(toggle => { if (remain >= toggle.sizeMB) { remain -= toggle.sizeMB; toggle.checkbox.checked = true; } else { toggle.checkbox.checked = false; } }); } else { resourcePackToggles.forEach(toggle => toggle.checkbox.checked = false); } }; defaultRating.resourceSizeTexts.forEach((sizeText, i) => { if (i > 0) { addElem(sizeCell, 'span').textContent = ' '; } const item = addElem(sizeCell, 'label'); const checkbox = addElem(item, 'input', null, { type: 'checkbox' }); addElem(item, 'span', null, { style: 'vertical-align:bottom' }).textContent = sizeText; resourcePackToggles.push({ checkbox, sizeMB: defaultRating.resourceSizeMBs[i] }); checkbox.addEventListener('change', computeAutofillScore); }); resourcePackToggles.sort(comparator(toggle => toggle.sizeMB, true)); baseScoreInputField.addEventListener('change', updateSizeCheckboxes); baseScoreInputField.addEventListener('keyup', updateSizeCheckboxes); } addElem(form, 'div'); addElem(form, 'dt').textContent = '加亮置顶'; const attCell = addElem(form, 'dd'); const ownBoughtLabel = addElem(attCell, 'label', 'rinsp-quickping-attr-ownbought'); const ownBoughtField = addElem(ownBoughtLabel, 'input'); ownBoughtLabel.appendChild(document.createTextNode('自购 / 原创首发')); ownBoughtField.setAttribute('type', 'checkbox'); if (defaultRating.ownBought) { ownBoughtField.checked = true; } const ownTranslateLabel = addElem(attCell, 'label', 'rinsp-quickping-attr-owntranslate'); const ownTranslateField = addElem(ownTranslateLabel, 'input'); ownTranslateLabel.appendChild(document.createTextNode('红字 (例: 个人汉化游戏)')); ownTranslateField.setAttribute('type', 'checkbox'); const compilationLabel = addElem(attCell, 'label', 'rinsp-quickping-attr-compilation'); const compilationField = addElem(compilationLabel, 'input'); compilationLabel.appendChild(document.createTextNode('优秀合集')); compilationField.setAttribute('type', 'checkbox'); const extraScores = []; preset.extraScoreAdjustments.forEach(extra => { const extraOffsetLabel = addElem(attCell, 'label', 'rinsp-quickping-attr-extra'); const extraOffsetField = addElem(extraOffsetLabel, 'input'); extraOffsetLabel.appendChild(document.createTextNode(extra.label)); extraOffsetField.setAttribute('type', 'checkbox'); extraScores.push({ amount: extra.amount, reason: extra.reason||'', checkbox: extraOffsetField }); }); addElem(form, 'div'); addElem(form, 'dt').textContent = '评分加乘'; const multiScoreCell = addElem(form, 'dd'); const multiScoreInputField = addElem(multiScoreCell, 'input'); multiScoreInputField.setAttribute('type', 'text'); multiScoreInputField.setAttribute('size', '12'); addElem(form, 'div'); addElem(form, 'dt').textContent = '置顶'; const headtopCell = addElem(form, 'dd'); const headtopInputField = addElem(headtopCell, 'input'); headtopInputField.setAttribute('type', 'text'); headtopInputField.setAttribute('size', '12'); addElem(form, 'div'); addElem(form, 'dt').textContent = '其他'; const notifyCell = addElem(form, 'dd'); const notifyLabel = addElem(notifyCell, 'label'); const notifyField = addElem(notifyLabel, 'input'); notifyLabel.appendChild(document.createTextNode('评分消息通知')); notifyField.setAttribute('type', 'checkbox'); addElem(notifyCell, 'small').textContent = ' (加亮及置顶消息一律自动通知)'; // NO_PING_NOTIFICATION notifyField.checked = !userConfig.adminNoScoreNotifByDefault; addElem(form, 'div'); addElem(form, 'dt').textContent = '评分理由'; const reasonCell = addElem(form, 'dd', 'rinsp-quickping-reason-cell'); const reasonField = addElem(reasonCell, 'textarea'); const templateColumn = addElem(reasonCell, 'div'); templateColumn.appendChild(addReasonSelector(reasonField)); addElem(form, 'div'); addElem(form, 'dt').textContent = '最终效果'; const previewSection = addElem(form, 'div', 'rinsp-quickping-preview-section'); const previewCell = addElem(previewSection, 'div'); baseScoreInputField.addEventListener('change', () => updatePreview()); baseScoreInputField.addEventListener('keyup', () => updatePreview()); multiBaseScoreInputField.addEventListener('change', () => updatePreview()); multiBaseScoreInputField.addEventListener('keyup', () => updatePreview()); multiScoreInputField.addEventListener('change', () => updatePreview()); multiScoreInputField.addEventListener('keyup', () => updatePreview()); headtopInputField.addEventListener('change', () => updatePreview()); headtopInputField.addEventListener('keyup', () => updatePreview()); ownBoughtField.addEventListener('change', () => updatePreview()); ownTranslateField.addEventListener('change', () => updatePreview()); compilationField.addEventListener('change', () => updatePreview()); extraScores.forEach(extra => { extra.checkbox.addEventListener('change', () => { if (extra.reason) { const currentReason = reasonField.value.trim(); if (extra.checkbox.checked) { if (currentReason.length === 0) { reasonField.value = extra.reason; } } else { if (currentReason === extra.reason) { reasonField.value = ''; } } } updatePreview(); }); }); function updatePreview() { previewCell.textContent = ''; previewCell.setAttribute('class', ''); const defaultHeadtopDays = getDefaultHeadtopDays(); if (defaultHeadtopDays == null) { headtopInputField.setAttribute('placeholder', '自动 (无)'); } else { if (defaultHeadtopDays[0] > 0) { headtopInputField.setAttribute('placeholder', `自动 (${defaultHeadtopDays[0]}天)`); } else if (defaultHeadtopDays[1] > 0) { headtopInputField.setAttribute('placeholder', '自动 (旧帖省略)'); } else { headtopInputField.setAttribute('placeholder', '自动 (无)'); } } multiScoreInputField.setAttribute('placeholder', '自动 (无)'); const finalScore = getFinalScore(); const headtopDays = getFinalHeadtopDays(); if (finalScore == null || headtopDays == null) { previewCell.classList.add('rinsp-quickping-status-error'); if (!baseScoreInputField.value.trim()) { previewCell.textContent = ''; } else { previewCell.textContent = '输入错误 请更正'; } } else { if (finalScore.total < 0) { let text = addElem(previewCell, 'span'); text.appendChild(document.createTextNode('扣分 ')); addElem(previewCell, 'b', null, { style: 'color: red' }).textContent = `${finalScore.total} SP`; } else if (finalScore.multiplier > 1) { let text = addElem(previewCell, 'span'); text.appendChild(document.createTextNode('评分')); if (finalScore.nonboostedScore > 0) { text.appendChild(document.createTextNode(` +${finalScore.boostedScore} SP`)); addElem(text, 'small').textContent = `(x${finalScore.multiplier})`; text.appendChild(document.createTextNode(` +${finalScore.nonboostedScore} SP`)); } else { addElem(text, 'small').textContent = `(x${finalScore.multiplier})`; text.appendChild(document.createTextNode(` +${finalScore.total} SP`)); } multiScoreInputField.setAttribute('placeholder', `自动 (x${finalScore.multiplier})`); } else { addElem(previewCell, 'span').textContent = `评分 +${finalScore.total} SP`; multiScoreInputField.setAttribute('placeholder', '自动 (无)'); } if (headtopDays > 0) { addElem(previewCell, 'span').textContent = ' | '; addElem(previewCell, 'span').textContent = '置顶 ' + headtopDays + '天'; } const hlParams = getHighlightParams(); if (hlParams.color || hlParams.bold) { addElem(previewCell, 'span').textContent = ' | '; const hlElem = addElem(previewCell, hlParams.bold ? 'b' : 'span'); hlElem.textContent = '加亮'; if (hlParams.color) { hlElem.setAttribute('style', 'color:' + hlParams.color); } } } } updatePreview(); function getHighlightParams() { let color = compilationField.checked ? '#FF00FF' : ownBoughtField.checked ? '#0000FF' : ownTranslateField.checked ? '#FF0000' : null; let bold = compilationField.checked || ownBoughtField.checked || ownTranslateField.checked; return { color, bold }; } function getDefaultScoreMultiplier() { return ownBoughtField.checked || ownTranslateField.checked ? 10 : 1; } function getDefaultHeadtopDays() { const baseDays = compilationField.checked ? preset.headtopDaysCompilation : ownBoughtField.checked || ownTranslateField.checked ? preset.headtopDaysBought : 0; if (postAgeInDays >= baseDays) { return [0, baseDays]; } return [baseDays, baseDays]; } function getFinalHeadtopDays() { if (headtopInputField.value.trim().length === 0) { return getDefaultHeadtopDays()[0]; } else { let days = headtopInputField.value.trim() * 1; if (!Number.isInteger(days) || days < 0) return null; // invalid return days; } } function getFinalScore() { let scoreMultiplier; if (multiScoreInputField.value.trim().length === 0) { scoreMultiplier = getDefaultScoreMultiplier(); } else { scoreMultiplier = multiScoreInputField.value.trim() * 1; if (!Number.isInteger(scoreMultiplier) || scoreMultiplier < 1) return null; // invalid } let baseScore = baseScoreInputField.value * 1; if (Number.isNaN(baseScore)) { baseScore = getDefaultRating(baseScoreInputField.value).baseTotalScore; } let boostableBaseScore = (multiBaseScoreInputField.value.trim()||'9999.9') * 1; if (Number.isNaN(boostableBaseScore)) { boostableBaseScore = getDefaultRating(multiBaseScoreInputField.value).baseTotalScore; } if (Number.isNaN(baseScore) || baseScore == 0 || boostableBaseScore === 0) { return null; } let scoreOffset = 0; for (let extraScore of extraScores) { if (extraScore.checkbox.checked) { scoreOffset += extraScore.amount; } } if (baseScore < 0) { if (scoreMultiplier === 1) { const rawScore = Math.max(-99999, Math.min(-1, Math.floor(baseScore + scoreOffset))); return { total: rawScore, multiplier: 1, boostedScore: 0, nonboostedScore: rawScore }; } else { return null; } } if (scoreMultiplier > 1) { let boostedScore = Math.floor(Math.min(baseScore, boostableBaseScore) * scoreMultiplier); let nonboostedScore = Math.floor(Math.max(0, baseScore - boostableBaseScore)) + scoreOffset; if (boostedScore + nonboostedScore === 0) { nonboostedScore = 1; } return { total: boostedScore + nonboostedScore, multiplier: scoreMultiplier, boostedScore: boostedScore, nonboostedScore: nonboostedScore }; } else { const rawScore = Math.max(1, Math.floor(baseScore * scoreMultiplier)) + scoreOffset; return { total: rawScore, multiplier: scoreMultiplier, boostedScore: 0, nonboostedScore: rawScore }; } } const footer = addElem(borElem, 'ul', null, { style: 'text-align:center;padding:4px 0;' }); const submitButton = addElem(footer, 'input', 'btn', { type: 'button', value: '执行评分' }); submitButton.addEventListener('click', async function() { const finalHeadtopDays = getFinalHeadtopDays(); const finalScore = getFinalScore(); if (finalScore == null || finalHeadtopDays == null) { alert('输入错误 请更正'); } else { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); const notif = notifyField.checked ? true : NO_PING_NOTIFICATION; executeScoreThenReload(anchor, fid, tid, finalScore.total, finalHeadtopDays, getHighlightParams(), notif, reasonField.value.trim()); } }); footer.appendChild(document.createTextNode(' ')); const closeButton = addElem(footer, 'input', 'btn', { type: 'button', value: '关闭' }); closeButton.addEventListener('click', function() { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); }); }); } const RETRY_SIGNAL = 'RETRY_SIGNAL'; async function readResponse(resp) { const respText = await resp.text(); if (respText.indexOf('刷新不要快于') !== -1) { return Promise.resolve(RETRY_SIGNAL); } else if (!respText.match(/(操作完成|发帖完毕点击进入主题列表)/)) { const respDoc = new DOMParser().parseFromString(respText, 'text/html'); if (((respDoc.querySelector('#main .t table .h')||{}).textContent||'').trim().endsWith('提示信息')) { return Promise.reject(((respDoc.querySelector('#main .t table .f_one center')||{}).textContent||'').trim()); } else { return Promise.reject('不明错误'); } } else { return Promise.resolve(true); } } let lastRequestTime = 0; async function attemptRequest() { let interval = Date.now() - lastRequestTime; if (interval < MIN_REQUEST_INTERVAL) { await sleep(MIN_REQUEST_INTERVAL - interval); } lastRequestTime = Date.now(); } async function executeWithRetry(action) { let returnCode = await action(); while (returnCode === RETRY_SIGNAL) { returnCode = await action(); } return returnCode; } function encodeData(data) { return new URLSearchParams(data).toString(); } async function executeScoreThenReload(anchor, fid, tid, addPoint, headtopDays, highlightParams, sendNotif, reason, pid) { const action = async () => { await executeScoreFunction(fid, tid, addPoint, headtopDays, highlightParams, sendNotif, reason, pid); return '✔️评分操作已完成'; }; runWithProgressPopup(action, '评分操作中...', anchor, 1500) .catch((ex) => alert(String(ex))) .finally(() => document.location.reload()); } async function executeDeletePost(fid, tid, reason) { const url = `${document.location.origin}/mawhole.php`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData({ 'verify': verifyhash(), 'action': 'del', 'fid': fid, 'tidarray[]': tid, 'step': 2, 'ifdel': 1, 'ifmsg': 1, 'atc_content': reason||'', }) }) .then(async resp => { await readResponse(resp); return true; }); } async function executeDeleteReply(fid, tid, pid, reason) { const url = `${document.location.origin}/masingle.php?action=delatc`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData({ 'verify': verifyhash(), 'fid': fid, 'tid': tid, 'step': 3, 'selid[]': pid, 'ifdel': 1, 'ifmsg': 1, 'atc_content': reason||'', }) }) .then(async resp => { await readResponse(resp); return true; }); } async function executeScoreFunction(fid, tid, addPoint, headtopDays, highlightParams, sendNotif, reason, pid) { async function executePing(sp) { const url = `${document.location.origin}/operate.php?action=showping`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData({ 'verify': verifyhash(), 'selid[]': pid||'tpc', 'cid': 'money', 'addpoint': sp, 'step': 1, 'ifmsg': sendNotif && sendNotif !== NO_PING_NOTIFICATION ? 1 : 0, 'atc_content': reason||'', 'page': 1, 'tid': tid }) }) .then(async resp => { await readResponse(resp); return true; }); } async function executeHighlight() { if (!highlightParams.color && !highlightParams.bold) return Promise.resolve(true); const url = `${document.location.origin}/mawhole.php`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData({ 'verify': verifyhash(), 'action': 'edit', 'fid': fid, 'tidarray[]': tid, 'title1': highlightParams.color||'', 'title2': highlightParams.bold ? 1 : '', 'title3': '', 'title4': '', 'ifmsg': sendNotif ? 1 : 0, 'atc_content': '', 'timelimit': '', 'nextto': '', 'step': 2 }) }) .then(async resp => { await readResponse(resp); return null; }); } async function executeHeadtopic() { if (headtopDays <= 0) return Promise.resolve(true); const url = `${document.location.origin}/mawhole.php`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData({ 'verify': verifyhash(), 'action': 'headtopic', 'fid': fid, 'tidarray[]': tid, 'topped': 1, 'timelimit': headtopDays, 'ifmsg': sendNotif ? 1 : 0, 'atc_content': '', 'nextto': '', 'step': 2 }) }) .then(async resp => { await readResponse(resp); return null; }); } const extraBatches = Math.floor(addPoint / 99999); if (extraBatches > 0) { const lastLeftover = addPoint % 99999; await executeWithRetry(async () => executePing(lastLeftover)); const ping99999 = async () => executePing(99999); for (let i = 0; i < extraBatches; i++) { await executeWithRetry(ping99999); } } else { await executeWithRetry(async () => executePing(addPoint)); } await executeWithRetry(executeHighlight); await executeWithRetry(executeHeadtopic); } function enhanceShieldActionPage() { const uid = extraPageParams['uid']; if (uid == null) { return; } const form = document.querySelector('form[action="masingle.php?action=shield"]'); if (form == null) { return; } const userNameCell = form.querySelector('table > tbody > tr + tr.tr3 > td.tar + td'); const userName = userNameCell.textContent.trim(); userNameCell.innerHTML = ''; addElem(userNameCell, 'a', null, { href: `u.php?action-show-uid-${uid}.html`, target: '_blank' }).textContent = userName; if (uid !== myUserId) { userNameCell.append(document.createTextNode(' / 🚩记录已启用')); } async function handleSubmit() { const formData = new FormData(form); const step = formData.get('step') * 1; const fid = formData.get('fid') * 1; const tid = formData.get('tid') * 1; const pid = formData.get('pid') * 1 || 0; // tpc = 0 const reason = formData.get('atc_content'); if (uid !== myUserId) { await recordPunishment(uid, step === 5 ? PUNISH_TYPE_UNSHIELD : PUNISH_TYPE_SHIELD, '', reason, { fid, tid, pid }); } submitOriginalForm(form); } form.addEventListener('submit', evt => { evt.preventDefault(); handleSubmit(); return false; }); } // ban user is missing the reason preset selector function enhanceBanUserActionPage() { const form = document.querySelector('form[action="masingle.php?action=banuser"]'); if (form == null) { return; } const reasonField = form.querySelector('td > textarea[name="atc_content"]'); if (reasonField == null) { return; } const userNameCell = form.querySelector('table > tbody > tr + tr.tr3 > td.tar + td'); const userName = userNameCell.textContent.trim(); const uid = extraPageParams['uid']; if (uid) { userNameCell.innerHTML = ''; addElem(userNameCell, 'a', null, { href: `u.php?action-show-uid-${uid}.html`, target: '_blank' }).textContent = userName; userNameCell.append(document.createTextNode(' / 🚩记录已启用')); } const mainAreaName = form.querySelector('input[type="radio"][name="range"][value="0"]').nextSibling.textContent.trim(); const initFormData = new FormData(form); const reasonSelector = addReasonSelector(reasonField); reasonField.setAttribute('style', 'width: 250px; height: 135px;'); reasonSelector.setAttribute('style', 'display:inline-block;vertical-align:top'); reasonField.parentElement.appendChild(reasonSelector); const delRow = newElem('tr', 'tr3'); addElem(delRow, 'td', 'tar').textContent = '追加操作:'; const extraOptionCell = addElem(delRow, 'td'); const delCheckBox = addElem(extraOptionCell, 'input', null, { type: 'checkbox' }); extraOptionCell.append(document.createTextNode(` 删除${initFormData.get('pid') === 'tpc' ? '主题 (注意:如帖上有其他违规回复,请先返回处理)' : '回复'}`)); const reasonRow = reasonField.closest('tr'); reasonRow.parentElement.insertBefore(delRow, reasonRow.previousElementSibling); form.addEventListener('submit', evt => { evt.preventDefault(); const formData = new FormData(form); handleBanAndDelete(formData, delCheckBox.checked); return false; }); async function handleBanAndDelete(banFormData, alsoDelete) { const pid = banFormData.get('pid'); const fid = banFormData.get('fid'); const tid = banFormData.get('tid'); const page = banFormData.get('page'); const limit = banFormData.get('type') * 1 === 1 ? banFormData.get('limit') : -1; let actionSummary = '禁言'; if (limit > 0) { actionSummary += ` (${limit}天)`; } else { actionSummary += ' (永久)'; } if (banFormData.get('range') * 1 === 0) { actionSummary += ` / 版块 (${mainAreaName})`; } const reason = banFormData.get('atc_content'); if (uid) { await recordPunishment(uid, PUNISH_TYPE_BAN, actionSummary, reason, { fid, tid, pid, banDur: limit, areaName: mainAreaName, delete: alsoDelete }); } async function executeDeleteViolationMaterial() { if (alsoDelete) { if (pid === 'tpc') { return await executeDeletePost(fid, tid, reason); } else { return await executeDeleteReply(fid, tid, pid, reason); } } else { return true; } } function finish() { if (!alsoDelete) { document.location.href = `${document.location.origin}/read.php?tid=${tid}&page=${page||1}#${pid}`; } else if (pid === 'tpc') { document.location.href = `${document.location.origin}/u.php?action=show&username=${encodeURIComponent(userName)}`; } else { document.location.href = `${document.location.origin}/read.php?tid=${tid}&page=${page||1}`; } } addModalMask(); const popupMenu = createPopupMenu(LOADING_DIALOG_POPUP_MENU_ID, null); popupMenu.renderContent(async function(borElem) { const operationLabel = alsoDelete ? '禁言及删除' : '禁言'; // not happy about copy-n-paste ... borElem.innerHTML = `<div style="padding:16px 30px"><img src="images/loading.gif" align="absbottom">${operationLabel}操作中...</div>`; executeWithRetry(executeBan) .then(() => executeWithRetry(executeDeleteViolationMaterial)) .then(() => { borElem.innerHTML = `<div style="padding:16px 30px;font-size:1.5em">✔️${operationLabel}操作已完成</div>`; setTimeout(() => { closePopupMenu(LOADING_DIALOG_POPUP_MENU_ID); finish(); }, 1500); }) .catch(ex => { alert(String(ex)); finish(); }); }); async function executeBan() { const url = `${document.location.origin}/masingle.php?action=banuser`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData(banFormData) }) .then(async resp => { await readResponse(resp); return true; }); } } } function addUserList(target, uids) { target.append(document.createTextNode(' / 对象: ')); uids.forEach(uid => { target.append(document.createTextNode(' ')); addElem(target, 'a', null, { href: 'u.php?action-show-uid-' + uid + '.html', target: '_blank' }).textContent = '#' + uid; }); } function enhanceMawholeActionPage() { const form = document.querySelector('form[action="mawhole.php?"]'); if (form == null) { return; } const actionField = form.querySelector('input[type="hidden"][name="action"]'); if (actionField == null) { return; } if (actionField.value === 'del') { enhanceMawholeDelActionPage(form); } } function enhanceMawholeDelActionPage(form) { const uids = (extraPageParams.uids||'').split(':').map(x=>x*1).filter(x=>x>0); const tids = (extraPageParams.tids||'').split(':').map(x=>x*1).filter(x=>x>0); if (uids.length === 0 || uids.length !== tids.length) { return; } const ifdelCell = form.querySelector('input[type="radio"][name="ifdel"]').closest('th'); if (uids.filter(uid => uid !== myUserId).length > 0) { addElem(ifdelCell, 'span', 'rinsp-record-delatc-msg').textContent = ' / 🚩记录已启用'; } const mappings = new Map(); tids.forEach((tid, i) => mappings.set(tid, uids[i])); async function handleSubmit() { const formData = new FormData(form); if (formData.get('ifdel') === '1') { const fid = formData.get('fid') * 1; const tids = formData.getAll('tidarray[]').map(x=>x*1); const reason = formData.get('atc_content'); for (let tid of tids) { const uid = mappings.get(tid); if (uid && uid !== myUserId) { const threadTitle = form.querySelector(`th > a[href="read.php?tid-${tid}.html"]`).textContent; await recordPunishment(uid, PUNISH_TYPE_DELETE_THREAD, '', reason, { fid, tid, title: threadTitle }); } } } submitOriginalForm(form); } form.addEventListener('submit', evt => { evt.preventDefault(); handleSubmit(); return false; }); } function enhanceDeleteReplyActionPage() { const form = document.querySelector('form[action="masingle.php?action=delatc"]'); if (form == null) { return; } const countDisplayCell = form.querySelector('table > tbody > tr + tr.tr3 > td.tar + td'); const ifdelCell = form.querySelector('input[type="radio"][name="ifdel"]').closest('td'); const uids = (extraPageParams.uids||'').split(':').map(x=>x*1).filter(x=>x>0); if (uids.length > 0) { addUserList(countDisplayCell, uids); if (uids.filter(uid => uid !== myUserId).length > 0) { addElem(ifdelCell, 'span', 'rinsp-record-delatc-msg').textContent = ' / 🚩记录已启用'; } } async function handleSubmit() { const formData = new FormData(form); if (formData.get('ifdel') === '1') { const tid = formData.get('tid') * 1; const fid = formData.get('fid') * 1; const reason = formData.get('atc_content'); for (let uid of uids) { if (uid !== myUserId) { await recordPunishment(uid, PUNISH_TYPE_DELETE_REPLY, '', reason, { tid, fid }); } } } submitOriginalForm(form); } form.addEventListener('submit', evt => { evt.preventDefault(); handleSubmit(); return false; }); } function enhanceEndRewardActionPage() { const form = document.querySelector('form[action="job.php?action=endreward"]'); if (form == null) { return; } const bounty = extraPageParams.bounty * 1; if (Number.isNaN(bounty)) { return; } const pts = extraPageParams.pts * 1 || 0; const opsp = extraPageParams.opsp * 1 || 0; const ifmsgRow = form.querySelector('input[name="ifmsg"]').closest('tr'); const statusRow = newElem('tr', 'tr3'); addElem(statusRow, 'th').textContent = '悬赏'; addElem(statusRow, 'th').textContent = `最佳答案: ${bounty} SP` + (pts > 0 ? ` | 热心助人: ${pts}` : '') + ` | 楼主: ${opsp.toLocaleString('en-US')} SP`; ifmsgRow.parentNode.insertBefore(statusRow, ifmsgRow.previousElementSibling); const spRow = newElem('tr', 'tr3'); addElem(spRow, 'th').textContent = 'SP 影响'; const spCell = addElem(spRow, 'th'); const spInput = addElem(spCell, 'input', null, { type: 'text', size: 4 }); spCell.appendChild(document.createTextNode(' SP')); ifmsgRow.parentNode.insertBefore(spRow, ifmsgRow); const headerCell = form.querySelector('table tr > td.h'); headerCell.removeAttribute('colspan'); headerCell.parentNode.insertBefore(newElem('td', 'h'), headerCell); function addPresetButton(label, type, sp, ifmsg) { const presetButton = addElem(headerCell, 'a', 'rinsp-ping-preset-switch', { href: 'javascript:' }); presetButton.addEventListener('click', () => { form.querySelector(`input[name="type"][value="${type}"]`).checked = true; spInput.value = sp == null ? '' : '' + sp; if (ifmsg) { form.querySelector('input[name="ifmsg"][value="1"]').checked = true; } else { form.querySelector('input[name="ifmsg"][value="0"]').checked = true; } }); presetButton.textContent = label; headerCell.appendChild(document.createTextNode(' ')); return presetButton; } addPresetButton('取消悬赏', 1, bounty * 2 + pts, true).setAttribute('style', 'color:#080'); addPresetButton('返还押金', 2, bounty, true); addPresetButton('强行结案 (2倍)', 2, 0, true).setAttribute('style', 'color:#60C'); addPresetButton('不结帖 (3倍)', 2, -bounty, true).setAttribute('style', 'color:#D00'); form.addEventListener('submit', evt => { const sp = (spInput.value||'0') * 1; if (Number.isNaN(sp)) { evt.preventDefault(); return false; } if (sp === 0) { return; } evt.preventDefault(); const formData = new FormData(form); const tid = formData.get('tid'); async function executeEndReward() { await attemptRequest(); await fetch(form.action, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData(formData) }) .then(async resp => { await readResponse(resp); return true; }); } async function executeScore() { await attemptRequest(); const reason = sp < 0 ? '不结帖' : ''; await executeScoreFunction(48, tid, sp, 0, {}, true, reason); } function finish() { document.location.href = `${document.location.origin}/read.php?tid-${tid}.html`; } addModalMask(); executeWithRetry(executeEndReward) .then(() => executeScore()) .then(() => { finish(); }) .catch(ex => { alert(String(ex)); finish(); }); return false; }); } function enhancePingActionPage() { const form = document.querySelector('form[action="operate.php?action=showping"]'); if (form == null) { return; } const countDisplayCell = form.querySelector('table > tbody > tr + tr.tr3 + tr.tr3 > td.tar + th'); const pingStepCell = form.querySelector('input[name="step"]').closest('th'); const cidInput = form.querySelector('select[name="cid"]'); const addpointInput = form.querySelector('input[name="addpoint"]'); let logCheckbox = null; let onUpdate = () => {}; function addPresetButton(label, cid, addpoint, log, ifmsg, reason) { const target = form.querySelector('table tr > th.h + th.h'); const presetButton = addElem(target, 'a', 'rinsp-ping-preset-switch', { href: 'javascript:' }); presetButton.addEventListener('click', () => { cidInput.value = cid; addpointInput.value = '' + addpoint; if (logCheckbox) { logCheckbox.checked = log; } form.querySelector('input[name="step"][value="1"]').checked = true; if (ifmsg) { form.querySelector('input[name="ifmsg"][value="1"]').checked = true; } else { form.querySelector('input[name="ifmsg"][value="0"]').checked = true; } form.querySelector('textarea[name="atc_content"]').value = reason; onUpdate(); }); presetButton.textContent = label; target.appendChild(document.createTextNode(' ')); } addPresetButton('清除', 'money', 0, true, true, ''); addPresetButton('done', 'money', 1, true, true, 'done'); addPresetButton('+5', 'money', 5, true, true, ''); addPresetButton('+50', 'money', 50, true, true, ''); const uids = (extraPageParams.uids||'').split(':').map(x=>x*1).filter(x=>x>0); if (uids.filter(uid => uid !== myUserId).length > 0) { addUserList(countDisplayCell, uids); const recordOption = addElem(pingStepCell, 'span', 'rinsp-record-showping-option rinsp-hide'); recordOption.append(document.createTextNode(' / ')); logCheckbox = addElem(recordOption, 'input', null, { type: 'checkbox', style: 'vertical-align: text-bottom' }); logCheckbox.checked = true; recordOption.append(document.createTextNode('🚩启用扣分记录')); onUpdate = () => { if (addpointInput.value.startsWith('-')) { recordOption.classList.remove('rinsp-hide'); } else { recordOption.classList.add('rinsp-hide'); } }; addpointInput.addEventListener('keyup', evt => onUpdate()); } const soldSp = extraPageParams.sold * 1; if (soldSp > 0) { if (extraPageParams.badsell === 'opsell') { addPresetButton('楼主出售', 'money', -soldSp, true, true, '禁止楼主出售'); } else { addPresetButton('违例出售', 'money', -soldSp, true, true, ''); } } const ansSp = extraPageParams.answer * 1; if (ansSp > 0) { addPresetButton('小号自收', 'money', -ansSp, true, true, '禁止小号自收'); } async function handleSubmit() { if (logCheckbox && logCheckbox.checked) { const formData = new FormData(form); const addpoint = formData.get('addpoint') * 1; const step = formData.get('step') * 1; let punishType = null; let actionSummary = ''; if (step === 1 && addpoint < 0) { const cid = formData.get('cid'); if (cid === 'money') { punishType = PUNISH_TYPE_SP; actionSummary = 'SP' + addpoint; } else if (cid === 'rvrc') { punishType = PUNISH_TYPE_HP; actionSummary = 'HP' + addpoint; } } if (punishType) { const tid = formData.get('tid') * 1; const reason = formData.get('atc_content'); for (let uid of uids) { if (uid !== myUserId) { // actually impossible at this point await recordPunishment(uid, punishType, actionSummary, reason, { tid, amount: addpoint }); } } } } submitOriginalForm(form); } form.addEventListener('submit', evt => { evt.preventDefault(); handleSubmit(); return false; }); } function showBatchPingWindow(items) { const modelMask = addModalMask(); const popupMenu = createPopupMenu(SCORING_DIALOG_POPUP_MENU_ID, null, false); popupMenu.renderContent(async function(borElem) { const eForm = addElem(borElem, 'div', 'rinsp-batchsel-form rinsp-batchping-form'); const tableElem = addElem(eForm, 'table', null, { width: '850', cellspacing: '0', cellpadding: '0', style: 'table-layout:fixed' }); const colgroup = addElem(tableElem, 'colgroup'); addElem(colgroup, 'col', null, { style: 'width: 30px' }); addElem(colgroup, 'col'); addElem(colgroup, 'col', null, { style: 'width: 120px' }); addElem(colgroup, 'col', null, { style: 'width: 40px' }); addElem(colgroup, 'col', null, { style: 'width: 70px' }); addElem(colgroup, 'col', null, { style: 'width: 90px' }); addElem(colgroup, 'col', null, { style: 'width: 80px' }); addElem(colgroup, 'col', null, { style: 'width: 20px' }); // allow for scrollbar const tbodyElem = addElem(tableElem, 'tbody'); const trElem1 = addElem(tbodyElem, 'tr'); const thElem1_1 = addElem(trElem1, 'th', 'h', { colspan: '7' }); addElem(trElem1, 'th', 'h'); const frElem1 = addElem(thElem1_1, 'span', 'fr', { style: 'margin-top:2px;cursor:pointer' }); frElem1.addEventListener('click', function() { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); }); addElem(frElem1, 'img', null, { src: 'images/close.gif' }); thElem1_1.appendChild(document.createTextNode('批量评分')); const trElem2 = addElem(tbodyElem, 'tr', 'tr2 tac'); addElem(trElem2, 'td'); addElem(trElem2, 'td').textContent = '帖子标题'; addElem(trElem2, 'td').textContent = '发布者'; addElem(trElem2, 'td').textContent = '回复'; addElem(trElem2, 'td').textContent = '资源大小'; addElem(trElem2, 'td').textContent = '评分'; addElem(trElem2, 'td'); const pingableItems = []; items.forEach(function(item) { const itemTr = addElem(tbodyElem, 'tr', 'tr3 tac rinsp-batchsel-form-item'); const checkCell = addElem(itemTr, 'td'); const selectCheckbox = addElem(checkCell, 'input', null, { type: 'checkbox' }); const titleCell = addElem(itemTr, 'td', 'tal'); let postUrl = `read.php?tid-${item.tid}.html`; addElem(titleCell, 'a', null, { href: postUrl, target: '_blank' }).textContent = item.title; const posterCell = addElem(itemTr, 'td', 'rinsp-batchsel-form-op-cell'); if (item.opKnownName) { posterCell.classList.add('rinsp-batchsel-form-op-known'); posterCell.textContent = item.opKnownName; } else { posterCell.textContent = item.opName; } addElem(itemTr, 'td').textContent = '' + item.replyCount; addElem(itemTr, 'td').textContent = item.defaultScoreRating.resourceSizeTexts.join(' + '); const scoreCell = addElem(itemTr, 'td', 'rinsp-batchping-form-ping-cell'); const scoreControls = addElem(scoreCell, 'div'); const scoreInput = addElem(scoreControls, 'input', 'rinsp-batchping-form-sp-input', { type: 'text', size: '5' }); addElem(scoreControls, 'span').textContent = ' SP'; const unscoreCell = addElem(itemTr, 'td'); const markUnscoredCheckbox = addElem(unscoreCell, 'input', null, { type: 'checkbox' }); markUnscoredCheckbox.addEventListener('change', () => { if (markUnscoredCheckbox.checked) { selectCheckbox.checked = true; } sync(); }); selectCheckbox.addEventListener('change', () => { if (!selectCheckbox.checked) { markUnscoredCheckbox.checked = false; } sync(); }); addElem(unscoreCell, 'span').textContent = '不评分'; const defaultUnscored = item.opKnownName && item.opKnownName.startsWith(NEVER_SCORE_USER_PREFIX); const defaultChecked = item.opKnownName && item.opKnownName.startsWith(TRUSTED_SCORE_USER_PREFIX); const unavailable = item.defaultScoreRating.baseTotalScore <= 0 || item.defaultScoreRating.ownBought || item.defaultScoreRating.ownTranslate; if (defaultUnscored) { markUnscoredCheckbox.checked = true; } if (unavailable) { itemTr.classList.add('rinsp-batchsel-form-item-unavailable'); } else { if (defaultChecked || defaultUnscored) { selectCheckbox.checked = true; } scoreInput.value = '' + Math.min(99999, Math.max(1, Math.floor(item.defaultScoreRating.baseTotalScore))); } pingableItems.push({ item: item, selected: () => selectCheckbox.checked, score() { if (markUnscoredCheckbox.checked) { return -1; } const score = Math.floor(scoreInput.value * 1); if (score > 0 && score <= 99999) { return score; } return null; }, }); sync(); function sync() { itemTr.classList.remove('rinsp-batchsel-form-item-checked'); itemTr.classList.remove('rinsp-batchsel-form-item-unchecked'); itemTr.classList.remove('rinsp-batchping-form-item-unscore'); if (selectCheckbox.checked) { itemTr.classList.add('rinsp-batchsel-form-item-checked'); if (markUnscoredCheckbox.checked) { itemTr.classList.add('rinsp-batchping-form-item-unscore'); } } else { itemTr.classList.add('rinsp-batchsel-form-item-unchecked'); } } }); const footer = addElem(borElem, 'ul', null, { style: 'text-align:center;padding:4px 0;' }); const submitButton = addElem(footer, 'input', 'btn', { type: 'button', value: '执行评分' }); submitButton.addEventListener('click', () => { const pingItems = []; pingableItems.forEach(pItem => { if (pItem.selected()) { const score = pItem.score(); if (score != null) { pingItems.push({ item: pItem.item, score }); } } }); if (pingItems.length === 0) { showMessagePopup('没有项目', submitButton, 3000); return; } if (confirm(`执行评分: ${pingItems.length} 个项目 ?`)) { executeBatchPing(pingItems); } }); footer.appendChild(document.createTextNode(' ')); const closeButton = addElem(footer, 'input', 'btn', { type: 'button', value: '关闭' }); closeButton.addEventListener('click', function() { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); }); function executeBatchPing(pingItems) { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); runWithProgressPopup(action, '评分操作中...', null, 1500) .catch((ex) => alert(String(ex))) .finally(() => window.location.reload()); async function action(progressDisplay) { const itemList = addElem(progressDisplay, 'ul', 'rinsp-batchping-progress-list'); const displayItems = []; for (let pingItem of pingItems) { const li = addElem(itemList, 'li'); li.textContent = pingItem.item.title; displayItems.push(li); } await sleep(1000); for (let pingItem of pingItems) { if (pingItem.score > 0) { await executeScoreFunction(pingItem.item.fid, pingItem.item.tid, pingItem.score, 0, {}, true, ''); } else { await executeScoreFunction(pingItem.item.fid, pingItem.item.tid, 1, 0, {}, false, ''); } displayItems.shift().remove(); } await sleep(500); itemList.remove(); return '✔️已完成评分'; } } }); } function addDeleteFormMetadata() { const delButton = document.querySelector('a#del[href^="mawhole.php?action-del-"]'); if (delButton == null) { return null; } let gfPost = getPosts(document, 1)[0]; if (gfPost == null || gfPost.floor !== 0) { return; } const actionHref = delButton.getAttribute('href'); delButton.setAttribute('href', `${actionHref}#uids-${gfPost.postUid}-tids-${gfPost.tid}`); } function addBulkDeleteFormMetadata() { const form = document.querySelector('form[name="mawhole"][action="mawhole.php"]'); if (form == null) { return; } const submitButton = form.querySelector('input[type="button"][name="hello"][onclick]'); if (submitButton == null) { return; } submitButton.removeAttribute('onclick'); submitButton.addEventListener('click', evt => { const formData = new FormData(form); const tids = formData.getAll('tidarray[]'); if (formData.get('action') !== 'del' || tids.length === 0) { form.submit(); return; } const uids = []; tids.forEach(tid => { const tr = document.getElementById(`td_${tid}`).closest('tr'); const userLink = tr.querySelector('a.bl[href^="u.php?action-show-uid-"]'); uids.push(Number.parseInt(userLink.getAttribute('href').substring(22))); }); submitPostForm('mawhole.php#uids-' + Array.from(uids).join(':') + '-tids-' + Array.from(tids).join(':'), formData); }); } function addBatchPingFunction(fid) { const adminAreaAnchor = document.querySelector('form[name="mawhole"] > .t + .t5.tac > div > .btn[name="chkall"]'); if (adminAreaAnchor == null) { return; } const pingAllButton = newElem('input', 'btn', { type: 'button', name: 'chkall', value: '批量评分', style: 'float: left; margin-left: 1em; margin-right: -100%' }); adminAreaAnchor.closest('div').appendChild(pingAllButton); pingAllButton.addEventListener('click', () => { const threadList = readThreadList(); const unscoredThreads = threadList.filter(thread => !thread.row.classList.contains('rinsp-thread-filter-scored') && !thread.row.classList.contains('rinsp-thread-filter-miscored')); if (unscoredThreads.length === 0) { showMessagePopup('💡没有未评分帖子', pingAllButton, 3000); return; } const items = unscoredThreads.map(thread => { const defaultScoreRating = getDefaultRating(thread.title); const customNameEntry = userConfig.customUserHashIdMappings['#' + thread.op]; return { fid, tid: thread.tid, title: thread.title, opName: thread.opName, opKnownName: customNameEntry && customNameEntry[2], replyCount: thread.replyCount, defaultScoreRating }; }); showBatchPingWindow(items); }); } function showBatchSelectionWindow(items, action) { const modelMask = addModalMask(); const popupMenu = createPopupMenu(SCORING_DIALOG_POPUP_MENU_ID, null, false); popupMenu.renderContent(async function(borElem) { const eForm = addElem(borElem, 'div', 'rinsp-batchsel-form rinsp-batchping-form'); const tableElem = addElem(eForm, 'table', null, { width: '850', cellspacing: '0', cellpadding: '0', style: 'table-layout:fixed' }); const colgroup = addElem(tableElem, 'colgroup'); addElem(colgroup, 'col', null, { style: 'width: 30px' }); addElem(colgroup, 'col'); addElem(colgroup, 'col', null, { style: 'width: 120px' }); addElem(colgroup, 'col', null, { style: 'width: 120px' }); addElem(colgroup, 'col', null, { style: 'width: 70px' }); addElem(colgroup, 'col', null, { style: 'width: 160px' }); addElem(colgroup, 'col', null, { style: 'width: 20px' }); // allow for scrollbar const tbodyElem = addElem(tableElem, 'tbody'); const trElem1 = addElem(tbodyElem, 'tr'); const thElem1_1 = addElem(trElem1, 'th', 'h', { colspan: '6' }); addElem(trElem1, 'th', 'h'); const frElem1 = addElem(thElem1_1, 'span', 'fr', { style: 'margin-top:2px;cursor:pointer' }); frElem1.addEventListener('click', function() { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); }); addElem(frElem1, 'img', null, { src: 'images/close.gif' }); thElem1_1.appendChild(document.createTextNode('批量选择')); const trElem2 = addElem(tbodyElem, 'tr', 'tr2 tac'); const selCell = addElem(trElem2, 'td'); addElem(trElem2, 'td').textContent = '帖子标题'; addElem(trElem2, 'td').textContent = '论坛'; addElem(trElem2, 'td').textContent = '发布者'; addElem(trElem2, 'td').textContent = '回复'; addElem(trElem2, 'td').textContent = ''; addElem(trElem2, 'td'); const selAll = addElem(selCell, 'a', null, { href: 'javascript:void(0)' }); selAll.textContent = '☑️'; const selectableItems = []; items.forEach(function(item) { const itemTr = addElem(tbodyElem, 'tr', 'tr3 tac rinsp-batchsel-form-item'); const checkCell = addElem(itemTr, 'td'); const selectCheckbox = addElem(checkCell, 'input', null, { type: 'checkbox' }); selectCheckbox.addEventListener('change', () => sync()); const titleCell = addElem(itemTr, 'td', 'tal'); addElem(titleCell, 'a', null, { href: `read.php?tid-${item.tid}.html`, target: '_blank' }).textContent = item.title; const areaCell = addElem(itemTr, 'td', 'tal'); addElem(areaCell, 'a', null, { href: `thread.php?fid-${item.fid}.html`, target: '_blank' }).textContent = item.areaName; const posterCell = addElem(itemTr, 'td', 'rinsp-batchsel-form-op-cell'); const customNameEntry = userConfig.customUserHashIdMappings['#' + item.op]; const opKnownName = customNameEntry && customNameEntry[2]; if (opKnownName) { posterCell.classList.add('rinsp-batchsel-form-op-known'); posterCell.textContent = opKnownName; } else { posterCell.textContent = item.opName; } addElem(itemTr, 'td').textContent = '' + item.replyCount; const badRecordCell = addElem(itemTr, 'td'); userPunishRecordStore.get(item.op) .then(record => { if (record) { badRecordCell.appendChild(renderPunishTags(item.op, record)); } }); addElem(itemTr, 'td'); selectableItems.push({ item: item, selected: () => selectCheckbox.checked, setSelected(b) { selectCheckbox.checked = b; } }); sync(); function sync() { if (selectCheckbox.checked) { itemTr.classList.add('rinsp-batchsel-form-item-checked'); itemTr.classList.remove('rinsp-batchsel-form-item-unchecked'); } else { itemTr.classList.remove('rinsp-batchsel-form-item-checked'); itemTr.classList.add('rinsp-batchsel-form-item-unchecked'); } } }); selAll.addEventListener('click', () => { const toSelect = selectableItems.find(item => !item.selected()); selectableItems.forEach(item => item.setSelected(toSelect)); }); const footer = addElem(borElem, 'ul', null, { style: 'text-align:center;padding:4px 0;' }); const submitButton = addElem(footer, 'input', 'btn', { type: 'button', value: '决定选择' }); submitButton.addEventListener('click', () => { const selectedItems = []; selectableItems.forEach(pItem => { if (pItem.selected()) { selectedItems.push(pItem.item); } }); if (selectedItems.length === 0) { showMessagePopup('没有项目', submitButton, 3000); return; } if (confirm(`决定选择: ${selectedItems.length} 个项目 ?`)) { executeAction(selectedItems); } }); footer.appendChild(document.createTextNode(' ')); const closeButton = addElem(footer, 'input', 'btn', { type: 'button', value: '关闭' }); closeButton.addEventListener('click', function() { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); }); function executeAction(selectedItems) { modelMask.remove(); closePopupMenu(SCORING_DIALOG_POPUP_MENU_ID); action(selectedItems); } }); } function checkSomePosts(decideChecked, skipScroll) { let first = null; Array.from(document.querySelectorAll('input[type="checkbox"][name="selid[]"]')) .forEach(el => { const value = decideChecked(el); if (first == null && value) { first = el; } el.checked = value; }); if (first && !skipScroll) { first.scrollIntoView(); } } function addBatchPostSelectionControl() { const targetControlBar = document.querySelector('#main > .h2 > .fr.w'); if (targetControlBar.querySelector('.rinsp-reply-batch-sel')) { return; } function isUnscored(el) { return el.closest('table').querySelector('div[id^="mark_"]') == null; } function isBanned(el) { const span = el.closest('table').querySelector('.tpc_content div[id^="read_"] > span[style="color:black;background-color:#ffff66"]'); return span && span.textContent.trim() === '用户被禁言,该主题自动屏蔽!'; } function checkNotDuplicates() { checkDuplicates(true); } function checkDuplicates(invert) { const checkValue = !invert; const seen = new Set(); let first = null; const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"][name="selid[]"]')); checkboxes.forEach(el => { const a = el.closest('table').querySelector('.user-pic a[href^="u.php?action-show-uid-"]'); const uid = Number.parseInt(a.getAttribute('href').substring(22)); if (seen.has(uid)) { el.checked = checkValue; if (first == null) first = el; } else { el.checked = !checkValue; seen.add(uid); } }); if (first) { first.scrollIntoView(); } } const actions = []; actions.push({ label: '全选', action: () => checkSomePosts(()=>true) }); actions.push({ label: '未评分', action: () => checkSomePosts(isUnscored) }); actions.push({ label: '重复', action: () => checkDuplicates() }); actions.push({ label: '非重复', action: () => checkNotDuplicates() }); actions.push({ label: '已禁言', action: () => checkSomePosts(isBanned) }); actions.push({ label: '清除', class: 'rinsp-alert-menu-button', action: () => checkSomePosts(()=>false) }); targetControlBar.append(document.createTextNode(' | ')); const batchSel = addElem(targetControlBar, 'a', 'fn rinsp-reply-batch-sel'); batchSel.setAttribute('href', 'javascript:void(0)'); batchSel.textContent = '批量选择'; batchSel.addEventListener('click', () => { setupPopupMenu({ title: '批量选择', popupMenuId: 'BATCH_SEL_MENU', width: 150, anchor: batchSel, verticallyInverted: false, items: actions }); }); } function addBatchDeleteFunction() { const adminAreaAnchor = document.querySelector('#main > .fr'); if (adminAreaAnchor == null) { return; } const delAllButton = newElem('input', 'btn', { type: 'button', value: '批量删除', style: 'float: left; margin-left: 1em; margin-right: -100%' }); adminAreaAnchor.closest('#main').appendChild(delAllButton); delAllButton.addEventListener('click', () => { const threadList = readSearchThreadList(); const threads = threadList.map(thread => { return { fid: thread.areaId, areaName: thread.areaName, tid: thread.tid, title: thread.title, op: thread.op, opName: thread.opName, replyCount: thread.replyCount, }; }); showBatchSelectionWindow(threads, async selection => { const fidGroups = new Map(); selection.forEach(item => { let group = fidGroups.get(item.fid); if (group == null) { group = []; fidGroups.set(item.fid, group); } group.push(item); }); for (let [fid, group] of fidGroups) { openBulkDeletePage(fid, group); await sleep(1000); } function openBulkDeletePage(fid, items) { const formData = new FormData(); formData.set('action', 'del'); formData.set('fid', fid); const tids = []; const uids = []; items.forEach(item => { formData.append('tidarray[]', item.tid); tids.push(item.tid); uids.push(item.op); }); submitPostForm('mawhole.php#uids-' + Array.from(uids).join(':') + '-tids-' + Array.from(tids).join(':'), formData, '_blank'); } }); }); } async function handleOutZoneRequestPunish(post, deductSp, banDays) { if (!confirm(`执行求物处分 -${deductSp}SP + ${banDays > 0 ? `禁${banDays}天` : '删'}?`)) { return; } const threadTitle = (document.querySelector('#breadcrumbs .crumbs-item.current > strong')||{}).textContent; let reason = '禁止茶馆求物、出处、梯子机场、直求主题'; if (banDays > 0) { reason += `- 因SP不足,清零追加禁言`; } const action = async () => { const extraDeduction = deductSp - 5; if (extraDeduction > 0) { await executeScoreFunction(post.areaId, post.tid, -extraDeduction, 0, {}, true, reason); } if (banDays > 0) { await executeBan(); } await executeDeletePost(post.areaId, post.tid, reason); await recordPunishment(post.postUid, PUNISH_TYPE_DELETE_THREAD, '', `-${deductSp}SP ${reason}`, { fid: post.areaId, tid: post.tid, title: threadTitle }); return '✔️处分操作已完成'; }; async function executeBan() { const banFormData = new FormData(); banFormData.set('verify', verifyhash()); banFormData.set('fid', post.areaId); banFormData.set('tid', post.tid); banFormData.set('pid', 'tpc'); banFormData.set('type', 1); banFormData.set('limit', banDays); banFormData.set('page', 1); banFormData.set('step', 3); banFormData.set('ifmsg', 1); banFormData.set('atc_content', reason); const url = `${document.location.origin}/masingle.php?action=banuser`; await attemptRequest(); await fetch(url, { method: 'POST', mode: 'same-origin', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodeData(banFormData) }) .then(async resp => { await readResponse(resp); return true; }); } runWithProgressPopup(action, '处分操作中...', null, 1500) .catch((ex) => alert(String(ex))) .finally(() => document.location.reload()); } async function handleMisclassificationPunish(post) { if (!confirm('执行分类错误 SP-50?')) { return; } await executeScoreThenReload(null, post.areaId, post.tid, -50, 0, {}, true, '分类错误'); } async function handleAwardBestAnswer(post, sp, multiplier) { const totalAmount = sp * multiplier; let suffix = multiplier > 1 ? ` (${multiplier}倍)` : ''; if (!confirm(`选为最佳答案 ${totalAmount} SP${suffix}?`)) { return; } await executeScoreThenReload(null, post.areaId, post.tid, totalAmount, 0, {}, true, '最佳答案', post.postId); } function addPostAdminControl(posts, page, userMap) { function buildMultiAccountMap() { const colors = [ '#f9c6c9', '#bcd4e6', '#faedcb', '#c5dedd', '#eddcd2', '#dbcdf0' ]; function nextColor() { const color = colors.shift(); colors.push(color); return color; } const ipToUsers = new Map(); const userToIps = new Map(); function addMapping(map, key, value) { const values = map.get(key); if (values) { values.add(value); } else { map.set(key, new Set([value])); } } posts.forEach(post => { const banButton = post.rootElem.querySelector('.tipad a[id^="banuser_"]'); if (banButton) { const button = post.rootElem.querySelector('.rinsp-user-admin-button'); if (button) { post.rootElem.classList.remove('rinsp-post-multi-uid'); button.parentElement.removeAttribute('style'); button.remove(); } const ip = (banButton.closest('.fr').textContent.match(/IP:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) \|/)||[])[1]||null; if (ip) { addMapping(ipToUsers, ip, post.postUid); addMapping(userToIps, post.postUid, ip); } } }); let nextGid = 1; const userGrouped = new Map(); ipToUsers.forEach((uids, ip) => { let group = userGrouped.get(ip); if (!group) { group = { gid: nextGid++, uids: new Set() }; userGrouped.set(ip, group); } for (let uid of uids) { group.uids.add(uid); const ips = userToIps.get(uid); for (let ip2 of ips) { if (ip2 !== ip) { let group2 = userGrouped.get(ip2); if (group2 && group2.gid !== group.gid) { for (let uid2 of group2.uids) { group.uids.add(uid2); } userGrouped.set(ip2, group); } } } } }); const multiAccountMap = new Map(); Array.from(userGrouped.values()) .filter(group => group.uids.size > 1) .forEach(group => { if (!group.color) { group.color = nextColor(); } for (let uid of group.uids) { multiAccountMap.set(uid, group); } }); return multiAccountMap; } function openActionPage(form, action) { const formData = new FormData(form); const postIds = new Set(formData.getAll('selid[]').map(pid => pid * 1)); const postUids = new Set(); posts.filter(post => postIds.has(post.postId)).forEach(post => { postUids.add(post.postUid); }); submitPostForm(action + '#uids-' + Array.from(postUids).join(':'), formData); } const multiAccountMap = buildMultiAccountMap(); let bountyStatus = null; let threadOwnerId = null; let bestAnswerMultiplier = 0; if (posts[0].areaId === 8 || posts[0].areaId === 9) { // ACG交流, 茶馆 document.querySelectorAll('.rinsp-punishrequest-switch').forEach(el => el.remove()); if (posts[0].floor === 0) { const showping = document.querySelector('li > #showping_tpc'); let punishrequest = document.querySelector('li > #punishrequest_tpc'); if (showping) { const tpcPing = document.querySelector('#mark_tpc'); if (!punishrequest) { if (tpcPing && tpcPing.textContent.match(/.*评分记录.*/) != null) { // skip: already received penalty } else { const li = newElem('li', 'rinsp-punishrequest-switch'); showping.parentNode.parentNode.insertBefore(li, showping.parentNode.nextElementSibling); punishrequest = addElem(li, 'a', null, { href: 'javascript:' }); punishrequest.textContent = '求物扣分'; const threadOwnerInfo = userMap.get(posts[0].postUid)||{ sp: 0 }; if (threadOwnerInfo.sp < 50) { punishrequest.addEventListener('click', () => handleOutZoneRequestPunish(posts[0], threadOwnerInfo.sp, 1)); } else { punishrequest.addEventListener('click', () => handleOutZoneRequestPunish(posts[0], 50, 0)); } } } } } } else if (posts[0].areaId === QUESTION_AND_REQUEST_AREA_ID) { // 求物区 bountyStatus = getBountyStatus(document); document.querySelectorAll('.rinsp-punishclassify-switch').forEach(el => el.remove()); if (posts[0].floor === 0) { threadOwnerId = posts[0].postUid; const showping = document.querySelector('li > #showping_tpc'); let punishclassify = document.querySelector('li > #punishclassify_tpc'); if (showping) { const tpcPing = document.querySelector('#mark_tpc'); if (!punishclassify) { if (tpcPing && tpcPing.textContent.match(/.*评分记录.*SP币:-50 .*分类.*/) != null) { // skip: already received penalty } else { const li = newElem('li', 'rinsp-punishclassify-switch'); showping.parentNode.parentNode.insertBefore(li, showping.parentNode.nextElementSibling); punishclassify = addElem(li, 'a', null, { href: 'javascript:' }); punishclassify.textContent = '分类错误'; punishclassify.addEventListener('click', () => handleMisclassificationPunish(posts[0])); } } if (bountyStatus.ended === 2 && bountyStatus.winner == null) { const pingSp = tpcPing ? (tpcPing.textContent.match(/.*评分记录.*SP币:\+?(-?\d+)/)||[null,0])[1] * 1 : 0; if (pingSp === 0) { bestAnswerMultiplier = 2; } else if (bountyStatus.sp === pingSp) { bestAnswerMultiplier = 1; } else if (bountyStatus.sp === -pingSp) { bestAnswerMultiplier = 3; } } if (bestAnswerMultiplier > 0) { posts.forEach(post => { if (bestAnswerMultiplier === 0) return; const mark = post.rootElem.querySelector('div[id^="mark_"] li'); if (mark && mark.textContent.indexOf('最佳答案') !== -1) { bestAnswerMultiplier = 0; } }); } } } const endrewardButton = document.querySelector('a[href^="job.php?action-endreward-tid-"]'); if (endrewardButton) { const endrewardActionHref = endrewardButton.getAttribute('href'); if (endrewardActionHref.indexOf('#') === -1) { const threadOwnerInfo = userMap.get(threadOwnerId)||{ sp: 0 }; endrewardButton.setAttribute('href', endrewardActionHref + '#bounty-' + bountyStatus.sp + '-pts-' + bountyStatus.pts + '-opsp-' + threadOwnerInfo.sp); } } } posts.forEach(post => { const selCheckbox = post.rootElem.querySelector('.fr input[type="checkbox"][name="selid[]"]'); if (selCheckbox) { const form = selCheckbox.closest('form[name="delatc"]'); if (form) { const deleteButton = selCheckbox.parentElement.querySelector('input[value="删除选定"][onclick]'); const pingButton = selCheckbox.parentElement.querySelector('input[value="评分选定"][onclick]'); if (deleteButton) { deleteButton.removeAttribute('onclick'); deleteButton.addEventListener('click', () => { openActionPage(form, 'masingle.php?action=delatc'); }); } if (pingButton) { pingButton.removeAttribute('onclick'); pingButton.addEventListener('click', () => { openActionPage(form, `operate.php?action=showping&page=${page}`); }); } if (selCheckbox.parentElement.querySelector('.rinsp-selallid-button') == null) { selCheckbox.parentElement.appendChild(document.createTextNode(' ')); const selAllButton = addElem(selCheckbox.parentElement, 'input', 'btn2 rinsp-selallid-button', { type: 'button', value: '全选' }); selAllButton.addEventListener('click', () => { const postIds = new Set(posts.filter(p => p.postUid === post.postUid).map(p => p.postId)); checkSomePosts(el => postIds.has(el.value * 1), true); }); } } } const showpingButton = post.rootElem.querySelector('.tipad ul > li > a[id^="showping_"]'); if (showpingButton) { const showpingActionHref = showpingButton.getAttribute('href'); if (showpingActionHref.indexOf('#') === -1) { let extraParams = []; post.rootElem.querySelectorAll('.r_one .tpc_content .tips').forEach(tips => { const match = tips.textContent.match(/最佳答案奖励:\s\(\+(\d+)\)\sSP币/); if (match) { extraParams.push('-answer-' + match[1] * 1); } }); let sellPrice = 0; let boughtCount = 0; post.contentElem.querySelectorAll('h6.quote.jumbotron > .s3.f12.fn').forEach(soldText => { const match = soldText.textContent.match(/此帖售价 (\d+) SP币,已有 (\d+) 人购买/); if (match) { sellPrice = Math.max(sellPrice, match[1]*1); boughtCount = match[2]*1; } }); if (boughtCount > 0 && bountyStatus != null) { boughtCount--; } const total = sellPrice * boughtCount; if (sellPrice > 0) { if (bountyStatus != null && post.postUid === threadOwnerId) { post.rootElem.classList.add('rinsp-post-illegal-sell'); extraParams.push('-badsell-opsell'); } if (total > 0) { extraParams.push(`-sold-${total}`); } } if (post.postUid !== threadOwnerId) { if (bountyStatus && bountyStatus.winner == null) { extraParams.push('-bounty-' + bountyStatus.sp); } if (bestAnswerMultiplier > 0) { const li = newElem('li', 'rinsp-markcorrect-switch'); showpingButton.parentNode.parentNode.insertBefore(li, showpingButton.parentNode.nextElementSibling); const markcorrect = addElem(li, 'a', null, { href: 'javascript:' }); markcorrect.textContent = '最佳答案'; markcorrect.addEventListener('click', () => handleAwardBestAnswer(post, bountyStatus.sp, bestAnswerMultiplier)); } } showpingButton.setAttribute('href', showpingActionHref + '#uids-' + post.postUid + '-fid-' + post.areaId + extraParams.join('')); } } const shieldButton = post.rootElem.querySelector('.tipad a[id^="shield_"]'); if (shieldButton) { const shieldActionHref = shieldButton.getAttribute('href'); if (shieldActionHref.indexOf('#') === -1) { shieldButton.setAttribute('href', shieldActionHref + '#uid-' + post.postUid); } } let adminBar; const banuserButton = post.rootElem.querySelector('.tipad a[id^="banuser_"]'); if (banuserButton) { const banuserActionHref = banuserButton.getAttribute('href'); if (banuserActionHref.indexOf('#') === -1) { banuserButton.setAttribute('href', banuserActionHref + '#uid-' + post.postUid + '-fid-' + post.areaId); } adminBar = banuserButton.parentElement; } if (adminBar) { const group = multiAccountMap.get(post.postUid); const button = newElem('a', 'rinsp-user-admin-button', { href: 'javascript:'}); button.textContent = '🔘'; let addExtraMenuItems = () => {}; if (group != null) { adminBar.prepend(button); adminBar.setAttribute('style', `outline: 2px solid ${group.color}; outline-offset: 2px`); post.rootElem.classList.add('rinsp-post-multi-uid'); addExtraMenuItems = (items => { if (document.body.classList.contains('rinsp-filter-multiip-focus-mode')) { items.push({ label: '取消IP筛选', action: () => { document.body.classList.remove('rinsp-filter-multiip-focus-mode'); posts.forEach(p => { p.rootElem.classList.remove('rinsp-post-multi-uid-selected'); }); } }); } else { items.push({ label: `只看相同IP用户 (${group.uids.size})`, action: () => { document.body.classList.add('rinsp-filter-multiip-focus-mode'); posts.forEach(p => { if (group.uids.has(p.postUid)) { p.rootElem.classList.add('rinsp-post-multi-uid-selected'); } }); } }); } }); } button.addEventListener('click', async () => { const items = []; addExtraMenuItems(items); if (items.length === 0) { return; } setupPopupMenu({ title: '请选择功能', popupMenuId: 'MULTI_ACCOUNT_MENU', width: 150, anchor: button, verticallyInverted: true, items }); }); } }); } function renderAdminHistoryPage(myUserId, userConfig) { const services = { async getLogEntries() { const punishRecords = await userPunishRecordStore.list(); const entries = []; punishRecords.forEach(record => { record.logs.forEach(entry => { entries.push(Object.assign({ uid: record.uid }, entry)); }); }); return entries; } }; renderCommonPunishHistoryPage(myUserId, 0, services); } function renderPunishHistoryPage(userId, userConfig) { const services = { async getLogEntries() { let punishRecord = await userPunishRecordStore.get(userId); if (punishRecord == null) { punishRecord = { uid: userId, logs: [] }; } return punishRecord.logs.map(entry => { return Object.assign({ uid: userId }, entry); }); } }; renderCommonPunishHistoryPage(userId, userId, services); } function renderCommonPunishHistoryPage(pageScopeUid, subjectUid, services) { const mainPane = document.querySelector('#u-content > #u-contentmain'); const sidePane = document.querySelector('#u-content > #u-contentside'); sidePane.innerHTML = ''; mainPane.innerHTML = '<div style="padding:16px 30px"><img src="images/loading.gif" align="absbottom">加载中...</div>'; async function init() { mainPane.innerHTML = ''; let entries = await services.getLogEntries(); const sortByTime = comparator('time', true); entries.sort(sortByTime); const punishTypeToId = new Map(); PUNISH_TYPES.forEach((type, i) => { punishTypeToId.set(type, i + 1); }); const now = Date.now(); const typeMap = new Map(); entries = entries.map(entry => { let punishTypeId = punishTypeToId.get(entry.type); if (punishTypeId == null) { punishTypeId = typeMap.size + 1; punishTypeToId.set(punishTypeId, entry.type); } typeMap.set(punishTypeId, entry.type); return Object.assign({ typeId: punishTypeId }, entry); }); const sectionFilter = addSectionFilter(typeMap, pageScopeUid, () => update(), {}); const table = addElem(mainPane, 'table', 'u-table rinsp-punish-history-table'); const thead = addElem(table, 'thead'); const tr1 = addElem(thead, 'tr'); const filterCell = addElem(tr1, 'td', null, { colspan: '3' }); const filterBox1 = addElem(filterCell, 'div', 'rinsp-quick-filter-box'); addElem(filterBox1, 'div').textContent = '🔍︎搜索'; const filterInput = addElem(filterBox1, 'input', null); addElem(filterBox1, 'div', null, { style: 'flex: 0 1 4em' }); if (subjectUid) { const addRecordButton = addElem(filterBox1, 'a', null, { href: 'javacript:'}); addRecordButton.textContent = '➕添加记录'; addRecordButton.addEventListener('click', async () => { let comment = prompt('请添加备注'); if (comment) { const newLogEntry = { time: Date.now(), type: PUNISH_TYPE_CUSTOM, summary: comment, reason: '', data: {} }; let updateRecord = await userPunishRecordStore.get(subjectUid); if (updateRecord == null) { updateRecord = { uid: subjectUid, logs: [] }; } updateRecord.logs.push(newLogEntry); await userPunishRecordStore.put(subjectUid, updateRecord); entries.unshift(newLogEntry); update(); } }); } addElem(filterBox1, 'div', null, { style: 'flex: 0 1 1em' }); const controlBlock = addElem(filterBox1, 'div'); controlBlock.appendChild(document.createTextNode('显示数: ')); const limitSelect = addElem(controlBlock, 'select'); addElem(limitSelect, 'option', null, { value: '100' }).textContent = '100'; addElem(limitSelect, 'option', null, { value: '200' }).textContent = '200'; addElem(limitSelect, 'option', null, { value: '500' }).textContent = '500'; addElem(limitSelect, 'option', null, { value: '1000' }).textContent = '1000'; limitSelect.value = '100'; limitSelect.addEventListener('change', () => update()); const tbody = addElem(table, 'tbody'); const tfoot = addElem(table, 'tfoot'); const statusRow = addElem(tfoot, 'tr'); const statusCell = addElem(statusRow, 'td', 'grey', { colspan: '2' }); let lastFilterString = ''; function update(filterChangeOnly) { const terms = filterInput.value.toLowerCase().trim().split(/\s+/g); const thisFilterString = terms.join(' '); if (filterChangeOnly && lastFilterString === thisFilterString) return; lastFilterString = thisFilterString; statusCell.textContent = ''; tbody.innerHTML = ''; if (entries.length === 0) { statusCell.textContent = '💡 没有记录'; return; } const countMap = new Map(); entries.forEach(entry => { countMap.set(entry.typeId, (countMap.get(entry.typeId)||0) + 1); }); sectionFilter.updateCounts(countMap); const limit = limitSelect.value * 1; const punishTypeId = sectionFilter.getCurrentFid(); let selectedEntries; // apply type filter, also clone the list for sorting if (punishTypeId > 0) { selectedEntries = entries.filter(entry => entry.typeId === punishTypeId); } else { selectedEntries = entries.slice(); } // apply text filter if (terms.length === 1 && terms[0] === '') { table.classList.remove('rinsp-table-filtered'); if (selectedEntries.length > limit) { statusCell.textContent = `💡只显示前${limit}条记录 / 共${selectedEntries.length}条`; } } else { table.classList.add('rinsp-table-filtered'); selectedEntries = selectedEntries.filter(entry => match(entry, terms)); if (selectedEntries.length === 0) { statusCell.textContent = '💡没有搜索结果'; } else if (selectedEntries.length > limit) { statusCell.textContent = `💡只显示前${limit}条搜索结果 / 共${selectedEntries.length}条`; } } if (selectedEntries.length > limit) { statusCell.textContent = `💡只显示前${limit}条记录 / 共${selectedEntries.length}条`; } selectedEntries.slice(0, limit).forEach(entry => { const row = addElem(tbody, 'tr'); switch (entry.type) { case PUNISH_TYPE_HP: row.classList.add('rinsp-profile-punish-tag-blood'); break; case PUNISH_TYPE_BAN: row.classList.add('rinsp-profile-punish-tag-ban'); break; case PUNISH_TYPE_DELETE_THREAD: case PUNISH_TYPE_DELETE_REPLY: case PUNISH_TYPE_SHIELD: row.classList.add('rinsp-profile-punish-tag-del'); break; case PUNISH_TYPE_UNSHIELD: row.classList.add('rinsp-profile-punish-tag-none'); break; default: row.classList.add('rinsp-profile-punish-tag-misc'); } setAreaScoped(row, entry.typeId); const th = addElem(row, 'th'); addElem(th, 'div').textContent = entry.type; addElem(th, 'span', 'gray f9').textContent = ' [ ' + getAgeString((now - entry.time) / 60000) + ' ]'; const descCell = addElem(row, 'td', 'rinsp-punish-history-summary-cell'); const details = addElem(descCell, 'details'); const summary = addElem(details, 'summary'); if (entry.summary) { addElem(summary, 'span').textContent = entry.summary; addElem(summary, 'div').textContent = entry.reason; } else if (entry.reason) { addElem(summary, 'span').textContent = entry.reason; } else { addElem(summary, 'span', 'gray').textContent = ' - '; } const fullContent = addElem(details, 'div', 'rinsp-punish-history-data'); const dataKeys = Object.keys(entry.data||{}); if (dataKeys.length === 0) { addElem(fullContent, 'span', 'gray f9').textContent = '没有其他资讯'; } else { dataKeys.forEach(key => { const value = entry.data[key]; if (value != null) { const pair = addElem(fullContent, 'div'); addElem(pair, 'label').textContent = key; pair.appendChild(document.createTextNode(' ')); addElem(pair, 'span').textContent = value; } }); } const delCell = addElem(row, 'td', 'rinsp-punish-history-del-cell'); const delButton = addElem(delCell, 'a'); delButton.textContent = '🗑️'; delButton.addEventListener('click', async () => { if (confirm('删除这条记录?')) { let updateRecord = await userPunishRecordStore.get(entry.uid); if (updateRecord != null) { updateRecord.logs = updateRecord.logs.filter(log => log.time != entry.time); await userPunishRecordStore.put(entry.uid, updateRecord); } entries = entries.filter(r => r !== entry); update(); } }); if (!subjectUid) { addElem(delCell, 'div'); const userLink = addElem(delCell, 'a', 'gray f9', { href: `u.php?action-trade-uid-${entry.uid}.html`, target: '_blank' }); userLink.textContent = `#${entry.uid}`; } }); } let enqueueTimer = null; filterInput.addEventListener('keyup', () => { if (enqueueTimer) clearTimeout(enqueueTimer); enqueueTimer = setTimeout(() => update(true), 200); }); filterInput.addEventListener('change', () => { if (enqueueTimer) clearTimeout(enqueueTimer); update(true); }); update(); } function match(entry, searchTerms) { foreach_term: for (let searchTerm of searchTerms) { if ((entry.summary||'').toLowerCase().indexOf(searchTerm) !== -1) continue; if ((entry.reason||'').toLowerCase().indexOf(searchTerm) !== -1) continue; if (entry.data) { for (let value of Object.values(entry.data)) { if (typeof value === 'string') { if (value.toLowerCase().indexOf(searchTerm) !== -1) continue foreach_term; } } } return false; } return true; } init(); } function createPunishRecordAccess() { const punishRecordCache = new Map(); return { async getPunishRecord(uid) { let record = punishRecordCache.get(uid); if (record == null) { record = userPunishRecordStore.get(uid); punishRecordCache.set(uid, record); } return await record; } }; } async function findUserName(userId) { const doc = await fetchGetPage(`${document.location.origin}/sendemail.php?uid-${userId}.html`); const sendToField = doc.querySelector('input[name="sendtoname"]'); if (sendToField) { return sendToField.value; } const userNameBold = doc.querySelector('#main .t .f_one center > b'); if (userNameBold && userNameBold.parentElement.textContent.indexOf('不接受邮件') !== -1) { return userNameBold.textContent; } throw new Error('错误'); } return { enableQuickPingFunction, autoFillShowPingPage, enhancePingActionPage, enhanceEndRewardActionPage, enhanceMawholeActionPage, enhanceDeleteReplyActionPage, enhanceReasonSelector, enhanceShieldActionPage, enhanceBanUserActionPage, addBatchPingFunction, addBatchDeleteFunction, addBatchPostSelectionControl, addDeleteFormMetadata, addBulkDeleteFormMetadata, addPostAdminControl, renderAdminHistoryPage, renderPunishHistoryPage, createPunishRecordAccess, findUserName }; } function createLocalStorageProperty(name, defaultValue) { return { get() { let value = localStorage.getItem(name); if (value != null || defaultValue == null) { return value; } return defaultValue; }, set(value) { if (value == null) { localStorage.removeItem(name); } else { localStorage.setItem(name, value); } } }; } function applyDarkTheme() { document.documentElement.classList.forEach(cls => { if (cls.startsWith('rinsp-dark-theme-')) { document.documentElement.classList.remove(cls); } }); if (darkModeEnabledProperty.get()) { document.documentElement.classList.add('rinsp-dark-mode-set'); document.documentElement.classList.add(`rinsp-dark-theme-${darkModeThemeProperty.get()}`); } else { document.documentElement.classList.remove('rinsp-dark-mode-set'); } if (isDarkMode()) { document.documentElement.classList.add('rinsp-dark-mode'); return true; } else { document.documentElement.classList.remove('rinsp-dark-mode'); return false; } } function isDarkMode() { if (darkModeEnabledProperty.get()) { if (darkModeFollowsSystemProperty.get()) { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } else { return true; } } return false; } const DOMAIN_REDIRECT_ENABLED_KEY = 'domain-redirect-enabled'; async function setDomainRedirectEnabled(active) { if (active) { await GM.setValue(DOMAIN_REDIRECT_ENABLED_KEY, document.location.origin); } else { await GM.deleteValue(DOMAIN_REDIRECT_ENABLED_KEY); } } async function getDomainRedirectTarget() { let value = await GM.getValue(DOMAIN_REDIRECT_ENABLED_KEY); if (value && value.match(/\*?https?:\/\/.*/)) { // fix corrupt "*undefined" value return value; } else { return null; } } function requiresDomainRedirect() { // exclude redirection for the following pages switch (document.location.pathname) { case '/login.php': case '/register.php': case '/admin.php': return false; } return document.querySelector('#user-login > a[href="register.php"]') != null; } async function attemptDomainRedirect() { let domainRedirectTarget = await getDomainRedirectTarget(); if (domainRedirectTarget && domainRedirectTarget.startsWith('*')) { domainRedirectTarget = domainRedirectTarget.substring(1); if (document.location.origin === domainRedirectTarget) { return false; } else { GM.setValue(DOMAIN_REDIRECT_ENABLED_KEY, domainRedirectTarget); } } if (domainRedirectTarget) { await GM.setValue(DOMAIN_REDIRECT_ENABLED_KEY, '*' + domainRedirectTarget); // deactivate the target until is it reachable window.location.replace(`${domainRedirectTarget}${document.location.pathname}${document.location.search}${document.location.hash}`); } return true; } function setSafeMode(active) { if (active) { localStorage.setItem('rinsp-safe-mode', '1'); document.documentElement.classList.add('rinsp-safe-mode'); } else { localStorage.removeItem('rinsp-safe-mode'); document.documentElement.classList.remove('rinsp-safe-mode'); } } function isSafeMode() { return localStorage.getItem('rinsp-safe-mode') === '1'; } function setSafeModeShowMyAvater(active) { if (active) { localStorage.setItem('rinsp-safe-mode-allow-myself', '1'); document.documentElement.classList.add('rinsp-safe-mode-allow-myself'); } else { localStorage.removeItem('rinsp-safe-mode-allow-myself'); document.documentElement.classList.remove('rinsp-safe-mode-allow-myself'); } } function isSafeModeShowMyAvater() { return localStorage.getItem('rinsp-safe-mode-allow-myself') === '1'; } function setFastLoadMode(active) { if (active) { localStorage.setItem('rinsp-fastload-mode', '1'); } else { localStorage.setItem('rinsp-fastload-mode', '0'); } } function isFastLoadMode() { return localStorage.getItem('rinsp-fastload-mode') !== '0'; } function setFastLoadLazyImageMode(active) { if (active) { localStorage.setItem('rinsp-fastload-mode-lazyimg', '1'); } else { localStorage.removeItem('rinsp-fastload-mode-lazyimg'); } } function isFastLoadLazyImageMode() { return localStorage.getItem('rinsp-fastload-mode-lazyimg') === '1'; } //============= utils =============// let verifyhashCache = null; function verifyhash() { if (unsafeWindow.verifyhash) { return unsafeWindow.verifyhash; } if (verifyhashCache) { return verifyhashCache; } const hiddenField = document.querySelector('form[name="FORM"][action="post.php?"] input[type="hidden"][name="verify"][value]'); if (hiddenField) { verifyhashCache = hiddenField.value; return hiddenField.value; } for (let scriptElement of document.querySelectorAll('head > script')) { const match = scriptElement.textContent.match(/;var verifyhash = '([A-Za-z0-9]{8})';/); if (match) { verifyhashCache = match[1]; return match[1]; } } alert('无法取得操作验证码 (运作环境错误,请用篡改猴及谷歌火狐等主流浏览器)'); } // accounting for any deferred image loading, returning the original image src function getImgSrc(img) { return img.getAttribute('data-rinsp-defer-src') || img.getAttribute('src'); } function getByteLength(text) { if (textEncoder == null) return 0; return textEncoder.encode(text).length; } function truncateByByteLength(text, limit, ellipsis) { let diff = getByteLength(text) - limit; if (diff > 0) { if (ellipsis) { diff += getByteLength(ellipsis); } const chars = Array.from(text); while (diff > 0) { diff -= getByteLength(chars.pop()); } if (ellipsis) { chars.push(ellipsis); } return chars.join(''); } else { return text; } } function addGlobalStyle(parent, css) { const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = css; parent.appendChild(style); } function parseSpAmount(text) { const match = text.match(/([0-9一二两三四五六七八九十]+ ?[kKwW万千]?) *的? *(sp|SP)/); if (match) { match[1] = match[1].replace(/十([^一二三四五六七八九])/g, '10$1').replace(/十/g, '1').replace(/一/g, '1').replace(/[二两]/g, '2').replace(/三/g, '3').replace(/四/g, '4').replace(/五/g, '5').replace(/六/g, '6').replace(/七/g, '7').replace(/八/g, '8').replace(/九/g, '9'); if (match[1].endsWith('w') || match[1].endsWith('W') || match[1].endsWith('万')) { return parseInt(match[1]) * 10000; } else if (match[1].endsWith('千') || match[1].endsWith('k') || match[1].endsWith('K')) { return parseInt(match[1]) * 1000; } else { const amt = match[1] * 1; return amt === 99999 ? 0 : amt; } } else { return 0; } } function getBountyStatus(doc) { const tpc = doc.querySelector('#td_tpc'); if (tpc == null) { return null; } const bountyTimeElem = tpc.closest('table').querySelector('.tpc_content .tips .s3'); if (bountyTimeElem == null) { return null; } let bountyAmount = 0; let helppointAmount = 0; let winnerHashId = null; tpc.closest('table').querySelectorAll('.tpc_content .tips .tac').forEach(elem => { const msg = elem.textContent.trim(); const bountyMatch = msg.match(/最佳答案: (\d+)\s+SP币/); const helpPtsMatch = msg.match(/热心助人剩余点数: (\d+)\s+SP币/); const winnerMatch = msg.match(/最佳答案获得者: ([a-z0-9]{8})/); if (bountyMatch) { bountyAmount = bountyMatch[1] * 1; } if (helpPtsMatch) { helppointAmount = helpPtsMatch[1] * 1; } if (winnerMatch) { winnerHashId = winnerMatch[1]; } }); const statusMessage = bountyTimeElem.textContent.trim(); let bountyUntil; if (statusMessage === '此帖悬赏结束') { bountyUntil = -1; } else if (statusMessage === '此帖悬赏中(剩余时间:已结束)...') { bountyUntil = Date.now(); } else { const bountyHourLeft = (statusMessage.match(/此帖悬赏中\(剩余时间:(\d+)小时\)/)||[])[1] * 1; bountyUntil = Date.now() + (bountyHourLeft||0) * 3600000; } return { sp: bountyAmount, pts: helppointAmount, winner: winnerHashId, bountyUntil, ended: bountyUntil === -1 ? 2 : Date.now() > bountyUntil ? 1 : 0 }; } function findErrorMessage(doc) { let err = doc.querySelector('#main .t .f_one center'); return err ? err.textContent.trim()||'不明错误' : null; } function getAgeString(ageMins) { if (ageMins >= 1440) { return Math.floor(ageMins / 1440) + '天前'; } else if (ageMins >= 60) { return Math.floor(ageMins / 60) + '小时前'; } else if (ageMins > 1) { return Math.floor(ageMins) + '分钟前'; } else { return '刚刚'; } } function addElem(parent, tag, styleClass, attrs) { const elem = newElem(tag, styleClass, attrs); parent.appendChild(elem); return elem; } function newElem(tag, styleClass, attrs) { const elem = document.createElement(tag); if (styleClass) { styleClass.split(' ').forEach(function(cls) { elem.classList.add(cls); }); } if (attrs) { for (let name of Object.keys(attrs)) { elem.setAttribute(name, attrs[name]); } } return elem; } async function runWithLock(key, duration, job) { const prop = `rinsp-lock-${key}`; async function acquire() { while (true) { const randToken = (Math.random() * 10000000000).toFixed(0); const lockUntil = Date.now() + duration; localStorage.setItem(prop, `${randToken}-${lockUntil}`); await sleep(0); const acquired = (localStorage.getItem(prop)||'').split('-', 2); if (acquired[0] === randToken) { return true; } let waitTime = lockUntil - Date.now(); if (waitTime > duration * 2) { // invalid lock, retry acquire localStorage.removeItem(prop); } else { await sleep(waitTime); } } } await acquire(); return await Promise.resolve(job()) .finally(() => { localStorage.removeItem(prop); }); } // polyfill replacement function Object_hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function sleep(timeout) { return new Promise(function(resolve) { setTimeout(() => resolve(true), timeout); }); } /* cyrb53 (c) 2018 bryc (github.com/bryc) License: Public domain. Attribution appreciated. A fast and simple 53-bit string hash function with decent collision resistance. Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. */ function cyrb53(str, seed) { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); return 4294967296 * (2097151 & h2) + (h1 >>> 0); } function checkDevMode() { return localStorage.getItem('RIN_PLUS_DEV_MODE') === '1'; } function setCookie(name, value, hours) { var expires = ''; if (hours) { var date = new Date(); date.setTime(date.getTime() + (hours*60*60*1000)); expires = '; expires=' + date.toUTCString(); } document.cookie = name + '=' + (value || '') + expires + '; path=/'; } function console_info() { let out = document.querySelector('#rinsp-debug-console'); if (out == null) { out = addElem(document.body, 'pre', null, { id: 'rinsp-debug-console', style: 'position:fixed;top:20px;left:20px;width:40em;max-height:30em;outline:5px solid #CCC;background:#FFF9;color:#000;overflow:auto;z-index:99999;' }); } out.textContent += Array.from(arguments).join(', ') + '\n'; out.scrollTop = 10000000; } /* ==== START: script initialisation ==== */ function readPinnedPicWallFids() { return (localStorage.getItem(PIC_WALL_PREF_KEY)||'').split(' ').map(fid=>fid*1).filter(fid=>!!fid); } function redirectToDesktopVersion() { if (document.location.pathname === '/simple/index.php') { const pageMatch = document.location.search.match(/\^?t(\d+)(?:_(\d+))?\.html$/); if (pageMatch) { setCookie('mobilever', 1, 720); window.location.replace(`${document.location.origin}/read.php?tid=${pageMatch[1]}&fpage=0&toread=&page=${pageMatch[2]||1}`); return true; } } } function attemptPicWallRedirect() { if (window.location.pathname === '/thread.php') { const fid = (window.location.search.match(/[&?]fid[=-](\d+)/)||[])[1] * 1; if (fid) { const fids = readPinnedPicWallFids(); if (fids.includes(fid)) { window.location.replace(`${window.location.origin}/thread_new.php${window.location.search}${window.location.hash}`); return true; } } } } const AVATAR_LOADING_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhlgCWAKIAANPT083Nzd3d3cXFxerq6uPj48PDw+3t7SH5BAAAAAAALAAAAACWAJYAAAP/eLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBILBqPyKRyyWw6n9CodEqtWq/YrHbL7Xq/4LB4TC6bz+i0es1uu9/wuHxOr9vv+Lx+z+/7/4CBgoOEhYaHiImKi4yNjhQCAQaTlJWWl5iVAQJ5BZmfoJ8FeAChpqcAeJIGnCUCkwGqk6Mlngaxd6u0JLa4droOkQMABBu9sga7Crawxs25sw2llcoWx9DJ0pbVFde/0QzMt87j2NzCxOS+dcC1z9/Z7uXw3CDe7OAZBAABAakN9+i0w1BgQKUBxRYEnDPQmkFLA3YtlNOwAD8A9QQ8vBRxkNk7fPEOvKr0b4FGUAM4TYzTbpqlAAlHhkq5Eo4uAqswwXSJ6qPASZFmntKJbOPQo/NAIl2a9CfTpeucPj0adQ7PqaZK1hGHNVQ9OUG7ftr0qKzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza4abAAA7'; const IMAGE_LOADING_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhIAAgAKIDAO3t7fn5+fr6+h8iJP7+/tTU1P///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAIAAgAAADozi63P4wyqmKvTjrCwsQQSiO5AgUnUCsbOu6AvoU6goGb07E6RpgNZ1N5qCtMgAhizernS5JFgCAazGLwWd09SxUhz1dDLkkNoy53yZ6PQdb3Q2tzUDD5Rn6wr7Da05hBGp+GmFxhBg9h4gcMwGLjBYdkXgUlpeYmZqbnJ2WiQ0CUwIPoh9FGQ1TUw+rAKigC66tq7CNC6akDrm2kp5nt7/CnAkAIfkEBQoAAwAsAgAYABwABgAAAxw4I6L+EAI1R60x0wea81r4PaAoXmaEWQqTZmgCACH5BAkKAAMALAIAGAAcAAYAAAMiOAOg/hAGFlwpMcsXwr2VljUO4H2kCKWLcqlaqIQMHFNDAgAh+QQFCgADACwAAAAAIAAgAAADSji63P4wykmrvTjrzbv/YCiOZGmeaKqubOuaAQAEm+wERV7QjSALD0AO0BDqiEUZkoHT8RTGoUO5XDSlVt3O4QMAp1im7ImxvUgJACH5BAkKAAMALAIAGAAcAAYAAAMjOFAF8zDKKMRzEM+dcYkftwlACQjhk4qUeWoKy5UZA8uUNSQAIfkEBQoAAwAsAAAAACAAIAAAAz84utz+MMpJq7046827/2AojmRpnmiqrmzrmkVcjLLsCAAgPLje1DFHLvcYAn5A4bC4ZABnjd7ulps6ba9sJAEAIfkEBQoAAwAsAgAYABEABgAAAxc4MMruQrDl6KP2aQm154yQZVPZnB+XAAAh+QQJCgADACwCABgAEQAGAAADHTgBEPPwsedgjaMUGQEOmrZ0X1g00RVpFqN23pAAACH5BAUKAAMALAAAAAAgACAAAANAOLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s654AAAVxkAFFITdBntuWng+4wPl2FWEOqTAuMUamQlkgWmKz2uuVAAAh+QQJCgADACwCABgAEQAGAAADHTgjou6gFADpUxYLwEF7Bbh1nxOaWPaoFnNBEpUAACH5BAUKAAMALAAAAAAgACAAAAM1OLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s675KIReZAACCM8/Yfes72sUHAO4wNpyRJzrCWgkAIfkEBQoAAwAsDQAYABEABgAAAxc4MMruQrDl6KP2aQm154yQZVPZnB+XAAAh+QQJCgADACwNABgAEQAGAAADHTgBEPPwsedgjaMUGQEOmrZ0X1g00RVpFqN23pAAACH5BAUKAAMALAAAAAAgACAAAAM/OLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s674LAECBHGRAUcxNoOs3i+8XjP12l6GOZ/wxKzlk71i0yGg22CgBADs='; const IMAGE_LOADING_PLACEHOLDER_DARK = 'data:image/gif;base64,R0lGODlhIAAgAKIGAEVNUjA2OSImKD1ESCAjJTxDRx8iJDIyMiH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAIAAgAAADsWi63P4wyqnKuDjrPQocQiiOZCkMn0CsbOu6Z9qGb03ED7gKVqfaLJxDRwh0LAFgEJX7GTHJYOC3lK0ChUL0iqHemEMvzJhFVps23rE8iAobRNilTPe9GXEWmc73geFie3x9WFY3FoOJiFYna4qDMoKPiRAFApKTkA9ZjpmaFKChoqOkpaanEwEAAAEOIw+vDaqrrA2xtiIOtLS4IbC5DburvQK/vg0Fux6oyKvLzNCnCQAh+QQFCgAGACwCABgAHAAGAAADIGhGpP4wlvJCiDg7Ym3T0lNw3QeKqGGd2OW4gsC205UAACH5BAkKAAYALAIAGAAcAAYAAAMkaCai/hCKEJoJCsftRBlDwTwWFwUgWJHmhqaYprXSF5a0Oa0JACH5BAUKAAYALAAAAAAgACAAAANJaLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s65pCEAibQDdCMQzFzdi2h0zWCOx2AQfQt4gNmcZjEgdUDmc/HY+pWAqHVqwm+CIlAAAh+QQJCgAGACwCABgAHAAGAAADJmhRFfYwykjICyADNzsUD1hoQOF5FkSMmnlO6UNgGvdKoJEvze0nACH5BAUKAAYALAAAAAAgACAAAANAaLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s65oBAARjLM+OoAvPzjO3W27XIwKDgKGuuGQUggXlr+FzPAHRlzaSAAAh+QQFCgAGACwCABgAEQAGAAADHWhWpf6EvElDUM1lR6wl2zJ1XvdcFWoEjDoJgpIAACH5BAkKAAYALAIAGAARAAYAAAMfaBIh9vAJp8oYhcbweLgXF2nKB4oQOVkY2b1lE81QAgAh+QQFCgAGACwAAAAAIAAgAAADQmi63P4wykmrvTjrzbv/YCiOZGmeaKqubOueggAJQSBjde0IxTAUtwotF1QEfL7AZVgrGo5I5SUnZfB8wExs1ny9EgAh+QQJCgAGACwCABgAEQAGAAADImhGpO6hlKBEfSqADUJzn1NwQBEu2MgVhtVihsZRMBZNRgIAIfkEBQoABgAsAAAAACAAIAAAAzloutz+MMpJq7046827/2AojmRpnmiqrmzrvkoAAEEm3IIjzzSG5w0ez4dzCGfEm6MgLIyYACeslQAAIfkEBQoABgAsDQAYABEABgAAAx1oVqX+hLxJQ1DNZUesJdsydV73XBVqBIw6CYKSAAAh+QQJCgAGACwNABgAEQAGAAADH2gSIfbwCafKGIXG8Hi4FxdpygeKEDlZGNm9ZRPNUAIAIfkEBQoABgAsAAAAACAAIAAAA0Noutz+MMpJq7046827/2AojmRpnmiqrmzrvosgQEIQzJhtO0IxDAVcpaYTKgK/X+BCtBkNyOTyopsyer9gRkZzwkYJADs='; function deferImageLoading(doc) { if (DEBUG_MODE) console.info('[STAGE] deferImageLoading'); const dark = isDarkMode(); const defaultImagePath = `${document.location.origin}/images/face/`; doc.querySelectorAll('.user-pic img[src], #u-portrait img.pic[src]:not([data-org-img])').forEach(img => { if (img.src.startsWith(defaultImagePath)) return; // skip default avatar images img.dataset.rinspDeferSrc = img.src; img.src = AVATAR_LOADING_PLACEHOLDER; }); doc.querySelectorAll('.gonggao #cate_thread img[src], .tpc_content img[src]:not([src^="images/"]), #info_base img[src]:not([src^="images/"])').forEach(img => { img.dataset.rinspDeferSrc = img.src; img.src = dark ? IMAGE_LOADING_PLACEHOLDER_DARK : IMAGE_LOADING_PLACEHOLDER; }); } const RESUME_IMG_LOAD_DELAY = 0; function resumeImageLoading(doc) { const lazy = isFastLoadLazyImageMode(); setTimeout(() => { if (DEBUG_MODE) console.info('[STAGE] resumeImageLoading'); doc.querySelectorAll('img[data-rinsp-defer-src]').forEach(img => { if (lazy) { img.setAttribute('loading', 'lazy'); } if (img.dataset.orgImg) { if (img.dataset.rinspDeferSrc) { img.dataset.orgImg = img.dataset.rinspDeferSrc; } } else { img.classList.add('rinsp-img-loading'); img.addEventListener('load', () => { img.classList.remove('rinsp-img-loading'); }); img.src = img.dataset.rinspDeferSrc; } delete img.dataset.rinspDeferSrc; }); }, RESUME_IMG_LOAD_DELAY); } function initWhenPageRootLoaded() { if (!document.documentElement || document.documentElement.dataset.rinspInstalled * 1 > 0) { return; } document.documentElement.dataset.rinspInstalled = '1'; if (window.matchMedia) { try { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { applyDarkTheme(); }); } catch (ignore) {} } if (applyDarkTheme()) { // attempt to reduce flashing of unstyled content document.documentElement.setAttribute('style', 'filter:invert(0.5) filter:brightness(0.2)'); } else { document.documentElement.setAttribute('style', 'filter:invert(0.5) filter:brightness(1.9)'); } if (isSafeMode()) { document.documentElement.classList.add('rinsp-safe-mode'); } if (isSafeModeShowMyAvater()) { document.documentElement.classList.add('rinsp-safe-mode-allow-myself'); } } function initWhenPageHeadLoaded() { if (!document.head || document.documentElement.dataset.rinspInstalled * 1 > 1) { return; } document.documentElement.dataset.rinspInstalled = '2'; addGlobalStyle(document.head, customCss); addGlobalStyle(document.head, darkThemeCss); if (DEV_MODE) { addGlobalStyle(document.head, devCss); } } function initWhenPageDomLoaded() { if (!document.body || document.documentElement.dataset.rinspInstalled * 1 > 2) { return; } document.documentElement.dataset.rinspInstalled = '3'; document.documentElement.removeAttribute('style'); if (findErrorMessage(document) === '论坛设置:刷新不要快于 1 秒') { document.documentElement.dataset.rinspInstalled = '9'; // skip all subsequent initialization setTimeout(() => document.location.reload(), 1000); return; } if (requiresDomainRedirect()) { attemptDomainRedirect().then(redirecting => { if (redirecting) { document.documentElement.dataset.rinspInstalled = '9'; // skip all subsequent initialization } }); } if (isFastLoadMode()) { deferImageLoading(document); init().catch(ex => console.error(ex)); } } function initWhenPageComplete() { if (document.readyState !== 'complete' || document.documentElement.dataset.rinspInstalled * 1 > 3) { return; } document.documentElement.dataset.rinspInstalled = '4'; document.documentElement.removeAttribute('style'); // unhide those used to be unstyled content if (isFastLoadMode()) { resumeImageLoading(document); } else { init().catch(ex => console.error(ex)); } } function begin() { if (redirectToDesktopVersion()) { return; } if (attemptPicWallRedirect()) { return; } let fallbackTimer = null; function handleInitStages() { if (DEBUG_MODE) console.info('[STAGE] ' + document.readyState); switch (document.readyState) { case "loading": initWhenPageRootLoaded(); initWhenPageHeadLoaded(); break; case "interactive": initWhenPageRootLoaded(); initWhenPageHeadLoaded(); initWhenPageDomLoaded(); break; case "complete": if (fallbackTimer) { clearTimeout(fallbackTimer); fallbackTimer = null; } document.removeEventListener('readystatechange', handleInitStages); initWhenPageRootLoaded(); initWhenPageHeadLoaded(); initWhenPageDomLoaded(); initWhenPageComplete(); break; } } handleInitStages(); if (document.readyState !== 'complete') { document.addEventListener('readystatechange', handleInitStages); // force a recheck after N seconds, in case a dodgy GM script plugin is in use fallbackTimer = setTimeout(() => { handleInitStages(); }, 5000); } if (DEBUG_MODE) { document.addEventListener('DOMContentLoaded', () => { console.info('[STAGE] DOMContentLoaded'); }); } } if (document.location.pathname !== '/admin.php' && document.location.href !== 'about:blank') { begin(); } })();