您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Custom news viewer for browndust2.com using Vue 3 and Tailwind CSS
当前为
// ==UserScript== // @name browndust2.com news viewer (Vue 3 + Tailwind CSS) // @namespace http://tampermonkey.net/ // @version 1.4.0 // @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: #475569; } .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: #475569; } .content-box strong { color: black; font-weight: bold; } .content-box p { margin-bottom:16px; } details[open] > summary { color: black; } `; 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 searchInput = ref(''); const showAll = ref(false); const language = ref('tw') const readNews = ref(new Set()); const updateReadNews = (id) => { if (readNews.value.has(id)) return; readNews.value.add(id); localStorage.setItem('readNews', JSON.stringify(Array.from(readNews.value))); }; const isNewsRead = computed(() => (id) => readNews.value.has(id)); const filteredData = computed(() => { const keyword = searchInput.value.trim().toLowerCase(); if (!keyword) return data.value; return data.value.filter(item => { const { lowercaseFields } = item; return Object.values(lowercaseFields).some(field => field.includes(keyword)); }); }); 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(); const currentLang = languageOptions.find(option => option.value === language.value); return _time.toLocaleString(currentLang.dateFormat.locale, currentLang.dateFormat.options); } 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>`; const closeButton = `<div class="flex justify-center pt-3"><button class="close-details mt-4 px-3 py-1 bg-slate-600 hover:bg-slate-500 text-white rounded-full text-xs" onclick="closeDetails(${id})">關閉</button></div>`; return content + oriLink + closeButton; } const closeDetails = (id) => { const detailsElement = document.querySelector(`details[data-detail-id="${id}"]`); if (detailsElement) { detailsElement.open = false; } }; const languageOptions = [ { label: '繁體中文', value: 'tw', dateFormat: { locale: 'zh-TW', options: { weekday: 'narrow', year: 'numeric', month: '2-digit', day: '2-digit' } } }, { label: '日本語', value: 'jp', dateFormat: { locale: 'ja-JP', options: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' } } }, { label: 'English', value: 'en', dateFormat: { locale: 'en-US', options: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' } } }, { label: '한국어', value: 'kr', dateFormat: { locale: 'ko-KR', options: { weekday: 'narrow', year: 'numeric', month: '2-digit', day: '2-digit' } } }, { label: '简体中文', value: 'cn', dateFormat: { locale: 'zh-CN', options: { weekday: 'narrow', year: 'numeric', month: '2-digit', day: '2-digit' } } }, ] const handleLangChange = () => { searchInput.value = ''; load(); } 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); // 新增 lowercaseFields用於篩選功能 data.value = json.data.reverse().map(item => ({ ...item, lowercaseFields: { id: item.id.toString().toLowerCase(), content: (item.attributes.content || '').toLowerCase(), newContent: (item.attributes.NewContent || '').toLowerCase(), tag: (item.attributes.tag || '').toLowerCase(), subject: (item.attributes.subject || '').toLowerCase() } })); // 更新 newsMap,但不需包含 lowercaseFields newsMap.value.clear(); data.value.forEach(item => { const { lowercaseFields, ...itemWithoutLowercaseFields } = item; newsMap.value.set(item.id, itemWithoutLowercaseFields); }); } catch (error) { console.error('Error fetching or processing data:', error); } } onMounted(() => { load() window.closeDetails = closeDetails; const storedReadNews = JSON.parse(localStorage.getItem('readNews') || '[]'); readNews.value = new Set(storedReadNews); }); return { visibleData, searchInput, showAll, language, languageOptions, formatTime, show, handleLangChange, updateReadNews, isNewsRead, }; } }); // 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-white"> <header class="sticky top-0 left-0 h-[60px] border-b border-slate-600 flex justify-between items-center bg-white 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-white border border-neutral-300 text-black placeholder-neutral-500" tabindex="1"> </label> <div class="flex gap-3 items-center"> <label class="cursor-pointer flex gap-1 items-center"> <select v-model="language" @change="handleLangChange" class="py-1 px-2 block w-full rounded text-sm disabled:pointer-events-none bg-white border border-neutral-300 text-black placeholder-neutral-500" 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 bg-white border border-gray-200 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none"> 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" :data-detail-id="item.id" class="rounded overflow-hidden shadow shadow-black/30"> <summary class="pl-4 pr-2 py-2 cursor-pointer transition duration-200" @click="updateReadNews(item.id)" :class="[isNewsRead(item.id) ? 'text-gray-500' : 'text-black','font-bold bg-slate-100 hover:bg-slate-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"> <time :datetime="item.attributes.publishedAt" :title="item.attributes.publishedAt"> {{ formatTime(item.attributes.publishedAt) }} </time> - {{ item.attributes.subject }} </summary> <div class="bg-white p-6 whitespace-pre-wrap content-box" v-html="show(item.id)"></div> </details> </div> </div> `; app.mount('#app'); } })();