// ==UserScript== // @name Append Tag Searching Tub // @name:ja niconico タグ検索タブを追加 // @description Adds “Keyword”, “Tags”, “My List”, “Images” and “Live” search tabs to all of the Niconico search boxes. // @description:ja 『niconico』各サービスの検索窓について、「キーワード」「タグ」「マイリスト」「静画」「生放送」検索タブが5つとも含まれるように補完します。 // @namespace http://loda.jp/script/ // @version 5.1.0 // @match https://www.nicovideo.jp/ // @match https://www.nicovideo.jp/? // @match https://www.nicovideo.jp/# // @match https://www.nicovideo.jp/tag/* // @match https://www.nicovideo.jp/related_tag/* // @match https://www.nicovideo.jp/mylist* // @match https://www.nicovideo.jp/search/* // @match *://seiga.nicovideo.jp/* // @match https://live.nicovideo.jp/* // @match https://com.nicovideo.jp/* // @match *://blog.nicovideo.jp/en_info/* // @match *://tw.blog.nicovideo.jp/* // @require https://gitcdn.xyz/cdn/greasemonkey/gm4-polyfill/a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.js // @require https://greasyfork.org/scripts/17895/code/polyfill.js?version=625392 // @require https://greasyfork.org/scripts/19616/code/utilities.js?version=230651 // @require https://greasyfork.org/scripts/17896/code/start-script.js?version=112958 // @license MPL-2.0 // @compatible Edge 非推奨 / Deprecated // @compatible Firefox // @compatible Opera // @compatible Chrome // @grant GM.setValue // @grant GM_setValue // @grant GM.getValue // @grant GM_getValue // @grant GM.deleteValue // @grant GM_deleteValue // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @connect www.nicovideo.jp // @run-at document-start // @icon data:image/vnd.microsoft.icon;base64,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 // @author 100の人 // @homepageURL https://greasyfork.org/scripts/268 // @downloadURL none // ==/UserScript== 'use strict'; // L10N Gettext.setLocalizedTexts({ /*eslint-disable quote-props, max-len */ 'en': { 'キーワード': 'Keyword', '動画をキーワードで検索': 'Search Video by Keyword', 'タグ': 'Tags', '動画をタグで検索': 'Search Video by Tag', 'マイリスト': 'My List', 'マイリストを検索': 'Search My List', '静画': 'Images', '静画を検索': 'Search Images', '生放送': 'Live', '番組を探す': 'Search Live Program', 'マンガ': 'Comics', }, 'zh': { 'キーワード': '關鍵字', '動画をキーワードで検索': '', 'タグ': '標籤', '動画をタグで検索': '', 'マイリスト': '我的清單', 'マイリストを検索': '搜尋我的清單', '静画': '靜畫', '静画を検索': '搜尋靜畫', '生放送': '生放送', '番組を探す': '搜尋節目', 'マンガ': '漫畫', }, /*eslint-enable quote-props, max-len */ }); /** * 追加したタブバーから新しいタブで検索結果を開いたとき、選択中のタブを元に戻す遅延時間 (ミリ秒)。 * @constant {number} */ const CURRENT_TAB_RESTORATION_DELAY = 1000; /** * 表示しているページの種類。 * @type {string} */ let pageType; // ページの種類を取得 switch (location.host) { case 'www.nicovideo.jp': if (location.pathname === '/') { // 総合トップページ pageType = 'top'; } else if (location.pathname.startsWith('/search/')) { // 動画キーワード検索ページ pageType = 'videoSearch'; } else if (location.pathname.startsWith('/mylist_search')) { // マイリスト検索ページ pageType = 'mylist'; } else if (/^\/(?:(?:tag|related_tag)\/|(?:mylist|recent|newarrival|openlist|video_catalog)(?:\/|$))/ .test(location.pathname)) { // 動画タグ検索ページと公開マイリスト等 pageType = 'tag'; } else if (location.pathname.startsWith('/user/')) { // ユーザーページ pageType = 'user'; } break; case 'seiga.nicovideo.jp': pageType = location.pathname.startsWith('/search/') // 静画検索ページ ? 'imageSearch' // 静画ページ : 'image'; break; case 'live.nicovideo.jp': pageType = location.pathname.startsWith('/search') // 生放送検索ページ ? 'liveSearch' // 生放送ページ : 'live'; break; case 'blog.nicovideo.jp': // 英語版ニコニコインフォ pageType = 'info_en'; break; case 'tw.blog.nicovideo.jp': // 台湾版ニコニコインフォ pageType = 'info_tw'; break; } // 上部メニューが追加されるまで待機 let targetParentIdFirefox, isTargetFirefox; switch (pageType) { case 'imageSearch': case 'image': isTargetFirefox = target => target.id === 'wrapper'; break; case 'liveSearch': targetParentIdFirefox = 'body_header'; break; case 'info_en': targetParentIdFirefox = 'container-inner'; break; case 'info_tw': targetParentIdFirefox = 'header'; break; } startScript( prepare, parent => parent.classList.contains('siteHeaderGlovalNavigation'), target => target.id === 'siteHeaderLeftMenu', () => document.getElementById('siteHeaderLeftMenu'), { isTargetParent: targetParentIdFirefox ? parent => parent.id === targetParentIdFirefox : parent => parent.localName === 'body', isTarget: isTargetFirefox || (target => target.id === 'siteHeader'), } ); /** * ページの種類別に、実行する関数を切り替える。 */ function prepare() { Gettext.setLocale(pageType === 'community' ? document.getElementById('siteHeaderNotification').dataset.nicoLocale.replace('_', '-') : document.documentElement.lang); if (pageType.startsWith('info_')) { // 英語版、または台湾版のニコニコインフォなら // 生放送へのリンクを取得 const itemLive = document.querySelector('#siteHeader [href*="://live.nicovideo.jp/"]').parentElement; // 生放送リンクの複製 const item = itemLive.cloneNode(true); // リンク文字を変更 item.getElementsByTagName('span')[0].textContent = _('静画'); // アドレスを変更 item.getElementsByTagName('a')[0].href = 'https://seiga.nicovideo.jp/'; // ヘッダに静画へのリンクを追加 itemLive.before(item); return; } switch (pageType) { case 'videoSearch': // 動画キーワード startScript( addTagSearchTabAboveSearchBox, parent => parent.classList.contains('formSearch'), target => target.id === 'search_united_form', () => document.getElementById('search_united_form'), { isTargetParent: parent => parent.localName === 'body', isTarget: target => target.localName === 'section', } ); break; case 'mylist': // マイリスト startScript( addTagSearchTabAboveSearchBox, parent => parent.id === 'form_search', target => target.id === 'search_united_form', () => document.getElementById('search_united_form'), { isTargetParent: parent => parent.id === 'PAGEMAIN', isTarget: target => target.id === 'PAGEBODY', } ); break; case 'top': // トップページ addTagSearchButtonToTopPage(); break; case 'imageSearch': // 静画キーワード startScript( addTagSearchTabAboveSearchBox, parent => parent.id === 'usearch_form', target => target.id === 'usearch_form_input', () => document.getElementById('usearch_form_input'), { isTargetParent: parent => parent.id === 'wrapper', isTarget: target => target.id === 'main', } ); break; case 'image': // 静画 startScript( careteTabsBarToSearchBox, parent => parent.id === 'head_search_form', target => target.id === 'search_button', () => document.getElementById('search_button'), { isTargetParent: parent => parent.id === 'header_block', isTarget: () => true, } ); break; case 'liveSearch': // 生放送キーワード startScript( addTagSearchTabAboveSearchBox, parent => parent.classList.contains('search-input-area'), target => target.classList.contains('search-form'), () => document.getElementsByClassName('search-form')[0] ); break; case 'live': // 生放送 startScript( careteTabsBarToSearchBox, parent => parent.classList.contains('search_program'), target => target.classList.contains('search_word'), () => document.getElementsByClassName('search_word')[0], { isTargetParent: parent => parent.localName === 'body', isTarget: target => target.id === 'page_header', }); break; case 'tag': if (document.doctype.publicId) { // 公開マイリスト等 startScript( addOtherServiceTabsAboveSearchBox, parent => parent.id === 'search_tab', target => target.id === 'target_m', () => document.getElementById('target_m'), { isTargetParent: parent => parent.id === 'PAGEMAIN', isTarget: target => target.id === 'PAGEBODY', } ); } else { // 動画タグ startScript( addOtherServiceTabsAboveSearchBox, parent => parent.classList.contains('videoSearchOption'), target => target.classList.contains('optMylist'), () => document.getElementsByClassName('optMylist')[0], { isTargetParent: parent => parent.localName === 'body', isTarget: target => target.localName === 'header', } ); } break; case 'user': // ユーザー startScript( addImageLinkToUserPageMenu, parent => parent.localName === 'body', target => target.classList.contains('optionOuter'), () => document.getElementsByClassName('optionOuter')[0] ); break; } } /** * 各サービスのキーワード検索ページの検索窓に、動画の「タグ」検索タブを追加する。 */ function addTagSearchTabAboveSearchBox() { // マイリスト検索タブの取得 const mylistTab = document.querySelector('.tab_table td:nth-of-type(2), #search_frm_a a:nth-of-type(2), .search_tab_list li:nth-of-type(2), .seachFormA a:nth-of-type(2), li:nth-of-type(2).search-tab-item'); // マイリスト検索タブの複製 const tagTab = mylistTab.cloneNode(true); // タブ名を変更 const anchor = tagTab.tagName.toLowerCase() === 'a' ? tagTab : tagTab.getElementsByTagName('a')[0]; let tabNameNode = anchor.getElementsByTagName('div'); tabNameNode = (tabNameNode.length > 0 ? tabNameNode[0].firstChild : anchor.firstChild); tabNameNode.data = _('タグ') + (pageType === 'liveSearch' ? '' : ' ( '); // クラス名を変更・動画件数をリセット const searchCount = tagTab.querySelector('strong, span'); switch (pageType) { case 'videoSearch': searchCount.classList.remove('more'); break; case 'mylist': searchCount.style.removeProperty('color'); break; case 'imageSearch': searchCount.classList.remove('search_value_em'); searchCount.classList.add('search_value'); break; } searchCount.textContent = '-'; if (searchCount.id) { // 生放送 searchCount.id = 'search_count_tag'; } // 検索語句を取得 const searchWordsPattern = /(?:\/(?:search|tag|mylist_search)\/|[?&]keyword=)([^?]+)/g; const result = location.href.match(searchWordsPattern); const searchWords = result ? searchWordsPattern.exec(result[pageType === 'liveSearch' ? result.length - 1 : 0])[1] : ''; // タグが付いた動画件数を取得・表示 if (searchWords && location.host !== 'www.live.nicovideo.jp') { GM.xmlHttpRequest({ method: 'GET', url: 'https://www.nicovideo.jp/tag/' + searchWords, onload: function (response) { const responseDocument = new DOMParser().parseFromString(response.responseText, 'text/html'); const total = responseDocument.querySelector('.tagCaption .dataValue .num').textContent; const trimmedThousandsSep = total.replace(/,/g, ''); if (trimmedThousandsSep >= 100) { // 動画件数が100件を超えていれば switch (pageType) { case 'videoSearch': searchCount.classList.add('more'); break; case 'mylist': searchCount.style.color = '#CC0000'; break; case 'imageSearch': searchCount.classList.remove('search_value'); searchCount.classList.add('search_value_em'); break; case 'liveSearch': searchCount.classList.add('strong'); break; } } switch (pageType) { case 'mylist': searchCount.textContent = ' ' + total + ' '; break; case 'videoSearch': case 'imageSearch': searchCount.textContent = total; break; case 'liveSearch': searchCount.textContent = trimmedThousandsSep; break; } }, }); } // 非アクティブタブを取得 const inactiveTab = document.querySelector('.tab_0, .tab1, .search_tab_list a:not(.active), .search-tab-anchor'); // クラス名を変更 anchor.className = inactiveTab.className; // アドレスを変更 anchor.href = 'https://www.nicovideo.jp/tag/' + searchWords + inactiveTab.search; // タグ検索タブを追加 mylistTab.parentNode.insertBefore(tagTab, mylistTab); if (pageType === 'liveSearch') { mylistTab.parentNode.insertBefore(new Text(' '), mylistTab); } else if (inactiveTab.classList.contains('tab1')) { // GINZAバージョン mylistTab.parentNode.insertBefore(tagTab.previousSibling.cloneNode(true), mylistTab); } } /** * ニコニコ動画の上部に表示されている検索窓に、「静画」「生放送」を検索するタブを追加する。 */ function addOtherServiceTabsAboveSearchBox() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ``); // タブリストの取得 const mylistTab = document.querySelector('#target_t, .optMylist'); // タブの複製・追加 mylistTab.parentElement.append(...[ { type: 'image', title: _('静画を検索'), url: 'https://seiga.nicovideo.jp/search', text: _('静画'), }, { type: 'live', title: _('番組を探す'), url: 'https://live.nicovideo.jp/search', text: _('生放送'), }, ].map(function (option) { const tab = mylistTab.cloneNode(true); if (mylistTab.classList.contains('optMylist')) { // GINZAバージョン tab.classList.remove('optMylist'); tab.classList.add('opt' + option.type[0].toUpperCase() + option.type.slice(1)); tab.dataset.type = option.type; tab.getElementsByTagName('a')[0].textContent = option.text; } else { // 公開マイリスト等 tab.id = 'target_' + option.type[0]; tab.title = option.title; tab.setAttribute('onclick', tab.getAttribute('onclick').replace(/'.+?'/, '\'' + option.url + '\'')); tab.textContent = option.text; } return tab; })); GreasemonkeyUtils.executeOnUnsafeContext(/* global Nico */ function () { eval('Nico.Navigation.HeaderSearch.Controller.search = ' + Nico.Navigation.HeaderSearch.Controller.search.toString().replace(/(switch.+?{[^}]+)/, `$1; break; case "image": d = "https://seiga.nicovideo.jp/search/" + e; break; case "live": d = "https://live.nicovideo.jp/search/" + e; break; `)); }); } /** * 静画・生放送の上部に表示されている検索窓に、「動画キーワード」「動画タグ」「マイリスト」「静画」「生放送」を検索するタブバーを設置する。 */ function careteTabsBarToSearchBox() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ``); /** * 静画検索のtargetパラメータの値。 * @type {string} */ let imageSearchParamValue = 'illust'; const form = document.querySelector('[action$="search"]'); const textField = form[pageType === 'image' ? 'q' : 'keyword']; if (pageType === 'image') { // 静画の場合 const pathnameParts = document.querySelector('#logo > h1 > a').pathname.split('/'); switch (pathnameParts[1]) { case 'manga': imageSearchParamValue = 'manga'; break; case 'book': imageSearchParamValue = pathnameParts[2] === 'r18' ? 'book_r18' : 'book'; break; } } form.insertAdjacentHTML('afterbegin', `
`); const defaultCurrentTabAnchor = form.querySelector('.current a'); document.addEventListener('click', function (event) { if (event.button !== 2 && event.target.matches('[action$="search"] > ul > li > a')) { // タブが副ボタン以外でクリックされたとき let searchWord = textField.value.trim(); if (pageType === 'image' && textField.value === textField.defaultValue) { // 静画の場合、検索窓の値が既定値と一致していれば空欄とみなす searchWord = ''; } if (searchWord) { // 検索語句が入力されていれば switchTab(event.target); event.target.pathname = event.target.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord)); setTimeout(function () { // リンク先を新しいタブで開いたとき switchTab(defaultCurrentTabAnchor); }, CURRENT_TAB_RESTORATION_DELAY); } else { // 検索語句が未入力なら event.preventDefault(); if (event.button === 0) { // 主ボタンでクリックされていれば switchTab(event.target); } } } }); // TabSubmitをインストールしているとマウスボタンを取得できず、中クリック時にも同じタブで検索してしまうため分割 form.addEventListener('click', function (event) { if (event.target.type === (pageType === 'image' ? 'image' : 'submit')) { // 送信ボタンをクリックしたとき const searchWord = textField.value !== textField.defaultValue && textField.value.trim(); if (searchWord) { event.stopPropagation(); event.preventDefault(); const anchor = form.querySelector('.current a'); anchor.pathname = anchor.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord)); location.assign(anchor.href); } } }, true); addEventListener('pageshow', function (event) { if (event.persisted) { // 履歴にキャッシュされたページを再表示したとき switchTab(defaultCurrentTabAnchor); } }); /** * 選択しているタブを切り替える。 * @param {HTMLAnchorElement} target - 切り替え先のタブのリンク。 */ function switchTab(target) { form.getElementsByClassName('current')[0].classList.remove('current'); target.parentElement.classList.add('current'); if (pageType === 'image') { // 静画 if (textField.defaultValue === textField.value) { // 検索語句が未入力なら textField.defaultValue = textField.value = target.title; } else { // 検索語句が入力されていれば textField.defaultValue = target.title; } } else { // 生放送 textField.placeholder = target.title; } } } /** * 総合トップページの検索窓に、動画「タブ」「マイリスト」検索ボタンを追加する。 */ function addTagSearchButtonToTopPage() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ``); // 静画検索ボタンの取得 const refItem = document.querySelector('.CrossSearch-service[data-service="seiga"]'); const tagItem = refItem.cloneNode(true); tagItem.textContent = _('タグ'); tagItem.dataset.service = 'tag'; tagItem.dataset.baseUrl = 'https://www.nicovideo.jp/tag/'; refItem.before(tagItem); const mylist = refItem.cloneNode(true); mylist.textContent = _('マイリスト'); mylist.dataset.service = 'mylist'; mylist.dataset.baseUrl = 'https://www.nicovideo.jp/mylist_search/'; refItem.before(mylist); } /** * ユーザーページ左側のメニューに、静画へのリンクを追加する。 */ function addImageLinkToUserPageMenu() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ``); const nextItem = document.getElementsByClassName('stampTab')[0]; const item = nextItem.cloneNode(true); const classList = item.classList; classList.remove('stampTab', 'active'); classList.add('imageTab'); const anchor = item.getElementsByTagName('a')[0]; anchor.href = 'https://seiga.nicovideo.jp/user/illust/' + /[0-9]+/.exec(anchor.pathname)[0]; anchor.lastChild.data = _('静画'); nextItem.prepend(item); }