Greasy Fork

Auto Dark Mode for Penana Mobile

Automatically switch the theme between light and dark, based on the browser’s color scheme preference.

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

// ==UserScript==
// @name               Auto Dark Mode for Penana Mobile
// @name:zh-TW         Penana 手機版自動黑暗模式
// @description        Automatically switch the theme between light and dark, based on the browser’s color scheme preference.
// @description:zh-TW  根據瀏覽器的佈景主題設定,自動從明亮和黑暗模式間切換。
// @icon               https://wsrv.nl/?url=https://static2.penana.com/img/mobile/app-icon/ios/128.png
// @author             Jason Kwok
// @namespace          https://jasonhk.dev/
// @version            1.0.2
// @license            MIT
// @match              https://m.penana.com/*
// @match              https://android.penana.com/*
// @run-at             document-end
// @grant              GM.getValue
// @grant              GM.setValue
// @grant              GM.registerMenuCommand
// @require            https://unpkg.com/[email protected]/dist/i18n.object.min.js
// @require            https://unpkg.com/[email protected]/uuid-random.min.js
// @supportURL         https://greasyfork.org/scripts/488029/feedback
// ==/UserScript==

const LL = (function()
{
    const translations =
    {
        "en": {
            COMMAND: {
                SETTING: "Change Light Theme Setting",
            },
            SETTING: {
                SUBTITLE: "Light Theme Setting",
                LIGHT: "Light",
                WHITE: "White",
                BROWN: "Brown",
                SAVE: "Save",
            },
        },
        "zh-TW": {
            COMMAND: {
                SETTING: "更改明亮主題設定",
            },
            SETTING: {
                SUBTITLE: "明亮主題設定",
                LIGHT: "明亮",
                WHITE: "白色",
                BROWN: "褐色",
                SAVE: "儲存",
            },
        },
    };

    let locale = "en";
    for (let _locale of navigator.languages.map((language) => new Intl.Locale(language)))
    {
        if (_locale.language === "zh")
        {
            _locale = new Intl.Locale("zh", { region: _locale.maximize().region });
        }
;
        if (_locale.baseName in translations)
        {
            locale = _locale.baseName;
            break;
        }
    }

    return i18nObject(locale, translations[locale]);
})();

const EVENT_KEY = uuid();

const query = matchMedia("(prefers-color-scheme: dark)");

GM.registerMenuCommand(LL.COMMAND.SETTING(), async () =>
{
    await showLightThemeSetting();
    updateTheme(query);
});

window.addEventListener(`${EVENT_KEY}:ready`, () =>
{
    query.addEventListener("change", updateTheme);
    updateTheme(query);
});

function getLightTheme()
{
    return GM.getValue("light_theme", "lighttheme");
}

function getExpectedTheme(isDarkMode)
{
    return isDarkMode ? Promise.resolve("darktheme") : getLightTheme();
}

function getCurrentTheme()
{
    switch (true)
    {
        case document.body.classList.contains("darktheme"):
            return "darktheme";
        case document.body.classList.contains("whitetheme"):
            return "whitetheme";
        case document.body.classList.contains("browntheme"):
            return "browntheme";
        default:
            return "lighttheme";
    }
}

async function updateTheme({ matches: isDarkMode })
{
    const expectedTheme = await getExpectedTheme(isDarkMode);
    if (getCurrentTheme() !== expectedTheme)
    {
        window.dispatchEvent(new CustomEvent(`${EVENT_KEY}:setTheme`, { detail: expectedTheme }));
    }
}

