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

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

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

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