Greasy Fork is available in English.

LinuxDo自定义🛠️

为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。

  1. // ==UserScript==
  2. // @name LinuxDo自定义🛠️
  3. // @name:en LinuxDo Custom🛠️
  4. // @name:zh-CN LinuxDo自定义🛠️
  5. // @description 为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。
  6. // @description:en Adds customizable features such as logos, click count visualization, image resize, and quick bookmarking to LinuxDo
  7. // @description:zh-CN 为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊、详情展开、页面加宽、发帖时间显示 等功能。
  8. // @version 0.6.3
  9. // @author Yearly
  10. // @match https://linux.do/*
  11. // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iIzExMSIgZD0iTTAgMGg0OHYxNkgweiIvPjxwYXRoIGZpbGw9IiNlZWUiIGQ9Ik0wIDE2aDQ4djE2SDB6Ii8+PHBhdGggZmlsbD0iI0ZiMCIgZD0iTTAgMzJoNDh2MTZIMHoiLz48cGF0aCBmaWxsPSIjMDhmYSIgZD0iTTIzIDIwYzQgMCA4IDUgOSA4bDktMTBjMi0yIDUtOCAzLTEwbC00LTNjLTItMi02IDItOCA0em01IDhjMC0xLTItNC02LTUtNi0xLTEyIDQtMTIgMTAgMCA1LTggNy05IDcgNiAyIDkgNCAxNSAyIDUtMiAxMy03IDExLTE0Ii8+PC9zdmc+
  12. // @license MIT
  13. // @grant GM_addStyle
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_deleteValue
  18. // @namespace http://tampermonkey.net/
  19. // @supportURL https://greasyfork.org/scripts/499029
  20. // @homepageURL https://greasyfork.org/scripts/499029
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. var settings = {};
  25.  
  26. const default_main_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NTAgNDUwIj48cmVjdCB3aWR0aD0iNDUwIiBoZWlnaHQ9IjQ1MCIgcng9IjUwJSIgZmlsbD0iI2VlZSIvPjxwYXRoIGQ9Ik0xNjEgMTkwYy02IDE0LTQ4IDU4LTQ0IDEwMiAxNiAxODQgNzIgNjAgMTU2IDEwNiAwIDAgMTUwLTg0IDMwLTIyMC0zNC00OC00LTg2LTI2LTExOHMtNjAtMzQtODgtNCAxMiA3NC0yOCAxMzQiLz48cGF0aCBkPSJNMzA5IDI4MnMxOC0zNi0xNi02MmMzMiAzNCAxMiA2NCAxMiA2NGgtNmMtMi03MC0yMC0zMi00Ni0xNTYgMzAtMzQtMjgtNjQtMjgtOGgtMThjMi00OC00MC0yNC0xNiAxMC0yIDc0LTQ2IDEwNC00NiAxNTYtMTQtMzYgMTItNjQgMTItNjRzLTM2IDMwLTE0IDc0IDYyIDM0IDM0IDU0YzQ0IDMwIDExMiAxMCAxMTAtNTQgMi0xNiA0NC0xMCA0OC02cy02LTgtMjYtOE0xOTcgMTI2Yy0xNC00LTEwLTIyLTQtMjJzMTYgMTQgNCAyMm0zOCAyYy0xMC0xNC0yLTI4IDgtMjZzMTAgMjYtOCAyNiIgZmlsbD0iI2ZmZiIvPjxnIGZpbGw9IiNmYjIiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSIyIj48cGF0aCBkPSJtMTQzIDMwMiA0MiA2MGMyMiAxNCAxMCA3MC01MCA0Mi0zNC0xMC02Mi04LTY2LTI2czgtMjAgNi0yOGMtOC00NCAyOC0yMiAzOC00NHMxMC0zMiAzMC00bTIyNCAyOGMtOC0xMiAwLTM0LTI4LTMyLTEyIDI0LTQ2IDQ4LTQ4IDAtMjAgMC02IDQ4LTE0IDcwLTE4IDU0IDM0IDU4IDU2IDMybDUyLTM2YzQtNiAxMC0xMi0xOC0zNE0xODMgMTQ2Yy02LTEyIDIyLTI4IDMyLTI4czI0IDggMzggMTIgOCAxOCA0IDIwLTI2IDIwLTQyIDIwLTIwLTE2LTMyLTI0Ii8+PHBhdGggZD0iTTE4MyAxNDRjMTYgMTIgMzQgMjIgNzAtNiIvPjwvZz48L3N2Zz4=";
  27.  
  28. const default_wide_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMTAiIGhlaWdodD0iMTEwIiB2aWV3Ym94PSItNSAtNSAzMTAgMTEwIj48cmVjdCB3aWR0aD0iOTkiIGhlaWdodD0iOTkiIHJ4PSIxMDAlIiBmaWxsPSIjRjFGMUYxIi8+PHRleHQgeD0iMjQiIHk9Ijc2IiBmb250LXNpemU9Ijc3IiBmaWxsPSIjRmMzIiBmb250LXdlaWdodD0iYm9sZCI+TDwvdGV4dD48dGV4dCB4PSI3MCIgeT0iNzUiIGZvbnQtc2l6ZT0iNDYiIGZpbGw9IiM4ODgiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZXR0ZXItc3BhY2luZz0iNSI+SU5VWDwvdGV4dD48dGV4dCB4PSIyMDUiIHk9Ijc3IiBmb250LXNpemU9IjcwIiBmaWxsPSIjRUVFIiBmb250LXdlaWdodD0iNjYwIj5EbzwvdGV4dD48L3N2Zz4=";
  29.  
  30. const settingsConfig = {
  31. class_label_topic: "💠话题内容相关:",
  32. quick_mark : { type: 'checkbox', label: '快速收藏 ', default: true, style:'', info:'在帖子上增加一个⭐用于快速收藏到书签' },
  33. cnts_colorful : { type: 'checkbox', label: '点击数可视化', default: true, style:'', info:'点击数彩色高亮,数越大,颜色越红' },
  34. image_view : { type: 'checkbox', label: '增强大图查看', default: true, style:'', info:'在大图查看时,支持滚轮缩放和鼠标拖动位置' },
  35. spoiler_noblur: { type: 'checkbox', label: '去除模糊', default: false, style:'', info:'去除剧透字段的模糊,使其直接显示' },
  36. details_open : { type: 'checkbox', label: '详情展开', default: false, style:'', info:'直接展开被折叠的详情' },
  37. topic_scroll : { type: 'checkbox', label: '帖子限高', default: true, style:'', info:'帖子内容限高,太长的帖子会变成滚动查看的元素' },
  38. show_floor_num: { type: 'checkbox', label: '显示楼层号', default: true, style:'', info:'在每层帖子增加楼层号显示' },
  39. show_floor_time : { type: 'checkbox', label: '更精确的回复时间', default: true, style:'', info:'帖子的回复时间改为绝对时间并精确到分钟' },
  40. auto_words_patch : { type: 'checkbox', label: '隐藏式字数补丁', default: false, style:'', info:'自动添加不可见的字数补丁' },
  41. image_mini : { type: 'checkbox', label: '显示小图', default: false, style:'margin-bottom:5px;', info:'让帖子中的图都变小,在鼠标悬停时显示大图' },
  42. image_mini_H : { type: 'number', label: '  小图高度', default: "70", dependsOn: 'image_mini', style:'font-size:14px; margin:5px 10px;' , info:'(单位px,建议设为大于50的数)' },
  43. image_mini_W : { type: 'number', label: '  小图宽度', default: "100", dependsOn: 'image_mini', style:'font-size:14px; margin:5px 10px;' , info:'(单位px,建议设为大于50的数)' },
  44.  
  45. class_label_list: "💠话题列表相关:",
  46. show_up_time : { type: 'checkbox', label: '显示话题时间', default: true, style:'', info:'话题列表的帖子显示创建/更新时间,老的帖子会褪色泛黄' },
  47. order_created : { type: 'checkbox', label: '按创建排序', default: true, style:'', info:'首页导航的[新]改成新创建排序' },
  48. avatar_bigger : { type: 'checkbox', label: '发布者头像调整', default: true, style:'', info:'话题列表的发布者头像显示调整细节' },
  49.  
  50. class_label_all: "💠通用:",
  51.  
  52. sidebar_class : { type: 'checkbox', label: '侧栏类别分级显示', default: true, style:'', info:'侧栏分类按层级显示、细节调整、支持折叠/展开' },
  53. red_dot_hidden: { type: 'checkbox', label: '去除小黄点/小红点', default: false, style:'', info:'所有的小黄点/小红点都不再显示' },
  54. goto_top_end : { type: 'checkbox', label: '快速顶部/底部', default: true, style:'', info:'在右下角新增按钮,可点击到顶部/底部' },
  55. wider_page : { type: 'checkbox', label: '超宽显示', default: false, style:'', info:'让页面显示尽量宽' },
  56. thin_header : { type: 'checkbox', label: '窄的顶栏', default: false, style:'', info:'让(Header)顶栏变窄' },
  57. open_in_new : { type: 'checkbox', label: '新标签页打开', default: false, style:'', info:'让所有链接默认从新标签页打开' },
  58. icon_custom : { type: 'checkbox', label: '自定义图标', default: false, style:'margin-bottom:5px;' , info:'始皇说不建议这样,所以我让鼠标悬停时能看眼原LOGO' },
  59. icon_main : { type: 'text', label: '  主图标URL', default: default_main_icon, dependsOn: 'icon_custom', style:'font-size:14px; margin:5px 10px;', info:'' },
  60. icon_wide : { type: 'text', label: '  宽图标URL', default: default_wide_icon, dependsOn: 'icon_custom', style:'font-size:14px; margin:5px 10px;', info:'' },
  61.  
  62. class_label_end: "",
  63. };
  64.  
  65. Object.keys(settingsConfig).forEach(key => {
  66. settings[key] = GM_getValue(key, settingsConfig[key].default);
  67. });
  68.  
  69. GM_registerMenuCommand('Custom Settings', openSettings);
  70.  
  71. function openSettings() {
  72. if (document.querySelector('div#linuxdo-custom-setting')) {
  73. return;
  74. }
  75. const shadow = document.createElement('div');
  76. shadow.style = `position: fixed; top: 0%; left: 0%; z-index:8888; width:100vw; height:100vh; background: #2229;`;
  77. const panel = document.createElement('div');
  78. panel.style = `max-width: calc(100% - 100px); width: max-content; position: fixed; top: 50%; left: 50%; z-index:9999; transform: translate(-50%, -50%); background-color: var(--secondary); color:var(--primary); padding:15px 25px; box-shadow: 0px 0px 15px #000d; max-height: calc(95vh - 40px); overflow-y: auto;`;
  79. panel.id = "linuxdo-custom-setting"
  80. let html = `
  81. <style type="text/css">
  82. :scope label {font-size:16px; display:flex; justify-content:space-between; align-items:center; margin:10px;}
  83. :scope label span {color:#6bc; font-size:12px; font-weight:normal; padding:0 6px; margin-right:auto;}
  84. :scope label input {margin:0 5px 0 15px;}
  85. :scope label input[type=text] {width:350px; padding:1px; font-size:14px;}
  86. :scope label input[type=number] {width:70px; padding: 0 0 0 10px; text-align:center;}
  87. :scope label input[type=checkbox] {background:pink;}
  88. :scope label input[disabled] {background: #CCC;}
  89. :scope label button {user-select: none; color: #333; padding: 6px 12px; margin-top:10px; border-radius:5px; border:none; line-height: normal;}
  90. :scope hr {display: block; height: 1px; margin: 0.5em 0; background:var(--primary); padding: 0;}
  91. </style>
  92. <h2 style="text-align:center; margin-top:.5rem;">LinuxDo Custom Settings</h2>
  93. `;
  94. Object.keys(settingsConfig).forEach(key => {
  95. const cfg = settingsConfig[key];
  96. if(typeof(cfg) == 'string'){
  97. html += `<hr><span style="margin-top:5px;">${cfg}</span>`;
  98. } else {
  99. const val = settings[key];
  100. const checked = cfg.type === 'checkbox' && val ? 'checked' : '';
  101. const disabled = cfg.dependsOn && !settings[cfg.dependsOn] ? 'disabled' : '';
  102. html += `<label style="${cfg.style}">${cfg.label}<span>${cfg.info}</span><input type="${cfg.type}" id="ujs_set_${key}" value="${val}" ${checked} ${disabled} ></label>`;
  103. }
  104. });
  105. html += `
  106. <label><button id="ld_userjs_apply" style="font-weight: bold; background:var(--tertiary); color:var(--secondary)">保存并刷新</button>
  107. <span></span><button id="ld_userjs_save">仅保存</button>
  108. <span></span><button id="ld_userjs_reset">重置</button>
  109. <span></span><button id="ld_userjs_close">取消</button></label>`;
  110. panel.innerHTML = html;
  111.  
  112. document.body.append(shadow, panel);
  113.  
  114. Object.keys(settingsConfig).forEach(key => {
  115. if (settingsConfig[key].dependsOn) {
  116. document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).addEventListener('change', updateDependencies);
  117. }
  118. });
  119.  
  120. function updateDependencies() {
  121. Object.keys(settingsConfig).forEach(key => {
  122. if (settingsConfig[key].dependsOn) {
  123. document.getElementById(`ujs_set_${key}`).disabled = !document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).checked;
  124. }
  125. });
  126. }
  127.  
  128. document.querySelector('button#ld_userjs_save').addEventListener('click', () => {
  129. Object.keys(settingsConfig).forEach(key => {
  130. const element = document.getElementById(`ujs_set_${key}`);
  131. if (element) {
  132. settings[key] = element.type === 'checkbox' ? element.checked : element.value;
  133. GM_setValue(key, settings[key]);
  134. }
  135. });
  136. alert('Settings saved!');
  137. panel.remove();
  138. });
  139.  
  140. document.querySelector('button#ld_userjs_apply').addEventListener('click', () => {
  141. Object.keys(settingsConfig).forEach(key => {
  142. const element = document.getElementById(`ujs_set_${key}`);
  143. if (element) {
  144. settings[key] = element.type === 'checkbox' ? element.checked : element.value;
  145. GM_setValue(key, settings[key]);
  146. }
  147. });
  148. window.location.reload();
  149. });
  150.  
  151. document.querySelector('button#ld_userjs_reset').addEventListener('click', () => {
  152. Object.keys(settingsConfig).forEach(key => {
  153. GM_deleteValue(key);
  154. });
  155. window.location.reload();
  156. });
  157.  
  158. function setting_hide() {
  159. panel.remove();
  160. shadow.remove();
  161. }
  162.  
  163. document.querySelector('button#ld_userjs_close').addEventListener('click', () => setting_hide());
  164.  
  165. shadow.onclick = () => setting_hide();
  166.  
  167. updateDependencies();
  168. }
  169.  
  170. // Function 1: Custom Logo
  171. if (settings.icon_custom) {
  172. GM_addStyle(`
  173. #site-logo {
  174. object-fit: scale-down;
  175. object-position: -999vw;
  176. background-size: cover;
  177. background-repeat: no-repeat;
  178. background-image: url('${settings.icon_main}');
  179. opacity: 1;
  180. transition: opacity 0.5s ease;
  181. }
  182. #site-logo.logo-big {
  183. background-image: url('${settings.icon_wide}');
  184. }
  185. #site-logo.logo-mobile {
  186. background-image: url('${settings.icon_wide}');
  187. }
  188. #site-logo:hover {
  189. object-position: unset;
  190. background-image: none;
  191. }`);
  192.  
  193. function replaceIcon() {
  194. document.querySelector('link[rel="icon"]').href = settings.icon_main;
  195. }
  196.  
  197. const observer = new MutationObserver(replaceIcon);
  198. observer.observe(document.head, { childList: true, subtree: true });
  199.  
  200. replaceIcon();
  201. }
  202.  
  203. // Function 2: Click Counts Visualization
  204. if (settings.cnts_colorful) {
  205. (function countsColorful() {
  206. const badges = document.querySelectorAll("span.badge.badge-notification.clicks");
  207. let values = Array.from(badges, badge => parseInt(badge.title || badge.textContent));
  208. let maxValue = Math.max(...values);
  209. let minValue = Math.min(...values);
  210. if (maxValue < 100 || (maxValue - minValue < 10)) maxValue = maxValue * 1.5;
  211. badges.forEach(badge => {
  212. if (!badge.style.backgroundColor) {
  213. const number = parseInt(badge.title || badge.textContent);
  214. const hue = 180 - (number / maxValue) * 180;
  215. badge.style.backgroundColor = `hsl(${hue}, 50%, 50%)`;
  216. badge.style.color = "#fff";
  217. const sl = document.createElement('span');
  218. sl.style = `height: 1em; display: inline-block; float: right; background: hsl(${hue}, 50%, 50%); width: ${100 * (number / maxValue)}px;`;
  219. badge.after(sl);
  220. }
  221. });
  222. setTimeout(countsColorful, 1500);
  223. })();
  224. }
  225.  
  226. // Function 3: Image Resize and Drag
  227. if (settings.image_view) {
  228. let sizePercent = 80;
  229. let isDragging = false;
  230. let startX, startY, initialX, initialY;
  231.  
  232. function adjustSize(event) { //mfp-container mfp-image-holder mfp-s-ready
  233. let contentImg = document.querySelector('section#discourse-lightbox img');
  234. if (contentImg) {
  235. let delta = event.deltaY > 0 ? -10 : 10;
  236. sizePercent += delta;
  237. if (sizePercent > 300) sizePercent = 300;
  238. if (sizePercent < 5) sizePercent = 5;
  239.  
  240. contentImg.style.width = sizePercent + '%';
  241. contentImg.style.maxWidth = sizePercent + '%';
  242. contentImg.style.maxHeight = sizePercent + '200%';
  243. }
  244. }
  245.  
  246. function startDrag(event) {
  247. let contentImg = document.querySelector('section#discourse-lightbox img');
  248. if (contentImg) {
  249. isDragging = true;
  250. startX = event.clientX;
  251. startY = event.clientY;
  252. initialX = contentImg.offsetLeft;
  253. initialY = contentImg.offsetTop;
  254. event.preventDefault();
  255. }
  256. }
  257.  
  258. function drag(event) {
  259. if (isDragging) {
  260. let contentImg = document.querySelector('section#discourse-lightbox img');
  261. if (contentImg) {
  262. let dx = event.clientX - startX;
  263. let dy = event.clientY - startY;
  264. contentImg.style.left = (initialX + dx) + 'px';
  265. contentImg.style.top = (initialY + dy) + 'px';
  266. }
  267. }
  268. }
  269.  
  270. function stopDrag(event) {
  271. isDragging = false;
  272. }
  273.  
  274. let observer = new MutationObserver(function(mutations) {
  275. mutations.forEach(function(mutation) {
  276. mutation.addedNodes.forEach(function(node) {
  277. let contentImg = document.querySelector('section#discourse-lightbox img');
  278. if (contentImg) {
  279. document.querySelector('section#discourse-lightbox').onwheel = adjustSize;
  280. contentImg.onmousedown = startDrag;
  281. contentImg.onmouseup = stopDrag;
  282. contentImg.onmousemove = drag;
  283. contentImg.style.cursor = "move";
  284.  
  285. function stopClickEvent(event) {
  286. event.stopImmediatePropagation();
  287. event.preventDefault();
  288. }
  289. contentImg.addEventListener('click', stopClickEvent, true);
  290. }
  291. });
  292. });
  293. });
  294.  
  295. observer.observe(document.body, { childList: true, subtree: true });
  296.  
  297. }
  298. // Function 3: Image Resize and Drag
  299. if (settings.image_view) {
  300. let sizePercent = 80;
  301. let isDragging = false;
  302. let startX, startY, initialX, initialY;
  303.  
  304. function adjustSize(event) {
  305. let contentImg = document.querySelector('div.mfp-content img');
  306. let contentDiv = document.querySelector('div.mfp-content');
  307. if (contentImg) {
  308. let delta = event.deltaY > 0 ? -10 : 10;
  309. sizePercent += delta;
  310. if (sizePercent > 150) sizePercent = 150;
  311. if (sizePercent < 5) sizePercent = 5;
  312.  
  313. contentImg.style.width = sizePercent + '%';
  314. contentImg.style.maxWidth = sizePercent + '%';
  315. contentImg.style.height = sizePercent + '%';
  316. contentImg.style.maxHeight = sizePercent + '%';
  317.  
  318. contentDiv.style.width = contentImg.clientWidth;
  319. }
  320. }
  321.  
  322. function startDrag(event) {
  323. let contentDiv = document.querySelector('div.mfp-content > div.mfp-figure');
  324. if (contentDiv) {
  325. isDragging = true;
  326. startX = event.clientX;
  327. startY = event.clientY;
  328. initialX = contentDiv.offsetLeft;
  329. initialY = contentDiv.offsetTop;
  330. event.preventDefault();
  331. }
  332. }
  333.  
  334. function drag(event) {
  335. if (isDragging) {
  336. let contentImg = document.querySelector('div.mfp-content > div.mfp-figure');
  337. if (contentImg) {
  338. let dx = event.clientX - startX;
  339. let dy = event.clientY - startY;
  340. contentImg.style.left = (initialX + dx) + 'px';
  341. contentImg.style.top = (initialY + dy) + 'px';
  342. contentImg.style.position= "relative";
  343. }
  344. }
  345. }
  346.  
  347. function stopDrag(event) {
  348. isDragging = false;
  349. }
  350.  
  351. let observer = new MutationObserver(function(mutations) {
  352. mutations.forEach(function(mutation) {
  353. mutation.addedNodes.forEach(function(node) {
  354. if (document.querySelector('div.mfp-content img')) {
  355. let contentDiv = document.querySelector('div.mfp-container.mfp-image-holder.mfp-s-ready');
  356. let figureDiv = document.querySelector('div.mfp-content > div.mfp-figure > figure img');
  357. contentDiv.onwheel = adjustSize;
  358. contentDiv.onmouseup = stopDrag;
  359. figureDiv.onmousedown = startDrag;
  360. figureDiv.onmousemove = drag;
  361. figureDiv.style.cursor = "move";
  362. }
  363. });
  364. });
  365. });
  366.  
  367. observer.observe(document.body, { childList: true, subtree: true });
  368.  
  369. }
  370. // Function 4: Quick Bookmark
  371. if (settings.quick_mark) {
  372. const starSvg = `<svg class="svg-icon" aria-hidden="true" style="text-indent: 1px; transform: scale(1); width:18px; height:18px;">
  373. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
  374. <path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path></svg></svg> `;
  375. let markMap = new Map();
  376.  
  377. function handleResponse(xhr, successCallback, errorCallback) {
  378. xhr.onreadystatechange = function() {
  379. if (xhr.readyState === 4) {
  380. if (xhr.status === 200) {
  381. successCallback(xhr);
  382. } else {
  383. errorCallback(xhr);
  384. }
  385. }
  386. };
  387. }
  388.  
  389. function deleteStarMark(mark_btn, data_id) {
  390. if (markMap.has(data_id)) {
  391. const mark_id = markMap.get(data_id);
  392. var xhr = new XMLHttpRequest();
  393. xhr.open('DELETE', `/bookmarks/${mark_id}`, true);
  394. xhr.setRequestHeader('Content-Type', 'application/json');
  395. xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
  396. xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
  397.  
  398. handleResponse(xhr, (xhr) => {
  399. mark_btn.style.color = '#777';
  400. mark_btn.title = "收藏";
  401. mark_btn.onclick = () => addStarMark(mark_btn, data_id);
  402. }, (xhr) => {
  403. alert('删除失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
  404. });
  405.  
  406. xhr.send();
  407. }
  408. }
  409.  
  410. function TryParseJson(str) {
  411. try {
  412. const jsonObj = JSON.parse(str);
  413. return JSON.stringify(jsonObj, null, 1);
  414. } catch (error) {
  415. return str;
  416. }
  417. }
  418.  
  419. function addStarMark(mark_btn, data_id) {
  420. const xhr = new XMLHttpRequest();
  421. xhr.open('POST', '/bookmarks', true);
  422. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
  423. xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
  424. xhr.setRequestHeader('discourse-logged-in', ' true');
  425. xhr.setRequestHeader('discourse-present', ' true');
  426. xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
  427. const postData = `name=%E6%94%B6%E8%97%8F&auto_delete_preference=3&bookmarkable_id=${data_id}&bookmarkable_type=Post`;
  428.  
  429. handleResponse(xhr, (xhr) => {
  430. mark_btn.style.color = '#fdd459';
  431. mark_btn.title = "删除收藏";
  432. mark_btn.onclick = () => deleteStarMark(mark_btn, data_id);
  433. }, (xhr) => {
  434. alert('收藏失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
  435. });
  436.  
  437. xhr.send(postData);
  438. }
  439.  
  440. function addMarkBtn() {
  441. let articles = document.querySelectorAll("article[data-post-id]");
  442. if (articles.length <= 0) return;
  443.  
  444. articles.forEach(article => {
  445. const target = article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > div.actions");
  446. if (target && !article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > span.star-bookmark")) {
  447. const dataPostId = article.getAttribute('data-post-id');
  448. const starButton = document.createElement('span');
  449.  
  450. starButton.innerHTML = starSvg;
  451. starButton.className = "star-bookmark";
  452. starButton.style.cursor = 'pointer';
  453. starButton.style.margin = '0px 12px';
  454.  
  455. if (markMap.has(dataPostId)) {
  456. starButton.style.color = '#fdd459';
  457. starButton.title = "删除收藏";
  458. starButton.onclick = () => deleteStarMark(starButton, dataPostId);
  459. } else {
  460. starButton.style.color = '#777';
  461. starButton.title = "收藏";
  462. starButton.onclick = () => addStarMark(starButton, dataPostId);
  463. }
  464. target.after(starButton);
  465. }
  466. });
  467. }
  468.  
  469. function getStarMark() {
  470. let articles = document.querySelectorAll("article[data-post-id]");
  471. if (articles.length <= 0) return;
  472.  
  473. const currentUserElement = document.querySelector('#current-user button > img[src]');
  474.  
  475. function extractUsername(srcString) {
  476. const regex = /\/user_avatar\/linux\.do\/([^\/]+)\/\d+\//;
  477. const match = srcString.match(regex);
  478.  
  479. if (match && match[1]) {
  480. return match[1];
  481. } else {
  482. return null;
  483. }
  484. }
  485.  
  486. if(!currentUserElement) return;
  487.  
  488. const currentUsername = extractUsername(currentUserElement.getAttribute('src'));
  489.  
  490. if(!currentUsername) return;
  491.  
  492. const xhr = new XMLHttpRequest();
  493. xhr.open('GET', `/u/${currentUsername}/user-menu-bookmarks`, true);
  494. xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
  495.  
  496. handleResponse(xhr, (xhr) => {
  497. var response = JSON.parse(xhr.responseText);
  498. response.bookmarks.forEach(mark => {
  499. markMap.set(mark.bookmarkable_id.toString(), mark.id.toString());
  500. });
  501. addMarkBtn();
  502. }, (xhr) => {
  503. console.error('GET请求失败:', xhr.statusText);
  504. });
  505.  
  506. xhr.send();
  507. }
  508.  
  509. let lastUpdateMarkTime = 0;
  510. let lastUpdateButnTime = 0;
  511. function mutationCallback() {
  512. const currentTime = Date.now();
  513. if (currentTime - lastUpdateMarkTime > 9000) {
  514. setTimeout(getStarMark, 500);
  515. lastUpdateMarkTime = currentTime;
  516. }
  517. if (currentTime - lastUpdateButnTime > 1000) {
  518. setTimeout(addMarkBtn, 500);
  519. lastUpdateButnTime = currentTime;
  520. }
  521. }
  522.  
  523. const mainNode = document.querySelector("#main-outlet");
  524. if (mainNode) {
  525. const observer = new MutationObserver(mutationCallback);
  526. observer.observe(mainNode, { childList: true, subtree: true });
  527. }
  528.  
  529. getStarMark();
  530. }
  531.  
  532. // Function 5: mini article image show
  533. if (settings.image_mini) {
  534. let _H = parseInt(settings.image_mini_H);
  535. let _W = parseInt(settings.image_mini_W);// transition: max-width 0.5s ease-in-out, max-height 0.5s ease-in-out;
  536.  
  537. GM_addStyle(`
  538. article div.topic-body div.regular.contents img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {
  539. max-width : ${_W}px;
  540. max-height : ${_H}px;
  541. object-fit: contain;
  542. }`);
  543.  
  544. var imageMiniTimer = setInterval(function() {
  545. var images = document.querySelectorAll('article div.topic-body div.regular.contents img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji)');
  546. if (images.length >= 1) {
  547. for (var i = 0; i < images.length; i++) {
  548. let img = images[i];
  549. let image_src = null;
  550. let src_height = null;
  551.  
  552. let urls = img.getAttribute('srcset')
  553. if (urls) {
  554. urls = urls.match(/https:\/\/[^,\s]+/g);
  555. image_src = urls[urls.length - 1];
  556. }else{
  557. image_src = img.src;
  558. }
  559.  
  560. src_height = img.naturalHeight || img.height;
  561.  
  562. if (img.parentElement.matches('a.lightbox')) {
  563. img = img.parentElement;
  564. if (!image_src) {
  565. image_src = img.getAttribute('href');
  566. }
  567. }
  568. //console.log(image_src)
  569. img.image_src = image_src;
  570. img.src_height = src_height;
  571.  
  572. let previewDiv = null;
  573.  
  574. if (document.getElementById('hover-preview-img') == null) {
  575. previewDiv = document.createElement('div');
  576. previewDiv.id = 'hover-preview-img';
  577. previewDiv.style = 'position: fixed; z-index:999; top:-10px; max-width: 0px; max-height 0px; opacity: 0; transition: max-width 0.3s ease-in-out, max-height 0.3s ease-in-out, left 0.3s ease-in-out , opacity 0.3s ease-in-out , top 0.3s ease-in-out;'; // display:none;
  578. document.body.appendChild(previewDiv);
  579. let fullSizeImg = document.createElement('img');
  580. fullSizeImg.className = 'full-size-image';
  581. previewDiv.appendChild(fullSizeImg);
  582. } else {
  583. previewDiv = document.getElementById('hover-preview-img');
  584. }
  585.  
  586. img.addEventListener('mouseenter', function(event) {
  587. let previewDiv = document.getElementById('hover-preview-img');
  588. let fullSizeImg = previewDiv.querySelector('.full-size-image');
  589. previewDiv.style.display = 'block';
  590. previewDiv.style.background="#FFFE";
  591. previewDiv.style.boxShadow="1px 1px 5px #555";
  592. previewDiv.style.padding="0px";
  593.  
  594. previewDiv.style.left= event.clientX + 20 + 'px';
  595. previewDiv.style.maxWidth = '99vw';
  596. previewDiv.style.maxHeight = '99vh';
  597.  
  598. this.title="";
  599. fullSizeImg.src = this.image_src;
  600. fullSizeImg.style.width = '';
  601. fullSizeImg.style.height = '';
  602. fullSizeImg.style.maxWidth = '100%';
  603. fullSizeImg.style.maxHeight = '100%';
  604. previewDiv.style.top = event.clientY - this.src_height/2 + 'px';
  605. previewDiv.style.opacity = 1;
  606.  
  607. fullSizeImg.onload = function() {
  608. console.log(previewDiv.offsetTop , fullSizeImg.naturalHeight , window.innerHeight);
  609. if (previewDiv.offsetTop + fullSizeImg.naturalHeight > window.innerHeight - 5) {
  610. previewDiv.style.top = window.innerHeight - 5 - fullSizeImg.naturalHeight + 'px';
  611. }
  612. };
  613.  
  614. });
  615.  
  616. img.addEventListener('mouseleave', function() {
  617. let previewDiv = document.getElementById('hover-preview-img');
  618. previewDiv.style.top = "-10px";
  619. previewDiv.style.maxWidth = "0px";
  620. previewDiv.style.maxHeight = "0px";
  621. previewDiv.style.opacity = 0;
  622. });
  623. }
  624. }
  625. }, 1000);
  626. }
  627.  
  628. // Function 6: remove spoiler blurred
  629. if (settings.spoiler_noblur) {
  630. GM_addStyle(`
  631. .spoiler-blurred {
  632. filter: drop-shadow(0px 0px 3px #BBB)!important;
  633. }
  634. .spoiler-blurred img {
  635. filter: drop-shadow(0px 0px 3px #BBB)!important;
  636. }`);
  637. }
  638.  
  639. // Function 7: details open
  640. if (settings.details_open) {
  641. function open_detail() {
  642. let details = document.querySelectorAll("article details");
  643. details.forEach(detail => {
  644. if (detail.opened != true) {
  645. detail.open = true;
  646. detail.opened = true;
  647. }
  648. });
  649. setTimeout(open_detail, 990);
  650. }
  651. setTimeout(open_detail, 900);
  652. }
  653.  
  654. // Function 8: wider page
  655. if (settings.wider_page) {
  656. GM_addStyle(`
  657. #main-outlet-wrapper {
  658. max-width: 100%!important;
  659. }
  660. body.has-sidebar-page header.d-header > div.wrap {
  661. max-width: 100%!important;
  662. }
  663. .topic-body {
  664. width: 100%!important;
  665. }
  666. :root {
  667. --d-max-width: 100%!important;
  668. }
  669. article .topic-map.--op {
  670. max-width: 100%;
  671. }
  672. div#reply-control .reply-area {
  673. width: calc(100% - 2em);
  674. }
  675. @media screen and (min-width: 925px) {
  676. #main-outlet .container.posts {
  677. grid-template-columns: auto 120px;
  678. }
  679. }`);
  680. }
  681.  
  682. // Function 9: thin_header
  683. if (settings.thin_header) {
  684. GM_addStyle(`
  685. .d-header {
  686. height: 2.5em !important;
  687. }
  688. .d-header .extra-info-wrapper .title-wrapper {
  689. display: flex;
  690. flex-direction: row;
  691. }
  692. .d-header div.title-wrapper > h1.header-title {
  693. width: auto;
  694. font-size: large;
  695. }
  696. .d-header #site-logo {
  697. height: 2em !important;
  698. }
  699. .d-header .d-header-icons .icon img.avatar {
  700. height: 2em !important;
  701. }`);
  702. }
  703.  
  704. // Function 10: topic contents scroll
  705. if (settings.topic_scroll) {
  706. GM_addStyle(`
  707. article div.topic-body .regular.contents .cooked {
  708. max-height: 60vh;
  709. overflow-y: auto;
  710. scrollbar-width: thin;
  711. scrollbar-color: #aaaa #1111;
  712. }
  713. article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-track {
  714. background: #1111;
  715. }
  716. article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-thumb {
  717. background: #aaaa;
  718. }
  719. article div.topic-body .regular.contents .cooked ::-webkit-scrollbar-thumb:hover {
  720. background: #0008;
  721. }`);
  722. }
  723.  
  724. // Function 11: order by Created
  725. if (settings.order_created) {
  726. function orderByCreated() {
  727. const a_new = document.querySelector("ul#navigation-bar > li.new.ember-view.nav-item_new > a");
  728. if ( a_new && a_new.href.endsWith("/new")) {
  729. a_new.parentNode.title = "按最新创建排序";
  730. a_new.href = a_new.href.replace("/new","/latest?order=created");
  731. a_new.style.filter="drop-shadow(0px 0px 1px var(--quaternary))"; // #8FF8
  732. }
  733. setTimeout(orderByCreated, 990);
  734. }
  735. setTimeout(orderByCreated, 900);
  736. }
  737.  
  738. // Function 12: 显示发帖时间和最新回复时间
  739. if (settings.show_up_time) {
  740.  
  741. function getHue(date, currentDate) {
  742. const diff = Math.abs(currentDate - date);
  743. const baseday = 30 * 24 * 60 * 60 * 1000; // 30 day
  744. const diffRatio = Math.min( Math.log(diff / baseday + 1), 1);
  745. return 120 - (140 * diffRatio); // green to red
  746. }
  747.  
  748. function formatDate(date) {
  749. const year = date.getFullYear();
  750. const month = String(date.getMonth() + 1).padStart(2, '0');
  751. const day = String(date.getDate()).padStart(2, '0');
  752. const hours = String(date.getHours()).padStart(2, '0');
  753. const minutes = String(date.getMinutes()).padStart(2, '0');
  754.  
  755. return `${year} ${month} ${day} ${hours}:${minutes}`;
  756. }
  757.  
  758. function parseDate(dateStr) {
  759. let parts;
  760.  
  761. // Check if the string is in Chinese format
  762. if (dateStr.match(/(\d+)\s*年\s*(\d+)\s*月\s*(\d+)\s*日\s*(\d+):(\d+)/)) {
  763. parts = dateStr.match(/(\d+)\s*年\s*(\d+)\s*月\s*(\d+)\s*日\s*(\d+):(\d+)/);
  764. return new Date(parts[1], parts[2] - 1, parts[3], parts[4], parts[5]);
  765. }
  766.  
  767. // Check if the string is in English format
  768. if (dateStr.match(/(\w+)\s*(\d+),\s*(\d+)\s*(\d+):(\d+)\s*(am|pm)/i)) {
  769. parts = dateStr.match(/(\w+)\s*(\d+),\s*(\d+)\s*(\d+):(\d+)\s*(am|pm)/i);
  770. const monthMap = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
  771. let hour = parseInt(parts[4], 10);
  772. if (parts[6].toLowerCase() === 'pm' && hour < 12) {
  773. hour += 12;
  774. } else if (parts[6].toLowerCase() === 'am' && hour === 12) {
  775. hour = 0;
  776. }
  777. return new Date(parts[3], monthMap[parts[1]], parts[2], hour, parts[5]);
  778. }
  779.  
  780. throw new Error('Unsupported date format' + dateStr);
  781. }
  782.  
  783. GM_addStyle(`
  784. .topic-list .topic-list-data.age.activity {
  785. width: 11em;
  786. padding: 0px 5px;
  787. }
  788. .topic-list .topic-list-data.age.activity > a.post-activity{
  789. font-size: 14px;
  790. text-align: left;
  791. display: block;
  792. text-wrap: nowrap;
  793. padding: 8px 5px;
  794. }`);
  795.  
  796. function creatTimeShow() {
  797. document.querySelectorAll(".num.topic-list-data.age.activity").forEach(function (item) {
  798. const timeSpan = item.querySelector("a.post-activity")
  799. if (timeSpan.innerText.length > 10) {
  800. return;
  801. }
  802. const timeInfo = item.title;
  803. let createDateString = timeInfo.match(/创建日期:([\d 年 月 日 :]+)/) || timeInfo.match(/Created: ([\w, \d:apm ]+)/);
  804. let updateDateString = timeInfo.match(/最新:([\d 年 月 日 :]+)/) || timeInfo.match(/Latest: ([\w, \d:apm ]+)/);
  805. createDateString = (createDateString[1] ?? '').trim();
  806. const createDate = parseDate(createDateString);
  807. const currentDate = new Date();
  808. const createHue = getHue(createDate, currentDate);
  809. const formatCreateDate = formatDate(createDate);
  810. timeSpan.innerHTML = `<span style="color: hsl(${createHue}, 35%, 50%);">创建:${formatCreateDate}</span><br>`;
  811. if (updateDateString) {
  812. updateDateString = (updateDateString[1] ?? '').trim();
  813. const updateDate = parseDate(updateDateString);
  814. const updateHue = getHue(updateDate, currentDate);
  815. const formatNewDate = formatDate(updateDate);
  816. timeSpan.innerHTML += `<span style="color: hsl(${updateHue}, 35%, 50%);">最新:${formatNewDate}</span>`
  817. } else {
  818. timeSpan.innerHTML += `<span style="color:#888;">最新:暂无回复</span>`
  819. }
  820.  
  821. const pastDays = Math.abs(createDate - currentDate) / (24 * 60 * 60 * 1000);
  822. const topicTitle = item.parentNode.querySelector(".main-link")
  823. const topicUsers = item.parentNode.querySelector(".topic-list-data.posters")
  824. if ( pastDays > 30) {
  825. topicTitle.style.opacity = 0.9;
  826. topicTitle.style.filter = "grayscale(10%) sepia(10%)";
  827. if ( pastDays > 60) {
  828. topicTitle.style.opacity = 0.8;
  829. topicTitle.style.filter = "sepia(40%) brightness(85%)";
  830. if(topicUsers) topicUsers.style.filter = "sepia(40%) brightness(85%)";
  831. }
  832. if ( pastDays > 120) {
  833. topicTitle.style.filter = "sepia(90%) brightness(85%)";
  834. if(topicUsers) topicUsers.style.filter = "sepia(90%) brightness(85%)";
  835. }
  836. }
  837. })
  838. setTimeout(creatTimeShow, 990);
  839. }
  840. setTimeout(creatTimeShow, 900);
  841. }
  842.  
  843. // Function 13: 快速点击到顶部/底部
  844. if (settings.goto_top_end) {
  845. function scrollTimeline(offset) {
  846. let element = document.querySelector(".timeline-padding");
  847. if (!element) {
  848. window.scrollTo({
  849. top: 9999 * offset,
  850. behavior: "smooth"
  851. });
  852. if (window.location.href.includes("/t/topic/")) {
  853. if(offset < 1) window.location.href = window.location.href.replace(/\/t\/topic\/(\d+).*/, '/t/topic/$1')
  854. else window.location.href = window.location.href.replace(/\/t\/topic\/(\d+).*/, '/t/topic/$1/99999')
  855. }
  856. return;
  857. }
  858. const event = new MouseEvent('click', {
  859. bubbles: true,
  860. cancelable: true,
  861. clientX: element.getBoundingClientRect().left + 0,
  862. clientY: element.getBoundingClientRect().top + offset,
  863. });
  864.  
  865. element.dispatchEvent(event);
  866. }
  867.  
  868. var toTop = document.createElement("button");
  869. var toEnd = document.createElement("button");
  870. var scrollBtns = document.createElement("div");
  871.  
  872. scrollBtns.className = "goto_top_end";
  873. toTop.innerText = "⬆️顶部";
  874. toEnd.innerText = "⬇️最新";
  875.  
  876. GM_addStyle(`
  877. .goto_top_end{
  878. position: fixed; bottom: 2px; right: 65px; z-index: 1000; border:none;
  879. }
  880. .goto_top_end > button {
  881. background-color:#0005;
  882. color:#eee;
  883. border:none;
  884. padding: 10px;
  885. margin: 0px 5px;
  886. border-radius: 5px;
  887. cursor: pointer;
  888. }
  889. @media screen and (max-width: 924px) {
  890. .goto_top_end{
  891. right: 160px;
  892. }
  893. }
  894. `);
  895.  
  896. toTop.onclick = function() {
  897. scrollTimeline(0)
  898. };
  899.  
  900. toEnd.onclick = function() {
  901. scrollTimeline(500)
  902. };
  903.  
  904. document.body.appendChild(scrollBtns);
  905. scrollBtns.append(toTop, toEnd);
  906. }
  907.  
  908. // Function 14: 显示帖子楼层号
  909. if (settings.show_floor_num) {
  910. GM_addStyle(`.post-info.floor-number {color: #9CD; margin-left: 1em;}`)
  911. function showPostNumber() {
  912. const posts = document.querySelectorAll('article[id^="post_"]');
  913. posts.forEach(post => {
  914. let floorInfo = post.querySelector('.post-infos > .floor-number');
  915. if(!floorInfo) {
  916. const postId = post.id;
  917. const floorNumber = postId.split('_')[1];
  918. const postInfos = post.querySelector('.post-infos');
  919. if (postInfos) {
  920. floorInfo = document.createElement('div');
  921. floorInfo.className = 'post-info floor-number';
  922. floorInfo.textContent = `#${floorNumber}`;
  923. postInfos.append(floorInfo);
  924. }
  925. }
  926. });
  927. setTimeout(showPostNumber, 1990);
  928. }
  929. setTimeout(showPostNumber, 1900);
  930. }
  931.  
  932. // Function 15: 自动字数补丁
  933. if (settings.auto_words_patch) {
  934. const fillContent = '<div></div>\n\n';
  935. function handleTextarea(textarea) {
  936. if (textarea.dataset.handled) return;
  937. textarea.dataset.handled = 'true';
  938.  
  939. let mask = document.querySelector("span#text-input-padding-mask");
  940. if (!mask) {
  941. mask = document.createElement('span');
  942. textarea.before(mask);
  943. mask.style = "width:95%; max-width:10em; margin: -2em 8px 4px; height:2.5em; background-color: var(--secondary); z-index:1; display:none";
  944. mask.id = "text-input-padding-mask";
  945. }
  946.  
  947. let toolbar = document.querySelector('div.d-editor-button-bar[role="toolbar"]');
  948. if (toolbar) toolbar.style.zIndex = 2;
  949.  
  950. function updateContent() {
  951. const currentContent = textarea.value;
  952.  
  953. if (document.querySelector('div.characters-required.ember-view').innerText.length > 0) {
  954. if (!textarea.value.startsWith(fillContent)) {
  955. textarea.value = fillContent + currentContent;
  956. }
  957. }
  958.  
  959. if (textarea.value.startsWith(fillContent)) {
  960. textarea.style.marginTop = "-3.5em";
  961. mask.style.display = "";
  962. mask.style.borderBottom = "1px solid #fd58";
  963.  
  964. const contentWithoutFill = textarea.value.replace(fillContent, '');
  965. if (contentWithoutFill.length === 0) {
  966. textarea.value = contentWithoutFill;
  967. textarea.style = "";
  968. mask.style.display = "none";
  969. mask.style.borderBottomColor = "";
  970. }
  971. } else {
  972. textarea.style = "";
  973. mask.style.display = "none";
  974. mask.style.borderBottomColor = "";
  975. }
  976.  
  977. // 阻止光标进入 fillContent 区域
  978. textarea.addEventListener('select', preventSelectionInFillContent);
  979. textarea.addEventListener('click', preventSelectionInFillContent);
  980. textarea.addEventListener('keydown', preventCursorInFillContent);
  981. }
  982.  
  983. function preventSelectionInFillContent(e) {
  984. if (!textarea.value.startsWith(fillContent)) {
  985. return;
  986. }
  987. textarea.style.caretColor = "transparent";
  988. let start = textarea.selectionStart;
  989. let end = textarea.selectionEnd;
  990. if (start < fillContent.length) start = fillContent.length;
  991. if (end < fillContent.length) end = fillContent.length;
  992. if ( start != textarea.selectionStart || end != textarea.selectionEnd) {
  993. textarea.setSelectionRange(start, end);
  994. }
  995. textarea.style.caretColor = "";
  996. }
  997.  
  998. function preventCursorInFillContent(e) {
  999. if (!textarea.value.startsWith(fillContent)) {
  1000. return;
  1001. }
  1002. if (["ArrowUp","ArrowLeft","Home"].includes(e.key)) {
  1003. textarea.style.caretColor = "transparent";
  1004. setTimeout(function() {
  1005. let start = textarea.selectionStart;
  1006. let end = textarea.selectionEnd;
  1007. if (start < fillContent.length) start = fillContent.length;
  1008. if (end < fillContent.length) end = fillContent.length;
  1009. if ( start != textarea.selectionStart || end != textarea.selectionEnd) {
  1010. textarea.setSelectionRange(start, end);
  1011. }
  1012. textarea.style.caretColor = "";
  1013. },0);
  1014. }
  1015. }
  1016.  
  1017. updateContent();
  1018. textarea.addEventListener('input', updateContent);
  1019. }
  1020.  
  1021. function observeDOM() {
  1022. const targetNode = document.body;
  1023. const config = { childList: true, subtree: true };
  1024.  
  1025. const callback = function(mutationsList, observer) {
  1026. for(let mutation of mutationsList) {
  1027. if (mutation.type === 'childList') {
  1028. const textarea = document.querySelector('#reply-control .d-editor-container textarea.ember-text-area.ember-view.d-editor-input');
  1029. if (textarea) {
  1030. handleTextarea(textarea);
  1031. }
  1032. }
  1033. }
  1034. };
  1035.  
  1036. const observer = new MutationObserver(callback);
  1037. observer.observe(targetNode, config);
  1038. }
  1039.  
  1040. observeDOM();
  1041. }
  1042.  
  1043. // Function 16: 新窗口打开
  1044. if (settings.open_in_new) {
  1045. // Capture all click events and open links in a new tab
  1046. document.addEventListener('click', function(event) {
  1047. let anchor = event.target.closest('a');
  1048. if (anchor && anchor.href) {
  1049. console.log("A")
  1050. event.preventDefault();
  1051. window.open(anchor.href, '_blank');
  1052. }
  1053. }, true);
  1054. }
  1055.  
  1056. // Function 17: 显示回复时间
  1057. if (settings.show_floor_time) {
  1058. GM_addStyle(`
  1059. div.topic-body.clearfix > div.topic-meta-data > div.post-infos > div.post-info.post-date > a.widget-link.post-date > span.relative-date {
  1060. visibility: hidden;
  1061. }
  1062. div.topic-body.clearfix > div.topic-meta-data > div.post-infos > div.post-info.post-date > a.widget-link.post-date > span.relative-date::after {
  1063. content: attr(title);
  1064. visibility: visible;
  1065. }`);
  1066. }
  1067.  
  1068. // Function 18: 主题相关人头像显示调整
  1069. if (settings.avatar_bigger) {
  1070. GM_addStyle(`
  1071.  
  1072. .sidebar-wrapper > #d-sidebar > div.sidebar-footer-wrapper .sidebar-footer-container:before {
  1073. border-bottom: solid 1px #8888;
  1074. background:none;
  1075. }
  1076.  
  1077. div.sidebar-sections .sidebar-section ul > li.sidebar-section-link-wrapper > a.sidebar-section-link {
  1078. padding-left: 2.5em;
  1079. }
  1080.  
  1081. div#main-outlet-wrapper {
  1082. --d-sidebar-width: 15em;
  1083. }
  1084.  
  1085. .topic-list td.topic-list-data.posters {
  1086. height: auto;
  1087. padding: 0.33em;
  1088. width: 110px;
  1089. }
  1090. .topic-list td.posters.topic-list-data > a:first-child:not([style*="display: none"]) > img {
  1091. width: 48px;
  1092. height: 48px;
  1093. }
  1094. @media screen and (max-width: 850px) {
  1095. .topic-list .topic-list-data.posters a.latest > img {
  1096. width: 48px;
  1097. height: 48px;
  1098. }
  1099. .topic-list td.topic-list-data.posters {
  1100. width: 52px;
  1101. }
  1102. }
  1103. `);
  1104.  
  1105. function fristAvatarBigger() {
  1106. document.querySelectorAll(`.topic-list td.posters.topic-list-data > a:first-child > img,
  1107. .topic-list td.posters.topic-list-data > a.latest > img`).forEach(function (img) {
  1108. if (img.src.includes("/24/")) {
  1109. img.src = img.src.replace("/24/","/48/");
  1110. }
  1111. });
  1112. setTimeout(fristAvatarBigger, 1990);
  1113. }
  1114. setTimeout(fristAvatarBigger, 900);
  1115. }
  1116.  
  1117. // Function 19: 边栏显示调整
  1118. if (settings.sidebar_class) {
  1119.  
  1120. function link_wrapper_add_item(name, url, svg_id) {
  1121. let sidebar = document.querySelector("ul#sidebar-section-content-外部链接");
  1122. if(!sidebar) {
  1123. return;
  1124. }
  1125. if(sidebar.querySelector(`[data-link-name="${name}"]`)) return;
  1126.  
  1127. let add_li = document.createElement('li');
  1128. sidebar.append(add_li);
  1129. add_li.className = "sidebar-section-link-wrapper"
  1130. add_li.innerHTML = `
  1131. <a href="${url}" rel="noopener noreferrer" target="_blank" data-link-name="${name}" class="sidebar-section-link sidebar-row" title="来自L站元宇宙,站内佬友提供服务,非官方服务">
  1132. <span class="sidebar-section-link-prefix icon" style="color: #7AA;">
  1133. <svg class="fa d-icon d-icon-far-eye svg-icon prefix-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#${svg_id}"></use></svg>
  1134. </span>
  1135. <span class="sidebar-section-link-content-text">${name}</span>
  1136. </a>`;
  1137. }
  1138.  
  1139. link_wrapper_add_item("WIKI", "https://wiki.linux.do/", "fab-wikipedia-w");
  1140. link_wrapper_add_item("导航", "https://nav.linux.do/", "rocket");
  1141.  
  1142. function add_sel_btns() {
  1143. let btn_menu_save = document.querySelector("#ember3 > div.modal-container > div.modal.d-modal.sidebar__edit-navigation-menu__modal.-large.sidebar__edit-navigation-menu__categories-modal > div > div.d-modal__footer > div.sidebar__edit-navigation-menu__footer > button.btn.btn-text.sidebar__edit-navigation-menu__save-button")
  1144.  
  1145. if (!btn_menu_save) {
  1146. setTimeout(add_sel_btns, 1200);
  1147. return;
  1148. }
  1149.  
  1150. let btns = document.querySelectorAll("#ember3 > div.modal-container > div.modal.d-modal.sidebar__edit-navigation-menu__modal.-large.sidebar__edit-navigation-menu__categories-modal > div > div.d-modal__footer > div.sidebar__edit-navigation-menu__footer > button.btn")
  1151. if (btns.length > 2) {
  1152. setTimeout(add_sel_btns, 2200);
  1153. return;
  1154. }
  1155.  
  1156. let btn_sel_all = document.createElement("button");
  1157. btn_sel_all.innerText = "全选";
  1158. btn_sel_all.className="btn btn-text btn-primary";
  1159. btn_sel_all.style="margin-left:8px;"
  1160. btn_sel_all.onclick = (function () {
  1161. document.querySelectorAll("[id^=sidebar-categories-form__input--").forEach(function(point) {
  1162. if(point.checked!=true){point.click();}
  1163. })
  1164. })
  1165.  
  1166. let btn_sel_none = document.createElement("button");
  1167. btn_sel_none.innerText = "全不选";
  1168. btn_sel_none.className="btn btn-text btn-primary";
  1169. btn_sel_none.style="margin-left:8px;"
  1170. btn_sel_none.onclick = (function () {
  1171. document.querySelectorAll("[id^=sidebar-categories-form__input--").forEach(function(point) {
  1172. if(point.checked==true){point.click();}
  1173. })
  1174. })
  1175.  
  1176. let btn_sel_not = document.createElement("button");
  1177. btn_sel_not.innerText = "反选";
  1178. btn_sel_not.className="btn btn-text btn-primary";
  1179. btn_sel_not.style="margin-left:8px;"
  1180. btn_sel_not.onclick = (function () {
  1181. document.querySelectorAll("[id^=sidebar-categories-form__input--").forEach(function(point) {
  1182. point.click();
  1183. })
  1184. })
  1185.  
  1186. if(btn_menu_save){
  1187. btn_menu_save.after(btn_sel_all);
  1188. btn_menu_save.after(btn_sel_none);
  1189. btn_menu_save.after(btn_sel_not);
  1190. }
  1191.  
  1192. setTimeout(add_sel_btns, 3200);
  1193. }
  1194.  
  1195. add_sel_btns();
  1196.  
  1197. function path_depth() {
  1198. const links = document.querySelectorAll('#main-outlet-wrapper #d-sidebar #sidebar-section-content-categories > li.sidebar-section-link-wrapper > a[href]');
  1199. links.forEach(link => {
  1200. let href = link.getAttribute("href");
  1201. //console.log(href);
  1202. let path_depth= href.split('/').length - 1;
  1203. if (path_depth <= 3) {
  1204. link.style.paddingLeft = '1.5em';
  1205.  
  1206. if ((path_depth == 3) && (link.nextSibling.tagName != "SPAN") ) {
  1207.  
  1208. let hrefx = href.replace(/\d+$/,'');
  1209. const subTopic = document.querySelectorAll(`#sidebar-section-content-categories > li.sidebar-section-link-wrapper > a[href^="${hrefx}"]:not([href="${href}"]`);
  1210.  
  1211. if(subTopic.length > 0) {
  1212. let btn_ls = document.createElement("span");
  1213. btn_ls.innerText = '-';
  1214.  
  1215. btn_ls.onclick = (function () {
  1216. if( btn_ls.innerText == '-') {
  1217. subTopic.forEach(function(item) {
  1218. item.parentElement.style.fontSize = "0px";
  1219. item.parentElement.style.paddingLeft = "100%";
  1220. setTimeout(() => {
  1221. item.parentElement.style.height = '0px';
  1222. item.parentElement.style.overflow = 'hidden';
  1223. }, 300);
  1224. });
  1225. btn_ls.innerText = '+'
  1226. } else {
  1227. subTopic.forEach(function(item) {
  1228. item.parentElement.style="";
  1229. });
  1230. btn_ls.innerText = '-'
  1231. }
  1232. })
  1233. link.after(btn_ls);
  1234. }
  1235. }
  1236. }
  1237. });
  1238.  
  1239. let categories = document.querySelector("#sidebar-section-content-categories");
  1240. let categories_ctrl = document.querySelector("#categories_ctrl");
  1241. if (categories && !categories_ctrl) {
  1242. var newDiv = document.createElement("div");
  1243. newDiv.id = 'categories_ctrl'
  1244. var button1 = document.createElement("button");
  1245. button1.innerHTML = "展开子分类";
  1246. button1.onclick = function() {
  1247. links.forEach(link => {
  1248. let href = link.getAttribute("href");
  1249. let path_depth= href.split('/').length - 1;
  1250. if (path_depth <= 3) {
  1251. if ((path_depth == 3) && (link.nextSibling.tagName == "SPAN") ) {
  1252. link.nextSibling.innerText = '-';
  1253. let hrefx = href.replace(/\d+$/,'');
  1254. const subTopic = document.querySelectorAll(`#sidebar-section-content-categories > li.sidebar-section-link-wrapper > a[href^="${hrefx}"]:not([href="${href}"]`);
  1255. subTopic.forEach(function(item) {
  1256. item.parentElement.style="";
  1257. });
  1258. }
  1259. }
  1260. });
  1261. };
  1262. var button2 = document.createElement("button");
  1263. button2.innerHTML = "折叠子分类";
  1264. button2.onclick = function() {
  1265. links.forEach(link => {
  1266. let href = link.getAttribute("href");
  1267. let path_depth= href.split('/').length - 1;
  1268. if (path_depth <= 3) {
  1269. if ((path_depth == 3) && (link.nextSibling.tagName == "SPAN") ) {
  1270. link.nextSibling.innerText = '+';
  1271. let hrefx = href.replace(/\d+$/,'');
  1272. const subTopic = document.querySelectorAll(`#sidebar-section-content-categories > li.sidebar-section-link-wrapper > a[href^="${hrefx}"]:not([href="${href}"]`);
  1273. subTopic.forEach(function(item) {
  1274. item.parentElement.style.fontSize = "0px";
  1275. item.parentElement.style.paddingLeft = "100%";
  1276. setTimeout(() => {
  1277. item.parentElement.style.height = '0px';
  1278. item.parentElement.style.overflow = 'hidden';
  1279. }, 300);
  1280. });
  1281. }
  1282. }
  1283. });
  1284. };
  1285. newDiv.appendChild(button1);
  1286. newDiv.appendChild(button2);
  1287. categories.insertBefore(newDiv, categories.firstChild);
  1288. }
  1289.  
  1290.  
  1291. setTimeout(path_depth, 2000);
  1292. }
  1293. path_depth();
  1294.  
  1295.  
  1296. GM_addStyle(`
  1297. #categories_ctrl {
  1298. margin-left: auto;
  1299. margin-right: 0px;
  1300. padding: 0px;
  1301. width: fit-content;
  1302. font-size: 12px;
  1303. }
  1304. #categories_ctrl > button {
  1305. background-color: transparent;
  1306. padding: 3px;
  1307. border: 1px solid #8888;
  1308. margin-left: 5px;
  1309. }
  1310. #categories_ctrl > button:hover {
  1311. background-color: #8888;
  1312. }
  1313.  
  1314. #main-outlet-wrapper #d-sidebar #sidebar-section-content-categories > li.sidebar-section-link-wrapper > span:hover {
  1315. background-color: #8888;
  1316. }
  1317.  
  1318. #main-outlet-wrapper #d-sidebar #sidebar-section-content-categories > li.sidebar-section-link-wrapper > span {
  1319. padding: 3px;
  1320. width: 16px;
  1321. border: 1px solid #8888;
  1322. text-align: center;
  1323. line-height: 16px;
  1324. font-size: 20px;
  1325. cursor: pointer;
  1326. }
  1327.  
  1328. #main-outlet-wrapper #d-sidebar #sidebar-section-content-categories > li.sidebar-section-link-wrapper {
  1329. transition: all 0.2s ease-out;
  1330. }
  1331.  
  1332. `);
  1333.  
  1334. }
  1335.  
  1336. // Function 20: 小黄点/小红点隐藏
  1337. if (settings.red_dot_hidden) {
  1338. GM_addStyle(`
  1339. .icon.unread {
  1340. visibility: hidden;
  1341. }
  1342. .chat-channel-unread-indicator {
  1343. visibility: hidden;
  1344. }
  1345. .badge.badge-notification.new-topic {
  1346. visibility: hidden;
  1347. }
  1348. .badge.badge-notification.unread-posts {
  1349. visibility: hidden;
  1350. }
  1351.  
  1352. `);
  1353. }
  1354.  
  1355. })();
  1356.