browndust2.com news viewer

custom news viewer for sucking browndust2.com

目前为 2024-10-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name browndust2.com news viewer
  3. // @namespace Violentmonkey Scripts
  4. // @match https://www.browndust2.com/robots.txt
  5. // @grant none
  6. // @version 1.3.0
  7. // @author Rplus
  8. // @description custom news viewer for sucking browndust2.com
  9. // @license WTFPL
  10. // ==/UserScript==
  11.  
  12. document.head.innerHTML = `
  13. <link rel="icon" type="image/png" sizes="16x16" href="/img/seo/favicon.png">
  14. `;
  15.  
  16. document.body.innerHTML = `
  17. <form id="filterform">
  18. Filter
  19. <input type="search" name="q" tabindex="1" id="searchinput">
  20. <style id="filter_style"></style>
  21. </form>
  22.  
  23. <div class="list" id="list" data-query=""></div>
  24. <hr>
  25. <label class="showall-label">
  26. <input type="checkbox" class="showall" >
  27. show all list
  28. </label>
  29. <style>
  30. *, *::before, *::after {
  31. box-sizing: border-box;
  32. }
  33. body {
  34. max-width: 1200px;
  35. margin: 0 auto;
  36. background-color: #e5cc9c;
  37. }
  38.  
  39. img {
  40. max-width: 100%;
  41. }
  42.  
  43. h2 {
  44. display: inline;
  45. position: relative;
  46. font-size: inherit;
  47. margin: 0;
  48.  
  49. & span {
  50. font-weight: 400;
  51. font-size: smaller;
  52. vertical-align: middle;
  53. opacity: .5;
  54. }
  55. }
  56. @media (max-width:750px) {
  57. details summary {
  58. text-indent: -1.1em;
  59. padding-left: 1.5em;
  60.  
  61. &::marker {
  62. font-size: smaller;
  63. }
  64. }
  65. h2 span {
  66. position: absolute;
  67. top: -15px;
  68. left: 1.5em;
  69. font-size: 12px;
  70. opacity: .3;
  71. }
  72. }
  73.  
  74. .ctx {
  75. white-space: pre-wrap;
  76. background-color: #fff9;
  77. padding: 1em;
  78.  
  79. & [style*="background-color"],
  80. & [style*="font-size"],
  81. & [style*="font-family"] {
  82. font-size: inherit !important;
  83. font-family: inherit !important;
  84. background-color: unset !important;
  85. }
  86. }
  87.  
  88. .list {
  89. list-style: none;
  90. margin: 2em 0;
  91. padding-left: 50px;
  92. }
  93.  
  94. summary {
  95. position: relative;
  96. top: 0;
  97. background-color: #dfb991;
  98. padding: 1em 1em .75em;
  99. min-height: 50px;
  100. cursor: pointer;
  101.  
  102. &::before {
  103. content: '';
  104. position: absolute;
  105. inset: 0;
  106. background-color: #fff1;
  107. pointer-events: none;
  108. opacity: 0;
  109. transition: opacity .1s;
  110. }
  111.  
  112. :target & {
  113. box-shadow: inset 0 -.5em #0003;
  114. }
  115.  
  116. &:hover::before {
  117. opacity: 1;
  118. }
  119.  
  120. & > img {
  121. position: absolute;
  122. top: 0;
  123. right: 100%;
  124. width: 50px;
  125. height: 50px;
  126. }
  127. }
  128.  
  129. details {
  130. margin-block-start: 1em;
  131.  
  132. &[open] summary {
  133. position: sticky;
  134. background-color: #ceac71;
  135. box-shadow: inset 0 -.5em #0003;
  136. }
  137. }
  138.  
  139. #filterform {
  140. position: fixed;
  141. top: 0;
  142. left: 0;
  143. transition: opacity .2s;
  144. opacity: .1;
  145.  
  146. &:hover,
  147. &:focus-within {
  148. opacity: .75;
  149. }
  150. }
  151.  
  152. body:not(:has(.showall:checked))
  153. .list[data-query=""]
  154. details:nth-child(n + 20) {
  155. display: none;
  156. }
  157.  
  158. .showall-label {
  159. position: sticky;
  160. bottom: 0;
  161. display: block;
  162. width: fit-content;
  163. margin: 0 1em 0 auto;
  164. padding: .25em 1em .25em .5em;
  165. background-color: #0002;
  166. border-radius: 1em 1em 0 0;
  167. cursor: pointer;
  168. }
  169. </style>
  170. `;
  171.  
  172. let data = [];
  173. let news_map = new Map();
  174. let query_arr = [];
  175. let id_arr = [];
  176.  
  177. function render(id = 34) {
  178. list.innerHTML = data.map(i => {
  179. let info = i.attributes;
  180. // let ctx = info.NewContent || info.content;
  181. let time = format_time(info.publishedAt);
  182. return `
  183. <details name="item" data-id="${i.id}" id="news-${i.id}">
  184. <summary>
  185. <img src="https://www.browndust2.com/img/newsDetail/tag-${info.tag}.png" width="36" height="36" alt="${info.tag}" title="#${info.tag}">
  186. <h2>
  187. <span>
  188. #${i.id} -
  189. <time datetime="${info.publishedAt}" title="${info.publishedAt}">${time}</time>
  190. </span>
  191. ${info.subject}
  192. </h2>
  193. </summary>
  194. <article class="ctx"></article>
  195. </details>
  196. `;
  197. }).join('');
  198.  
  199. list.querySelectorAll('details').forEach(d => {
  200. d.addEventListener('toggle', show);
  201. })
  202.  
  203. if (id) {
  204. auto_show(id);
  205. }
  206. }
  207.  
  208. function auto_show(id) {
  209. let target = list.querySelector(`details[data-id="${id}"]`);
  210. if (target) {
  211. target.open = true;
  212. show({ target, });
  213. }
  214. }
  215.  
  216. function show({ target, }) {
  217. if (!target.open) {
  218. return;
  219. }
  220.  
  221. let id = +target.dataset.id;
  222. let ctx = target.querySelector(':scope > article.ctx');
  223. location.hash = `news-${id}`;
  224. if (!ctx) {
  225. return;
  226. }
  227.  
  228. if (ctx.dataset?.init !== '1') {
  229. ctx.dataset.init = '1';
  230.  
  231. let info = news_map.get(id)?.attributes;
  232. let ori_link = `<a href="https://www.browndust2.com/zh-tw/news/view?id=${id}" target="_bd2news" title="official link">#</a>`;
  233. if (!info) {
  234. ctx.innerHTML = ori_link;
  235. return;
  236. }
  237.  
  238. let content = (info.content || info.NewContent);
  239. content = content.replace(/\<img\s/g, '<img loading="lazy" ');
  240. ctx.innerHTML = content + ori_link;
  241. }
  242. }
  243.  
  244. function format_time(time) {
  245. let _time = time ? new Date(time) : new Date();
  246. return _time.toLocaleString('zh-TW', {
  247. weekday: 'narrow',
  248. year: 'numeric',
  249. month: '2-digit',
  250. day: '2-digit',
  251. });
  252. }
  253.  
  254. function query_kwd() {
  255. // console.time('query');
  256. let value = searchinput.value.trim();
  257. // console.log('query', value);
  258. if (!value) {
  259. filter_style.textContent = '';
  260. list.dataset.query = '';
  261. return;
  262. }
  263.  
  264. let matched_ids = query_arr.map((i, index) => {
  265. // if (!i.includes(value)) {
  266. // if (i.indexOf(value) === -1) {
  267. let regex = new RegExp(value, 'i');
  268. return regex.test(i) ? id_arr[index] : null;
  269. })
  270. .filter(Boolean);
  271.  
  272. if (matched_ids.length) {
  273. list.dataset.query = value;
  274. } else {
  275. list.dataset.query = '';
  276. }
  277.  
  278. let selectors = matched_ids.map(i => `details[data-id="${i}"]`).join();
  279. filter_style.textContent = `
  280. details {display:none;}
  281. ${selectors} { display: block; }
  282. `;
  283. // console.timeEnd('query');
  284. }
  285.  
  286. function debounce(func, wait, immediate) {
  287. var timeout;
  288. return function() {
  289. var context = this, args = arguments;
  290. clearTimeout(timeout);
  291. if (immediate && !timeout) func.apply(context, args);
  292. timeout = setTimeout(function() {
  293. timeout = null;
  294. if (!immediate) func.apply(context, args);
  295. }, wait);
  296. };
  297. }
  298.  
  299. let data_url = window.test_data_url || 'https://www.browndust2.com/api/newsData_tw.json';
  300. fetch(data_url)
  301. .then(r => r.json())
  302. .then(d => {
  303. data = d.data.reverse();
  304. tags = [...new Set(data.map(i => {
  305. let info = i.attributes;
  306. news_map.set(i.id, i);
  307. id_arr.push(i.id);
  308. query_arr.push([
  309. i.id,
  310. info.content,
  311. info.NewContent,
  312. `#${info.tag}`,
  313. info.subject,
  314. ].join());
  315. return i.attributes.tag;
  316. }))];
  317.  
  318. // console.log(data[900], tags);
  319. let id = new URL(location.href)?.searchParams?.get('id') || data[data.length - 1].id || 34;
  320. render(id);
  321. });
  322.  
  323. filterform.addEventListener('submit', e => e.preventDefault());
  324. searchinput.addEventListener('input', debounce(query_kwd, 300));