Greasy Fork

Greasy Fork is available in English.

5ch 画像&動画etc

惰性で作りました。嫌な部分は改変して使って下さい。時々改修するかも。

目前为 2023-01-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         5ch 画像&動画etc
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @description  惰性で作りました。嫌な部分は改変して使って下さい。時々改修するかも。
// @author       匿名Cat
// @match        https://*.5ch.net/test/read.cgi/*/*
// @match        http://*.5ch.net/test/read.cgi/*/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js
// @resource     bootstrap.min.css https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @icon         https://www.google.com/s2/favicons?domain=5ch.net
// @grant        GM_download
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';
  $.noConflict()

  const extract5chURL = hrefStr => hrefStr?.match(/(?:http:\/\/jump\.5ch\.net\/\?)(.+)/)?.[1]
  const optionalHttps = hrefStr => /^https?:\/\//.test(hrefStr) ? hrefStr : 'https://' + hrefStr

  // 設定開始================
  const settings = {
    imgExts: ['jpg', 'png', 'webp', 'jpeg', 'gif', 'webp'],
    keys: { download: ['d'], removePreview: ['c', 'Escape']}
  }

  // 直接生成型画像 サイズ
  const size = '10rem'

  const forHost = {
    0: {
      matcher: /https?:\/\/(?!hebi|leia).+\.5ch\.net\/test\/read.cgi\//,
      getSrc: extract5chURL,
      append: '.message',
    },
    "hebi.5ch.net": {
      getSrc: href => href,
      append: 'dd'
    },
    "leia.5ch.net": {
      getSrc: extract5chURL,
      append: 'dd'
    }
  }
  const userScriptId = "ch_im_and_video__"
  // 設定終了==============

  // bootstrap style読み込み
  GM_addStyle(GM_getResourceText("bootstrap.min.css"))

  const style = `
  body { font-size: 1.5rem; }

  /* 画像 */
  *[div="thumb5ch"] { display: inline-block; }
  .container.container_body { margin: unset 0; display: flex; position: relative; }
  .container.container_body>.contents { max-width: 60vw; }
  .preview-container { flex: 1; position: relative; }
  .preview-container:before { content: ""; display: block; padding-top: 75%; }
  .preview-container>img { position: sticky; top: 0; left: 0; bottom: 0; right: 0; width: 100%; max-height: 100vh; object-fit: contain; z-index: 3;}
  a.image { font-size: 0; }

  /* ナビゲーションバー */
  #${userScriptId}nav { z-index: 3; position: fixed; width: 100%; padding: 0; margin: 0; top: 0; }
  #${userScriptId}extract_im { cursor: pointer; }
  .dropdown-menu { font-size: inherit; }

  /* 5ch公式のGUI */
  .topmenu.centered, .bottommenu.centered { display: none; }
  .navbar-fixed-top.search-header .input-group { display: none; }
  .submitbtn.btn { font-size: inherit; }
  .rBtn { border: none; }
 `

  // util
  const d = document
  const isImageUrl = url => new RegExp(`\\.${settings.imgExts.join('|')}$`, 'g').test(url)
  const utilUnion = arr => [...new Set(arr)]

  jQuery(d).ready($ => {
    const $body = $(d.body)

    $body.append($('<style>').addClass(userScriptId).text(style))

    // ナビゲーションバーを追加
    const $nav = $('<nav>').attr('id', `${userScriptId}nav`).addClass('navbar navbar-expand navbar-light bg-light')
    $nav.html(`<div class="container-fluid">
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">掲示板に戻る</a>
        </li>
        <li class="nav-item">
          <a id="${userScriptId}extract_im" class="nav-link">画像スレ抽出</a>
        </li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
            関連スレ
          </a>
          <ul id="${userScriptId}relations" class="dropdown-menu" aria-labelledby="navbarDropdown"></ul>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
        </li>
      </ul>
    </div>
  </div>`)
    $body.prepend($nav)

    // 5ch公式設定ボタンをnavに移動
    $(".search-setting.dropdown").appendTo($nav.find("#navbarSupportedContent")[0])

    // 書き込みボタンを強調表示
    $(".submitbtn.btn").addClass('btn btn-primary')

    const urlObj = new URL(location.href)
    const [,{getSrc, append}] = Object.entries(forHost).find(([host, {matcher}]) => host.split(',').includes(urlObj.host) || matcher?.test(location.href))
    const imageExtSelectors = settings.imgExts.map(ext => `a[href$=".${ext}"]`).join(',')
    const $links = $('.escaped a')
    const $imageLinks = $(`a.image,${imageExtSelectors}`)
    const imageStyle = {maxHeight: size, maxWidth: size}

    // プレビュー画面を用意
    const $thread = $('.container.container_body')
    $thread.wrapInner($('<div>').addClass('contents'))
    const $previewContainer = $('<div>').addClass('preview-container')
    $thread.append($previewContainer)
    // プレビュー関数
    const addPreview = imgUrl => $previewContainer.append($('<img>', {src: imgUrl}))
    const removePreview = () => $previewContainer.empty()

    $(d).on('keydown', e => { if (settings.keys.removePreview.includes(e.key)) removePreview() })

    // 描画時
    $links.get().forEach(link => {
      const $link = $(link)
      const href = getSrc($link.attr('href'))
      if (typeof href !== 'string') return
      const patterns = [/^.+:\/\/(?:www|m)\.youtube\.com.*?v=([\w-=]+).*$/, /^.+:\/\/youtu\.be\/([\w-=]+).*$/, /^.+:\/\/www\.youtube\.com\/embed\/([\w-=]+).*$/]
      let match
      for (const pattern of patterns) {
        const mt = href.match(pattern)
        if (mt) { match = mt; break }
      }
      if (!match) {
        // 5ch外部サイト中継ページ無効化
        if (!isImageUrl(href)) $link.attr('href', href)
        return
      }
      // 以降 YouTube iframe 生成
      $link.text('')
      $link.after(
        $('<iframe>', {src: `https://www.youtube.com/embed/${match?.[1]}?controls=1`})
        .attr({frameborder: 0, allowfullscreen: ''})
        .css({width: '40rem', height: '22.5rem'})
      )
    })
    // 小サイズ画像の見た目の処理
    $imageLinks.get().forEach(imageLink => {
      const $imageLink = $(imageLink)
      // 画像リンクのURL表記を削除
      //$imageLink.contents().get().filter(el => el.nodeType === 3).forEach(textNode => textNode.parentNode.removeChild(textNode))
      setTimeout(() => {
        if ($imageLink.children('[div="thumb5ch"]')[0]) return
        // 小サイズ画像が生成されなかったら
        const imgUrl = optionalHttps(getSrc($imageLink.attr('href')))
        if (!imgUrl) return
        const $addImg = $('<img>', {src: imgUrl}).css(imageStyle)
        $imageLink.closest(append).append($addImg)
        $addImg.on('mouseover', e => {
          removePreview()
          addPreview(imgUrl)
        })
      }, 2000)
      $imageLink.children('div').css({display: 'inline', width: 'initial'})
        // 改行を削除
      $imageLink.next('br').remove()
    })

    // マウスホバーしたら画像をプレビュー
    let previewImgUrl
    $imageLinks.on('click', e => e.preventDefault())
    $imageLinks.on('mouseover', e => {
      removePreview()
      const $target = $(e.currentTarget)
      const imgUrl = optionalHttps(getSrc($target.attr('href')))
      if (!imgUrl) return
      previewImgUrl = imgUrl
      addPreview(imgUrl)
    })

    // スペースキー押下でプレビュー画像ダウンロード
    $(d).on('keydown', e => {
      if (!settings.keys.download.includes(e.key) || !previewImgUrl) return
      GM_download({url: previewImgUrl, name: previewImgUrl.replace(/^.+\//, ''), onerror: console.warn})
    })

    const posts = $('.post').get()

    // 画像スレだけ抽出機能
    posts.forEach(post => {// 改行削除、正規化
      $(post).css({display: 'block'}).next('br').remove()
    })
    const $notImgPosts = $(posts.filter(post => {
      const $post = $(post)
      return !$post.find('.image')[0] && !$post.find('a').get().some(a => isImageUrl($(a).attr('href')))
    }))
    let toggle = true
    const on = () => $notImgPosts.css({display: 'none'})
    const off = () => $notImgPosts.css({display: 'block'})
    $(`#${userScriptId}extract_im`).click(() => {
      ;(toggle ? on() : off())
      toggle = !toggle
    })

    // 関連スレURL候補 抽出
    const regMain = /(https:.+?)\/(\d+)(\/[\d-]+)?/
    const mainLocationHref = location.href.match(regMain)?.[1]
    if (mainLocationHref) {
      const relationHrefs = posts.flatMap(post => {
        return $(post).find('.escaped').find('a').get().map(a => {
          const href = a.getAttribute('href')
          const idx = href.indexOf(mainLocationHref)
          return idx < 0 ? undefined : href
        }).filter(Boolean)
      })
      utilUnion(relationHrefs).forEach(relationHref => {
        let url
        try { url = new URL(relationHref) } catch (e) { console.warn(e) }
        const txt = relationHref.match(regMain)?.[2] ?? url.pathname ?? relationHref
        const $li = $('<li>').addClass('dropdown-item')
        const a = $('<a>').attr({ href: relationHref, target: '_blank' }).text(txt)
        $li.append(a).appendTo(`#${userScriptId}relations`)
      })
    }
  })
})();