browndust2.com news viewer

custom news viewer for sucking browndust2.com

Tính đến 10-08-2025. Xem phiên bản mới nhất.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        browndust2.com news viewer
// @namespace   Violentmonkey Scripts
// @match       https://www.browndust2.com/robots.txt
// @grant       none
// @version     1.6.1
// @author      Rplus
// @description custom news viewer for sucking browndust2.com
// @require     https://unpkg.com/[email protected]/dist/localforage.min.js#sha384-MTDrIlFOzEqpmOxY6UIA/1Zkh0a64UlmJ6R0UrZXqXCPx99siPGi8EmtQjIeCcTH
// @require     https://unpkg.com/[email protected]/lib/marked.umd.js#sha256-XF49G0iwZLps3m+Jg99uZv4DSzMQYF1NxTXcT0XXjmU=
// @require     https://unpkg.com/[email protected]/dist/purify.min.js#sha256-ieH6dkfLSVNw06mXrOQ4f10V2fTFrxI1LFPapACVYoc=
// @@run-at     document-end
// @license     WTFPL
// ==/UserScript==

document.head.insertAdjacentHTML(
	'beforeend',
	`<link rel="icon" type="image/png" sizes="16x16" href="/img/seo/favicon.png">`
);

document.body.innerHTML = `
<form id="filterform">
	Filter
	<input type="search" name="q" tabindex="1" id="searchinput">
	<style id="filter_style"></style>
</form>

<div class="list" id="list" data-query=""></div>
<hr>
<input type="reset" value="Delete all cached data" id="delete_btn">

<select id="lang_select"></select>

<label class="showall-label">
	<input type="checkbox" class="showall" >
	show all list
</label>

<style>
*, *::before, *::after {
	box-sizing: border-box;
}
body {
	max-width: 1200px;
	margin: 0 auto;
	background-color: #e5cc9c;
	color: #111;
}

img {
	max-width: 100%;
}

h2 {
	display: inline;
	font-size: inherit;
	margin: 0;

	& span {
		font-weight: 400;
		font-size: smaller;
		vertical-align: middle;
		opacity: .5;
	}
}
@media (max-width:750px) {
	details summary {
		text-indent: -1.1em;
		padding-left: 1.5rem;
		padding: .8em .5em .5em 1.5em;

		&::marker {
			_font-size: smaller;
		}
	}
	h2 {
		position: relative;
	}
	h2 span {
		position: absolute;
		left: 1.2rem;
		bottom: 100%;
		font-size: 11px;
		opacity: .4;
	}
}

.ctx {
	background-color: #fff9;
	padding: 1em;

	p {
		white-space: pre-wrap;
	}

	& [style*="background-color"],
	& [style*="font-size"],
	& [style*="font-family"] {
		font-size: inherit !important;
		font-family: inherit !important;
		background-color: unset !important;
	}
}

.list {
	list-style: none;
	margin: 2em 0;
	padding-left: 50px;
}

summary {
	position: relative;
	top: 0;
	background-color: #dfb991;
	min-height: 50px;
	cursor: pointer;
	padding: .5em;
	place-content: center;

	&::before {
		content: '';
		position: absolute;
		inset: 0;
		background-color: #fff1;
		pointer-events: none;
		opacity: 0;
		transition: opacity .1s;
	}

	:target & {
		box-shadow: inset 0 -.5em #0003;
	}

	&:hover::before {
		opacity: 1;
	}

	& > img {
		position: absolute;
		top: 0;
		right: 100%;
		width: 50px;
		height: 50px;
	}
}

summary a {
	color: inherit;
	text-decoration: none;
	pointer-events: none;

	&:visited {
		color: #633;
	}
}

details {
	margin-block-start: 1em;

	&[open] summary {
		position: sticky;
		background-color: #ceac71;
		box-shadow: inset 0 -.5em #0003;
	}
}

#filterform {
	position: fixed;
	top: 0;
	left: 0;
	transition: opacity .2s;
	opacity: .1;

	&:hover,
	&:focus,
	&:focus-within {
		opacity: 1;
	}
}

body:not(:has(.showall:checked))
	.list[data-query=""]
		details:nth-child(n + 20) {
	display: none;
}

.showall-label {
	position: sticky;
	bottom: 0;
	display: block;
	width: fit-content;
	margin: 0 1em 0 auto;
	padding: .25em 1em .25em .5em;
	background-color: #0002;
	border-radius: 1em 1em 0 0;
	cursor: pointer;
}
</style>
`;

let data = [];
let news_map = new Map();
let query_arr = [];
let id_arr = [];
const lang_map = {
	'en-us': {
		full: 'en-us',
		fn: 'en',
	},
	'zh-tw': {
		full: 'zh-tw',
		fn: 'tw',
	},
	'zh-cn': {
		full: 'zh-cn',
		fn: 'cn',
	},
	'ja-jp': {
		full: 'ja-jp',
		fn: 'jp',
	},
	'ko-kr': {
		full: 'ko-kr',
		fn: 'kr',
	},
};
let lang = get_lang();

function render(id) {
	list.innerHTML = data.map(i => {
		let info = i.attributes;
		// let ctx = info.NewContent || info.content;
		let time = format_time(info.publishedAt);
		return `
			<details name="item" data-id="${i.id}" id="news-${i.id}">
				<summary>
					<img src="https://www.browndust2.com/img/newsDetail/tag-${info.tag}.png" width="36" height="36" alt="${info.tag}" title="#${info.tag}">
					<h2>
						<span>
							#${i.id} -
							<time datetime="${info.publishedAt}" title="${info.publishedAt}">${time}</time>
						</span>
						<a href="?id=${i.id}#news-${i.id}" tabindex="-1">${info.subject}</a>
					</h2>
				</summary>
				<article class="ctx"></article>
			</details>
		`;
	}).join('');

	list.querySelectorAll('details').forEach(d => {
		d.addEventListener('toggle', show);
	});

	list.addEventListener('click', (e) => {
		if (e.target.tagName === 'A' && (e.target.tabIndex === -1)) {
			e.preventDefault();
			console.log(123, e.target, e.target.href);
			history.pushState('', null, e.target.href);
		}
	});

	if (id) {
		auto_show(id);
	}
}

