# 帰ニコ用時間移動スクリプト
帰ニコ用時間移動スクリプト v0.2
# ブックマークレット版
帰ニコ用時間移動スクリプトv0.2
# userscript版
// ==UserScript== // @name kinico_timeseeker // @version 0.2 // @description 帰ニコ用時間移動スクリプト // @match https://www.nicovideo.jp/watch/* // @grant none // @run-at document-start // ==/UserScript== (function () { // =====編集可能な変数===== const toolPriority = 100; // editable: 同じ位置に挿入されるツール同士の順序値 大が上 const toolPosTop = true; // editable: ツール位置を入力欄より上にするか下にするか /** @return {HTMLElement} */ let tag = (tagname = "div", {a, s, e, t} = {})=>{ let dom = document.createElement(tagname); for(let k in a)dom.setAttribute(k, a[k]); for(let k in s)dom.style[k] = s[k]; for(let k in e)dom.addEventListener(k, e[k]); if(t)dom.textContent = t; return dom; }; const buttonStyle = {borderRadius: "5px", padding: "0px 3px 0px 3px", border: "solid 1px #000", background: "#ddd", color: "#000"}; const containerBuild = ( title = [], menu = [], body = [], onRemoveTodo = [], )=>{ let container = tag("div", { s: {border: "2px solid #000", padding: 0, margin: 0}, }); let header = tag("div", { s: {border: "2px solid #666", display: "grid", grid: "auto-flow/auto max-content"}, }); header.append( ...title, tag("button", {t: "\u2026", s: buttonStyle, e: {click: (e)=>{ e.stopPropagation(); menuDom.toggleAttribute("hidden"); }}}), ); let menuDom = tag("div", {a: {hidden: "hidden"}, s: {textAlign: "right"}}); menuDom.append( ...menu, tag("button", {t: "remove", s: buttonStyle, e: {click: ()=>{[...onRemoveTodo].map(v=>v());}}}), ); container.append( header, menuDom, ...body, ); container.remove(); onRemoveTodo.push(()=>{container.remove();}); return container; }; const addMaker = (posText, priority, name, parent, defaultBefore)=>{ if(!Number.isFinite(priority))return; return (element)=>{ element.dataset.toolPosition = posText; element.dataset.toolPriority = priority; element.dataset.toolName = name; let afterTool = [...parent.childNodes] .find(v=>{ if(v.dataset.toolPosition != posText)return false; let targetPriority = v.dataset.toolPriority - 0; if(targetPriority == priority)return name.localeCompare(v.dataset.toolName || "") < 0; return targetPriority < priority; }); parent.insertBefore(element, afterTool ?? defaultBefore()); }; }; // 通常時コメント入力欄より前、フルスクリーン時にコメント入力欄の下に表示される場所 const inPlayerAddMaker = (priority, name)=>{ let base = document.querySelector(`.grid-area_\\[player\\]`)?.childNodes[0]; if(!base)return; return addMaker("underVideo", priority, name, base, ()=>{ if(base.childNodes[0]?.querySelector("textarea"))return null; return [...base.childNodes].at(-1); }); }; // コメント入力欄より後 const afterInputAddMaker = (priority, name)=>{ let base = document.querySelector(`.grid-area_\\[player\\]`); if(!base)return; return addMaker("afterComment", priority, name, base, ()=>null); }; const toolName = "kinico_timeseeker"; const verString = "0.2"; (async()=>{ let endFlag = false; let onRemoveTodo = [()=>{endFlag = true; onRemoveTodo = [];}]; try{ /** @type {{element:HTMLVideoElement, opp:any}} */ let videoObj; let containerAdder; const finder = ()=>{ let videoElement = document.querySelector("video[data-name=video-content]"); if(!videoElement)return; let rootDom = document.querySelector("main"); if(!rootDom)return; let root = rootDom[Object.keys(rootDom).find(v=>v.startsWith("__reactFiber"))]; if(!root)return; let pool = [root]; while(pool[0]){ let t = pool.pop(); if(t?.memoizedProps?.playerOperation){ return {element: videoElement, opp: t.memoizedProps.playerOperation}; } if(t.sibling)pool.push(t.sibling); if(t.child)pool.push(t.child); } return; }; { // page match let i = 10; while(!(containerAdder ??= toolPosTop ? inPlayerAddMaker(toolPriority, toolName + verString) : afterInputAddMaker(toolPriority, toolName + verString))){ if(i < 0)throw "target page not match (dom)"; await new Promise(res=>setTimeout(res, 500)); i--; } } let uiBody = tag("div", {s: {display: "grid", grid: "auto-flow/repeat(5, 1fr) 12ex repeat(5, 1fr)", height: "2lh"}}); { // ui build const seek = (time)=>{ videoObj.opp.currentTime.set( Math.min(videoObj.opp.duration, Math.max(0, time)), ); }; const seekButtonBuilder = (str = "", moveVpos)=>tag( "button", { t: str, e: { click: ()=>{ seek((Math.floor(videoObj.opp.currentTime.get() * 100) + moveVpos + 0.1) / 100); }, }, s: buttonStyle, }); /** @type {HTMLInputElement} */ let timeShow = tag("input", {a: {type: "text"}, s: {color: "#000", backgroundColor: "#ddd"}, e: { change: ()=>{ /** @type {string} */ let valText = timeShow.value ?? ""; let {1:hour = "0", 2:min = "0", 3:sec = "0", 4:subsec = ".0"} = valText.replace(/\s/, "") .match(/(?:(?:([0-9]*):)?([0-9]*):)?([0-9]+)(\.[0-9]+)?/) ?? {}; let targetTime = ((hour - 0) * 60 + (min - 0)) * 60 + ((sec + subsec) - 0); seek(targetTime + (subsec.length < 4 ? 0.001 : 0)); }, }}); const timeShowUpdate = ()=>{ if(endFlag)return; if(document.activeElement != timeShow){ if(!videoObj){ timeShow.value = "--"; }else { let vpos = Math.floor(videoObj.opp.currentTime.get() * 100); let subsec = vpos % 100; let sec = (vpos - subsec) % 6000; let min = vpos - sec - subsec; const strMake = (v)=>Math.round(v).toString().padStart(2, "0"); timeShow.value = `${strMake(min / 6000)}:${strMake(sec / 100)}.${strMake(subsec)}`; } } window.requestAnimationFrame(timeShowUpdate); }; timeShowUpdate(); let seekUiInputs = [ seekButtonBuilder("-10 ", -1000), seekButtonBuilder("-3 ", -300), seekButtonBuilder("-1 ", -100), seekButtonBuilder("-0.1 ", -10), seekButtonBuilder("-0.01", -1), timeShow, seekButtonBuilder("+0.01", +1), seekButtonBuilder("+0.1 ", +10), seekButtonBuilder("+1 ", +100), seekButtonBuilder("+3 ", +300), seekButtonBuilder("+10 ", +1000), ]; uiBody.append(...seekUiInputs); // videoが空か監視して動画遷移を検知し // videoObjを更新する必要がある const videoObjUpdater = async()=>{ seekUiInputs.map(v=>v.disbled = true); { videoObj = null; let i = 10; while(!(videoObj ??= finder())){ if(i < 0)throw "target page not match (playerOperation)"; await new Promise(res=>setTimeout(res, 500)); i--; } } seekUiInputs.map(v=>v.disbled = false); console.log(videoObj.opp); videoObj.element.addEventListener("emptied", videoObjUpdater, {once: true}); }; videoObjUpdater(); } let container = containerBuild( [`KiNico timeseeker ${verString}`], [], [uiBody], onRemoveTodo, ); containerAdder(container); }catch(e){ console.log(`${toolName}${verString} error:`, e); // eslint-disable-line onRemoveTodo.map(v=>v()); } })(); })();
# 更新履歴
v0.2 fix:動画遷移した時に機能しなくなる
v0.1 初版