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

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

当前为 2024-10-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name browndust2.com news viewer (Vue 3 + Tailwind CSS)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.0
  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 info = item.attributes;
  100. return [
  101. item.id.toString(),
  102. info.content,
  103. info.NewContent,
  104. info.tag,
  105. info.subject
  106. ].some(field => field && field.toLowerCase().includes(keyword));
  107. });
  108. });
  109.  
  110. const visibleData = computed(() => {
  111. if (showAll.value) return filteredData.value;
  112. return filteredData.value.slice(0, 20);
  113. });
  114.  
  115. function formatTime(time) {
  116. const _time = time ? new Date(time) : new Date();
  117. return _time.toLocaleString('zh-TW', {
  118. weekday: 'narrow',
  119. year: 'numeric',
  120. month: '2-digit',
  121. day: '2-digit',
  122. });
  123. }
  124.  
  125. function show(id) {
  126. const info = newsMap.value.get(parseInt(id))?.attributes;
  127. if (!info) return '';
  128. const content = (info.content || info.NewContent).replace(/\<img\s/g, '<img loading="lazy" ');
  129. 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>`;
  130. 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>`;
  131. return content + oriLink + closeButton;
  132. }
  133.  
  134. const closeDetails = (id) => {
  135. const detailsElement = document.querySelector(`details[data-detail-id="${id}"]`);
  136. if (detailsElement) {
  137. detailsElement.open = false;
  138. }
  139. };
  140.  
  141. const languageOptions = [
  142. {
  143. label: '繁體中文',
  144. value: 'tw'
  145. },
  146. {
  147. label: '日本語',
  148. value: 'jp'
  149. },
  150. {
  151. label: 'English',
  152. value: 'en'
  153. },
  154. {
  155. label: '한국어',
  156. value: 'kr'
  157. },
  158. {
  159. label: '简体中文',
  160. value: 'cn'
  161. },
  162. ]
  163.  
  164. const handleLangChange = () => {
  165. searchInput.value = '';
  166. load();
  167. }
  168.  
  169. async function load() {
  170. const dataUrl = `https://www.browndust2.com/api/newsData_${language.value}.json`;
  171.  
  172. try {
  173. const response = await fetch(dataUrl);
  174. const json = await response.json();
  175. console.log('Data fetched successfully, item count:', json.data.length);
  176. data.value = json.data.reverse();
  177. data.value.forEach(item => {
  178. newsMap.value.set(item.id, item);
  179. });
  180. } catch (error) {
  181. console.error('Error fetching or processing data:', error);
  182. }
  183. }
  184.  
  185. onMounted(() => {
  186. load()
  187. window.closeDetails = closeDetails;
  188. });
  189.  
  190. return {
  191. visibleData,
  192. searchInput,
  193. showAll,
  194. language,
  195. languageOptions,
  196. formatTime,
  197. show,
  198. handleLangChange,
  199. };
  200. }
  201. });
  202.  
  203. // Create a container for the Vue app
  204. const appContainer = document.createElement('div');
  205. appContainer.id = 'app';
  206. document.body.innerHTML = '';
  207. document.body.appendChild(appContainer);
  208.  
  209. // Add the Vue template
  210. appContainer.innerHTML = `
  211. <div class=" w-full min-h-[100dvh] relative bg-slate-900">
  212. <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">
  213. <label class="flex gap-1 items-center">
  214. Filter
  215. <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">
  216. </label>
  217. <div class="flex gap-3 items-center">
  218. <label class="cursor-pointer flex gap-1 items-center">
  219. <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">
  220. <option v-for="option in languageOptions" :key="option.value" :value="option.value">
  221. {{ option.label }}
  222. </option>
  223. </select>
  224. </label>
  225. <label class="cursor-pointer flex gap-1 items-center">
  226. <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">
  227. Show all list
  228. </label>
  229. </div>
  230. </header>
  231. <div class="flex flex-col mx-auto w-full max-w-7xl py-8 px-3 space-y-4">
  232. <details v-for="item in visibleData" :key="item.id" :data-detail-id="item.id" class="rounded overflow-hidden">
  233. <summary class="pl-4 pr-2 py-2 cursor-pointer bg-slate-700 hover:bg-slate-600 active:bg-slate-600 transition duration-200">
  234. <img :src="'https://www.browndust2.com/img/newsDetail/tag-' + item.attributes.tag + '.png'"
  235. :alt="item.attributes.tag" :title="'#' + item.attributes.tag"
  236. class="w-10 h-10 inline-block mr-2">
  237. #{{ item.id }} -
  238. <time :datetime="item.attributes.publishedAt" :title="item.attributes.publishedAt">
  239. {{ formatTime(item.attributes.publishedAt) }}
  240. </time>
  241. {{ item.attributes.subject }}
  242. </summary>
  243. <div class="bg-gray-700/50 p-4 whitespace-pre-wrap content-box" v-html="show(item.id)"></div>
  244. </details>
  245. </div>
  246. </div>
  247. `;
  248.  
  249. app.mount('#app');
  250. }
  251. })();