豆瓣小组功能增强

豆瓣小组展示功能增强:高亮包含指定关键字的帖子;隐藏包含指定关键字的帖子;去除标题省略号,展示全部文本;新标签页打开帖子;展示是否是楼主的标识;展示楼层号;淡化已读帖子标题;增加帖子内内容跳转; 去广告功能

  1. // ==UserScript==
  2. // @name 豆瓣小组功能增强
  3. // @version 0.2.1.0
  4. // @license MIT
  5. // @namespace https://tcatche.github.io/
  6. // @description 豆瓣小组展示功能增强:高亮包含指定关键字的帖子;隐藏包含指定关键字的帖子;去除标题省略号,展示全部文本;新标签页打开帖子;展示是否是楼主的标识;展示楼层号;淡化已读帖子标题;增加帖子内内容跳转; 去广告功能
  7. // @author tcatche
  8. // @match https://www.douban.com/group/*
  9. // @homepageURL https://github.com/tcatche/douban-group-enhance
  10. // @supportURL https://github.com/tcatche/douban-group-enhance/issues
  11. // @grant none
  12. // ==/UserScript==
  13. (function() {
  14. const utils = {
  15. // save user config
  16. saveConfig: config => {
  17. const configString = JSON.stringify(config);
  18. localStorage.setItem('douban_group_enhance_config', configString);
  19. },
  20. // load user config
  21. getConfig: () => {
  22. const configString = localStorage.getItem('douban_group_enhance_config');
  23. const oldConfigString = localStorage.getItem('douban_group_filter_config');
  24. try {
  25. const config = JSON.parse(configString || oldConfigString);
  26. return config;
  27. } catch (e) {
  28. return {};
  29. }
  30. },
  31. bindedEles: [],
  32. bindClick: function(selector, callback) {
  33. this.bindedEles.push(selector);
  34. $(selector).click(callback);
  35. },
  36. unbindClick: (selector) => {
  37. $(selector).unbind();
  38. },
  39. unbindAllClick: function() {
  40. this.bindedEles.forEach(selector => {
  41. $(selector).click(callback);
  42. })
  43. }
  44. }
  45. const createEnhancer = () => {
  46. // run user filters
  47. const runFilter = (config, self) => {
  48. const title = self.attr('title') || '';
  49. const isInInclude = title => (config.include || []).find(keyword => title.indexOf(keyword) >= 0);
  50. const isInDeclude = title => (config.declude || []).find(keyword => title.indexOf(keyword) >= 0);
  51. const isTitleInInclude = isInInclude(title);
  52. const isTitleInDeclude = isInDeclude(title);
  53. if (isTitleInInclude && !isTitleInDeclude) {
  54. self.addClass('douban_group_enhance_highlight');
  55. }
  56. if (isInDeclude(title)) {
  57. self.parents('tr').hide();
  58. }
  59. }
  60. // open in new tab
  61. const runOpenInNewTab = (config, self) => {
  62. if (config.openInNewTab) {
  63. self.attr('target', '_blank');
  64. }
  65. }
  66. // show full title without cliped!
  67. const runShowFullTitle = (config, self) => {
  68. if (config.showFullTitle) {
  69. const title = self.attr('title') || self.text();
  70. self.text(title);
  71. }
  72. }
  73.  
  74. // run fade visited topic
  75. const runFadeVisitedTitle = config => {
  76. if (config.fadeVisited) {
  77. if ($('#fadeVisitedStyle').length === 0) {
  78. $('body').append(`
  79. <style id="fadeVisitedStyle" class="douban_group_added">
  80. .topics .td-subject a:visited,
  81. .title a:visited {
  82. color: #ddd
  83. }
  84. .douban_group_enhance_highlight:visited{
  85. color: #ddd;
  86. background: #ccc;
  87. }
  88. </style>
  89. `);
  90. }
  91. } else {
  92. $('#fadeVisitedStyle').remove();
  93. }
  94. }
  95. // show reply number
  96. const runShowReplyNumber = (options, self, index) => {
  97. if (options.config.showReplyNumber) {
  98. const replyHead = self.find('h4')[0];
  99. const isInserted = $(replyHead).find('.douban_group_enhance_replay_number').length > 0;
  100. if (!isInserted) {
  101. const start = +(options.params.start || 0);
  102. const replayNumber = start + 1 + index;
  103. $(replyHead).append(`<span class="douban_group_enhance_replay_tag douban_group_enhance_replay_number douban_group_added">${replayNumber}楼</span>`);
  104. }
  105. } else {
  106. $('.douban_group_enhance_replay_number').remove();
  107. }
  108. }
  109. // show if is topic owner
  110. const runShowOwnerTag = (options, self) => {
  111. if (options.config.showOwnerTag) {
  112. const replyHead = self.find('h4')[0];
  113. const isInserted = $(replyHead).find('.douban_group_enhance_owner_tag').length > 0;
  114. if (!isInserted) {
  115. const replyName = self.find('h4 a').text().trim();
  116. if (replyName === options.topicUser) {
  117. $(replyHead).append('<span class="douban_group_enhance_replay_tag douban_group_enhance_owner_tag douban_group_added">楼主</span>');
  118. }
  119. }
  120. } else {
  121. $('.douban_group_enhance_owner_tag').remove();
  122. }
  123. }
  124. // add jump to top, comments and pager button
  125. const runAddJumptoButton = options => {
  126. if (options.config.jumpTo) {
  127. const isAdded = $('#douban_group_enhance_jump').length > 0;
  128. if (!isAdded) {
  129. $(document.body).append(`
  130. <div id="douban_group_enhance_jump" class="douban_group_enhance_jump douban_group_added">
  131. 跳转到:
  132. <span class="douban_group_enhance_jump_target douban_group_enhance_jump_target_title">标题</span>/
  133. <span class="douban_group_enhance_jump_target douban_group_enhance_jump_target_comments">评论</span>/
  134. <span class="douban_group_enhance_jump_target douban_group_enhance_jump_target_end">页尾</span>
  135. </div>
  136. `);
  137. setTimeout(() => {
  138. utils.bindClick('.douban_group_enhance_jump_target_title', e => {
  139. $('h1')[0].scrollIntoView({behavior: 'smooth'});
  140. });
  141. utils.bindClick('.douban_group_enhance_jump_target_comments', e => {
  142. $('.topic-reply ')[0].scrollIntoView({behavior: 'smooth'});
  143. });
  144. utils.bindClick('.douban_group_enhance_jump_target_end', e => {
  145. $('#footer')[0].scrollIntoView({behavior: 'smooth'});
  146. });
  147. }, 0)
  148. }
  149. } else {
  150. $('.douban_group_enhance_jump').remove();
  151. }
  152. }
  153. // run remove google ads
  154. const runRemoveAd = options => {
  155. if (options.config.removeAd) {
  156. setTimeout(function() {
  157. $('[ad-status]').remove()
  158. })
  159. }
  160. }
  161.  
  162. const runEnhancer = config => {
  163. const isTopicDetailPage = location.pathname.indexOf('/group/topic/') >= 0;
  164. const search = location.search ? location.search.substr(1) : '';
  165. const params = {};
  166. search.split('&').filter(v => !!v).map(item => {
  167. const items = item.split('=');
  168. if (items.length >= 1) {
  169. params[items[0]] = items[1];
  170. }
  171. });
  172. const global = {
  173. config: config,
  174. params: params,
  175. };
  176.  
  177. runRemoveAd(global);
  178.  
  179. if (isTopicDetailPage) {
  180. // 帖子内容
  181. $('#comments li').each(function(index) {
  182. global.topicUser = $('.topic-doc .from > a').text().trim();
  183. const $this = $(this);
  184. runShowReplyNumber(global, $this, index);
  185. runShowOwnerTag(global, $this);
  186. });
  187. runAddJumptoButton(global);
  188. } else {
  189. // 帖子列表
  190. $('.topics .td-subject a, .title a').each(function() {
  191. const $this = $(this);
  192. runFilter(config, $this);
  193. runOpenInNewTab(config, $this);
  194. runShowFullTitle(config, $this);
  195. });
  196. runFadeVisitedTitle(config);
  197. }
  198. }
  199. // init form elements
  200. const initDom = () => {
  201. // init config dom
  202. let configDivHtml = `
  203. <div id="douban_group_enhance_container" class="douban_group_enhance douban_group_added">
  204. <div class="douban_group_enhance_mask"></div>
  205. <div class="douban_group_enhance_inner">
  206. <div class="douban_group_enhance_inner_content">
  207. <h1>小组优化设置</h1>
  208. <h2>通用设置</h2>
  209. <div class="douban_group_enhance_config_block">
  210. <input type="checkbox" id="removeAd" value="1">
  211. 勾选则去广告
  212. </div>
  213. <h2>帖子列表页优化</h2>
  214. <div class="douban_group_enhance_config_block">请填入要高亮的关键字,多个关键字用空格隔开:</div>
  215. <textarea placeholder="请填入要高亮的关键字,多个关键字用空格隔开"></textarea>
  216. <br />
  217. <div class="douban_group_enhance_config_block">请填入要排除的关键字,多个关键字用空格隔开:</div>
  218. <textarea placeholder="请填入要排除的关键字,多个关键字用空格隔开 "></textarea>
  219. <div class="douban_group_enhance_config_block">
  220. <input type="checkbox" id="openInNewTab" value="1">
  221. 勾选则使用新标签打开帖子
  222. </div>
  223. <div class="douban_group_enhance_config_block">
  224. <input type="checkbox" id="showFullTitle" value="1">
  225. 勾选则去除标题省略号,显示完整标题
  226. </div>
  227. <div class="douban_group_enhance_config_block">
  228. <input type="checkbox" id="fadeVisited" value="1">
  229. 勾选则淡化已经访问过的帖子标题(无痕/隐私模式下不生效)
  230. </div>
  231.  
  232. <h2>帖子主题页优化</h2>
  233. <div class="douban_group_enhance_config_block">
  234. <input type="checkbox" id="showReplyNumber" value="1">
  235. 勾选则显示帖子里回复的楼层号
  236. </div>
  237. <div class="douban_group_enhance_config_block">
  238. <input type="checkbox" id="showOwnerTag" value="1">
  239. 勾选则为楼主添加“楼主”的标签
  240. </div>
  241. <div class="douban_group_enhance_config_block">
  242. <input type="checkbox" id="jumpTo" value="1">
  243. 勾选则添加跳转到标题、评论、页码位置的按钮(在屏幕左下角)
  244. </div>
  245. <p class="douban_group_enhance_buttons">
  246. <button id="douban_group_enhance_sure" class="douban_group_enhance_button">确定</button>
  247. <button id="douban_group_enhance_cancel" class="douban_group_enhance_button" >取消</button>
  248. </p>
  249. </div>
  250. </div>
  251. </textarea>
  252. `;
  253. let styleHtml = `
  254. <style id="douban_group_enhance_style" class="douban_group_added">
  255. .douban_group_enhance_config {
  256. color: #ca6445;
  257. padding: 5px 20px;
  258. font-size: 13px;
  259. background: #fae9da;
  260. font-weight: normal;
  261. cursor: pointer;
  262. }
  263. .douban_group_enhance {
  264. width: 100vw;
  265. height: 100vh;
  266. position: absolute;
  267. top: 0;
  268. left: 0;
  269. display:none;
  270. }
  271. .douban_group_enhance_mask {
  272. position: absolute;
  273. background: rgba(0,0,0,.6);
  274. width: 100%;
  275. height: 100%;
  276. z-index: 99;
  277. }
  278. .douban_group_enhance_inner {
  279. width: 500px;
  280. text-align: center;
  281. margin: auto;
  282. top: 100px;
  283. position: relative;
  284. background: #fff;
  285. padding: 30px;
  286. height: 300px;
  287. overflow: auto;
  288. z-index: 100;
  289. }
  290. .douban_group_enhance_config_block {
  291. margin-top: 5px;
  292. }
  293. .douban_group_enhance_inner_content {
  294. text-align: left;
  295. }
  296. .douban_group_enhance_inner_content h1 {
  297. padding: 0;
  298. }
  299. .douban_group_enhance_inner_content h2 {
  300. color: #037b82;
  301. margin-top: 20px;
  302. }
  303. .douban_group_enhance_inner textarea {
  304. width: 100%;
  305. height: 60px;
  306. resize: auto;
  307. resize: vertical;
  308. min-height: 50px;
  309. padding: 10px;
  310. }
  311. .douban_group_enhance_inner textarea:focus {
  312. border: 1px solid #072;
  313. box-shadow: 0px 0px 1px 0px #072;
  314. }
  315. .douban_group_enhance_buttons {
  316. float: right;
  317. }
  318. a.douban_group_enhance_highlight {
  319. background: #037b82;
  320. color: #fff;
  321. }
  322. .douban_group_enhance_replay_tag {
  323. float: right;
  324. color: #666;
  325. padding: 0 5px;
  326. }
  327. .douban_group_enhance_button {
  328. padding: 5px 20px;
  329. font-size: 13px;
  330. border: 1px solid #037b82;
  331. color: #037b82;
  332. background-color: #f0f6f3;
  333. font-weight: normal;
  334. cursor: pointer;
  335. }
  336. .douban_group_enhance_button:hover {
  337. background-color: #037b82;
  338. color: #fff;
  339. }
  340. .douban_group_enhance_jump {
  341. position: fixed;
  342. bottom: 10px;
  343. left: 10px;
  344. background: #f0f6f3;
  345. border-radius: 2px;
  346. padding: 6px;
  347. }
  348. .douban_group_enhance_jump_target {
  349. cursor: pointer;
  350. color: #037b82;
  351. padding-right: 5px;
  352. }
  353. .douban_group_enhance_jump_target:hover {
  354. font-weight: bold;
  355. }
  356. </style>
  357. `;
  358. $(document.body).append(configDivHtml);
  359. $(document.body).append(styleHtml);
  360. // init config btn
  361. const insertPos = $('#db-global-nav .top-nav-doubanapp');
  362. if (insertPos && insertPos[0]) {
  363. $(insertPos[0]).after('<div id="douban_group_enhance_config" class="top-nav-doubanapp douban_group_added"><span class="douban_group_enhance_button">小组增强插件设置</span></div>');
  364. }
  365. }
  366. // init dom events
  367. const initDomEvents = () => {
  368. const $contain = $('#douban_group_enhance_container');
  369. const $body = $(document.body);
  370. // bind events
  371. utils.bindClick('#douban_group_enhance_config', e => {
  372. $contain.show();
  373. $body.css('overflow', 'hidden');
  374. });
  375. utils.bindClick('#douban_group_enhance_cancel', e => {
  376. $contain.hide();
  377. $body.css('overflow', 'initial');
  378. });
  379. utils.bindClick('.douban_group_enhance_mask', e => {
  380. $contain.hide();
  381. $body.css('overflow', 'initial');
  382. });
  383. utils.bindClick('#douban_group_enhance_sure', e => {
  384. const config = {
  385. include: $('#douban_group_enhance_container textarea')[0].value.split(' ').filter(v => !!v),
  386. declude: $('#douban_group_enhance_container textarea')[1].value.split(' ').filter(v => !!v),
  387. openInNewTab: $('#openInNewTab')[0].checked,
  388. showFullTitle: $('#showFullTitle')[0].checked,
  389. showReplyNumber: $('#showReplyNumber')[0].checked,
  390. showOwnerTag: $('#showOwnerTag')[0].checked,
  391. fadeVisited: $('#fadeVisited')[0].checked,
  392. jumpTo: $('#jumpTo')[0].checked,
  393. removeAd: $('#removeAd')[0].checked,
  394. }
  395. utils.saveConfig(config);
  396. runEnhancer(config);
  397. $contain.hide();
  398. $body.css('overflow', 'initial');
  399. });
  400. }
  401. // init form values
  402. const initDomValue = config => {
  403. $('#douban_group_enhance_container textarea')[0].value = (config.include || []).join(' ');
  404. $('#douban_group_enhance_container textarea')[1].value = (config.declude || []).join(' ');
  405. $('#openInNewTab')[0].checked = config.openInNewTab;
  406. $('#showFullTitle')[0].checked = config.showFullTitle;
  407. $('#showReplyNumber')[0].checked = config.showReplyNumber;
  408. $('#showOwnerTag')[0].checked = config.showOwnerTag;
  409. $('#fadeVisited')[0].checked = config.fadeVisited;
  410. $('#jumpTo')[0].checked = config.jumpTo;
  411. $('#removeAd')[0].checked = config.removeAd;
  412. }
  413. const init = () => {
  414. const config = utils.getConfig() || {};
  415. initDom();
  416. initDomValue(config);
  417. initDomEvents();
  418. runEnhancer(config);
  419. }
  420. const destory = () => {
  421. // remove dom events
  422. utils.unbindAllClick();
  423. // remove all added elements
  424. $('#.douban_group_added').remove();
  425. }
  426. return {
  427. init,
  428. destory,
  429. _version: '0.2.1.0'
  430. }
  431. }
  432.  
  433. // init
  434. if (window.doubanEnhancer) {
  435. const enhancer = createEnhancer();
  436. if (!doubanEnhancer._version) {
  437. doubanEnhancer._version = '0'
  438. }
  439. if (window.doubanEnhancer._version < enhancer._version) {
  440. if (doubanEnhancer.destory) {
  441. doubanEnhancer.destory();
  442. }
  443. window.doubanEnhancer = enhancer;
  444. doubanEnhancer.init();
  445. }
  446. } else {
  447. window.doubanEnhancer = createEnhancer();
  448. doubanEnhancer.init();
  449. }
  450. })();