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

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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');
    }
})();