帰ニコ用時間移動スクリプト 帰ニコ用時間移動スクリプトv0.3 240908 ブックマークレット版 帰ニコ用時間移動スクリプトv0.3 userscript版 // ==UserScript== // @name kinico_timeseeker // @version 0.3 // @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 containerBuild = ( title = [], menu = [], body = [], onRemoveTodo = [], )=>{ let container = tag("div", { s: {border: "2px solid #000", padding: 0, margin: 0}, }); container.attachShadow({mode: "open"}); let root = container.shadowRoot; let header = tag("div", { s: {border: "2px solid #666", display: "grid", grid: "auto-flow/auto max-content"}, }); header.append( ...title, tag("button", {t: "\u2026", 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", e: {click: ()=>{[...onRemoveTodo].map(v=>v());}}}), ); root.append( header, menuDom, ...body, ); 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.3"; (async()=>{ let endFlag = false; let onRemoveTodo = [()=>{endFlag = true; onRemoveTodo = [];}]; try{ let containerAdder; let playerOppCache; const player = ()=>{ const checker = (v)=> v?.memoizedProps?.playerOperation && "isVideoAd" in v.memoizedProps && "isLoading" in v.memoizedProps && "isStalled" in v.memoizedProps; if(checker(playerOppCache))return playerOppCache.memoizedProps; 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(checker(t)){ playerOppCache = t; return t.memoizedProps; } 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)) || !player() ){ if(i < 0)throw "target page not match (dom)"; await new Promise(res=>setTimeout(res, 500)); i--; } } console.log(player()); { // ui build let uiBody = tag("div", {s: {display: "grid", grid: "auto-flow/repeat(5, 1fr) 12ex repeat(5, 1fr)", height: "2lh"}}); let container = containerBuild( [`KiNico timeseeker ${verString}`], [], [uiBody], onRemoveTodo, ); const seek = (time)=>{ let opp = player()?.playerOperation; if(!opp)return; opp.currentTime.set( Math.min(opp.duration, Math.max(0, time)), ); }; const seekButtonBuilder = (str = "", moveVpos)=>tag( "button", { t: str, e: { click: ()=>{ let opp = player()?.playerOperation; if(!opp)return; seek((Math.floor(opp.currentTime.get() * 100) + moveVpos + 0.1) / 100); }, }, }); /** @type {HTMLInputElement} */ let timeShow = tag("input", {a: {type: "text"}, 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)); timeShowUpdateBody(); }, keydown: (e)=>{ e.stopPropagation(); }, }}); 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), ]; let lastDisabled = false; const timeShowUpdateBody = ()=>{ let opp = player()?.playerOperation; if(!opp){ timeShow.value = "--"; return; } let vpos = Math.floor(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)}`; }; const timeShowUpdate = ()=>{ if(endFlag)return; if(container.shadowRoot.activeElement != timeShow){ let p = player(); if(!p || p.isStalled || p.isLoading || p.isVideoAd){ if(!lastDisabled)seekUiInputs.map(v=>v.disabled = true); lastDisabled = true; timeShow.value = "--"; }else { if(lastDisabled)seekUiInputs.map(v=>v.disabled = false); lastDisabled = false; timeShowUpdateBody(); } } window.requestAnimationFrame(timeShowUpdate); }; timeShowUpdate(); uiBody.append(...seekUiInputs); containerAdder(container); } }catch(e){ console.log(`${toolName}${verString} error:`, e); // eslint-disable-line onRemoveTodo.map(v=>v()); } })(); })(); 更新履歴 v0.3 提供ゾーンに対応・入力中ショートカットキーを無効に・その他挙動改善 v0.2 fix:動画遷移した時に機能しなくなる v0.1 初版 コンテンツリスト 帰ニコ用時間移動スクリプト kinico_timeseeker_v0.1.html kinico_timeseeker_v0.2.html kinico_timeseeker_v0.3.html