function showLightThemeSetting()
{
    return new Promise(async (resolve) =>
    {
        const popupWrapper = document.createElement("div");
        popupWrapper.id = uuid();
        popupWrapper.classList.add("popupwrap", "narrowpopup", "scroll");

        const popupMask = document.createElement("div");
        popupMask.classList.add("mask", "pseudoclose");

        const popup = document.createElement("div");
        popup.classList.add("popup", "logregpopup", "popupbox", "invitebox", "scroll");

        const closeButton = document.createElement("a");
        closeButton.classList.add("close", "ignorelink", "newclose", "nonsticky", "floatright");
        closeButton.innerText = "×";

        const contents = document.createElement("div");
        contents.classList.add("padding10px");

        const subtitle = document.createElement("span");
        subtitle.classList.add("popup_subtitle", "font15px");
        subtitle.innerText = LL.SETTING.SUBTITLE();

        const form = document.createElement("form");
        form.style = "margin-top: 20px;";
        form.addEventListener("submit", async (event) =>
        {
            event.preventDefault();

            const options = new FormData(form);
            await GM.setValue("light_theme", options.get("light_theme"));

            window.dispatchEvent(new CustomEvent(`${EVENT_KEY}:hidePopup`, { detail: `#${popupWrapper.id}` }));
        });

        const lightThemeLabelWrapper = document.createElement("p");

        const lightThemeLabel = document.createElement("label");

        const lightThemeRadio = document.createElement("input");
        lightThemeRadio.classList.add("with-gap");
        lightThemeRadio.name = "light_theme";
        lightThemeRadio.type = "radio";
        lightThemeRadio.required = true;
        lightThemeRadio.value = "lighttheme";

        const lightThemeText = document.createElement("span");
        lightThemeText.innerText = LL.SETTING.LIGHT();

        const whiteThemeLabelWrapper = document.createElement("p");

        const whiteThemeLabel = document.createElement("label");

        const whiteThemeRadio = document.createElement("input");
        whiteThemeRadio.classList.add("with-gap");
        whiteThemeRadio.name = "light_theme";
        whiteThemeRadio.type = "radio";
        whiteThemeRadio.required = true;
        whiteThemeRadio.value = "whitetheme";

        const whiteThemeText = document.createElement("span");
        whiteThemeText.innerText = LL.SETTING.WHITE();

        const brownThemeLabelWrapper = document.createElement("p");

        const brownThemeLabel = document.createElement("label");

        const brownThemeRadio = document.createElement("input");
        brownThemeRadio.classList.add("with-gap");
        brownThemeRadio.name = "light_theme";
        brownThemeRadio.type = "radio";
        brownThemeRadio.required = true;
        brownThemeRadio.value = "browntheme";

        const brownThemeText = document.createElement("span");
        brownThemeText.innerText = LL.SETTING.BROWN();

        const saveButton = document.createElement("button");
        saveButton.classList.add("btn", "waves-effect", "waves-light");
        saveButton.innerHTML = `${LL.SETTING.SAVE()} <i class="material-icons right">save</i>`;

        lightThemeLabel.append(lightThemeRadio, lightThemeText);
        lightThemeLabelWrapper.append(lightThemeLabel);
        whiteThemeLabel.append(whiteThemeRadio, whiteThemeText);
        whiteThemeLabelWrapper.append(whiteThemeLabel);
        brownThemeLabel.append(brownThemeRadio, brownThemeText);
        brownThemeLabelWrapper.append(brownThemeLabel);
        form.append(lightThemeLabelWrapper, whiteThemeLabelWrapper, brownThemeLabelWrapper, saveButton);
        contents.append(subtitle, form);
        popup.append(closeButton, contents);
        popupWrapper.append(popupMask, popup);
        document.body.append(popupWrapper);

        const theme = await getLightTheme();
        switch (theme)
        {
            case "lighttheme":
                lightThemeRadio.checked = true;
                break;
            case "whitetheme":
                whiteThemeRadio.checked = true;
                break;
            case "browntheme":
                brownThemeRadio.checked = true;
                break;
        }

        const observer = new MutationObserver((records) =>
        {
            observer.disconnect();

            resolve();
            popupWrapper.remove();
        });

        observer.observe(popupWrapper, { attributes: true, attributeFilter: ["style"] });
    });
}

const PageScript = ({ EVENT_KEY }) =>
{
    const interval = setInterval(() =>
    {
        if ("setTheme" in window)
        {
            clearInterval(interval);
            window.dispatchEvent(new CustomEvent(`${EVENT_KEY}:ready`));
        }
    }, 100);

    window.addEventListener(`${EVENT_KEY}:setTheme`, ({ detail: theme }) =>
    {
        setTheme(theme);
        setCookie("darktheme", theme, 9999);
    });

    window.addEventListener(`${EVENT_KEY}:hidePopup`, ({ detail: selector }) =>
    {
        $(selector).hide();
    });
};

const scriptWrapper = document.createElement("div");
scriptWrapper.style.display = "none";
const shadowRoot = scriptWrapper.attachShadow({ mode: "closed" });
const script = document.createElement("script");
script.textContent = `(${PageScript})(${JSON.stringify({ EVENT_KEY })}); //# sourceURL=userscript://page/${encodeURI(GM.info.script.name)}.js`;
shadowRoot.append(script);
(document.body ?? document.head ?? document.documentElement).append(scriptWrapper);