Greasy Fork 还支持 简体中文。

browndust2.com news viewer

custom news viewer for sucking browndust2.com

目前為 2024-10-25 提交的版本,檢視 最新版本

  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.7
  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. color: #111;
  43. }
  44.  
  45. img {
  46. max-width: 100%;
  47. }
  48.  
  49. h2 {
  50. display: inline;
  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.5rem;
  65. padding: .8em .5em .5em 1.5em;
  66.  
  67. &::marker {
  68. _font-size: smaller;
  69. }
  70. }
  71. h2 {
  72. position: relative;
  73. }
  74. h2 span {
  75. position: absolute;
  76. left: 1.2rem;
  77. bottom: 100%;
  78. font-size: 11px;
  79. opacity: .4;
  80. }
  81. }
  82.  
  83. .ctx {
  84. white-space: pre-wrap;
  85. background-color: #fff9;
  86. padding: 1em;
  87.  
  88. & [style*="background-color"],
  89. & [style*="font-size"],
  90. & [style*="font-family"] {
  91. font-size: inherit !important;
  92. font-family: inherit !important;
  93. background-color: unset !important;
  94. }
  95. }
  96.  
  97. .list {
  98. list-style: none;
  99. margin: 2em 0;
  100. padding-left: 50px;
  101. }
  102.  
  103. summary {
  104. position: relative;
  105. top: 0;
  106. background-color: #dfb991;
  107. min-height: 50px;
  108. cursor: pointer;
  109. padding: .5em;
  110. place-content: center;
  111.  
  112. &::before {
  113. content: '';
  114. position: absolute;
  115. inset: 0;
  116. background-color: #fff1;
  117. pointer-events: none;
  118. opacity: 0;
  119. transition: opacity .1s;
  120. }
  121.  
  122. :target & {
  123. box-shadow: inset 0 -.5em #0003;
  124. }
  125.  
  126. &:hover::before {
  127. opacity: 1;
  128. }
  129.  
  130. & > img {
  131. position: absolute;
  132. top: 0;
  133. right: 100%;
  134. width: 50px;
  135. height: 50px;
  136. }
  137. }
  138.  
  139. summary a {
  140. color: inherit;
  141. text-decoration: none;
  142. pointer-events: none;
  143.  
  144. &:visited {
  145. color: #633;
  146. }
  147. }
  148.  
  149. details {
  150. margin-block-start: 1em;
  151.  
  152. &[open] summary {
  153. position: sticky;
  154. background-color: #ceac71;
  155. box-shadow: inset 0 -.5em #0003;
  156. }
  157. }
  158.  
  159. #filterform {
  160. position: fixed;
  161. top: 0;
  162. left: 0;
  163. transition: opacity .2s;
  164. opacity: .1;
  165.  
  166. &:hover,
  167. &:focus-within {
  168. opacity: .75;
  169. }
  170. }
  171.  
  172. body:not(:has(.showall:checked))
  173. .list[data-query=""]
  174. details:nth-child(n + 20) {
  175. display: none;
  176. }
  177.  
  178. .showall-label {
  179. position: sticky;
  180. bottom: 0;
  181. display: block;
  182. width: fit-content;
  183. margin: 0 1em 0 auto;
  184. padding: .25em 1em .25em .5em;
  185. background-color: #0002;
  186. border-radius: 1em 1em 0 0;
  187. cursor: pointer;
  188. }
  189. </style>
  190. `;
  191.  
  192. let data = [];
  193. let news_map = new Map();
  194. let query_arr = [];
  195. let id_arr = [];
  196.  
  197. function render(id) {
  198. list.innerHTML = data.map(i => {
  199. let info = i.attributes;
  200. // let ctx = info.NewContent || info.content;
  201. let time = format_time(info.publishedAt);
  202. return `
  203. <details name="item" data-id="${i.id}" id="news-${i.id}">
  204. <summary>
  205. <img src="https://www.browndust2.com/img/newsDetail/tag-${info.tag}.png" width="36" height="36" alt="${info.tag}" title="#${info.tag}">
  206. <h2>
  207. <span>
  208. #${i.id} -
  209. <time datetime="${info.publishedAt}" title="${info.publishedAt}">${time}</time>
  210. </span>
  211. <a href="?id=${i.id}#news-${i.id}" tabindex="-1">${info.subject}</a>
  212. </h2>
  213. </summary>
  214. <article class="ctx"></article>
  215. </details>
  216. `;
  217. }).join('');
  218.  
  219. list.querySelectorAll('details').forEach(d => {
  220. d.addEventListener('toggle', show);
  221. });
  222.  
  223. list.addEventListener('click', (e) => {
  224. if (e.target.tagName === 'A') {
  225. e.preventDefault();
  226. console.log(123, e.target, e.target.href);
  227. history.pushState('', null, e.target.href);
  228. }
  229. });
  230.  
  231. if (id) {
  232. auto_show(id);
  233. }
  234. }
  235.  
  236. function auto_show(id) {
  237. let target = list.querySelector(`details[data-id="${id}"]`);
  238. if (target) {
  239. target.open = true;
  240. show({ target, });
  241. }
  242. }
  243.  
  244. function show({ target, }) {
  245. if (!target.open) {
  246. target.scrollIntoView({behavior:'smooth', block: 'nearest'});
  247. return;
  248. }
  249.  
  250. let id = +target.dataset.id;
  251. let ctx = target.querySelector(':scope > article.ctx');
  252.  
  253. if (!ctx || ctx.dataset?.init === '1' || !id) {
  254. return;
  255. }
  256.  
  257. // target.scrollIntoView({behavior:'smooth', block: 'nearest'});
  258. let info = news_map.get(id)?.attributes;
  259. location.hash = `news-${id}`;
  260. history.pushState(`news-${id}`, null, `?id=${id}#news-${id}`);
  261. document.title = `#${id} - ${info.subject}`;
  262.  
  263. ctx.dataset.init = '1';
  264.  
  265. let ori_link = `<a href="https://www.browndust2.com/zh-tw/news/view?id=${id}" target="_bd2news" title="official link">#</a>`;
  266. if (!info) {
  267. ctx.innerHTML = ori_link;
  268. return;
  269. }
  270.  
  271. let content = (info.content || info.NewContent);
  272. content = content.replace(/\<img\s/g, '<img loading="lazy" ');
  273. ctx.innerHTML = content + ori_link;
  274. }
  275.  
  276. function format_time(time) {
  277. let _time = time ? new Date(time) : new Date();
  278. return _time.toLocaleString('zh-TW', {
  279. weekday: 'narrow',
  280. year: 'numeric',
  281. month: '2-digit',
  282. day: '2-digit',
  283. });
  284. }
  285.  
  286. function query_kwd() {
  287. // console.time('query');
  288. let value = searchinput.value?.trim()?.toLowerCase();
  289. // console.log('query', value);
  290. if (!value) {
  291. filter_style.textContent = '';
  292. list.dataset.query = '';
  293. return;
  294. }
  295.  
  296. let matched_ids = query_arr.map((i, index) => {
  297. let regex = new RegExp(value);
  298. return regex.test(i) ? id_arr[index] : null;
  299. })
  300. .filter(Boolean);
  301.  
  302. if (matched_ids.length) {
  303. list.dataset.query = value;
  304. } else {
  305. list.dataset.query = '';
  306. }
  307.  
  308. let selectors = matched_ids.map(i => `details[data-id="${i}"]`).join();
  309. filter_style.textContent = `
  310. details {display:none;}
  311. ${selectors} { display: block; }
  312. `;
  313. // console.timeEnd('query');
  314. }
  315.  
  316. // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_debounce
  317. function debounce(func, wait, immediate) {
  318. var timeout;
  319. return function() {
  320. var context = this, args = arguments;
  321. clearTimeout(timeout);
  322. if (immediate && !timeout) func.apply(context, args);
  323. timeout = setTimeout(function() {
  324. timeout = null;
  325. if (!immediate) func.apply(context, args);
  326. }, wait);
  327. };
  328. }
  329.  
  330. let data_url = `https://www.browndust2.com/api/newsData_tw.json?${+new Date()}`;
  331. if (window.test_data_url) {
  332. data_url = window.test_data_url;
  333. }
  334.  
  335. async function get_data() {
  336. try {
  337. let is_newer = await check_newer_data();
  338. console.log({is_newer});
  339. if (!is_newer) {
  340. return await localforage.getItem('data');
  341. }
  342.  
  343. let response = await fetch(data_url);
  344. if (!response.ok) {
  345. throw new Error('fetch error', response);
  346. }
  347. let json = await response.json();
  348. let _data = json.data.reverse();
  349. localforage.setItem('etag', response.headers.get('etag'));
  350. localforage.setItem('data', _data);
  351. return _data;
  352. } catch(e) {
  353. throw new Error(e);
  354. }
  355. }
  356.  
  357. async function check_newer_data() {
  358. // // debug
  359. // let data_url = 'https://www.browndust2.com/api/newsData_tw.json';
  360. let old_etag = await localforage.getItem('etag') || '';
  361. if (!old_etag) {
  362. console.log({old_etag});
  363. return true;
  364. }
  365. let response = await fetch(data_url, { method: 'HEAD', });
  366. let new_etag = response.headers.get('etag');
  367. console.log({new_etag, old_etag});
  368. return old_etag !== new_etag;
  369. }
  370.  
  371. async function init() {
  372. list.innerHTML = 'loading...';
  373. data = await get_data();
  374. console.log({data});
  375. data.forEach(i => {
  376. let info = i.attributes;
  377. let string = [
  378. i.id,
  379. info.content,
  380. info.NewContent,
  381. `#${info.tag}`,
  382. info.subject,
  383. ].join().toLowerCase();
  384.  
  385. id_arr.push(i.id);
  386. news_map.set(i.id, i);
  387. query_arr.push(string);
  388. });
  389.  
  390. let id = new URL(location.href)?.searchParams?.get('id') || data[data.length - 1].id;
  391. render(id);
  392. }
  393.  
  394. init();
  395.  
  396. filterform.addEventListener('submit', e => e.preventDefault());
  397. searchinput.addEventListener('input', debounce(query_kwd, 300));
  398. delete_btn.addEventListener('click', () => {
  399. localforage.clear();
  400. location.reload();
  401. });