browndust2.com news viewer

custom news viewer for sucking browndust2.com

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

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