browndust2.com news viewer (Vue 3 + Tailwind CSS)

Custom news viewer for browndust2.com using Vue 3 and Tailwind CSS

目前为 2024-10-18 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name browndust2.com news viewer (Vue 3 + Tailwind CSS)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.1
  5. // @description Custom news viewer for browndust2.com using Vue 3 and Tailwind CSS
  6. // @author SouSeiHaku
  7. // @match https://www.browndust2.com/robots.txt
  8. // @grant none
  9. // @run-at document-end
  10. // @license WTFPL
  11. // ==/UserScript==
  12.  
  13. /*
  14. * This script is based on the original work by Rplus:
  15. * @name browndust2.com news viewer
  16. * @namespace Violentmonkey Scripts
  17. * @version 1.2.0
  18. * @author Rplus
  19. * @description custom news viewer for sucking browndust2.com
  20. * @license WTFPL
  21. *
  22. * Modified and extended by SouSeiHaku
  23. */
  24.  
  25. (function () {
  26. 'use strict';
  27.  
  28. function addScript(src) {
  29. return new Promise((resolve, reject) => {
  30. const script = document.createElement('script');
  31. script.src = src;
  32. script.onload = resolve;
  33. script.onerror = reject;
  34. document.head.appendChild(script);
  35. });
  36. }
  37.  
  38. function addGlobalStyle() {
  39. const style = document.createElement('style');
  40. style.textContent = `
  41. body {
  42. color: white;
  43. }
  44.  
  45. .content-box * {
  46. font-size: 1rem !important;
  47. }
  48.  
  49. .content-box [style*="font-size"] {
  50. font-size: 1rem !important;
  51. }
  52.  
  53. .content-box span[style*="font-size"],
  54. .content-box p[style*="font-size"],
  55. .content-box div[style*="font-size"] {
  56. font-size: 1rem !important;
  57. color: #d1d5db;
  58. }
  59.  
  60. .content-box strong {
  61. color: white;
  62. }
  63.  
  64. .content-box p {
  65. margin-bottom:16px;
  66. }
  67.  
  68. `;
  69. document.head.appendChild(style);
  70. }
  71.  
  72. Promise.all([
  73. addScript('https://unpkg.com/vue@3/dist/vue.global.js'),
  74. addScript('https://cdn.tailwindcss.com')
  75. ]).then(() => {
  76. addGlobalStyle();
  77. initializeApp();
  78. }).catch(error => {
  79. console.error('Error loading scripts:', error);
  80. });
  81.  
  82. function initializeApp() {
  83. if (!window.Vue) return;
  84.  
  85. const { createApp, ref, computed, onMounted } = Vue;
  86.  
  87. const app = createApp({
  88. setup() {
  89. const data = ref([]);
  90. const newsMap = ref(new Map());
  91. const searchInput = ref('');
  92. const showAll = ref(false);
  93. const language = ref('tw')
  94.  
  95. const filteredData = computed(() => {
  96. const keyword = searchInput.value.trim().toLowerCase();
  97. if (!keyword) return data.value;
  98. return data.value.filter(item => {
  99. const { lowercaseFields } = item;
  100. return Object.values(lowercaseFields).some(field => field.includes(keyword));
  101. });
  102. });
  103.  
  104. const visibleData = computed(() => {
  105. if (showAll.value) return filteredData.value;
  106. return filteredData.value.slice(0, 20);
  107. });
  108.  
  109. function formatTime(time) {
  110. const _time = time ? new Date(time) : new Date();
  111. return _time.toLocaleString('zh-TW', {
  112. weekday: 'narrow',
  113. year: 'numeric',
  114. month: '2-digit',
  115. day: '2-digit',
  116. });
  117. }
  118.  
  119. function show(id) {
  120. const info = newsMap.value.get(parseInt(id))?.attributes;
  121. if (!info) return '';
  122. const content = (info.content || info.NewContent).replace(/\<img\s/g, '<img loading="lazy" ');
  123. 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>`;
  124. 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>`;
  125. return content + oriLink + closeButton;
  126. }
  127.  
  128. const closeDetails = (id) => {
  129. const detailsElement = document.querySelector(`details[data-detail-id="${id}"]`);
  130. if (detailsElement) {
  131. detailsElement.open = false;
  132. }
  133. };
  134.  
  135. const languageOptions = [
  136. {
  137. label: '繁體中文',
  138. value: 'tw'
  139. },
  140. {
  141. label: '日本語',
  142. value: 'jp'
  143. },
  144. {
  145. label: 'English',
  146. value: 'en'
  147. },
  148. {
  149. label: '한국어',
  150. value: 'kr'
  151. },
  152. {
  153. label: '简体中文',
  154. value: 'cn'
  155. },
  156. ]
  157.  
  158. const handleLangChange = () => {
  159. searchInput.value = '';
  160. load();
  161. }
  162.  
  163. async function load() {
  164. const dataUrl = `https://www.browndust2.com/api/newsData_${language.value}.json`;
  165.  
  166. try {
  167. const response = await fetch(dataUrl);
  168. const json = await response.json();
  169. console.log('Data fetched successfully, item count:', json.data.length);
  170.  
  171. // 新增 lowercaseFields用於篩選功能
  172. data.value = json.data.reverse().map(item => ({
  173. ...item,
  174. lowercaseFields: {
  175. id: item.id.toString().toLowerCase(),
  176. content: (item.attributes.content || '').toLowerCase(),
  177. newContent: (item.attributes.NewContent || '').toLowerCase(),
  178. tag: (item.attributes.tag || '').toLowerCase(),
  179. subject: (item.attributes.subject || '').toLowerCase()
  180. }
  181. }));
  182.  
  183. // 更新 newsMap,但不需包含 lowercaseFields
  184. newsMap.value.clear();
  185. data.value.forEach(item => {
  186. const { lowercaseFields, ...itemWithoutLowercaseFields } = item;
  187. newsMap.value.set(item.id, itemWithoutLowercaseFields);
  188. });
  189. } catch (error) {
  190. console.error('Error fetching or processing data:', error);
  191. }
  192. }
  193.  
  194. onMounted(() => {
  195. load()
  196. window.closeDetails = closeDetails;
  197. });
  198.  
  199. return {
  200. visibleData,
  201. searchInput,
  202. showAll,
  203. language,
  204. languageOptions,
  205. formatTime,
  206. show,
  207. handleLangChange,
  208. };
  209. }
  210. });
  211.  
  212. // Create a container for the Vue app
  213. const appContainer = document.createElement('div');
  214. appContainer.id = 'app';
  215. document.body.innerHTML = '';
  216. document.body.appendChild(appContainer);
  217.  
  218. // Add the Vue template
  219. appContainer.innerHTML = `
  220. <div class=" w-full min-h-[100dvh] relative bg-slate-900">
  221. <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">
  222. <label class="flex gap-1 items-center">
  223. Filter
  224. <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">
  225. </label>
  226. <div class="flex gap-3 items-center">
  227. <label class="cursor-pointer flex gap-1 items-center">
  228. <select v-model="language" @change="handleLangChange" 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">
  229. <option v-for="option in languageOptions" :key="option.value" :value="option.value">
  230. {{ option.label }}
  231. </option>
  232. </select>
  233. </label>
  234. <label class="cursor-pointer flex gap-1 items-center">
  235. <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">
  236. Show all list
  237. </label>
  238. </div>
  239. </header>
  240. <div class="flex flex-col mx-auto w-full max-w-7xl py-8 px-3 space-y-4">
  241. <details v-for="item in visibleData" :key="item.id" :data-detail-id="item.id" class="rounded overflow-hidden">
  242. <summary class="pl-4 pr-2 py-2 cursor-pointer bg-slate-700 hover:bg-slate-600 active:bg-slate-600 transition duration-200">
  243. <img :src="'https://www.browndust2.com/img/newsDetail/tag-' + item.attributes.tag + '.png'"
  244. :alt="item.attributes.tag" :title="'#' + item.attributes.tag"
  245. class="w-10 h-10 inline-block mr-2">
  246. #{{ item.id }} -
  247. <time :datetime="item.attributes.publishedAt" :title="item.attributes.publishedAt">
  248. {{ formatTime(item.attributes.publishedAt) }}
  249. </time>
  250. {{ item.attributes.subject }}
  251. </summary>
  252. <div class="bg-gray-700/50 p-4 whitespace-pre-wrap content-box" v-html="show(item.id)"></div>
  253. </details>
  254. </div>
  255. </div>
  256. `;
  257.  
  258. app.mount('#app');
  259. }
  260. })();