添加自定义css和js(广告屏蔽等)

可自定义css选择器屏蔽页面广告,添加js脚本

当前为 2021-01-28 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         添加自定义css和js(广告屏蔽等)
// @description  可自定义css选择器屏蔽页面广告,添加js脚本
// @namespace    _cus_ad_sp
// @version      2.0.4
// @author       vizo
// updateUrl     http://dwz.win/ac4h
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/tiny-oss.min.js
// @require      https://cdn.jsdelivr.net/npm/vio-utils/utils.js
// @include      /https?\:\/\/(?!greasyfork).*/
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @noframes

// ==/UserScript==
'use strict'

;(function() {
  if (vio.isMobile()) return
  let k = GM_getValue(`_cfg_${location.host}`) || {}
  k = typeof k === 'string' ? JSON.parse(k) : k
  if (k.css && !k.disCSS) {
    GM_addStyle(`sty9z1p52{}\n${k.css}\n`)
  }
})();

GM_addStyle(`
  #wp5sn [hidden] {
    display: none !important;
  }
  #wp5sn, #wp5sn * {
    margin: 0;
    padding: 0;
    box-sizing: border-box !important;
  }
  #wp5sn {
    width: 28vw;
    height: 68vh;
    min-width: 400px;
    padding: 30px;
    background: #fff;
    border-radius: 3px;
    font-family: sans-serif,"HelveticaNeue",Helvetica,"PingFangSC","MicrosoftYaHei","HiraginoSansGB",Arial;
    line-height: 1.5;
    font-size: 12px;
    resize: both;
    box-shadow: 0 0 5px #ccc;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 50050;
    margin: auto;
  }
  #wp5sn .mwp_5c {
    height: 100%;
    display: flex;
    flex-direction: column;
  }
  #wp5sn .tit1v {
    color: #555;
    font-size: 18px;
    text-align: center;
    margin-bottom: 15px;
  }
  #wp5sn .c7d-item {
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
  }
  #wp5sn .allset-item {
    flex: 1;
  }
  #wp5sn .css-item-xh {
    flex: 3;
    transition: flex .3s;
  }
  #wp5sn .js-item-xh {
    flex: 1;
    transition: flex .3s;
  }
  #wp5sn .mwp_5c.expandJS .css-item-xh {
    flex: 1;
  }
  #wp5sn .mwp_5c.expandJS .js-item-xh {
    flex: 3;
  }
  #wp5sn .stiz {
    font-size: 14px;
    color: #555;
    margin-bottom: 3px;
    position: relative;
    text-align: left;
    display: flex;
  }
  #wp5sn .tamp-cfg-modal {
    width: 250px;
    height: 170px;
    padding: 10px;
    border-radius: 2px;
    background: #f1f1f1;
    position: absolute;
    top: -50px;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
    z-index: 2;
  }
  #wp5sn .tamp-cfg-modal .txa-cfg {
    width: 100%;
    height: 100%;
    resize: none;
    color: #777 !important;
    font-family: Consolas;
    font-size: 12px;
    padding: 6px;
    overflow-y: auto;
    border: 1px solid #e6e6e6;
    background: #fafafa;
  }
  #wp5sn .tamp-cfg-modal .txa-cfg::-webkit-input-placeholder {
    color: #ccc !important;
  }
  #wp5sn .stiz .s0l {
    flex: 1;
  }
  #wp5sn .stiz .s0r {
    color: #09e;
    cursor: pointer;
    user-select: none;
    margin-left: 10px;
  }
  #wp5sn .stiz .s0r.on {
    color: #9a9a9a;
  }
  #wp5sn .oss-zbtn {
    color: #c7c7c7;
    cursor: pointer;
    user-select: none;
    margin-right: 10px;
  }
  #wp5sn .view-all-set {
    color: #09e;
    cursor: pointer;
    user-select: none;
  }
  #wp5sn .st1k {
    display: flex;
  }
  #wp5sn .st1k .s1p {
    flex: 1;
  }
  #wp5sn .st1k .s2p {
    width: 65px;
    color: #09e;
    cursor: pointer;
    text-align: right;
    white-space: nowrap;
    overflow: hidden;
  }
  #wp5sn .inpy {
    flex: 0 0 auto;
    height: 32px;
    border: 1px solid #ddd;
    color: #555;
    border-radius: 2px;
    padding: 0 10px;
    outline: none;
  }
  #wp5sn .inpy:focus {
    border: 1px solid #c1c1c1;
  }
  #wp5sn .txtr1z {
    width: 100%;
    flex: 1;
    color: #555;
    padding: 6px;
    line-height: 1.4;
    overflow-x: hidden;
    overflow-y: auto;
    border-radius: 2px;
    border: 1px solid #ddd;
    background: #fff;
    font-size: 12px;
    resize: none;
    white-space: pre-line;
    outline: none;
    font-family: Consolas,sans-serif,"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei";
  }
  #wp5sn .txtr1z::-webkit-input-placeholder {
    color: #c5c5c5;
  }
  #wp5sn .txtr1z:focus {
    border: 1px solid #39e;
  }
  #wp5sn .txtr1z.disabled {
    color: #999;
    background: #f5f5f5;
  }
  #wp5sn .txtr1z::-webkit-scrollbar {
    width: 4px;
  }
  #wp5sn .txtr1z::-webkit-scrollbar-corner,
  #wp5sn .txtr1z::-webkit-scrollbar-track {
    background-color: #fff;
  }
  #wp5sn .txtr1z::-webkit-scrollbar-thumb {
    background: #fff;
  }
  #wp5sn .txtr1z:hover::-webkit-scrollbar-thumb {
    background: #e1e1e1;
  }
  #wp5sn .txtr1z:hover::-webkit-scrollbar-corner,
  #wp5sn .txtr1z:hover::-webkit-scrollbar-track {
    background-color: #f7f7f7;
  }
  #wp5sn .btn-w {
    margin-top: 5px;
    display: flex;
    flex-direction: row-reverse;
  }
  #wp5sn .btn-w .c5kbtn {
    width: 90px;
    height: 32px;
    border-radius: 2px;
    margin-right: 10px;
    font-family: sans-serif,"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei" !important;
    cursor: pointer;
    outline: none;
    border: 0;
  }
  #wp5sn .btn-w .c5kbtn.b1 {
    color: #fff !important;
    background: #09e !important;
  }
  #wp5sn .btn-w .c5kbtn.b2 {
    color: #555 !important;
    background: #f1f1f1 !important;
  }
  #wp5sn .btn-w .c5kbtn:first-child {
    margin-right: 0;
  }
  #wp5sn .btn-w .c5kbtn:focus {
    border: 0;
  }
  #wp5sn .btn-w .c5kbtn:hover {
    opacity: 0.9;
  }
`)

