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

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

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

  1. // ==UserScript==
  2. // @name browndust2.com news viewer (Vue 3 + Tailwind CSS)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.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. .content-box * {
  45. font-size: 1rem !important;
  46. }
  47. .content-box [style*="font-size"] {
  48. font-size: 1rem !important;
  49. }
  50. .content-box span[style*="font-size"],
  51. .content-box p[style*="font-size"],
  52. .content-box div[style*="font-size"] {
  53. font-size: 1rem !important;
  54. color: #d1d5db;
  55. }
  56.  
  57. .content-box strong {
  58. color: white;
  59. }
  60.  
  61. .content-box p {
  62. margin-bottom:16px;
  63. }
  64.  
  65. `;
  66. document.head.appendChild(style);
  67. }
  68.  
  69. Promise.all([
  70. addScript('https://unpkg.com/vue@3/dist/vue.global.js'),
  71. addScript('https://cdn.tailwindcss.com')
  72. ]).then(() => {
  73. addGlobalStyle();
  74. initializeApp();
  75. }).catch(error => {
  76. console.error('Error loading scripts:', error);
  77. });
  78.  
  79.  
  80.  
  81. function initializeApp() {
  82. if (!window.Vue) {
  83. return;
  84. }
  85.  
  86. const { createApp, ref, computed, onMounted } = Vue;
  87.  
  88. const app = createApp({
  89. setup() {
  90. const data = ref([]);
  91. const newsMap = ref(new Map());
  92. const queryArr = ref([]);
  93. const idArr = ref([]);
  94. const searchInput = ref('');
  95. const showAll = ref(false);
  96.  
  97. const language = ref('tw')
  98.  
  99. const filteredData = computed(() => {
  100. if (!searchInput.value) return data.value;
  101. const regex = new RegExp(searchInput.value, 'i');
  102. return data.value.filter(item => {
  103. const info = item.attributes;
  104. return regex.test([
  105. item.id,
  106. info.content,
  107. info.NewContent,
  108. `#${info.tag}`,
  109. info.subject,
  110. ].join(''));
  111. });
  112. });
  113.  
  114. const visibleData = computed(() => {
  115. if (showAll.value) return filteredData.value;
  116. return filteredData.value.slice(0, 20);
  117. });
  118.  
  119. function formatTime(time) {
  120. const _time = time ? new Date(time) : new Date();
  121. return _time.toLocaleString('zh-TW', {
  122. weekday: 'narrow',
  123. year: 'numeric',
  124. month: '2-digit',
  125. day: '2-digit',
  126. });
  127. }
  128.  
  129. function show(id) {
  130. const info = newsMap.value.get(parseInt(id))?.attributes;
  131. if (!info) return '';
  132. const content = (info.content || info.NewContent).replace(/\<img\s/g, '<img loading="lazy" ');
  133. 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>`;
  134. return content + oriLink;
  135. }
  136.  
  137. const languageOptions = [
  138. {
  139. label: '繁體中文',
  140. value: 'tw'
  141. },
  142. {
  143. label: '日本語',
  144. value: 'jp'
  145. },
  146. {
  147. label: 'English',
  148. value: 'en'
  149. },
  150. {
  151. label: '한국어',
  152. value: 'kr'
  153. },
  154. {
  155. label: '简体中文',
  156. value: 'cn'
  157. },
  158. ]
  159.  
  160. async function load() {
  161. const dataUrl = `https://www.browndust2.com/api/newsData_${language.value}.json`;
  162.  
  163. try {
  164. const response = await fetch(dataUrl);
  165. const json = await response.json();
  166. console.log('Data fetched successfully, item count:', json.data.length);
  167. data.value = json.data.reverse();
  168. data.value.forEach(item => {
  169. newsMap.value.set(item.id, item);
  170. idArr.value.push(item.id);
  171. queryArr.value.push([
  172. item.id,
  173. item.attributes.content,
  174. item.attributes.NewContent,
  175. `#${item.attributes.tag}`,
  176. item.attributes.subject,
  177. ].join());
  178. });
  179. } catch (error) {
  180. console.error('Error fetching or processing data:', error);
  181. }
  182. }
  183.  
  184. onMounted(() => {
  185. load()
  186. });
  187.  
  188. return {
  189. visibleData,
  190. searchInput,
  191. showAll,
  192. language,
  193. languageOptions,
  194. formatTime,
  195. show,
  196. load
  197. };
  198. }
  199. });
  200.  
  201. // Create a container for the Vue app
  202. const appContainer = document.createElement('div');
  203. appContainer.id = 'app';
  204. document.body.innerHTML = '';
  205. document.body.appendChild(appContainer);
  206.  
  207. // Add the Vue template
  208. appContainer.innerHTML = `
  209. <div class=" w-full min-h-[100dvh] relative bg-slate-900">
  210. <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">
  211. <label class="flex gap-1 items-center">
  212. Filter
  213. <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">
  214. </label>
  215. <div class="flex gap-3 items-center">
  216. <label class="cursor-pointer flex gap-1 items-center">
  217. <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">
  218. <option v-for="option in languageOptions" :key="option.value" :value="option.value">
  219. {{ option.label }}
  220. </option>
  221. </select>
  222. </label>
  223. <label class="cursor-pointer flex gap-1 items-center">
  224. <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">
  225. Show all list
  226. </label>
  227. </div>
  228. </header>
  229. <div class="flex flex-col mx-auto w-full max-w-7xl py-8 px-3 space-y-4">
  230. <details v-for="item in visibleData" :key="item.id" class="rounded overflow-hidden">
  231. <summary class="pl-4 pr-2 py-2 cursor-pointer bg-slate-700 hover:bg-slate-600 active:bg-slate-600 transition duration-200">
  232. <img :src="'https://www.browndust2.com/img/newsDetail/tag-' + item.attributes.tag + '.png'"
  233. :alt="item.attributes.tag" :title="'#' + item.attributes.tag"
  234. class="w-10 h-10 inline-block mr-2">
  235. #{{ item.id }} -
  236. <time :datetime="item.attributes.publishedAt" :title="item.attributes.publishedAt">
  237. {{ formatTime(item.attributes.publishedAt) }}
  238. </time>
  239. {{ item.attributes.subject }}
  240. </summary>
  241. <div class="bg-gray-700/50 p-4 whitespace-pre-wrap content-box" v-html="show(item.id)"></div>
  242. </details>
  243. </div>
  244. </div>
  245. `;
  246.  
  247. app.mount('#app');
  248. }
  249. })();