browndust2.com news viewer

custom news viewer for sucking browndust2.com

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址