你看的时间太长了

You see it too long time.

// ==UserScript==
// @name 你看的时间太长了
// @namespace You see it too long time.
// @version 0.1.1
// @author 稻米鼠
// @description You see it too long time.
// @run-at document-idle
// @homepage https://meta.appinn.net/t/11501
// @supportURL https://meta.appinn.com/t/11501
// @match *://*/*
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// ==/UserScript==

// 模式 0:限制总观看时间;模式 1:限制每个网站观看时间
// const youSeeItTooLongTimeMode = 1
// 最大观看时长,单位:分钟
// const howMinutesCanYouWatchIt = 60
/* 以下无需修改 */
const oldLoadFun = window.onload
window.onload = async function(){
  oldLoadFun && oldLoadFun()
  // 获取当前所需时间对象
  const getNow = ()=>{
    const nowUTC = new Date()
    const now = Number((+nowUTC/1000).toFixed()) - nowUTC.getTimezoneOffset()*60
    const day = now - now%86400
    return { now, day }
  }
  // 储存当前数据
  const storeData = async (long)=>{
    await GM_setValue(window.location.hostname, JSON.stringify({
      day: start.day,
      long: long
    }))
  }
  // 获取存储的数据
  const getData = async (key=window.location.hostname)=>{
    if(await GM_getValue(key)){
      try {
        const temp = JSON.parse(await GM_getValue(key))
        // 如果有合理的数据,并且是当天数据
        if(temp.day && temp.day === start.day && temp.long){
          return temp
        }
      } catch (error) {}
    }
    return false
  }
  // 储存配置数据
  const storeConfig = async (mode=1, max=3600)=>{
    const config = {
      mode: isNaN(mode) ? 1 : mode,
      max: isNaN(max) ? 3600 : max
    }
    await GM_setValue('options', JSON.stringify(config))
    return config
  }
  // 获取配置数据
  const getConfig = async ()=>{
    if(await GM_getValue('options')){
      try {
        const temp = JSON.parse(await GM_getValue('options'))
        // 如果有合理的数据,并且是当天数据
        if(!isNaN(temp.mode) && !isNaN(temp.max)){
          return temp
        }
      } catch (error) {
        this.console.log(error)
      }
    }
    return await storeConfig()
  }
  // 修改网页标题
  const changeTitle = title=>{
    document.title = title+document.title.replace(/^【[\d:-]+】/i, '')
  }
  // 两位数字
  const dbNum = num=>{
    return num<10 ? '0'+num : num
  }
  // 窗口是否最小化
  const isMin = ()=>{
    let isWinMin = false;
    if (window.outerWidth != undefined) {
        isWinMin = window.outerWidth <= 160 && window.outerHeight <= 27;
    } else {
        isWinMin = window.screenTop < -30000 && window.screenLeft < -30000;
    }
    return isWinMin;
  }
  // 处理 hash 指令
  const hashCommand = async (hash, options)=>{
    if(/^#clear$/i.test(hash)){
      await storeData( 0 )
      window.location.hash = ''
      return
    }
    if(/^#clearAll$/i.test(hash)){
      const keys = await GM_listValues()
      for (let key of keys) {
        if(key==='options'){ continue }
        GM_deleteValue(key);
      }
      window.location.hash = ''
      return
    }
    if(/^#close$/i.test(hash)){
      await storeData( 1.25*options.max )
      window.location.hash = ''
      return
    }
    if(/^#add\d+$/i.test(hash)){
      let add = +hash.replace(/^#add(\d+)$/, '$1')
      add = isNaN(add) ? 0 : add*60
      const siteData = await getData()
      let historyLong = siteData ? siteData.long : 0
      historyLong = historyLong<add ? 0 : historyLong-add
      await storeData(historyLong)
      window.location.hash = ''
      return
    }
    if(/^#sub\d+$/i.test(hash)){
      let sub = +hash.replace(/^#sub(\d+)$/, '$1')
      sub = isNaN(sub) ? 0 : sub*60
      const siteData = await getData()
      let historyLong = siteData ? siteData.long : 0
      historyLong = historyLong+sub
      await storeData(historyLong)
      window.location.hash = ''
      return
    }
    if(/^#mode\d$/i.test(hash)){
      let mode = +hash.replace(/^#mode(\d)$/, '$1')
      mode = isNaN(mode) ? 1 : (mode===0 ? 0 : 1)
      await storeConfig(mode, options.max)
      window.location.hash = ''
      return
    }
    if(/^#max\d+$/i.test(hash)){
      let max = +hash.replace(/^#max(\d+)$/, '$1')
      if(isNaN(max)){ return }
      max = 60*max
      await storeConfig(options.mode, max)
      window.location.hash = ''
      return
    }
    if(/^#maxadd\d+$/i.test(hash)){
      let add = +hash.replace(/^#maxadd(\d+)$/, '$1')
      if(isNaN(add)){ return }
      const max = options.max + 60*add
      await storeConfig(options.mode, max)
      window.location.hash = ''
      return
    }
    if(/^#maxsub\d+$/i.test(hash)){
      let sub = +hash.replace(/^#maxsub(\d+)$/, '$1')
      if(isNaN(sub)){ return }
      const max = options.max - 60*sub
      await storeConfig(options.mode, max>=0 ? max : 0)
      window.location.hash = ''
      return
    }
  }
  // 时间更新
  const refreshTime = async ()=>{
    const n = getNow()  // 获取当前时间
    // 如果页面在后台或者窗口最小化,仅更新起始对象中的最后更新时间
    if(document.visibilityState !== 'visible' || isMin()){
      start.lastTime = n.now
      return
    }
    // 获取当前配置
    const options = await getConfig()
    // 检测指令
    await hashCommand(window.location.hash, options)
    // 如果是当天
    const siteData = await getData()
    start.historyLong = n.day === start.day
                        ? (siteData ? siteData.long : 0) + n.now - start.lastTime
                        : n.now%86400
    start.day = n.day
    start.lastTime = n.now
    await storeData(start.historyLong)

    let lastTime = options.max
    if(options.mode){
      lastTime = options.max - start.historyLong
    }else{
      const keys = await GM_listValues()
      for (let key of keys) {
        if(key==='options'){ continue }
        const m = await getData(key)
        if(m.day === start.day){
          lastTime -= m.long
        }else{
          m && GM_deleteValue(key);
        }
      }
    }
    if(lastTime < 0){
      const alpha = 1 + 4*lastTime/options.max
      const opacity = ( alpha>1 ? 1 : ( alpha<0 ? 0 : alpha ) ).toFixed(2)
      document.querySelector('body').style.opacity = opacity
      if(document.fullscreenElement){// 如果全屏
        document.fullscreenElement.style.opacity = opacity
      }
      if(alpha<0){
        if(document.fullscreenElement){ document.exitFullscreen() } // 如果全屏则退出
        document.querySelector('html').style.backgroundImage = 'url("https://i.v2ex.co/WJ15n12m.png")'
        document.querySelector('html').style.backgroundPosition = 'center top'
        document.querySelector('html').style.backgroundRepeat = 'no-repeat'
        document.querySelector('html').style.backgroundAttachment = 'fixed'
        window.clearInterval(timer)
      }
    }else{
      document.querySelector('body').style.opacity = 1
    }
    const lastTimeToShow = (lastTime>=0 ? '' : '-')
                          + dbNum( Math.floor( Math.abs(lastTime)/60) )
                          + ':'
                          + dbNum( Math.abs(lastTime)%60 )
    changeTitle('【'+lastTimeToShow+'】')
  }
  // 初始化 启动程序
  const start = getNow()
  const siteData = await getData()
  start.lastTime = start.now
  start.historyLong = siteData ? siteData.long : 0
  await storeData( start.historyLong )

  refreshTime()
  const timer = window.setInterval(refreshTime, 1e3)
}