function auto_show(id) {
	let target = list.querySelector(`details[data-id="${id}"]`);
	if (target) {
		target.open = true;
		show({ target, });
	}
}

function show({ target, }) {
	if (!target.open) {
		target.scrollIntoView({behavior:'smooth', block: 'nearest'});
		return;
	}

	let id = +target.dataset.id;
	let ctx = target.querySelector(':scope > article.ctx');


	// target.scrollIntoView({behavior:'smooth', block: 'nearest'});
	let info = news_map.get(id)?.attributes;
	location.hash = `news-${id}`;
	history.pushState(`news-${id}`, null, `?id=${id}#news-${id}`);
	document.title = `#${id} - ${info.subject}`;

	if (!ctx || ctx.dataset?.init === '1' || !id) {
		return;
	}
	ctx.dataset.init = '1';

	let ori_link = `<a href="https://www.browndust2.com/${lang.full}/news/view?id=${id}" target="_bd2news" title="official link">#</a>`;
	if (!info) {
		ctx.innerHTML = ori_link;
		return;
	}

	let content = (info.NewContent || info.content);
	content = content.replace(/\<img\s/g, '<img loading="lazy" ');
	ctx.innerHTML = DOMPurify.sanitize(marked.parse(content + ori_link));
}

function format_time(time) {
	let _time = time ? new Date(time) : new Date();
	return _time.toLocaleString('zh-TW', {
		weekday: 'narrow',
		year: 'numeric',
		month: '2-digit',
		day: '2-digit',
	});
}

function query_kwd() {
	// console.time('query');
	let value = searchinput.value?.trim()?.toLowerCase();
	// console.log('query', value);
	if (!value) {
		filter_style.textContent = '';
		list.dataset.query = '';
		return;
	}

	let matched_ids = query_arr.map((i, index) => {
		let regex = new RegExp(value);
		return regex.test(i) ? id_arr[index] : null;
	})
	.filter(Boolean);

	if (matched_ids.length) {
		list.dataset.query = value;
	} else {
		list.dataset.query = '';
	}

	let selectors = matched_ids.map(i => `details[data-id="${i}"]`).join();
	filter_style.textContent = `
		details {display:none;}
		${selectors} { display: block; }
	`;
	// console.timeEnd('query');
}

// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_debounce
function debounce(func, wait, immediate) {
	var timeout;
	return function() {
		var context = this, args = arguments;
		clearTimeout(timeout);
		if (immediate && !timeout) func.apply(context, args);
		timeout = setTimeout(function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		}, wait);
	};
}

function get_lang() {
	let matched_langs = [
		new URL(location.href)?.searchParams?.get('lang') || '',
		localStorage.getItem('lang') || '',
		navigator.language.toLowerCase() || '',
		...(navigator.languages?.map(s => s.toLowerCase()) || [])
	]
	.filter(i => lang_map[i]);
	return lang_map[matched_langs[0]] || lang_map['zh-tw'];
}

let data_url = `https://www.browndust2.com/api/newsData_${lang.fn}.json?${+new Date()}`;
if (window.test_data_url) {
	data_url = window.test_data_url;
}

async function get_data() {
	try {
		let cached_etag = await localforage.getItem(`etag-${lang.fn}`) || '';
		let response = await fetch(data_url, {
			method: 'GET',
			cache: 'no-store',
			headers: {
				'If-None-Match': cached_etag,
			}
		});
		let new_etag = response.headers.get('etag');

		console.log(response);
		console.log({cached_etag, new_etag});

		if (response.status === 304) { // cached
			return await localforage.getItem(`data-${lang.fn}`);
		} else if (!response.ok) {
			throw new Error('fetch error', response);
		}

		let json = await response.json();
		let _data = json.data.reverse();
		localforage.setItem(`etag-${lang.fn}`, new_etag);
		localforage.setItem(`data-${lang.fn}`, _data);
		return _data;
	} catch(e) {
		throw new Error(e);
	}
}

async function init() {
	let qs_lang = new URL(location.href)?.searchParams?.get('lang') || '';
	if (qs_lang) {
		localStorage.setItem('lang', qs_lang);
	}
	lang_select.innerHTML = Object.values(lang_map).map(i => `<option value="${i.full}" ${i.full === lang.full ? 'selected' : ''}>${i.full}</option>`).join('');

	list.innerHTML = 'loading...';
	data = await get_data();
	console.log({data});
	data.forEach(i => {
		let info = i.attributes;
		let string = [
			i.id,
			info.content,
			info.NewContent,
			`#${info.tag}`,
			info.subject,
		].join().toLowerCase();

		id_arr.push(i.id);
		news_map.set(i.id, i);
		query_arr.push(string);
	});

	let id = new URL(location.href)?.searchParams?.get('id') || data[data.length - 1].id;
	render(id);

}

init();

lang_select.addEventListener('change', e => {
	let url = new URL(location.href);
	url.searchParams.set('lang', e.target.value)
	location.search = url.search;
});
filterform.addEventListener('submit', e => e.preventDefault());
searchinput.addEventListener('input', debounce(query_kwd, 300));
delete_btn.addEventListener('click', () => {
	localforage.clear();
	location.reload();
});