Advertisement
mufuuuu

[Bookmarklet] video要素の再生時間を表示&操作 [タイムスタンプ]

Aug 15th, 2021
2,222
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. javascript: void((() => {
  2.     /* Video Controller Bookmarklet 2021-08-15 by むふー@4chouyou */
  3.  
  4.     /* タイムスタンプのフォーマットの設定 */
  5.     const HIDE_HOUR_IF_ZERO = true; /* hが0のときに省略する */
  6.     const APPLY_PADDING_TO_MIN = true; /* hが省略されたときもmの位の0埋めを行う */
  7.     const APPLY_PADDING_TO_HOUR = false; /* hの位の0埋めを行う */
  8.     const COPY_WITH_SPACE = true; /* 末尾に半角スペースを足してコピーする */
  9.  
  10.     /* スタイルの設定 */
  11.     const PANEL_MIN_WIDTH = '342px'; /* パネルの最小幅 */
  12.     const TIMESTAMP_EDITOR_HEIGHT = '200px'; /* エディターの高さ */
  13.     const FONT_SIZE = '72px'; /* 再生時間表示のフォントサイズ */
  14.     const FONT_COLOR = 'rgba(255, 255, 255, 1)'; /* フォントカラー */
  15.     const BG_COLOR = 'rgba(28, 28, 28, 0.5)'; /* 背景色 */
  16.     const BG_BLUR_RADIUS = '3px'; /* 背景のぼかし */
  17.     const OPACITY = '1'; /* 透明度 */
  18.  
  19.     /* その他の設定 */
  20.     const PANEL_EMBEDDED = true; /* 最初からパネルがプレイヤーに埋め込まれる */
  21.     const TOOLBAR_COLLAPSED = true; /* 最初からツールバーが折りたたまれる */
  22.     const EDITOR_COLLAPSED = false; /* 最初からエディターが折りたたまれる */
  23.     const USE_CLIPBOARD_READING = false; /* クリップボードを読み込む機能を有効にする */
  24.     const GET_TIME_FROM_TEXT_ELEMENT = true; /* テキスト要素から再生時間を取得する */
  25.     const TIME_SELECTOR = [ /* 再生時間を取得する要素の指定 */
  26.         'span[class^="___elapsed-time___"] span:first-child', /*ニコニコ生放送*/
  27.         'div[class^="___elapsed-time___"] span[class^="___time-score___"] span[class^="___value___"]', /*ニコニコ生放送*/
  28.         '.volume-btn-wrapper + div .label + span', /*Mildomライブ*/
  29.         'span[class^="ControlBar__Time"]', /*OPENREC*/
  30.         '.bilibili-live-player-video-controller-duration-btn > div > span', /*bilibiliライブ*/
  31.         '.tip-wrap > div > .text', /*bilibiliライブ*/
  32.         '.live-time', /*twitch*/
  33.         '#updatetimer', /*ツイキャス*/
  34.         '.messageAndTime .timeBox' /*LINE LIVE*/
  35.     ];
  36.     const PARENT_SELECTOR = [ /* パネルを埋め込むプレイヤー要素の指定 */
  37.         '.html5-video-player', /*YouTube*/
  38.         '.PlayerContainer', /*ニコニコ動画*/
  39.         'div[class^="___player-display-screen___"]', /*ニコニコ生放送*/
  40.         '.room__player-wrapper', /*Mildomアーカイブ*/
  41.         '.room_player_container > .wrapper', /*Mildomライブ*/
  42.         '.video-player-wrapper', /*OPENREC*/
  43.         '.bilibili-player-video-wrap', /*bilibili動画*/
  44.         '.bilibili-live-player' /*bilibiliライブ*/
  45.     ];
  46.     const PLAYBACK_RATE = [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 7.5, 10]; /* 再生速度のプリセット */
  47.     /* 設定ここまで */
  48.  
  49.  
  50.     let currentURL = document.URL;
  51.     let timeoutId, intervalId;
  52.     let currentTimeOffset;
  53.     let repeatStart = null;
  54.     let repeatEnd = null;
  55.     let timeTable, lastIndex;
  56.     let placeholderText = `Ctrl+Shift+Enter: 最後の行にタイムスタンプを挿入

  57. Ctrl+Shift+/: カーソル位置にタイムスタンプを挿入

  58. Ctrl+Shift+C: 現在のタイムスタンプをコピー

  59. Ctrl+Shift+↑ or ↓: 選択範囲のタイムスタンプの秒数を1増減

  60. Ctrl+Shift+Z or X: ±1秒シーク

  61. Ctrl+Shift+A or S: ±5秒シーク

  62. Ctrl+Shift+Space: 再生/一時停止
`;
  63.     let playbackRateIndex = PLAYBACK_RATE.indexOf(1);
  64.     let backupTrigger = false;
  65.     let parentFrame;
  66.     let video = getVideo();
  67.     if(!video) return;
  68.     let panel = document.querySelector('#bml-videoctrl-panel');
  69.     if(panel) panel.remove();
  70.     panel = document.createElement('div');
  71.     panel.id = 'bml-videoctrl-panel';
  72.     panel.style.right = '10px';
  73.     panel.style.top = '10px';
  74.     panel.setAttribute('active', 'current-time');
  75.     panel.setAttribute('ctrl', false);
  76.     panel.innerHTML = `
  77. <div class="panel-buttons flex">
  78.     <div id="bml-videoctrl-drag-handle" class="drag-handle flex-item"></div>
  79.     <button class="panel-button" command="toggle-embed" title="Toggle embed mode">[&lt; &gt;]</button>
  80.     <button class="panel-button" command="minimize" title="Minimize">[ _ ]</button>
  81.     <button class="panel-button" command="close" title="Close">[ x ]</button>
  82. </div>
  83. <div class="panel-content">
  84.     <div class="text-main" title="Copy timestamp">
  85.         <div class="current-time">
  86.             <span class="text-before-point"></span><span class="text-point smallest">.</span><span class="text-after-point smallest"></span>
  87.             <span class="current-playback-rate"></span>
  88.         </div>
  89.         <div class="copied smaller"></div>
  90.     </div>
  91.     <div class="flex">
  92.         <button class="toggle-toolbar-button flex-item" command="toggle-toolbar" title="Toggle toolbar"><span class="material-icons">menu</span></button>
  93.     </div>
  94.     <div class="toolbar-container">
  95.         <div class="toolbar flex">
  96.             <button class="toolbar-button flex-item" command="seek" value="-30" title="Seek -30 sec.">-30<span class="smaller">s</span></button>
  97.             <button class="toolbar-button flex-item" command="seek" value="-5" title="Seek -5 sec.">-5<span class="smaller">s</span></button>
  98.             <button class="toolbar-button flex-item" command="seek" value="-1" title="Seek -1 sec.">-1<span class="smaller">s</span></button>
  99.             <button class="toolbar-button flex-item" command="seek" value="1" title="Seek 1 sec.">+1<span class="smaller">s</span></button>
  100.             <button class="toolbar-button flex-item" command="seek" value="5" title="Seek 5 sec.">+5<span class="smaller">s</span></button>
  101.             <button class="toolbar-button flex-item" command="seek" value="30" title="Seek 30 sec.">+30<span class="smaller">s</span></button>
  102.             <button class="toolbar-button flex-item use-clipboard" command="paste-time" title="Paste time"><span class="material-icons">content_paste</span></button>
  103.         </div>
  104.         <div class="toolbar flex">
  105.             <button class="toolbar-button flex-item" command="repeat" value="start" title="AB repeat - Set A"><span class="material-icons">repeat</span><span class="smaller">A</span></button>
  106.             <button class="toolbar-button flex-item" command="repeat" value="end" title="AB repeat - Set B"><span class="material-icons">repeat</span><span class="smaller">B</span></button>
  107.             <button class="toolbar-button flex-item" command="playback-rate-decr" title="Decrease playback rate"><span class="material-icons">fast_rewind</span></button>
  108.             <button class="toolbar-button flex-item" command="playback-rate-incr" title="Increase playback rate"><span class="material-icons">fast_forward</span></button>
  109.             <button class="toolbar-button flex-item" command="prevframe" title="Previous frame">-1<span class="smaller">f</span></button>
  110.             <button class="toolbar-button flex-item" command="nextframe" title="Next frame">+1<span class="smaller">f</span></button>
  111.             <button class="toolbar-button flex-item" command="screenshot"><span class="material-icons" title="Take a screen shot">camera_alt</span></button>
  112.             <button class="toolbar-button flex-item" command="toggle-timestamp-editor" title="Toggle editor"><span class="material-icons">edit</span></button>
  113.         </div>
  114.         <div class="timestamp-editor-container" mode="edit">
  115.             <div class="timestamp-editor">
  116.                 <textarea class="content" placeholder="${placeholderText}"></textarea>
  117.                 <div class="toolbar flex">
  118.                     <button class="toolbar-button fix-item" command="timestamp-anchor-offset-editor" value="-1" title="-1sec">-1<span class="smaller">s</span></button>
  119.                     <button class="toolbar-button fix-item" command="timestamp-anchor-offset-editor" value="1" title="+1sec">+1<span class="smaller">s</span></button>
  120.                     <div class="flex-item"></div>
  121.                     <button class="toolbar-button fix-item" command="insert-timestamp" title="Insert timestamp"><span class="material-icons">add_circle</span></button>
  122.                     <div class="flex-item"></div>
  123.                     <button class="toolbar-button fix-item" command="copy-all"><span class="material-icons" title="Copy all">copy_all</span></button>
  124.                     <button class="toolbar-button fix-item" command="view-mode"><span class="material-icons" title="Done">done</span></button>
  125.                 </div>
  126.             </div>
  127.             <div class="timestamp-viewer">
  128.                 <div class="content"></div>
  129.                 <div class="toolbar flex">
  130.                     <button class="toolbar-button fix-item" command="timestamp-anchor-offset-viewer" value="-1" title="-1sec">-1<span class="smaller">s</span></button>
  131.                     <button class="toolbar-button fix-item" command="timestamp-anchor-offset-viewer" value="1" title="+1sec">+1<span class="smaller">s</span></button>
  132.                     <div class="flex-item"></div>
  133.                     <button class="timestamp-prev toolbar-button fix-item" command="timestamp-jump" title="Previous timestamp"><span class="material-icons">chevron_left</span></button>
  134.                     <button class="timestamp-replay toolbar-button fix-item" command="timestamp-jump" title="Current timestamp"><span class="material-icons">replay</span></button>
  135.                     <button class="timestamp-next toolbar-button fix-item" command="timestamp-jump" title="Next timestamp"><span class="material-icons">chevron_right</span></button>
  136.                     <div class="flex-item"></div>
  137.                     <button class="toolbar-button fix-item" command="copy-all"><span class="material-icons" title="Copy all">copy_all</span></button>
  138.                     <button class="toolbar-button fix-item" command="edit-mode"><span class="material-icons" title="Edit">edit</span></button>
  139.                 </div>
  140.             </div>
  141.             <div class="timestamp-backup">
  142.             </div>
  143.         </div>
  144.         <div class="repeat-toolbar toolbar flex" hidden>
  145.             <div class="repeat-text flex-item"></div>
  146.             <button class="toolbar-button fix-item" command="repeat" value="clear" title="Remove"><span class="material-icons">delete</span></button>
  147.         </div>
  148.     </div>
  149. </div>
  150. <a class="screenshot-dllink" target="_blank" hidden></a>
  151. `;
  152.     document.body.appendChild(panel);
  153.     loadBackup();
  154.     toggleEmbed(PANEL_EMBEDDED);
  155.     toggleToolbar(TOOLBAR_COLLAPSED);
  156.     let style = document.querySelector('#bml-videoctrl-style');
  157.     if(!style) {
  158.         style = document.createElement('style');
  159.         style.id = 'bml-videoctrl-style';
  160.         style.type = 'text/css';
  161.         document.head.appendChild(style);
  162.  
  163.         var isDrag = false;
  164.         var targetPanel, startX, startY, panelRight, panelTop, panelWidth, panelHeight, parentWidth, parentHeight;
  165.         document.addEventListener('mousedown', e => {
  166.             if(e.target.classList.contains('drag-handle')) {
  167.                 e.preventDefault();
  168.                 let panel = document.querySelector('#bml-videoctrl-panel');
  169.                 targetPanel = e.target.id;
  170.                 isDrag = true;
  171.                 startX = e.clientX;
  172.                 startY = e.clientY;
  173.                 panelRight = parseInt(panel.style.right);
  174.                 panelTop = parseInt(panel.style.top);
  175.                 panelWidth = panel.clientWidth;
  176.                 panelHeight = panel.clientHeight;
  177.                 if(panel.getAttribute('embedded') == 'true') {
  178.                     parentWidth = panel.parentNode.clientWidth;
  179.                     parentHeight = panel.parentNode.clientHeight;
  180.                 }else {
  181.                     parentWidth = document.documentElement.clientWidth;
  182.                     parentHeight = document.documentElement.clientHeight;
  183.                 }
  184.             };
  185.         });
  186.         document.addEventListener('mouseup', e => {
  187.             isDrag = false;
  188.         });
  189.         document.addEventListener('mousemove', e => {
  190.             if(isDrag && targetPanel == 'bml-videoctrl-drag-handle') {
  191.                 e.preventDefault();
  192.                 let panel = document.querySelector('#bml-videoctrl-panel');
  193.                 panel.style.right = Math.min(parentWidth - panelWidth, Math.max(0, panelRight - (e.clientX - startX))) + 'px';
  194.                 panel.style.top = Math.min(parentHeight - panelHeight, Math.max(0, panelTop + (e.clientY - startY))) + 'px';
  195.             }
  196.         });
  197.  
  198.         if(EDITOR_COLLAPSED) {
  199.             let editorContainer = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container');
  200.             editorContainer.hidden = true;
  201.         }
  202.         if(USE_CLIPBOARD_READING) {
  203.             navigator.clipboard.readText().then(s => {});
  204.             var isCtrlDown = false;
  205.             document.addEventListener('keydown', e => {
  206.                 let panel = document.querySelector('#bml-videoctrl-panel');
  207.                 if(!panel) return;
  208.                 if(e.ctrlKey && !isCtrlDown) {
  209.                     isCtrlDown = true;
  210.                     panel.setAttribute('ctrl', true);
  211.                 }
  212.             });
  213.             document.addEventListener('keyup', e => {
  214.                 let panel = document.querySelector('#bml-videoctrl-panel');
  215.                 if(!panel) return;
  216.                 if(!e.ctrlKey && isCtrlDown) {
  217.                     isCtrlDown = false;
  218.                     panel.setAttribute('ctrl', false);
  219.                 }
  220.             });
  221.         }else {
  222.             let useClipboards = document.querySelectorAll('#bml-videoctrl-panel .use-clipboard');
  223.             useClipboards.forEach(element => {
  224.                 element.hidden = true;
  225.             });
  226.         }
  227.     }
  228.     style.innerHTML = `
  229. @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap');
  230. @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@700&display=swap');
  231. @font-face {
  232.   font-family: 'Material Icons';
  233.   font-style: normal;
  234.   font-weight: 400;
  235.   src: url(https://fonts.gstatic.com/s/materialicons/v83/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
  236. }
  237. #bml-videoctrl-panel .material-icons {
  238.   font-family: 'Material Icons';
  239.   font-weight: normal;
  240.   font-style: normal;
  241.   font-size: 24px;
  242.   line-height: 1;
  243.   letter-spacing: normal;
  244.   text-transform: none;
  245.   display: inline-block;
  246.   white-space: nowrap;
  247.   word-wrap: normal;
  248.   direction: ltr;
  249.   -webkit-font-feature-settings: 'liga';
  250.   -webkit-font-smoothing: antialiased;
  251. }
  252.  
  253. #bml-videoctrl-panel [hidden] {
  254.     display: none !important;
  255. }
  256. #bml-videoctrl-panel {
  257.     --bml-toolbar-height: 36px;
  258.     position: absolute;
  259.     z-index: 2147483647;
  260.     background-color: ${BG_COLOR};
  261.     border-radius: 4px;
  262.     font-family: Roboto;
  263.     opacity: ${OPACITY};
  264.     backdrop-filter: blur(${BG_BLUR_RADIUS});
  265. }
  266. .ytp-player-minimized #bml-videoctrl-panel {
  267.     display: none;
  268. }
  269. #bml-videoctrl-panel ::-webkit-scrollbar {
  270.     width: 10px;
  271.     background-color: rgba(0,0,0,.1);
  272. }
  273. #bml-videoctrl-panel ::-webkit-scrollbar-thumb {
  274.     background-color: rgba(255,255,255,.25);
  275.     border: none;
  276.     border-radius: 5px;
  277. }
  278. #bml-videoctrl-panel ::-webkit-scrollbar-track {
  279.     background-color: rgba(0,0,0,.1);
  280. }
  281. #bml-videoctrl-panel[embedded="false"] {
  282.     position: fixed;
  283. }
  284. #bml-videoctrl-panel[ctrl="false"] .shown-on-ctrl,
  285. #bml-videoctrl-panel[ctrl="true"] .hidden-on-ctrl {
  286.     display: none;
  287. }
  288. #bml-videoctrl-panel button {
  289.     outline: none;
  290. }
  291. #bml-videoctrl-panel .flex {
  292.     display: flex;
  293. }
  294. #bml-videoctrl-panel .flex-item {
  295.     flex-grow: 1;
  296. }
  297. #bml-videoctrl-panel .fix-item {
  298.     flex-basis: var(--bml-toolbar-height);
  299.     flex-shrink: 0;
  300. }
  301. #bml-videoctrl-panel .smaller {
  302.     font-size: 75%;
  303. }
  304. #bml-videoctrl-panel .smallest {
  305.     font-size: 50%;
  306. }
  307. #bml-videoctrl-panel .panel-buttons {
  308.     box-sizing: border-box;
  309.     padding: 4px;
  310.     float: right;
  311. }
  312. #bml-videoctrl-panel .drag-handle {
  313.     border-radius: 4px;
  314.     background-color: rgba(255, 255, 255, .1);
  315.     cursor: move;
  316. }
  317. #bml-videoctrl-panel .panel-button {
  318.     width: initial;
  319.     margin-left: 5px;
  320.     padding: 0;
  321.     border: none;
  322.     background-color: transparent;
  323.     color: ${FONT_COLOR};
  324.     font-family: Roboto;
  325.     font-size: 11px;
  326.     cursor: pointer;
  327. }
  328. #bml-videoctrl-panel .panel-buttons {
  329.     width: 100%;
  330. }
  331. #bml-videoctrl-panel .toggle-toolbar-button {
  332.     width: 100%;
  333.     height: 24px;
  334.     padding: 0;
  335.     border: none;
  336.     background-color: transparent;
  337.     color: ${FONT_COLOR};
  338.     font-family: Roboto;
  339.     font-size: 20px;
  340.     font-weight: 700;
  341.     text-align: center;
  342.     line-height: 24px;
  343.     opacity: .5;
  344.     transition: opacity .2s ease-in-out;
  345.     cursor: pointer;
  346. }
  347. #bml-videoctrl-panel .toggle-toolbar-button:hover {
  348.     opacity: 1;
  349. }
  350. #bml-videoctrl-panel .panel-content {
  351.     padding: 5px;
  352.     min-width: ${PANEL_MIN_WIDTH};
  353. }
  354. #bml-videoctrl-panel .text-main {
  355.     margin: 16px 16px 0;
  356.     height: ${FONT_SIZE};
  357.     color: ${FONT_COLOR};
  358.     font-size: ${FONT_SIZE};
  359.     font-weight: 700;
  360.     line-height: ${FONT_SIZE};
  361.     text-align: center;
  362.     cursor: pointer;
  363.     user-select: none;
  364.     opacity: .8;
  365.     transition: opacity .2s ease-in-out;
  366. }
  367. #bml-videoctrl-panel .text-main:hover {
  368.     opacity: 1;
  369. }
  370. #bml-videoctrl-panel .text-main > div {
  371.     visibility: collapse;
  372.     height: 0;
  373. }
  374. #bml-videoctrl-panel[active="current-time"] .current-time,
  375. #bml-videoctrl-panel[active="paste-time"] .paste-time,
  376. #bml-videoctrl-panel[active="copied"] .copied {
  377.     visibility: visible;
  378.     height: 100%;
  379. }
  380. #bml-videoctrl-panel .current-playback-rate {
  381.     position: absolute;
  382.     top: 32px;
  383.     right: 8px;
  384.     font-size: 16px;
  385.     line-height: 16px;
  386. }
  387. #bml-videoctrl-panel .toolbar-container {
  388.     border-top: 1px solid rgba(255, 255, 255, .2);
  389. }
  390. #bml-videoctrl-panel .toolbar:not([hidden]) {
  391.     height: var(--bml-toolbar-height);
  392. }
  393. #bml-videoctrl-panel .toolbar-button {
  394.     width: 100%;
  395.     border: none;
  396.     background-color: transparent;
  397.     color: ${FONT_COLOR};
  398.     font-family: Roboto;
  399.     font-size: 20px;
  400.     font-weight: 700;
  401.     text-align: center;
  402.     white-space: nowrap;
  403.     opacity: .5;
  404.     transition: opacity .2s ease-in-out;
  405.     cursor: pointer;
  406. }
  407. #bml-videoctrl-panel .toolbar-button[disabled] {
  408.     cursor: default;
  409. }
  410. #bml-videoctrl-panel .toolbar-button:not([disabled]):hover {
  411.     opacity: 1;
  412. }
  413. #bml-videoctrl-panel .toolbar-button .material-icons {
  414.     margin: 5px 0;
  415. }
  416. #bml-videoctrl-panel .timestamp-editor-container {
  417.     border-top: 1px solid rgba(255, 255, 255, .2);
  418. }
  419. #bml-videoctrl-panel .timestamp-editor-container > div {
  420.     display: none;
  421. }
  422. #bml-videoctrl-panel .timestamp-editor-container[mode="edit"] .timestamp-editor,
  423. #bml-videoctrl-panel .timestamp-editor-container[mode="view"] .timestamp-viewer {
  424.     display: flex;
  425.     flex-direction: column;
  426. }
  427. #bml-videoctrl-panel .timestamp-editor-container .content {
  428.     width: 0;
  429.     min-width: 100%;
  430.     height: ${TIMESTAMP_EDITOR_HEIGHT};
  431.     overflow: auto;
  432.     font-size: 12px;
  433.     font-family: Roboto;
  434.     font-weight: 400;
  435.     line-height: 16px;
  436.     white-space: pre-wrap;
  437.     box-sizing: border-box;
  438.     color: ${FONT_COLOR};
  439.     border: none;
  440.     padding: 2px;
  441.     resize: none;
  442.     user-select: text;
  443. }
  444. #bml-videoctrl-panel .timestamp-editor .content {
  445.     background-color: rgba(0, 0, 0, .25);
  446. }
  447. #bml-videoctrl-panel .timestamp-editor .content::placeholder {
  448.     color: ${FONT_COLOR};
  449.     opacity: .5;
  450. }
  451. #bml-videoctrl-panel .timestamp-anker {
  452.     color: ${FONT_COLOR};
  453.     text-decoration: none;
  454. }
  455. #bml-videoctrl-panel .timestamp-anker:hover {
  456.     text-decoration: underline;
  457. }
  458. #bml-videoctrl-panel .timestamp-anker.current {
  459.     font-weight: bold;
  460. }
  461. #bml-videoctrl-panel .content-row {
  462.     display: inline-block;
  463.     width: 100%;
  464.     margin: 0 -2px;
  465.     padding: 0 2px;
  466. }
  467. #bml-videoctrl-panel .content-row.current {
  468.     background-color: rgba(255, 255, 255, .2);
  469. }
  470. #bml-videoctrl-panel .repeat-toolbar .repeat-text {
  471.     font-size: 16px;
  472.     font-family: Roboto;
  473.     font-weight: 400;
  474.     line-height: var(--bml-toolbar-height);
  475.     color: ${FONT_COLOR};
  476.     text-align: center;
  477.     opacity: .5;
  478. }`;
  479.     setEventListener();
  480.     intervalId = setInterval(timeupdate, 1 / 30);
  481.  
  482.     function setEventListener() {
  483.         let textMain = document.querySelector('#bml-videoctrl-panel .text-main');
  484.         textMain.addEventListener('click', e => {
  485.             let ft = copyTimestamp();
  486.             let copied = document.querySelector('#bml-videoctrl-panel .copied');
  487.             copied.textContent = '📋"' + ft + '"';
  488.             panel.setAttribute('active', 'copied');
  489.             if(timeoutId) {
  490.                 clearTimeout(timeoutId);
  491.             }
  492.             timeoutId = setTimeout(() => {
  493.                 if(panel.getAttribute('active') == 'copied') {
  494.                     panel.setAttribute('active', 'current-time');
  495.                 }
  496.             }, 2000);
  497.         });
  498.         let viewer = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content');
  499.         viewer.addEventListener('click', e => {
  500.             if(e.target.classList.contains('timestamp-anker')) {
  501.                 e.preventDefault();
  502.                 let video = getVideo();
  503.                 if(video) {
  504.                     let targetTime = parseInt(e.target.getAttribute('time'));
  505.                     video.currentTime = targetTime;
  506.                 }
  507.             }
  508.         });
  509.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  510.         editor.addEventListener('keydown', e => {
  511.             let keyCode = e.keyCode;
  512.             backupTrigger = true;
  513.             if(e.ctrlKey && e.shiftKey) {
  514.                 switch(keyCode) {
  515.                     case 38: { /* ↑: offset 1sec */
  516.                         e.preventDefault();
  517.                         setEditorTimestampAnchorOffset(1);
  518.                         break;
  519.                     }
  520.                     case 40: { /* ↓: offset -1sec */
  521.                         e.preventDefault();
  522.                         setEditorTimestampAnchorOffset(-1);
  523.                         break;
  524.                     }
  525.                     case 13: { /* Enter: insert timestamp (end) */
  526.                         e.preventDefault();
  527.                         insertTimestamp(true);
  528.                         break;
  529.                     }
  530.                     case 191: { /* /: insert timestamp */
  531.                         e.preventDefault();
  532.                         insertTimestamp();
  533.                         break;
  534.                     }
  535.                     case 67: { /* C: copy timestamp */
  536.                         e.preventDefault();
  537.                         copyTimestamp();
  538.                         break;
  539.                     }
  540.                     case 90: { /* Z: seek -1sec */
  541.                         e.preventDefault();
  542.                         seekVideo(-1, true);
  543.                         break;
  544.                     }
  545.                     case 88: { /* X: seek 1sec */
  546.                         e.preventDefault();
  547.                         seekVideo(1, true);
  548.                         break;
  549.                     }
  550.                     case 65: { /* A: seek -5sec */
  551.                         e.preventDefault();
  552.                         seekVideo(-5, true);
  553.                         break;
  554.                     }
  555.                     case 83: { /* S: seek 5sec */
  556.                         e.preventDefault();
  557.                         seekVideo(5, true);
  558.                         break;
  559.                     }
  560.                     case 32: { /* Space: play/pause video */
  561.                         e.preventDefault();
  562.                         let video = getVideo();
  563.                         if(video.paused) {
  564.                             video.play();
  565.                         }else {
  566.                             video.pause();
  567.                         }
  568.                         break;
  569.                     }
  570.                 }
  571.             }
  572.             e.stopImmediatePropagation();
  573.         });
  574.         let buttons = document.querySelectorAll('#bml-videoctrl-panel button');
  575.         buttons.forEach(button => {
  576.             button.addEventListener('click', e => {
  577.                 let command = button.getAttribute('command');
  578.                 switch(command) {
  579.                     case 'seek': {
  580.                         let sec = parseFloat(button.value);
  581.                         seekVideo(sec, true);
  582.                         break;
  583.                     }
  584.                     case 'paste-time': {
  585.                         let video = getVideo();
  586.                         navigator.clipboard.readText().then(string => {
  587.                             let targetTime = getTimeFromString(string);
  588.                             if(targetTime) {
  589.                                 video.currentTime = targetTime;
  590.                             }
  591.                         });
  592.                         break;
  593.                     }
  594.                     case 'prevframe': {
  595.                         let video = getVideo();
  596.                         if(!video.paused) {
  597.                             video.pause();
  598.                         }
  599.                         seekVideo(-1 / 30, false);
  600.                         break;
  601.                     }
  602.                     case 'nextframe': {
  603.                         let video = getVideo();
  604.                         if(!video.paused) {
  605.                             video.pause();
  606.                         }
  607.                         seekVideo(1 / 30, false);
  608.                         break;
  609.                     }
  610.                     case 'repeat': {
  611.                         let video = getVideo();
  612.                         let ct = getCurrentTime();
  613.                         switch(button.value) {
  614.                             case 'start': {
  615.                                 if(repeatEnd == null) {
  616.                                     repeatEnd = video.duration;
  617.                                 }
  618.                                 repeatStart = ct;
  619.                                 break;
  620.                             }
  621.                             case 'end': {
  622.                                 if(repeatStart == null) {
  623.                                     repeatStart = 0;
  624.                                 }
  625.                                 repeatEnd = ct;
  626.                                 break;
  627.                             }
  628.                             case 'clear': {
  629.                                 repeatStart = null;
  630.                                 repeatEnd = null;
  631.                                 break;
  632.                             }
  633.                             default: {
  634.                                 break;
  635.                             }
  636.                         }
  637.                         break;
  638.                     }
  639.                     case 'playback-rate-decr': {
  640.                         changePlaybackRate(-1);
  641.                         break;
  642.                     }
  643.                     case 'playback-rate-incr': {
  644.                         changePlaybackRate(1);
  645.                         break;
  646.                     }
  647.                     case 'screenshot': {
  648.                         let video = getVideo();
  649.                         if(video) {
  650.                             let canvas = document.createElement('canvas');
  651.                             canvas.width = video.videoWidth;
  652.                             canvas.height = video.videoHeight;
  653.                             let context = canvas.getContext('2d');
  654.                             context.drawImage(video, 0, 0, canvas.width, canvas.height);
  655.                             canvas.toBlob(b => {
  656.                                 let a = document.querySelector('#bml-videoctrl-panel .screenshot-dllink');
  657.                                 a.href = URL.createObjectURL(b);
  658.                                 let date = new Date();
  659.                                 let z = n => ('0' + n).slice(-2);
  660.                                 a.setAttribute('download', `${date.getFullYear()}${z(date.getMonth() + 1)}${z(date.getDate())}_${z(date.getHours())}${z(date.getMinutes())}${z(date.getSeconds())}_${document.title}_${sec2hms(getCurrentTime(GET_TIME_FROM_TEXT_ELEMENT), '')}.png`);
  661.                                 a.click();
  662.                             });
  663.                         }
  664.                         break;
  665.                     }
  666.                     case 'toggle-timestamp-editor': {
  667.                         let editorContainer = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container');
  668.                         if(editorContainer.hidden) {
  669.                             editorContainer.hidden = false;
  670.                             let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  671.                             editor.focus();
  672.                         }else {
  673.                             editorContainer.hidden = true;
  674.                         }
  675.                         break;
  676.                     }
  677.                     case 'view-mode': {
  678.                         switchEditorMode('view');
  679.                         timeTable = getTimeTable();
  680.                         lastIndex = null;
  681.                         updateTimestampControl();
  682.                         break;
  683.                     }
  684.                     case 'edit-mode': {
  685.                         switchEditorMode('edit');
  686.                         break;
  687.                     }
  688.                     case 'copy-all': {
  689.                         navigator.clipboard.writeText(getEditorContent());
  690.                         break;
  691.                     }
  692.                     case 'insert-timestamp': {
  693.                         insertTimestamp(true);
  694.                         break;
  695.                     }
  696.                     case 'timestamp-jump': {
  697.                         if(button.getAttribute('disabled') != 'true') {
  698.                             let index = parseInt(button.getAttribute('target-index'));
  699.                             let video = getVideo();
  700.                             if(index < 0) {
  701.                                 video.currentTime = 0;
  702.                             }else {
  703.                                 video.currentTime = timeTable[index][0];
  704.                             }
  705.                             scrollEditorContent();
  706.                         }
  707.                         break;
  708.                     }
  709.                     case 'timestamp-anchor-offset-viewer': {
  710.                         let offset = parseInt(button.value);
  711.                         setViewerTimestampAnchorOffset(offset);
  712.                         break;
  713.                     }
  714.                     case 'timestamp-anchor-offset-editor': {
  715.                         let offset = parseInt(button.value);
  716.                         setEditorTimestampAnchorOffset(offset);
  717.                         break;
  718.                     }
  719.                     case 'toggle-toolbar': {
  720.                         toggleToolbar();
  721.                         break;
  722.                     }
  723.                     case 'toggle-embed': {
  724.                         toggleEmbed(panel.parentNode.tagName == 'BODY');
  725.                         break;
  726.                     }
  727.                     case 'minimize': {
  728.                         let content = document.querySelector('#bml-videoctrl-panel .panel-content');
  729.                         content.hidden = !content.hidden;
  730.                         break;
  731.                     }
  732.                     case 'close': {
  733.                         clearInterval(intervalId);
  734.                         repeatStart = null;
  735.                         repeatEnd = null;
  736.                         panel.remove();
  737.                         break;
  738.                     }
  739.                     default: {
  740.                         break;
  741.                     }
  742.                 }
  743.             });
  744.         });
  745.     }
  746.  
  747.     function hms2sec(string) {
  748.         let result = string.match(/(?:(?<h>\d+)\:)?(?<m>[0-5]?\d)\:(?<s>[0-5]\d)(?![\d<])/);
  749.         return parseInt(result.groups.h || 0) * 3600 + parseInt(result.groups.m) * 60 + parseInt(result.groups.s);
  750.     }
  751.  
  752.     function sec2ft(sec, separator) {
  753.         if(separator == null) {
  754.             separator = ':';
  755.         }
  756.         let z = n => ('0' + n).slice(-2);
  757.         let h = Math.floor(sec / 3600);
  758.         if(APPLY_PADDING_TO_HOUR) h = z(h);
  759.         let m = z(Math.floor((sec / 60) % 60));
  760.         if(!APPLY_PADDING_TO_MIN && HIDE_HOUR_IF_ZERO && parseInt(h) == 0) m = parseInt(m);
  761.         let s = z(Math.floor(sec % 60));
  762.         let ft = (HIDE_HOUR_IF_ZERO && parseInt(h) == 0) ? `${m}${separator}${s}` : `${h}${separator}${m}${separator}${s}`;
  763.         return ft;
  764.     }
  765.  
  766.     function sec2hms(sec, separator) {
  767.         if(separator == null) {
  768.             separator = ':';
  769.         }
  770.         let z = n => ('0' + n).slice(-2);
  771.         let h = Math.floor(sec / 3600);
  772.         if(APPLY_PADDING_TO_HOUR) h = z(h);
  773.         let m = z(Math.floor((sec / 60) % 60));
  774.         let s = z(Math.floor(sec % 60));
  775.         return `${h}${separator}${m}${separator}${s}`;
  776.     }
  777.  
  778.     function sec2ms(sec) {
  779.         let z = n => ('0' + n).slice(-2);
  780.         let ms = z(Math.floor((sec % 1) * 100));
  781.         return `${ms}`;
  782.     }
  783.  
  784.     function getVideo() {
  785.         let videos = Array.from(document.querySelectorAll('video, bwp-video'));
  786.         let video;
  787.         if(videos.length == 1) {
  788.             video = videos[0];
  789.         }else {
  790.             videos.sort((a, b) => {
  791.                 const durationA = a.duration || 0;
  792.                 const durationB = b.duration || 0;
  793.                 return durationB - durationA;
  794.             });
  795.             for(let i = 0; i < videos.length; i++) {
  796.                 if(!videos[i].paused) {
  797.                     video = videos[i];
  798.                     break;
  799.                 }
  800.             }
  801.             if(!video) video = videos[0];
  802.         }
  803.         if(!video) {
  804.             let frames = window.frames;
  805.             for(let i = 0; i < frames.length; i++) {
  806.                 video = frames[i].document.querySelector('video, bwp-video');
  807.                 if(video) {
  808.                     parentFrame = frames[i];
  809.                     break;
  810.                 }
  811.             }
  812.         }
  813.         return video;
  814.     }
  815.  
  816.     function seekVideo(sec, roundFlag) {
  817.         let video = getVideo();
  818.         if(video) {
  819.             let targetTime = Math.min(video.duration, Math.max(0, getCurrentTime() + sec));
  820.             if(roundFlag) {
  821.                 targetTime = (sec > 0 || !video.paused) ? Math.floor(targetTime) : Math.ceil(targetTime);
  822.             }
  823.             video.currentTime = targetTime;
  824.         }
  825.     }
  826.  
  827.     function changePlaybackRate(delta) {
  828.         let video = getVideo();
  829.         let currentPlaybackRate = video.playbackRate;
  830.         let lowerPlaybackRate = currentPlaybackRate;
  831.         let higherPlaybackRate = currentPlaybackRate;
  832.         for(let i = 0; i < PLAYBACK_RATE.length; i++) {
  833.             if(PLAYBACK_RATE[i] < currentPlaybackRate) {
  834.                 lowerPlaybackRate = PLAYBACK_RATE[i];
  835.             }
  836.             if(PLAYBACK_RATE[i] > currentPlaybackRate) {
  837.                 higherPlaybackRate = PLAYBACK_RATE[i];
  838.                 break;
  839.             }
  840.         }
  841.         video.playbackRate = (delta == 1) ? higherPlaybackRate : (delta == -1) ? lowerPlaybackRate : (delta == 0) ? 1 : currentPlaybackRate;
  842.     }
  843.  
  844.     function getCurrentTime(flag) {
  845.         let video = getVideo();
  846.         if(flag) {
  847.             let time = document.querySelector(TIME_SELECTOR.join(','));
  848.             let r = time && time.textContent.match(/(?:(\d+)\:)?([0-5]?\d)\:([0-5]\d)/);
  849.             let ct = r && ((parseInt(r[1]) || 0) * 3600 + parseInt(r[2]) * 60 + parseInt(r[3]));
  850.             if(ct) {
  851.                 currentTimeOffset = ct - (video.currentTime - 0.5);
  852.                 if(Math.abs(currentTimeOffset) > 3) {
  853.                     return ct;
  854.                 }else {
  855.                     return video.currentTime;
  856.                 }
  857.             }else {
  858.                 if(currentTimeOffset && Math.abs(currentTimeOffset) > 3) {
  859.                     return video.currentTime + currentTimeOffset;
  860.                 }else {
  861.                     return video.currentTime;
  862.                 }
  863.             }
  864.         }else {
  865.             return video.currentTime;
  866.         }
  867.     }
  868.  
  869.     function getTimeFromString(string) {
  870.         if(!string) return;
  871.         let regexp, targetTime;
  872.         let sign = 0;
  873.         regexp = string.trim().match(/^\+/);
  874.         if(regexp) sign = 1;
  875.         regexp = string.trim().match(/^-/);
  876.         if(regexp) sign = -1;
  877.         /* secounds */
  878.         regexp = string.trim().match(/^[-+]?\d+(?:\.\d+)?$/);
  879.         if(regexp) targetTime = parseFloat(regexp[0]);
  880.         /* [hh:]mm:ss */
  881.         regexp = string.match(/(?:(\d+)\:)?([0-5]?\d)\:([0-5]\d)/);
  882.         if(regexp) targetTime = (parseInt(regexp[1]) || 0) * 3600 + parseInt(regexp[2]) * 60 + parseInt(regexp[3]);
  883.  
  884.         /* delta */
  885.         if(targetTime && sign != 0) {
  886.             targetTime = getCurrentTime() + targetTime * sign;
  887.         }
  888.         return targetTime;
  889.     }
  890.  
  891.     function getTimeTable() {
  892.         let timestamp = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content');
  893.         let anchors = Array.from(timestamp.querySelectorAll('.timestamp-anker[time]'));
  894.         let timeTable = [];
  895.         anchors.forEach(anchor => {
  896.             let index = -1;
  897.             for(let i = 0; i < timeTable.length; i++) {
  898.                 if(timeTable[i][0] == parseInt(anchor.getAttribute('time'))) {
  899.                     index = i;
  900.                     break;
  901.                 }
  902.             }
  903.             let timeItem = [];
  904.             if(index == -1) {
  905.                 timeItem.push(parseInt(anchor.getAttribute('time')));
  906.                 let regex = new RegExp('^.*' + anchor.textContent + '.*$', 'mu');
  907.                 timeItem.push(timestamp.textContent.match(regex)[0]);
  908.                 timeItem.push(anchor.textContent);
  909.                 timeTable.push(timeItem);
  910.             }
  911.         });
  912.         timeTable.sort((a, b) => {
  913.             return a[0] - b[0];
  914.         });
  915.         return timeTable;
  916.     }
  917.  
  918.     function getTimeTableIndex(time) {
  919.         if(!timeTable || timeTable.length == 0) return -1;
  920.         let index;
  921.         if(time < timeTable[0][0]) {
  922.             index = -1;
  923.         }else if(time >= timeTable[timeTable.length - 1][0]) {
  924.             index = timeTable.length - 1;
  925.         }else {
  926.             for(let i = 1; i < timeTable.length; i++) {
  927.                 if(timeTable[i][0] > time) {
  928.                     index = i - 1;
  929.                     break;
  930.                 }
  931.             }
  932.         }
  933.         return index;
  934.     }
  935.  
  936.     function toggleEmbed(boolean) {
  937.         let video = getVideo();
  938.         let parent;
  939.         let content = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container[mode="edit"] .timestamp-editor .content, #bml-videoctrl-panel .timestamp-editor-container[mode="view"] .timestamp-viewer .content');
  940.         let scrollTop = content.scrollTop;
  941.         if(!parentFrame && boolean) {
  942.             parent = document.querySelector(PARENT_SELECTOR.join(','));
  943.             if(!parent) {
  944.                 parent = video.parentNode.parentNode;
  945.             }
  946.         }else {
  947.             parent = document.body;
  948.         }
  949.         parent.appendChild(panel);
  950.         panel.setAttribute('embedded', !parentFrame && boolean);
  951.         content = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container[mode="edit"] .timestamp-editor .content, #bml-videoctrl-panel .timestamp-editor-container[mode="view"] .timestamp-viewer .content');
  952.         content.scrollTop = scrollTop;
  953.     }
  954.  
  955.     function toggleToolbar(boolean) {
  956.         let toolbar = document.querySelector('#bml-videoctrl-panel .toolbar-container');
  957.         if(boolean == null) {
  958.             toolbar.hidden = !toolbar.hidden;
  959.         }else {
  960.             toolbar.hidden = boolean;
  961.         }
  962.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  963.         editor.focus();
  964.     }
  965.  
  966.     function getEditorContent() {
  967.         let content = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container[mode="edit"] .timestamp-editor .content, #bml-videoctrl-panel .timestamp-editor-container[mode="view"] .timestamp-viewer .content');
  968.         return content.textContent || content.value;
  969.     }
  970.  
  971.     function editor2viewer() {
  972.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  973.         let viewer = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content');
  974.         let innerHTML = editor.value.replace(/(?:(\d+)\:)?([0-5]?\d)\:([0-5]\d)(?![\d<])/g, ($0, h, m, s) => {
  975.             let time = parseInt(h || 0) * 3600 + parseInt(m) * 60 + parseInt(s);
  976.             return `<a class="timestamp-anker" href="#" dir="auto" time="${time}">${$0}</a>`;
  977.         });
  978.         let regexp = new RegExp(`[^\n]+`, 'g');
  979.         innerHTML = innerHTML.replace(regexp, '<span class="content-row">$&</span>');
  980.         viewer.innerHTML = innerHTML;
  981.     }
  982.  
  983.     function viewer2editor() {
  984.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  985.         let viewer = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content');
  986.         editor.value = viewer.textContent;
  987.     }
  988.  
  989.     function switchEditorMode(mode) {
  990.         let editorContainer = document.querySelector('#bml-videoctrl-panel .timestamp-editor-container');
  991.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  992.         let viewer = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content');
  993.         if(editorContainer.getAttribute('mode') != mode) {
  994.             switch(mode) {
  995.                 case 'edit': {
  996.                     let scrollTop = viewer.scrollTop;
  997.                     viewer2editor();
  998.                     editorContainer.setAttribute('mode', 'edit');
  999.                     editor.scrollTop = scrollTop;
  1000.                     break;
  1001.                 }
  1002.                 case 'view': {
  1003.                     let scrollTop = editor.scrollTop;
  1004.                     editor2viewer();
  1005.                     editorContainer.setAttribute('mode', 'view');
  1006.                     viewer.scrollTop = scrollTop;
  1007.                     break;
  1008.                 }
  1009.             }
  1010.             let video = getVideo();
  1011.             video.focus();
  1012.         }
  1013.     }
  1014.  
  1015.     function scrollEditorContent(){
  1016.         let content = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .content:not(:hover)');
  1017.         if(content) {
  1018.             let video = getVideo();
  1019.             let index = getTimeTableIndex(video.currentTime);
  1020.             if(index < 0) {
  1021.                 content.scrollTop = 0;
  1022.             }else {
  1023.                 let contentRect = content.getBoundingClientRect();
  1024.                 let CurrentRow = content.querySelector(`.content-row.current`);
  1025.                 if(CurrentRow) {
  1026.                     let CurrentRowRect = CurrentRow.getBoundingClientRect();
  1027.                     if((CurrentRowRect.top - contentRect.top < 0) || (contentRect.bottom - CurrentRowRect.bottom < 0)) {
  1028.                         content.scrollTop = content.scrollTop + CurrentRowRect.top - contentRect.top - (contentRect.height / 2);
  1029.                     }
  1030.                 }
  1031.             }
  1032.         }
  1033.     }
  1034.  
  1035.     function copyTimestamp() {
  1036.         let ct = Math.floor(getCurrentTime(GET_TIME_FROM_TEXT_ELEMENT));
  1037.         let ft = sec2ft(ct);
  1038.         if(COPY_WITH_SPACE) ft += ' ';
  1039.         navigator.clipboard.writeText(ft);
  1040.         return ft;
  1041.     }
  1042.  
  1043.     function insertTimestamp(insertEnd) {
  1044.         let ct = Math.floor(getCurrentTime(GET_TIME_FROM_TEXT_ELEMENT));
  1045.         let ft = sec2ft(ct) + ' ';
  1046.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  1047.         editor.focus();
  1048.         if(insertEnd) {
  1049.             let insertNode = (editor.value) ? '\n' + ft : ft;
  1050.             editor.setSelectionRange(editor.value.length, editor.value.length);
  1051.             document.execCommand('insertText', false, insertNode);
  1052.             editor.scrollTop = editor.scrollHeight;
  1053.         }else {
  1054.             let value = editor.value;
  1055.             let posStart = editor.selectionStart;
  1056.             let posEnd = editor.selectionEnd;
  1057.             let range = value.slice(posStart, posEnd);
  1058.             let gap = range.length;
  1059.             let beforeNode = value.slice(0, posStart);
  1060.             let afterNode = value.slice(posEnd);
  1061.             let insertNode = ft;
  1062.             gap = insertNode.length - gap;
  1063.             document.execCommand('insertText', false, insertNode);
  1064.             editor.setSelectionRange(posEnd + gap, posEnd + gap);
  1065.         }
  1066.     }
  1067.  
  1068.     function setViewerTimestampAnchorOffset(offset) {
  1069.         backupTrigger = true;
  1070.         let anchor = document.querySelector('#bml-videoctrl-panel .timestamp-viewer .timestamp-anker.current');
  1071.         if(!anchor) return;
  1072.         let time = Math.max(0, hms2sec(anchor.textContent) + offset);
  1073.         anchor.textContent = sec2ft(time);
  1074.         anchor.setAttribute('time', time);
  1075.         let video = getVideo();
  1076.         video.currentTime = time;
  1077.         timeTable = getTimeTable();
  1078.         lastIndex = null;
  1079.         updateTimestampControl();
  1080.     }
  1081.  
  1082.     function setEditorTimestampAnchorOffset(offset) {
  1083.         backupTrigger = true;
  1084.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  1085.         editor.focus();
  1086.         let value = editor.value;
  1087.         let posStart = editor.selectionStart;
  1088.         let posEnd = editor.selectionEnd;
  1089.         let posDirection = editor.selectionDirection;
  1090.         if(posStart == posEnd) {
  1091.             posStart = value.slice(0, posStart).match(/[0-9:\-]*$/).index;
  1092.             posEnd = posStart + value.slice(posStart).match(/[0-9:\-]*/)[0].length;
  1093.             editor.setSelectionRange(posStart, posEnd);
  1094.         }
  1095.         let range = value.slice(posStart, posEnd);
  1096.         let gap = range.length;
  1097.         let beforeNode = value.slice(0, posStart);
  1098.         let afterNode = value.slice(posEnd);
  1099.         let insertNode = range.replace(/(-?)(?:(\d+)\:)?([0-5]?\d)\:([0-5]\d)(?!\d|\:)/g, ($0, minus, h, m, s) => {
  1100.             let sign, time, ft;
  1101.             sign = (minus) ? -1 : 1;
  1102.             time = sign * (parseInt(h || 0) * 3600 + parseInt(m) * 60 + parseInt(s)) + offset;
  1103.             sign = Math.sign(time);
  1104.             ft = (sign == -1) ? '-' : '';
  1105.             ft += sec2ft(Math.abs(time));
  1106.             return ft;
  1107.         });
  1108.         gap = insertNode.length - gap;
  1109.         document.execCommand('insertText', false, insertNode);
  1110.         editor.setSelectionRange(posStart, posEnd + gap, posDirection);
  1111.     }
  1112.  
  1113.     function getBackup() {
  1114.         return JSON.parse(localStorage.getItem('bml-videoctrl-backup'));
  1115.     }
  1116.  
  1117.     function saveBackup() {
  1118.         backupTrigger = false;
  1119.         let backup = getBackup() || [];
  1120.         for(let i = 0; i < backup.length; i++) {
  1121.             if(backup[i].url == document.URL) {
  1122.                 backup.splice(i, 1);
  1123.             }
  1124.         }
  1125.         backup.push({'url': document.URL, 'content': getEditorContent()});
  1126.         if(backup.length > 10) {
  1127.             backup.shift();
  1128.         }
  1129.         localStorage.setItem('bml-videoctrl-backup', JSON.stringify(backup));
  1130.     }
  1131.  
  1132.     function loadBackup() {
  1133.         let backup = getBackup() || [];
  1134.         let editor = document.querySelector('#bml-videoctrl-panel .timestamp-editor .content');
  1135.         editor.value = '';
  1136.         for(let i = 0; i < backup.length; i++) {
  1137.             if(backup[i].url == document.URL) {
  1138.                 editor.value = backup[i].content;
  1139.                 break;
  1140.             }
  1141.         }
  1142.         editor2viewer();
  1143.     }
  1144.  
  1145.     function updateTimeDisplay() {
  1146.         let ct = getCurrentTime(GET_TIME_FROM_TEXT_ELEMENT);
  1147.         let beforePoint = document.querySelector('#bml-videoctrl-panel .text-before-point');
  1148.         let afterPoint = document.querySelector('#bml-videoctrl-panel .text-after-point');
  1149.         beforePoint.textContent = sec2ft(ct);
  1150.         afterPoint.textContent = sec2ms(ct);
  1151.         let playbackRate = document.querySelector('#bml-videoctrl-panel .current-playback-rate');
  1152.         let video = getVideo();
  1153.         if(video.playbackRate == 1) {
  1154.             playbackRate.textContent = '';
  1155.         }else {
  1156.             playbackRate.textContent = `x${video.playbackRate.toFixed(2)}`;
  1157.         }
  1158.     }
  1159.  
  1160.     function updateTimestampControl() {
  1161.         if(!timeTable) return;
  1162.         let video = getVideo();
  1163.         let index = getTimeTableIndex(video.currentTime);
  1164.         if(lastIndex != null && lastIndex == index) return;
  1165.         lastIndex = index;
  1166.  
  1167.         /* 前のタイムスタンプボタン */
  1168.         let prev = document.querySelector('#bml-videoctrl-panel .timestamp-prev');
  1169.         if(index == -1) {
  1170.             prev.setAttribute('disabled', true);
  1171.         }else {
  1172.             prev.removeAttribute('disabled');
  1173.         }
  1174.         prev.title = (index == -1) ? '' : (index == 0) ? '00:00 Start' : timeTable[index - 1][1];
  1175.         prev.setAttribute('target-index', index - 1);
  1176.  
  1177.         /* 現在のタイムスタンプボタン */
  1178.         let replay = document.querySelector('#bml-videoctrl-panel .timestamp-replay');
  1179.         replay.title = (index == -1) ? '00:00 Start' : timeTable[index][1];
  1180.         replay.setAttribute('target-index', index);
  1181.  
  1182.         /* 次のタイムスタンプボタン */
  1183.         let next = document.querySelector('#bml-videoctrl-panel .timestamp-next');
  1184.         if(index == timeTable.length - 1) {
  1185.             next.setAttribute('disabled', true);
  1186.         }else {
  1187.             next.removeAttribute('disabled');
  1188.         }
  1189.         next.title = (index < timeTable.length - 1) ? timeTable[index + 1][1] : next.title = '';
  1190.         next.setAttribute('target-index', index + 1);
  1191.  
  1192.         /* 現在のタイムスタンプを強調 */
  1193.         let currentRows = Array.from(document.querySelectorAll('#bml-videoctrl-panel .timestamp-viewer .content-row.current'));
  1194.         currentRows.forEach(currentRow => {
  1195.             currentRow.classList.remove('current');
  1196.         });
  1197.         if(index != -1) {
  1198.             let anchors = Array.from(document.querySelectorAll('#bml-videoctrl-panel .timestamp-viewer .timestamp-anker[time]'));
  1199.             anchors.forEach(anchor => {
  1200.                 if(anchor.textContent == timeTable[index][2]) {
  1201.                     anchor.classList.add('current');
  1202.                     anchor.parentNode.classList.add('current');
  1203.                 }else {
  1204.                     anchor.classList.remove('current');
  1205.                 }
  1206.             });
  1207.         }else {
  1208.             let anchors = Array.from(document.querySelectorAll('#bml-videoctrl-panel .timestamp-viewer .timestamp-anker[time].current'));
  1209.             anchors.forEach(anchor => {
  1210.                 anchor.classList.remove('current');
  1211.             });
  1212.         }
  1213.         scrollEditorContent();
  1214.     }
  1215.  
  1216.     function checkRepeat() {
  1217.         let toolbar = document.querySelector('#bml-videoctrl-panel .repeat-toolbar');
  1218.         let text = document.querySelector('#bml-videoctrl-panel .repeat-toolbar .repeat-text');
  1219.         if(repeatStart !== null && repeatEnd !== null) {
  1220.             toolbar.hidden = false;
  1221.             text.textContent = `repeat: ${sec2ft(repeatStart)}.${sec2ms(repeatStart)} - ${sec2ft(repeatEnd)}.${sec2ms(repeatEnd)}`;
  1222.             let video = getVideo();
  1223.             if(video) {
  1224.                 let ct = getCurrentTime();
  1225.                 if(ct < repeatStart || ct > repeatEnd) {
  1226.                     video.currentTime = repeatStart;
  1227.                 }
  1228.             }
  1229.         }else {
  1230.             toolbar.hidden = true;
  1231.         }
  1232.     }
  1233.  
  1234.     function timeupdate() {
  1235.         let panel = document.querySelector('#bml-videoctrl-panel');
  1236.         if(!panel) {
  1237.             clearInterval(intervalId);
  1238.             return;
  1239.         }
  1240.         if(currentURL != document.URL) {
  1241.             currentURL = document.URL;
  1242.             loadBackup();
  1243.         }
  1244.         updateTimeDisplay();
  1245.         updateTimestampControl();
  1246.         checkRepeat();
  1247.         if(backupTrigger) {
  1248.             saveBackup();
  1249.         }
  1250.     }
  1251. })());
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement