Greasy Fork

主题切换

网站@media (prefers-color-scheme: dark|light)主题样式切换,深色模式和浅色模式的切换

// ==UserScript==
// @name         主题切换
// @namespace    http://tampermonkey.net/
// @version      0.1.10
// @description  网站@media (prefers-color-scheme: dark|light)主题样式切换,深色模式和浅色模式的切换
// @author       taumu
// @license      MIT
// @include      *://*.weixin.*
// @include      *://*.microsoft.*
// @include      *://sspai.*
// @run-at       document-start
// @require      https://unpkg.com/style-media-toggle
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @namespace    https://greasyfork.org/users/445432
// ==/UserScript==

;(function() {
  // eslint-disable-next-line
  'use strict'

  const mediaName = 'prefers-color-scheme'
  const { matches: isDark } = matchMedia(`(${mediaName}: dark)`)
  const { host, origin } = window.location
  const saveName = `${mediaName}:${host}`
  let isCallMatchMedia = false

  function getValue(name, defaultVal = true) {
    // eslint-disable-next-line no-undef
    return GM_getValue(name, defaultVal)
  }

  unsafeWindow.matchMedia = (mediaText, ...other) => {
    const result = matchMedia(mediaText, ...other)
    if (mediaText.includes(mediaName)) {
      isCallMatchMedia = true;
      const value = getValue(saveName)
      const matches = mediaText.includes('dark') ? value : !value

      return {
        ...result,
        matches,
        addListener: () => {}
      }
    }
    return result
  }

  function registerMenu(title, name) {
    const value = name && getValue(name)
    let rTitle = title
    if (name && value) {
      rTitle += '√'
    }
    // eslint-disable-next-line no-undef
    GM_registerMenuCommand(rTitle, () => {
      if (name) {
        // eslint-disable-next-line no-undef
        GM_setValue(name, !value)
        window.location.reload()
      } else {
        alert('当前系统主题下无可切换主题')
      }
    })
  }

  function getUrl(src, path) {
    if (['http', 'https'].some(head => path.startsWith(head))) return path

    const fixPath = path.replace(/["']/g, '')
    const sList = src.split('/')
    const pList = fixPath.split('/')
    const hostPath = sList
      .slice(0, 3)
      .filter(v => !!v)
      .join('//')
    let absPath
    if (fixPath.startsWith('/')) {
      absPath = pList.slice(1)
    } else {
      const lastIndex = pList.lastIndexOf('..')
      const index = lastIndex === -1 ? undefined : lastIndex + 1
      absPath = sList
        .slice(3, index && -index)
        .concat(pList.slice(index).filter(v => v !== '.'))
    }
    return `url(${hostPath}/${absPath.join('/')})`
  }

  function replaceCssText(cssText, href) {
    const lastIndex = href.lastIndexOf('/')
    const src = href.slice(0, lastIndex)
    return cssText.replace(/url\(([^)]*)\)/g, (match, p1) => {
      if (p1.includes('data:image')) {
        return match
      }
      return getUrl(src, p1)
    })
  }

  const herfSet = new Set()
  function replaceStyle(styleSheet) {
    const { href } = styleSheet
    if (herfSet.has(href)) return

    herfSet.add(href)
    // eslint-disable-next-line no-undef
    GM_xmlhttpRequest({
      method: 'GET',
      url: href,
      headers: {
        referer: origin
      },
      onload: res => {
        const { responseText } = res
        const style = document.createElement('style')
        style.innerText = replaceCssText(responseText, href)
        // eslint-disable-next-line no-param-reassign
        styleSheet.disabled = true
        document.head.appendChild(style)
      },
    })
  }

  // eslint-disable-next-line no-undef
  const mediaToggle = getMediaToggle({
    onError(e, styleSheet) {
      replaceStyle(styleSheet)
    },
  })

  const themeMap = new Map([
    [
      'dark',
      {
        isDefault: isDark,
        title: '深色模式',
        menuId: null,
      },
    ],
    [
      'light',
      {
        isDefault: !isDark,
        title: '浅色模式',
        menuId: null,
      },
    ],
  ])
  let first = false
  function toggle() {
    first = true
    const mediaMap = mediaToggle.get()
    const keys = Array.from(mediaMap.keys()).filter(key =>
      key.includes(mediaName)
    )

    themeMap.forEach((theme, k) => {
      const { menuId, title, isDefault } = theme
      if (keys.length || isCallMatchMedia) {
        if (isDefault) {
          const key = keys.find(item => item.includes(k))
          const media = key && mediaMap.get(key)
          const value = getValue(saveName)
          if (media) {
            media.toggle(!value)
          }
          // eslint-disable-next-line no-param-reassign
          if (!menuId) theme.menuId = registerMenu(title, saveName)
        }
      } else if (menuId) {
        // eslint-disable-next-line no-undef
        GM_unregisterMenuCommand(menuId)
        // eslint-disable-next-line no-param-reassign
        theme.menuId = null
      }
    })
  }

  mediaToggle.subscribe(toggle)
  if (!first) toggle()
})()