5ch Plus

5ch を拡張します

// ==UserScript==
// @name 5ch Plus
// @name:ja 5ch プラス
// @description 5ch を拡張します
// @author null-chan <ntCcZgFmUxGztDGCLuEtgrHkst2CjtrFU3eJxvqh@protonmail.ch>
// @version 0.1.0
// @run-at document-body
// @include http*://*5ch.net/*
// @include http*://imgur.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/twemoji/12.0.4/twemoji.min.js
// @grant none
// @copyright Copyright (c) 2019 null-chan
// @license MIT
// @namespace https://greasyfork.org/users/379206
// ==/UserScript==

(() => {

  'use strict'

  // ################ 設定 ################ //
  //
  // true = 有効 | false = 無効
  //
  const config = {
    background_url: 'https://picsum.photos/1920/1080', // 背景画像。 base64 使用可
    background_blur: true, // 背景画像のぼかし
    preview_limit: 100, // テキストファイルプレビュー時の文字数制限
  }
  // ################ 設定 ################ //

  $(window).on('load', () => console.info('window.on(\'load\')'))

  const parse_json = object => {
    let x
    let ret = ''
    for (x in object) {
      let y
      let dec = ''
      for (y in object[x]) dec += `${y}:${object[x][y]}!important;`
      ret += `${x}{${dec}}`
    }
    return ret
  }

  // https://stackoverflow.com/questions/1484506/random-color-generator
  const generateColor = () => {
    const letters = '0123456789abcdef'
    let color = ''
    for (let i = 0; i < 6; i++)
      color += letters[Math.floor(Math.random() * 16)]
    return color
  }

  const preset = {
    'imgur.com/[a-zA-Z0-9]{7}': {
      script: () => (location.href = $('meta[name="twitter:image"]').attr('content')),
    },
    global_5ch: {
      hostname: '5ch.net',
      style: {
        html: {
          'font-family': 'メイリオ,Arial',
          cursor: 'progress',
        },
        hidden: {
          display: 'none',
          opacity: 0,
        },
        'img.emoji': {
          height: '1em',
          width: '1em',
          'margin-bottom': '0.1em',
          'user-select': 'all',
        },
        'img.emoji::selection': {
          background: 'white',
        },
        '.gray_om': {
          color: '#808080',
        },
      },
      script: () => twemoji.parse(document.body),
    },
    '(.*[^jump][.]5ch[.]net/[a-z]{1,}/(#[0-9]{1,2})?)|.*[.]5ch[.]net/test/read.cgi/.*': {
      script: () => {
        $(window).on('load', () => {
          $('html').attr('style', 'cursor:default!important')
          $('a[href^="https://"],a[href^="http://"],a[href^="/"],a[href^="../"]').each(function () {
            let href = $(this).attr('href')
            console.info(`Detected link: %c${$(this).html()}%c - %c${href}`, 'text-decoration:underline', '', 'text-decoration:underline')
            if (/^(https?:)?\/\/jump[.]5ch[.]net\/[?]/.test($(this).attr('href'))) href = $(this).text()
            $(this)
              .addClass('rinku')
              .text(decodeURI($(this).text()))
              .attr({
                href: href,
                rel: 'noreferrer noopener',
                target: '_blank',
                title: 'ページは新しいタブで開かれます (rel="noreferrer noopener")',
              })
          })
          $(document).on('mouseenter', 'dl.thread a,.post .message .rinku,.post_hover .message .rinku', function () {
            console.info(`Hovered: ${$(this).attr('href')}`)
            if ($(this).hasClass('loadeding') ||
              $(this).hasClass('reply_link')) return
            let original_element = $(this).parents('.post_hover').length
              ? `#${$(this).parents('.post_hover').attr('data-ha')} .message .rinku:nth-of-type(${$(this).parents('.post_hover').find('.message .rinku').index(this) + 1})`
              : null
            $(this).addClass('loadeding').attr('title', 'Loading...')
            const uralu = (new RegExp('^(https?:)?//.*5ch[.]net/').test($(this).attr('href')) ? '' : 'https://cors-anywhere.herokuapp.com/')
              + encodeURI($(this).attr('href'))
            if (original_element) $(original_element).addClass('loadeding').attr('title', 'Loading...')
            console.info(`Loading: ${uralu}`)
            $.get(uralu, (res, _, xhr) => {
              const type = xhr.getResponseHeader('content-type')
              console.info(`Loaded: ${uralu} - ${type}`)
              let title
              let element
              if (type.includes('text/html')) {
                title = $(res).filter('title').text() || 'No title found'
                element = `<br><span class='gray_om'>${title}</span>`
              } else if (type.includes('image/')) {
                title = 'DuckDuckGo Image Proxy'
                element = `<br><span><img class='preview_img' title='クリックで非表示' src='https://proxy.duckduckgo.com/iu/?u=${$(this).attr('href')}'/><span class='preview_img hidden' style='color:#808080'>画像プレビューが非表示になっています。クリックで表示...</span></span>`
              } else if (type.includes('application/pdf')) {
                title = 'PDF ファイルのプレビューは対応していません'
                element = '<br><span class=\'gray_om\'>PDF ファイルのプレビューは対応していません</span>'
              } else if (type.includes('application/json')) {
                if (res.length > config.preview_limit) {
                  title = `${config.preview_limit}文字を超えているため、プレビューを表示できません`
                  element = `<br><span class='gray_om' title='制限文字数は設定から変更ができます'>${config.preview_limit}文字を超えているため、プレビューを表示できません</span>`
                  return
                }
                if (type.includes('application/json')) {
                  title = 'JavaScript Object Notation (JSON)'
                  element = `<br><span class='gray_om'>${JSON.stringify(res)}</span>`
                }
              } else {
                title = `Preview is not available - ${type}`
                element = `<br><span class='gray_om'>Preview is not available - ${type}</span>`
              }
              if (original_element) $(original_element).attr('title', title).after(element)
              $(this).attr('title', title).after(element)
            })
          })
          $(document).on('click', '.preview_img', function () {
            $(this).toggleClass('hidden')
            $(this).siblings('.preview_img').toggleClass('hidden')
            if ($(this).parents('.post_hover').length) {
              const original_element = `#${$(this).parents('.post_hover').attr('data-ha')} .message ${$(this)[0].tagName}.preview_img:nth-of-type(${$(this).parents('.post_hover').find('.message .preview_img').index(this) + 1})`
              $(original_element).toggleClass('hidden')
              $(original_element).siblings('.preview_img').toggleClass('hidden')
            }
          })
        })
      },
    },
    'http://.*5ch[.]net.*': {
      protocol: true,
      script: () => location.href = location.href.replace(new RegExp('^http://'), 'https://'),
    },
    '.*[^jump][.]5ch[.]net/[a-z]{1,}/(#[0-9]{1,2})?': {
      style: {
        body: {
          background: `url(${config.background_url}) center no-repeat fixed`,
          'background-size': 'cover',
        },
        'div[style="margin: 0; padding: 0 0 0 0.5em; border-top: 0.5em solid #BEB; border-bottom: 0.5em solid #BEB; border-radius: 0.50em / 0.50em; padding: 0 0 1.0em 0.5em; height: 25em; overflow-y: scroll; background: #BEB;"]': {
          height: 'auto',
          'max-height': '25em',
        },
        '.ADVERTISE_AREA,div[style="margin-top:10px;margin-bottom:10px;width:100%;text-align:center;"]': {
          display: 'none',
        },
        'p[style="margin: 0.75em 30% 0 30%; padding: 0.5em; border-radius: 0.50em / 0.50em; background: #FFF; color: #666;"]': {
          display: 'none',
        },
        'body > div': {
          background: 'rgba(0, 0, 0, 0.7)',
        },
        '.board_header,div[style="margin: 0; padding: 0 0 0 0.5em; border-top: 0.5em solid #BEB; border-bottom: 0.5em solid #BEB; border-radius: 0.50em / 0.50em; padding: 0 0 1.0em 0.5em; height: 25em; overflow-y: scroll; background: #BEB;"]': {
          background: 'rgba(0, 0, 0, 0.7)',
          color: 'white',
          'border-radius': '15px',
          border: 'none',
        },
        'p[style="margin:0; padding: 0; font-size: 0.75em; background: #BEB;"]': {
          background: 'none',
        },
        a: {
          color: '#42a5f5',
        },
        dd: {
          color: 'white',
        },
        '.thread a': {
          'text-shadow': '1px 1px 10px white',
          transition: 'all 0.2s ease-in-out',
        },
        '.thread a:hover': {
          'text-shadow': 'none',
        },
      },
    },
    '.*[.]5ch[.]net/test/read.cgi/.*': {
      style: {
        '.navbar-fixed-top': {
          background: 'rgba(0, 0, 0, 0.7)',
        },
        '.container_body': {
          background: 'none',
          margin: 0,
          padding: 0,
        },
        '#background': {
          height: '100%',
          width: '100%',
          background: `url(${config.background_url}) center no-repeat fixed`,
          'background-size': 'cover',
          'z-index': -1,
          position: 'fixed',
          top: 0,
          filter: `blur(${config.background_blur ? '5' : '0'}px)`,
          transition: 'all 0.3s linear',
        },
        '#background.cleek': {
          filter: 'blur(0)',
        },
        '#search-text,#search-button': {
          'border-radius': '30px 0 0 30px',
          background: 'none',
          border: '1px solid rgba(255,255,255,0.5)',
          filter: 'invert(0.4)',
        },
        '#search-button': {
          'border-radius': '0 30px 30px 0',
          'border-left': 'none',
          transition: 'all 0.2s ease-in-out',
        },
        '#search-text:focus,#search-button:hover': {
          filter: 'invert(0)',
        },
        '.title': {
          'margin-top': '70px',
          'font-size': '30px',
          color: '#f5f5f5',
          'text-shadow': '0 1px 10px #212121',
        },
        '.stoplight': {
          background: 'rgba(255, 23, 68, 0.6)',
          'margin-bottom': '10px',
          'text-align': 'center',
        },
        '.thread': {
          'text-align': 'center',
        },
        '.post,.post_hover,.menuitem': {
          'border-radius': '15px',
          background: 'rgba(0, 0, 0, 0.7)',
          'box-shadow': '5px 5px 30px black',
          transition: 'all 0.2s ease-in-out',
        },
        '.post,.post_hover': {
          'margin-bottom': '15px',
          'max-width': '40%',
          padding: 0,
        },
        '.meta': {
          background: 'rgba(0, 0, 0, 0.4)',
          'border-radius': '15px 15px 0 0',
          padding: '5px 10px 5px 10px',
          'text-align': 'left',
        },
        '.meta > *': {
          'word-break': 'break-all',
        },
        '.message': {
          margin: '0 10px',
          padding: '5px 0 10px',
          'text-align': 'left',
        },
        '.message > .escaped': {
          color: 'white',
        },
        '.message > .escaped > hr': {
          'border-top': '1px solid #616161',
        },
        '.meta > .number,.date,.margin_right': {
          color: '#bdbdbd',
        },
        '.be': {
          'margin-left': '5px',
        },
        '.escaped > a': {
          color: '#42a5f5',
          'text-shadow': '1px 1px 10px white',
          transition: 'all 0.2s ease-in-out',
        },
        '.escaped > a:hover': {
          'text-shadow': 'none',
        },
        '.meta a': {
          color: 'green',
        },
        'br,nav': {
          'user-select': 'none',
        },
        '.menuitem': {
          'box-shadow': '3px 3px 20px black',
        },
        '.menuitem:hover': {
          'box-shadow': '1px 1px 20px black',
        },
        '.footer': {
          padding: '50px 0 20px 0',
        },
        '.search-logo': {
          filter: 'invert(0.5)',
          transition: 'all 0.2s ease-in-out',
        },
        '.search-logo:hover': {
          filter: 'invert(1)',
        },
        '#post-form-inner': {
          background: 'rgba(0, 0, 0, 0.7)',
          border: 'none',
        },
        '.navbar-fixed-top,.pagestats,.menuitem,.topmenu,.bottommenu,.post,.post_hover': {
          border: 'none',
          color: 'white',
        },
        '.socialmedia,#banner,.ad--right,.ad--bottom': {
          display: 'none',
        },
        '.post[data-id="1002"]': {
          display: 'none',
        },
        '.margin_right': {
          'margin-right': '5px',
        },
        // https://qiita.com/semind/items/e18d5f3131de4904b2c9
        '.rainbow': {
          background: 'linear-gradient(to right,rgb(255,0,0),rgb(255,69,0),rgb(255,255,0),rgb(0,128,0),rgb(0,0,255),rgb(75,0,130),rgb(238,130,238))',
          'background-clip': 'text',
        },
        '.meta.rainbow *,.message.rainbow *': {
          color: 'transparent',
        },
        '.post img': {
          'max-width': '100%',
        },
        '#scroll': {
          position: 'fixed',
          background: 'rgba(0, 0, 0, 0.9)',
          width: '60px',
          height: '60px',
          'border-radius': '50px',
          right: '30px',
          bottom: '30px',
          'z-index': '9999',
          'font-size': '50px',
          'text-align': 'center',
        },
      },
      lang: {
        '.menuitem:contains(全部)': 'All',
        '.menuitem:contains(最新50)': 'Latest 50',
        '.menuitem:contains(スマホ版)': 'Mobile (Not Supported)',
        '.menuitem:contains(掲示板に戻る)': 'Back to Board Top',
        '.menuitem:contains(ULA版)': 'ULA (Not Supported)',
        '.metastats:contains(コメント)': ['コメント', ' Comments'],
      },
      script: () => {
        let long_timer
        let long_cleek = false
        $('body').append('<div id=\'scroll\'><b>↓</b></div>')
        $(window).scroll(function () {
          if ($(this).scrollTop() > $(window).height()) $('#scroll').text('↑').addClass('top')
          else $('#scroll').text('↓').removeClass('top')
        })
        $(document).on('click', '#scroll', function () { $('html').stop().animate({ scrollTop: $(this).hasClass('top') ? 0 : $(document).height() }, 500) })
        $('.container_body').append('<div id="background"></div>')
        $('#background')
          .on('dblclick', () => $('#background').toggleClass('cleek'))
          .on('mouseup', () => {
            if (long_cleek) $('#background').attr('style', `background:url(${config.background_url}?${new Date().getTime()}) center no-repeat fixed!important`)
            long_cleek = false
            clearTimeout(long_timer)
            return true
          })
          .on('mousedown', () => {
            long_timer = setTimeout(() => long_cleek = true, 500)
            return true
          })
        $('.post').hover(function () {
          const element = $(this).attr('data-userid') ? `.post[data-userid="${$(this).attr('data-userid')}"]` : this
          $(element).attr('style', 'transform:scale(1.05, 1.05)!important;box-shadow:none!important')
        }, function () {
          const element = $(this).attr('data-userid') ? `.post[data-userid="${$(this).attr('data-userid')}"]` : this
          $(element).removeAttr('style')
        })
        $(window).on('load', () => {
          const ids = []
          $('.post[data-id="1001"] .name b').text('オーバースレッドゥ')
          if ($('#1 hr').length) {
            const nodevalu = $('#1 hr')[0].nextSibling.nodeValue
            if (nodevalu.includes(':vvvvv:'))
              $('#1 .escaped').append('<br><span class=\'gray_om\'>強制コテハン</span>')
            else if (nodevalu.includes(':vvvvvv:'))
              $('#1 .escaped').append('<br><span class=\'gray_om\'>強制コテハン + IP表示</span>')
            else if (nodevalu.includes(':none:'))
              $('#1 .escaped').append('<br><span class=\'gray_om\'>ID非表示</span>')
            else
              $('#1 .escaped').append('<br><span class=\'gray_om\'>めんどいからここらへんみとけ<br>https://info.5ch.net/index.php/新生VIPQ2#.21extend:<br>https://info.5ch.net/index.php/BBS_SLIP</span>')
          }
          if ($('#\\31').attr('data-userid')) $(`.post[data-userid="${$('#\\31').attr('data-userid')}"] > .meta .name`).each(function () { $(this).after('<span class=\'margin_right\'>(スレ主)</span>') })
          $('.uid').each(function () {
            if (!$(this).text()) return $(this).css('color', '#bdbdbd').attr('title', 'IDないですこの人').text('No')
            const id = $(this).text().slice(3)
            $(this).text(id !== 'ead' ? id : 'Thread')
            const ids_ = ids.filter(item => item[0] === id)
            if (!ids_.length) {
              if (id === 'ead') return $(this).parents('.post').children('.meta,.message').addClass('rainbow')
              const color = generateColor()
              ids.push([id, color])
              if (id.toLowerCase().includes('krsw')) console.info(`KRSW Included: ${id}`)
              console.info(`%cID: ${id}, #${color}`, `color:#${color}`)
              $(this).attr('title', `#${color}`)
              return $(this).css('color', `#${color}`)
            }
            $(this).attr('title', `#${ids_[0][1]}`)
            return $(this).css('color', `#${ids_[0][1]}`)
          })
          $('.name b a[href^="mailto:"]').each(function () {
            let supeisharu = ['フメイ', $(this).attr('href').slice(7)]
            if (supeisharu[1] === 'sage') supeisharu[0] = 'サゲ'
            else if (supeisharu[1] === 'age') supeisharu[0] = 'アゲ'
            else if (['agete', 'sagete'].includes(supeisharu[1])) {
              if ($(this).contents().first()[0].textContent === 'Please Click Ad !!@アドセンスクリックお願いします ')
                $(this).contents().first()[0].textContent = 'Please Install Adblock !!@アドブロックインストールお願いします'
              $(this).children('small').css('margin-left', '5px')
              supeisharu[0] = supeisharu[1] === 'agete' ? 'アゲロー' : 'サゲロー'
            }
            else if (supeisharu[1].test(/@(yahoo.co.jp)$/)) supeisharu[0] === 'ヤホー'
            else if (supeisharu[1].test(/@(protonmail.com|protonmail.ch|pm.me)$/)) supeisharu[0] === 'プロト'
            $(this).parents('.meta').children('.date').before(`<a class='margin_right' href='${$(this).attr('href')}' title='${supeisharu[1]}' style='color:#81c784!important'>(${supeisharu[0]})</a>`)
          })
          $('.name a[href^="mailto:"]').each(function () { $(this).removeAttr('href') })
        })
        $('.date').each(function () {
          $(this).html($(this).text().replace(' ', '<span style="margin-right:5px!important"></span>'))
        })
      },
    },
  }
  Object.keys(preset).map(item => {
    const path = preset[item]
    if (item.startsWith('global')) {
      const global_match = location.hostname.endsWith(path.hostname)
      console.info(`%cMatch: global - ${path.hostname} - ${global_match}`, `color:${global_match ? 'green' : 'red'}`)
      if (global_match) {
        $('head').append(`<style type="text/css">${parse_json(path.style)}</style>`)
        return path.script()
      }
    }
    let url = location.href
    if (!preset[item].protocol) url = location.href.slice(location.protocol.length + 2)
    const match = new RegExp(`^${item}$`).test(url)
    console.info(`%cMatch: ^${item}$ - ${url} - ${match}`, `color:${match ? 'green' : 'red'}`)
    if (match) {
      if (typeof path.style === 'object') $('head').append(`<style type="text/css">${parse_json(path.style)}</style>`)
      if (typeof path.lang === 'object') Object.keys(path.lang).map(item => {
        if (typeof path.lang[item] === 'string')
          $(item).text(path.lang[item])
        else if (typeof path.lang[item] === 'object')
          $(item).html($(item).html().replace(path.lang[item][0], path.lang[item][1]))
      })
      if (typeof path.script === 'function') path.script()
    }
  })

})()