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

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

As of 2024-10-16. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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