// ==UserScript==
// @name browndust2.com news viewer (Vue 3 + Tailwind CSS)
// @namespace http://tampermonkey.net/
// @version 1.1.1
// @description Custom news viewer for browndust2.com using Vue 3 and Tailwind CSS
// @author SouSeiHaku
// @match https://www.browndust2.com/robots.txt
// @grant none
// @run-at document-end
// @license WTFPL
// ==/UserScript==
/*
* This script is based on the original work by Rplus:
* @name browndust2.com news viewer
* @namespace Violentmonkey Scripts
* @version 1.2.0
* @author Rplus
* @description custom news viewer for sucking browndust2.com
* @license WTFPL
*
* Modified and extended by SouSeiHaku
*/
(function () {
'use strict';
function addScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function addGlobalStyle() {
const style = document.createElement('style');
style.textContent = `
body {
color: white;
}
.content-box * {
font-size: 1rem !important;
}
.content-box [style*="font-size"] {
font-size: 1rem !important;
}
.content-box span[style*="font-size"],
.content-box p[style*="font-size"],
.content-box div[style*="font-size"] {
font-size: 1rem !important;
color: #d1d5db;
}
.content-box strong {
color: white;
}
.content-box p {
margin-bottom:16px;
}
`;
document.head.appendChild(style);
}
Promise.all([
addScript('https://unpkg.com/vue@3/dist/vue.global.js'),
addScript('https://cdn.tailwindcss.com')
]).then(() => {
addGlobalStyle();
initializeApp();
}).catch(error => {
console.error('Error loading scripts:', error);
});
function initializeApp() {
if (!window.Vue) {
return;
}
const { createApp, ref, computed, onMounted } = Vue;
const app = createApp({
setup() {
const data = ref([]);
const newsMap = ref(new Map());
const queryArr = ref([]);
const idArr = ref([]);
const searchInput = ref('');
const showAll = ref(false);
const language = ref('tw')
const filteredData = computed(() => {
if (!searchInput.value) return data.value;
const regex = new RegExp(searchInput.value, 'i');
return data.value.filter(item => {
const info = item.attributes;
return regex.test([
item.id,
info.content,
info.NewContent,
`#${info.tag}`,
info.subject,
].join(''));
});
});
const visibleData = computed(() => {
if (showAll.value) return filteredData.value;
return filteredData.value.slice(0, 20);
});
function formatTime(time) {
const _time = time ? new Date(time) : new Date();
return _time.toLocaleString('zh-TW', {
weekday: 'narrow',
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
function show(id) {
const info = newsMap.value.get(parseInt(id))?.attributes;
if (!info) return '';
const content = (info.content || info.NewContent).replace(/\<img\s/g, '<img loading="lazy" ');
const oriLink = `<a class="text-blue-400 underline mt-10" href="https://www.browndust2.com/zh-tw/news/view?id=${id}" target="_bd2news" title="official link">官方連結 ></a>`;
return content + oriLink;
}
const languageOptions = [
{
label: '繁體中文',
value: 'tw'
},
{
label: '日本語',
value: 'jp'
},
{
label: 'English',
value: 'en'
},
{
label: '한국어',
value: 'kr'
},
{
label: '简体中文',
value: 'cn'
},
]
async function load() {
const dataUrl = `https://www.browndust2.com/api/newsData_${language.value}.json`;
try {
const response = await fetch(dataUrl);
const json = await response.json();
console.log('Data fetched successfully, item count:', json.data.length);
data.value = json.data.reverse();
data.value.forEach(item => {
newsMap.value.set(item.id, item);
idArr.value.push(item.id);
queryArr.value.push([
item.id,
item.attributes.content,
item.attributes.NewContent,
`#${item.attributes.tag}`,
item.attributes.subject,
].join());
});
} catch (error) {
console.error('Error fetching or processing data:', error);
}
}
onMounted(() => {
load()
});
return {
visibleData,
searchInput,
showAll,
language,
languageOptions,
formatTime,
show,
load
};
}
});
// Create a container for the Vue app
const appContainer = document.createElement('div');
appContainer.id = 'app';
document.body.innerHTML = '';
document.body.appendChild(appContainer);
// Add the Vue template
appContainer.innerHTML = `
<div class=" w-full min-h-[100dvh] relative bg-slate-900">
<header class="sticky top-0 left-0 h-[60px] border-b border-slate-600 flex justify-between items-center bg-slate-900 z-10 px-3">
<label class="flex gap-1 items-center">
Filter
<input v-model="searchInput" type="search" class="py-1 px-2 block w-full rounded text-sm disabled:pointer-events-none bg-neutral-700 border-neutral-700 text-neutral-100 placeholder-neutral-500 focus:ring-neutral-600" tabindex="1">
</label>
<div class="flex gap-3 items-center">
<label class="cursor-pointer flex gap-1 items-center">
<select v-model="language" @change="load" class="py-1 px-2 block w-full rounded text-sm disabled:pointer-events-none bg-neutral-700 border-neutral-700 text-neutral-100 placeholder-neutral-500 focus:ring-neutral-600" tabindex="1">
<option v-for="option in languageOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label class="cursor-pointer flex gap-1 items-center">
<input v-model="showAll" type="checkbox" class="shrink-0 mt-0.5 border-gray-200 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800">
Show all list
</label>
</div>
</header>
<div class="flex flex-col mx-auto w-full max-w-7xl py-8 px-3 space-y-4">
<details v-for="item in visibleData" :key="item.id" class="rounded overflow-hidden">
<summary class="pl-4 pr-2 py-2 cursor-pointer bg-slate-700 hover:bg-slate-600 active:bg-slate-600 transition duration-200">
<img :src="'https://www.browndust2.com/img/newsDetail/tag-' + item.attributes.tag + '.png'"
:alt="item.attributes.tag" :title="'#' + item.attributes.tag"
class="w-10 h-10 inline-block mr-2">
#{{ item.id }} -
<time :datetime="item.attributes.publishedAt" :title="item.attributes.publishedAt">
{{ formatTime(item.attributes.publishedAt) }}
</time>
{{ item.attributes.subject }}
</summary>
<div class="bg-gray-700/50 p-4 whitespace-pre-wrap content-box" v-html="show(item.id)"></div>
</details>
</div>
</div>
`;
app.mount('#app');
}
})();