Greasy Fork

Greasy Fork is available in English.

5ch 画像&動画etc

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

当前为 2022-12-31 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         5ch 画像&動画etc
// @namespace    http://tampermonkey.net/
// @version      0.1
// @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'],
    keys: { download: ['d'], removePreview: ['c', 'Escape']}
  }

  const size = '40vw'

  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
        $imageLink.closest(append).append( $('<img>', {src: imgUrl}).css(imageStyle) )
      }, 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 $imgPosts = $(posts.filter(post => !$(post).find('.image')[0]))
    let toggle = true
    const on = () => $imgPosts.css({display: 'none'})
    const off = () => $imgPosts.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`)
      })
    }
  })
})();