惰性で作りました。嫌な部分は改変して使って下さい。時々改修するかも。
当前为
// ==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`) }) } }) })();