const G = {
  html: `
    <div id="wp5sn" v-show="dialog1s">
      <div class="mwp_5c" :class="{'expandJS': isExpandJS}">
        <div class="tit1v">设置</div>
        <div class="tamp-cfg-modal" v-show="isShowTampModal">
          <textarea class="txa-cfg" @change="changeTampCfg" v-model="tampCfgVal" placeholder="oss配置"></textarea>
        </div>
        <div class="c7d-item">
          <p class="stiz">
            <span class="s0l">打开面板快捷键</span>
            <span class="oss-zbtn" @click="hdlTgOssModal">oss</span>
            <span class="view-all-set" @click="hdlViewAllSet">{{ viewSetText }}</span>
          </p>
          <input type="text" class="inpy" v-model="eKey" placeholder="请输入a-z的任意字母">
        </div>
        <div class="c7d-item css-item-xh" v-show="!showAllSet">
          <p class="stiz">
            <span class="s0l">添加css(不含style标签)</span>
            <span class="s0r" :class="{on: disCSS}" @click="hdlTgDisCss">{{ disCssText }}</span>
          </p>
          <textarea class="txtr1z" :class="{'disabled': disCSS}" v-model="texCssVal" :readonly="disCSS" spellcheck="false" placeholder="请输入css代码" @click="hdlExpandJSJs(1)"></textarea>
        </div>
        <div class="c7d-item js-item-xh" v-show="!showAllSet">
          <p class="stiz">
            <span class="s0l">添加js(不含script标签)</span>
            <span class="s0r" :class="{on: disJS}" @click="hdlTgDisJs">{{ disJsText }}</span>
          </p>
          <textarea class="txtr1z" :class="{'disabled': disJS}" v-model="texJsVal" :readonly="disJS" spellcheck="false" placeholder="请输入js代码" @click="hdlExpandJSJs(2)"></textarea>
        </div>
        <div class="c7d-item allset-item" v-show="showAllSet">
          <p class="stiz st1k">
            <span class="s1p">已添加的网站(可删除) {{ addedNum }} 个 </span>
            <span class="s2p imt-c" @click="hdlImportCfg">导入配置</span>
            <span class="s2p ext-c" @click="hdlExportCfg">导出配置</span>
          </p>
          <input type="file" hidden ref="inp_hide" @change="hdlUpFile">  
          <textarea class="txtr1z" v-model="allAddedStr"></textarea>
        </div>
        <div class="btn-w">
          <button class="c5kbtn b2" @click="hdlCancel">取消</button>
          <button class="c5kbtn b1" @click="hdlSave">保存</button>
        </div>
      </div>
    </div>
  `,
}
const vm = new Vue({
  data() {
    return {
      // 模态框状态
      dialog1s: false,
      // 快捷键名称
      eKey: '',
      showAllSet: false,
      // css代码
      texCssVal: '',
      // js代码
      texJsVal: '',
      // 已添加的网站
      allAddedStr: '',
      disCSS: false,
      disJS: false,
      // 是否展开js
      isExpandJS: false,
      // oss配置modal
      isShowTampModal: false,
      tampCfgVal: '',
    }
  },
  computed: {
    viewSetText() {
      return this.showAllSet ? '查看当前网站' : '查看全部网站'
    },
    addedNum() {
      return this.allAddedStr
        .split('\n')
        .filter(v => !!v)
        .length
    },
    disCssText() {
      return this.disCSS ? '已禁用' : '禁用css'
    },
    disJsText() {
      return this.disJS ? '已禁用' : '禁用js'
    },
  },
  watch: {
    eKey(nVal) {
      this.eKey = /[a-z]/.test(nVal) ? nVal.slice(0, 1) : ''
      GM_setValue('_gus_keyboard', this.eKey)
    },
    dialog1s(nVal) {
      if (!nVal) {
        this.showAllSet = false
        this.isShowTampModal = false
      }
    },
  },
  methods: {
    setGmVal(val) {
      return GM_setValue(`_cfg_${location.host}`, val)
    },
    getGmVal() {
      let gmVal = GM_getValue(`_cfg_${location.host}`) || {}
      return typeof gmVal === 'string' ? JSON.parse(gmVal) : gmVal
    },
    hdlTgOssModal() {
      this.isShowTampModal = !this.isShowTampModal
    },
    hdlExpandJSJs(type) {
      this.isExpandJS = type === 2
    },
    changeTampCfg() {
      GM_setValue('tampOssCfg7n', this.tampCfgVal)
    },
    async hdlSave() {
      if (!this.showAllSet) {
        this.saveCssAndJs()
        this.initAddedWebToTextArea()
      } else {
        this.updateTextAreaValToGm()
        await this.saveJsonToOss('state')
        await this.saveJsonToOss('cfg')
        location.reload()
      }
      this.dialog1s = false
    },
    hdlCancel() {
      this.dialog1s = false
    },
    // 禁用css
    hdlTgDisCss() {
      this.disCSS = !this.disCSS
    },
    // 禁用js
    hdlTgDisJs() {
      this.disJS = !this.disJS
    },
    async saveCssAndJs() {
      let css = `<style class="sty9z1p52">${this.texCssVal}</style>`
      document.head.insertAdjacentHTML('beforeend', css)
      
      Array.from(document.querySelectorAll('.sty9z1p52')).forEach((v, i, y) => {
        if (i !== y.length - 1) {
          v.remove()
        } else {
          v.disabled = this.disCSS
        }
      })
      
      const gm = this.getGmVal()
      this.setGmVal({
        ...gm,
        css: this.texCssVal,
        js: this.texJsVal,
        disCSS: this.disCSS,
        disJS: this.disJS,
      })
      
      // 同步至oss
      await this.saveJsonToOss('state')
      await this.saveJsonToOss('cfg')
      
      if (gm.disJS !== this.disJS || gm.js !== this.texJsVal) {
        await vio.timeout(500)
        location.reload()
      }
      
    },
    saveJsonToOss(name) {
      let lsCfg = GM_getValue('tampOssCfg7n')
      if (!lsCfg) return Promise.resolve()
      lsCfg = typeof lsCfg === 'string' ? JSON.parse(lsCfg) : lsCfg
      const oss = new TinyOSS(lsCfg.ossParams)
      const lastTime = Date.now()
      const data = name === 'state' ? {lastTime} : this.getAllCfg()
      const blob = new Blob([JSON.stringify(data)], { type: 'text/json' })
      
      GM_setValue('tampCfgUpdateTime', lastTime)
      return new Promise(resolve => {
        try {
          oss.put(`json/tamp-cfg/${name}.json`, blob, {
            onprogress(e) {
              if (e.total > 0) {
                return resolve(true)
              }
            }
          })
        } catch (err) {
          console.log( '同步失败(saveJsonToOss):', err.message )
        }
      })
    },
    async updateCfgFromOss() {
      const lsCfg = this.getOssCfg()
      const gmLastTime = GM_getValue('tampCfgUpdateTime')
      if (lsCfg) {
        try {
          const res = (await (await fetch(lsCfg.state)).json())
          const { lastTime } = res
          const now = Date.now()
          if (
            !gmLastTime ||
            lastTime > gmLastTime ||
            now - gmLastTime > 30e3
          ) {
            const rCfg = await ((await fetch(lsCfg.cfg)).json())
            this.updateJsonToGm(rCfg)
            this.initAddedWebToTextArea()
            GM_setValue('tampCfgUpdateTime', now)
          }
        } catch (err) {
          console.log( '更新配置失败(updateCfgFromOss)::', err.message )
        }
      }
    },
    
    // 判断是否从远程更新
    async judgeIsUpdateCfgFromOss() {
      const gmTime = GM_getValue('pageRefreshTime')
      const now = Date.now()
      if (!gmTime || now - gmTime > 5000) {
        GM_setValue('pageRefreshTime', now)
        await this.updateCfgFromOss()
      }
    },
    
    updateTextAreaValToGm() {
      const gmArr = GM_listValues()
        .filter(v => v.startsWith('_cfg_'))
        .map(v => v.replace(/^_cfg_/, ''))
      const cArr = this.allAddedStr.split('\n')
      gmArr.forEach(v => {
        if (!cArr.some(c => v === c)) {
          GM_deleteValue(`_cfg_${v}`)
        }
      })
    },
    
    updateJsonToGm(obj) {
      if (!obj) return
      GM_listValues()
        .filter(v => /^_gus_|^_cfg_/.test(v))
        .forEach(v => {
          GM_deleteValue(v)
        })
      
      for (let k in obj) {
        GM_setValue(k, obj[k])
      }
    },
    
    tgCfgDialog() {
      this.dialog1s = !this.dialog1s
    },
    hdlViewAllSet() {
      this.showAllSet = !this.showAllSet
    },
    // 导入配置
    hdlImportCfg() {
      this.$refs.inp_hide.click()
    },
    // 导出配置
    hdlExportCfg() {
      const res = this.getAllCfg()
      vio.downloadText(JSON.stringify(res, null, 2), '1.json')
    },
    
    // 获取oss配置
    getOssCfg() {
      let lsCfg = GM_getValue('tampOssCfg7n')
      if (!lsCfg) return
      try {
        return typeof lsCfg === 'string' ? JSON.parse(lsCfg) : lsCfg
      } catch (err) {
        console.log('获取oss配置出错::', err.message)
      }
    },
    
    initOssVal() {
      const lsCfg = this.getOssCfg()
      if (lsCfg) {
        this.tampCfgVal = JSON.stringify(lsCfg)
      }
    },
    
    // 获取所有已配置的网站数据
    getAllCfg() {
      return GM_listValues()
        .filter(v => /^_gus_|^_cfg_/.test(v))
        .reduce((acc, v) => {
          let gmVal = GM_getValue(v)
          gmVal = typeof gmVal === 'string' && gmVal > 1 ? JSON.parse(gmVal) : gmVal
          return {
            ...acc,
            [v]: gmVal
          }
        }, {})
    },
    
    hdlUpFile(e) {
      let file = e.target.files[0]
      if (file) {
        let reader = new FileReader()
        reader.readAsText(file, 'utf-8')
        reader.onload = async (evt) => {
          try {
            oUp = JSON.parse(evt.target.result)
            this.updateJsonToGm(oUp)
            
            await this.saveJsonToOss('state')
            await this.saveJsonToOss('cfg')
            
            this.initCssJsVal()
            this.initAddedWebToTextArea()
            this.initAddedScript()
            this.initGMStyleClass()
            
            setTimeout(() => {
              location.reload()
            }, 200)
          } catch (e) {
            // 上传失败
          }
        }
      }
    },
    async resetCss() {
      this.texCssVal = ''
      this.disCSS = false
      this.setGmVal({
        ...this.getGmVal(),
        css: '',
        disCSS: false,
      })
      await this.saveJsonToOss('state')
      await this.saveJsonToOss('cfg')
      location.reload()
    },
    async resetJs() {
      this.texJsVal = ''
      this.disJS = false
      this.setGmVal({
        ...this.getGmVal(),
        js: '',
        disJS: false,
      })
      await this.saveJsonToOss('state')
      await this.saveJsonToOss('cfg')
      location.reload()
    },
    initEvt() {
      window.addEventListener('keydown', e => {
        if (
          !(/text|search|number|password|tel|url|email/.test(e.target.type)) &&
          e.target.tagName !== 'TEXTAREA' &&
          !e.altKey && 
          !e.ctrlKey &&
          e.key === this.eKey
        ) {
          this.tgCfgDialog()
        }
        if (/esc/i.test(e.key)) {
          this.dialog1s = false
        }
      })
    },
    initEkey() {
      this.eKey = GM_getValue('_gus_keyboard') || ''
    },
    initCssJsVal() {
      this.texCssVal = this.getGmVal().css || ''
      this.texJsVal = this.getGmVal().js || ''
      this.disCSS = !!this.getGmVal().disCSS
      this.disJS = !!this.getGmVal().disJS
    },
    initAddedWebToTextArea() {
      const gmArr = GM_listValues()
        .filter(v => v.startsWith('_cfg_'))
        .map(v => v.replace(/^_cfg_/, ''))
      this.allAddedStr = gmArr.join('\n')
    },
    initAddedScript() {
      let js = this.getGmVal().js
      let isDisabled = this.getGmVal().disJS
      if (js && !isDisabled) {
        let st = document.createElement('script')
        st.textContent = js
        document.body.appendChild(st)
      }
    },
    initGMStyleClass() {
      document.querySelectorAll('style').forEach(v => {
        if (!/\w{8}(-\w{4}){3}-\w{12}/.test(v.id)) {
          return
        }
        if (v.textContent.includes('sty9z1p52')) {
          v.classList.add('sty9z1p52')
        }
      })
    },
  },
  async mounted() {
    this.initEvt()
    this.initOssVal()
    await this.judgeIsUpdateCfgFromOss()
    this.initEkey()
    this.initCssJsVal()
    this.initAddedWebToTextArea()
    this.initAddedScript()
    this.initGMStyleClass()
  },
})

const initFunc = () => {
  if (vio.isMobile()) return
  document.addEventListener('DOMContentLoaded', () => {
    document.body.insertAdjacentHTML('beforeend', G.html)
    vm.$mount('#wp5sn')
    GM_registerMenuCommand('打开设置面板', vm.tgCfgDialog)
    GM_registerMenuCommand('清空当前网站添加的CSS', vm.resetCss)
    GM_registerMenuCommand('清空当前网站添加的JS', vm.resetJs)
  }, false)
}

initFunc()