Greasy Fork

Greasy Fork is available in English.

YoutubeChatOnPTT

connect ptt pushes to youtube chatroom

当前为 2020-12-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YoutubeChatOnPTT
// @name:zh-TW   Youtube聊天使顯示PTT推文
// @namespace    https://github.com/zoosewu/PTTChatOnYoutube
// @version      1.0.0
// @description  connect ptt pushes to youtube chatroom
// @description:zh-tw 連結PTT推文到Youtube聊天室
// @author       Zoosewu
// @match        https://www.youtube.com/watch?v=*
// @match        https://term.ptt.cc/*
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// @require      https://code.jquery.com/jquery-3.5.1.slim.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js
// @require      https://cdn.jsdelivr.net/npm/vue
// @connect      www.ptt.cc
// @homepageURL  https://github.com/zoosewu/PTTChatOnYoutube/tree/master/homepage
// @license      MIT
// ==/UserScript==
'use strict';
//user log
const reportmode = true;
//all log
const showalllog = false;
//dev log
const showPTTscreen = (false || reportmode || showalllog);
const showcommand = (false || reportmode || showalllog);
const showPostMessage = (false || reportmode || showalllog);
const showonMessage = (false || reportmode || showalllog);
const showalertmsg = true || showalllog;
//dev use 
const devmode = false;
const defaultopen = false;
const disablepttframe = false;
const simulateisstreaming = false;
// add listener to get msg
const msg = {
  targetorigin: "",
  ownorigin: "",
  targetWindow: null,
  PostMessage: function (msg, data) {
    if (this.targetWindow !== null) {
      const d = { m: msg, d: data };
      this.targetWindow.postMessage(d, this.targetorigin);
      if (showPostMessage) console.log(this.ownorigin + " message posted", d);
    }
  },
  onMessage: function (event) {
    // Check sender origin to be trusted
    if (event.origin !== msg.targetorigin) return;
    const data = event.data;
    if (showonMessage) console.log(msg.ownorigin + " onMessage", data);
    if (typeof (msg[data.m]) == "function") {
      msg[data.m].call(null, data.d);
    }
  },
}
if (window.addEventListener) {
  window.addEventListener("message", msg.onMessage, false);
}
else if (window.attachEvent) {
  window.attachEvent("onmessage", msg.onMessage, false);
}


let isTopframe = (window.top == window.self);
if (/www\.youtube\.com\/watch\?v=/.exec(window.location.href) !== null) {
  //check script work in right frame
  if (!isTopframe) throw new Error("Script Stopped when Youtube is not top frame");
  //init postmessage
  msg.targetorigin = "https://term.ptt.cc";
  msg.ownorigin = "https://www.youtube.com";
  msg["test"] = data => { console.log("test parent onmessage", data); };
  //-----
  console.log("Script started at " + window.location.href);
  runYoutubeScript();
  console.log("Youtube Script initialize finish.");
  //-----
}
else if (/term\.ptt\.cc/.exec(window.location.href) !== null) {
  //check script work in right frame
  if (isTopframe) throw new Error("Script Stopped when PTT is top frame");
  //init postmessage
  msg.ownorigin = "https://term.ptt.cc";
  msg.targetorigin = "https://www.youtube.com";
  msg.targetWindow = top;
  msg["test"] = data => { console.log("test child onmessage", data); };
  //-----
  console.log("Script started at " + window.location.href);
  runPTTScript();
  console.log("PTT Script initialize finish.");
  //-----
}
//Youtube---------------------------------------------------------------------------------------------------------------------
function runYoutubeScript() {
  const testPTTurl = "https://www.ptt.cc/bbs/C_Chat/M.1606557604.A.904.html";

  let ConnectAlertDiv;
  let AutoScrolling = true;
  let isstreaming;
  let streamtime = new Date();
  let streamtimeinput;
  //let urlPushData = {};
  let PTTpostdata = {};
  let gotomainchat = false;
  let pushdata = {
    AID: "",
    board: "",
    posttime: new Date(),
    lastpushtime: new Date(),
    lastendline: 0,
    pushes: [],
    pushcount: 0,
    nowpush: 0,
  };
  ChechChatInstanced();
  (function () {
    (function AddBootstrap(frame) {
      const frameHead = $("head", frame);
      const frameBody = $("body", frame);
      frameHead.append($(`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">`));

      frameBody.append($(`<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>`));
      frameBody.append($(`<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>`));
      frameBody.append($(`<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>`));

    })(document)
  })();
  function ChechChatInstanced() {
    const ChatContainer = $(`ytd-live-chat-frame`);
    const defaultChat = $(`iframe`, ChatContainer);
    if (defaultChat.length > 0) {
      console.log("chat frame instanced");
      ChatContainer.css({ "position": "relative" });
      player = document.getElementsByTagName("video")[0];

      InitChatApp(defaultChat);
    }
    else {
      setTimeout(ChechChatInstanced, 1000);
    }
  }

  function InitChatApp(defaultChatApp) {
    //console.log(defaultChatApp);
    const PTTAppCollapse = $(`<div id="PTTChat" class="collapse w-100 rounded-right rounded-bottom" style="z-index: 300; position: absolute;"></div>`);
    const PTTApp = $(`<div id="PTTChat-app" class="pttbg border rounded w-100 d-flex flex-column"></div>`);
    const PTTChatnavbar = $(`<ul id="PTTChat-navbar" class="nav nav-tabs justify-content-center" role="tablist"><li class="nav-item"><a class="nav-link ptttext bg-transparent" id="nav-item-Chat" data-toggle="tab" href="#PTTChat-contents-Chat" role="tab" aria-controls="PTTChat-contents-Chat" aria-selected="false">Chat</a></li><li class="nav-item"><a class="nav-link ptttext bg-transparent active" id="nav-item-Connect" data-toggle="tab" href="#PTTChat-contents-Connect" role="tab" aria-controls="PTTChat-contents-Connect" aria-selected="true">Connect</a></li><li class="nav-item"><a class="nav-link ptttext bg-transparent" id="nav-item-Setting" data-toggle="tab" href="#PTTChat-contents-Setting" role="tab" aria-controls="PTTChat-contents-Setting" aria-selected="false">Setting</a></li><li class="nav-item"><a class="nav-link ptttext bg-transparent" id="nav-item-PTT" data-toggle="tab" href="#PTTChat-contents-PTT" role="tab" aria-controls="PTTChat-contents-PTT" aria-selected="false">PTT畫面</a></li><li class="nav-item"><button class="nav-link ptttext bg-transparent" id="nav-item-TimeSet" type="button" data-toggle="collapse" data-target="#PTTChat-Time" aria-controls="PTTChat-Time" aria-expanded="false">時間</button></li></ul>
    `);
    const PTTChatContents = $(`<div id="PTTChat-contents" class="tab-content container d-flex flex-column ptttext"><div id="PTTChat-Time" class="w-100 ptttext collapse"><div id="PTTChat-Time-Setting"><form class="form-inline d-flex justify-content-center w-100"><!--         <button id="minus-time"class="btn btn-outline-secondary" type="button">&lt;</button>        --><div class="form-group mb-2"><label for="appt-time">實況開始時間 : </label> <input id="stream-time" type="time" name="stream-time" value="18:00"></div><!--         <button id="add-time" class="btn btn-outline-secondary" type="button">></button>        --></form></div></div><div class="tab-pane flex-grow-1 overflow-auto row fade" id="PTTChat-contents-Chat" role="tabpanel" aria-labelledby="nav-item-Chat" style="overscroll-behavior:contain"><ul id="PTTChat-contents-Chat-main" class="col mb-0"></ul><div id="PTTChat-contents-Chat-btn" class="collapse position-absolute" style="z-index:400;bottom:5%;left:50%;-ms-transform:translateX(-50%);transform:translateX(-50%)"><button id="AutoScroll" class="btn btn-primary" type="button" data-toggle="collapse" data-target="#PTTChat-contents-Chat-btn" aria-controls="PTTChat-contents-Chat-btn" aria-expanded="false">自動滾動</button></div></div><div class="tab-pane h-100 row fade show active" id="PTTChat-contents-Connect" role="tabpanel" aria-labelledby="nav-item-Connect"><div id="PTTChat-contents-Connect-main" class="col overflow-auto h-100 mb-0 p-4" data-spy="scroll" data-offset="0"></div><div id="PTTChat-contents-Connect-alert" class="position-relative container" style="top:-100%;z-index:400"></div></div><div class="tab-pane h-100 row fade" id="PTTChat-contents-Setting" role="tabpanel" aria-labelledby="nav-item-Setting"><ul id="PTTChat-contents-Setting-main" class="col overflow-auto h-100" data-spy="scroll" data-offset="0"></ul></div><div class="tab-pane h-100 row fade" id="PTTChat-contents-PTT" role="tabpanel" aria-labelledby="nav-item-PTT"><ul id="PTTChat-contents-PTT-main" class="col h-100 d-flex justify-content-center pr-0 pl-0" data-spy="scroll" data-offset="0"></ul></div></div>
    `);
    const MainBtn = $(`<a id="PTTMainBtn" class="btn btn-lg" type="button" data-toggle="collapse" data-target="#PTTChat" aria-expanded="false" aria-controls="PTTChat">P</a>`)

    PTTAppCollapse.insertBefore(defaultChatApp);
    PTTAppCollapse.append(PTTApp);
    setTimeout(() => {
      const YTbgcolor = getComputedStyle($('html')[0]).backgroundColor;
      let ptp, pid, ptm, pmsg, ptxt;
      if (YTbgcolor === "rgb(24, 24, 24)") {
        ptp = "#fff"; pid = "#ff6"; ptm = "#bbb"; pmsg = "#990"; ptxt = "#f8f9fa";
        PTTApp.addClass("border-white");
        MainBtn.addClass("btn-outline-light");
      }
      else {
        ptp = "#000"; pid = "#990"; ptm = "#bbb"; pmsg = "#550"; ptxt = "#343a40";
        PTTApp.addClass("border-dark");
        MainBtn.addClass("btn-outline-dark");
      }
      const PTTcss = `
      .ptype { color: ` + ptp + `; }
      .pid { color: ` + pid + `; }
      .ptime { color: ` + ptm + `; }

      .pmsg { color: `+ pmsg + `; }
      .ptttext { color: `+ ptxt + `; }
      .pttbg {background-color: ` + YTbgcolor + `}`;
      const style = document.createElement('style');
      if (style.styleSheet) {
        style.styleSheet.cssText = PTTcss;
      } else {
        style.appendChild(document.createTextNode(PTTcss));
      }
      $('head')[0].appendChild(style);

      //PTTApp.css({ "background-color": YTbgcolor });
    }, 100);
    MainBtn.insertBefore(defaultChatApp);
    MainBtn.css({ "z-index": "350", "position": "absolute" });

    if (defaultopen) {
      $(`#PTTMainBtn`)[0].click();
    }

    PTTApp.append(PTTChatnavbar);
    PTTApp.append(PTTChatContents);

    PTTChatContents.css({ "height": defaultChatApp[0].clientHeight * 0.6 + "px" });
    player.addEventListener('timeupdate', ScrollToTime);
    if (!devmode) {
      $(`#nav-item-PTT`, PTTChatnavbar).remove();
      $(`#nav-item-TimeSet`, PTTChatnavbar).remove();
    }

    /*------------------------------------CHAT------------------------------------*/

    PTTChat_Chat_Main = $(`#PTTChat-contents-Chat-main`, PTTChatContents);
    PTTChat_Chat = $(`#PTTChat-contents-Chat`, PTTChatContents);;
    const PTTChat_Chat_btn = $(`#PTTChat-contents-Chat-btn`, PTTChatContents);

    /*for (let index = 0; index < 90; index++) {
      PTTChat_Chat.append(PushGenerator(`scroll-targer-` + index, "推", "Zoosewu", "太神啦太神啦太神啦太神啦太神啦太神啦太神啦太神啦太神", "00:00"));
    }*/
    const streamtimecollapse = $(`#PTTChat-Time`, PTTChatContents);
    const lbtn = $(`#minus-time`, streamtimecollapse);
    const rbtn = $(`#add-time`, streamtimecollapse);


    streamtimeinput = $(`#stream-time`, PTTChatContents);
    streamtimeinput[0].addEventListener("input", function () {
      UpdateStreamTime();
      ScrollToTime();
    }, false);

    PTTChat_Chat[0].addEventListener("scroll", function () {
      const t = Date.now() - scriptscrolltime;
      if (t > 0) {
        //user scrolling
        AutoScrolling = false;
        PTTChat_Chat_btn.collapse('show');
        streamtimecollapse.collapse('show');
      }
      else {
        //script scrolling
        scriptscrolltime = Date.now() + 250;
      }
    });
    $(`#AutoScroll`, PTTChatContents)[0].addEventListener("click", function (event) {
      event.preventDefault();
      AutoScrolling = true;
      ForceScrollToTime(true);
      streamtimecollapse.collapse('hide');
    });
    /*------------------------------------Connect------------------------------------*/
    const PTTChat_Connect = $(`#PTTChat-contents-Connect-main`, PTTChatContents);
    ConnectAlertDiv = $(`#PTTChat-contents-Connect-alert`, PTTChatContents);

    const login = `<form>
    <div class="form-row mb-2">
      <div class="col-6">
        <label for="PTTid">PTT ID</label>
        <input id="PTTid" type="text" class="form-control" placeholder="PTT ID" autocomplete="off">
      </div>
      <div class="col-6">
        <label for="PTTpw">PTT密碼</label>
        <input id="PTTpw" type="text" class="form-control" placeholder="PTT密碼" style="-webkit-text-security: disc;"
          autocomplete="off">
      </div>
    </div>
    <button id="PTTlogin" type="button" class="btn btn-outline-secondary">Login</button>
  </form>`;
    const post = `<div class="input-group mb-3 mt-3">
    <label for="PTTpostaid">輸入文章AID</label>
    <input id="post0" type="text" class="form-control" placeholder="#1VobIvqD (C_Chat)" aria-label="post0"
      aria-describedby="basic-addon2" autocomplete="off">
    <div class="input-group-append">
      <button id="post0btn" class="btn btn-outline-secondary" type="button">讀取推文</button>
    </div>
  </div>`;
    const fakedata = '{"board":"Test","AID":"1VpKTOfx","title":"","posttime":"2020-12-06T21:04:22.000Z","pushes":[{"type":"→ ","id":"ZooseWu","content":"推文1","date":"2020-12-06T21:04:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文2","date":"2020-12-06T21:05:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文3","date":"2020-12-06T21:05:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"","date":"2020-12-06T21:05:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文5","date":"2020-12-06T21:05:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文678","date":"2020-12-06T21:05:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文100","date":"2020-12-06T21:06:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文101","date":"2020-12-06T21:06:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"推文102Y","date":"2020-12-06T21:10:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"123","date":"2020-12-06T21:11:00.000Z"},{"type":"推 ","id":"hu7592","content":"☂","date":"2020-12-06T22:24:00.000Z"},{"type":"→ ","id":"ss15669659","content":"☂","date":"2020-12-06T23:56:00.000Z"},{"type":"→ ","id":"ZooseWu","content":"hey","date":"2020-12-07T00:31:00.000Z"}],"startline":"127","endline":"149","percent":"100"}';
    const fakedata1 = '{"board":"Test","AID":"1VpKTOfx","title":"","posttime":"2020-12-06T21:04:22.000Z","pushes":[{"type":"→ ","id":"ZooseWu","content":"hey","date":"2020-12-07T00:31:00.000Z"}],"startline":"127","endline":"149","percent":"100"}';

    const fakebtn = $(`<button id="fakebtn" class="btn btn-outline-secondary" type="button">獲得假推文</button>`);

    PTTChat_Connect.append(login);
    const pptid = $(`#PTTid`, PTTChatContents);
    const pttpw = $(`#PTTpw`, PTTChatContents);
    const loginbtn = $(`#PTTlogin`, PTTChatContents);
    loginbtn[0].addEventListener("click", function () {
      const i = pptid[0].value;
      const p = pttpw[0].value;
      msg.PostMessage("login", { id: i, pw: p });
      //GetChatData(posturl, AlertMsg, postindex);
    });
    pptid[0].addEventListener("keyup", loginenter);
    pttpw[0].addEventListener("keyup", loginenter);
    function loginenter(event) {
      if (event.keyCode === 13) {
        event.preventDefault();
        loginbtn[0].click();
      }
    }
    PTTChat_Connect.append(post);

    const postinput = $(`#post0`, PTTChatContents);
    const postbtn = $(`#post0btn`, PTTChatContents);

    postinput[0].addEventListener("keyup", e => {
      if (e.keyCode === 13) {
        event.preventDefault();
        postbtn[0].click();
      }
    });

    postbtn[0].addEventListener("click", function () {
      const postAID = postinput[0].value;
      const result = /#(.+) \((.+)\)/.exec(postAID);
      if (!result || result.length <= 2) {
        ///
        AlertMsg(false, "文章AID格式錯誤,請重新輸入。");
      }
      else {
        gotomainchat = true;
        if (pushdata.AID === result[1] && pushdata.board === result[2]) {
          msg.PostMessage("getpost", { AID: pushdata.AID, board: pushdata.board, startline: pushdata.lastendline });
        }
        else {
          msg.PostMessage("getpost", { AID: result[1], board: result[2], startline: 0 });
        }
      }
    });

    if (devmode) {
      PTTChat_Connect.append(fakebtn);
      $(`#fakebtn`)[0].addEventListener("click", getfakedata);

      function getfakedata(e, f) {
        f = f || fakedata;
        console.log("分析假推文", f);
        const obj = JSON.parse(f, dateReviver);
        ParsePostData(obj);
        if (simulateisstreaming) setTimeout(getfakedata, 5000, null, fakedata1);
      }
    }
    /*------------------------------------Setting------------------------------------*/

    /*------------------------------------PTT畫面------------------------------------*/
    const PTTChat_PTT = $(`#PTTChat-contents-PTT-main`, PTTChatContents);
    //const PTTCHAT_PTTTab = $(`#nav-item-PTT`, PTTChatnavbar);
    const PTTFrame = $(`<iframe id="PTTframe" src="//term.ptt.cc/" style="zoom: 1.5">你的瀏覽器不支援 iframe</iframe>`);
    $(window).on('beforeunload', function () {
      PTTFrame.remove();
      return;
    });
    PTTChat_PTT.append(PTTFrame);
    if (disablepttframe) PTTFrame.remove();
    msg.targetWindow = PTTFrame[0].contentWindow;
    //PTTCHAT_PTTTab.css({ "display": "none" });
  }
  function AlertMsg(type, msg) {
    if (showalertmsg) console.log("Alert,type: " + type + ", msg: " + msg);
    let alerttype = type === true ? "alert-success" : "alert-danger";
    const Alart = `<div class="alert mt-3 fade show ` + alerttype + `" role="alert">  ` + msg + `</div>`;
    if (ConnectAlertDiv) ConnectAlertDiv.append(Alart);
    const al = $('.alert');
    setTimeout(() => { al.alert('close'); }, 2000);
  }
  msg["alert"] = data => { AlertMsg(data.type, data.msg); };

  let AotoScroller;
  let PTTChat_Chat_Main;
  let PTTChat_Chat;
  let scriptscrolltime = Date.now();
  function ScrollToTime(forceScroll) {
    //console.log("isstreaming state", isstreaming);
    if (isstreaming === undefined) {
      //console.log("try isstreaming", $('.ytp-live-badge.ytp-button[aria-label="直接跳至現場活動直播頻道。"]'), $('.ytp-live-badge.ytp-button[disabled=true]'));
      if ($('.ytp-live-badge.ytp-button')[0].getAttribute('disabled') === "") {
        console.log("This video is streaming.");
        isstreaming = true;
      }
      else if ($('.ytp-live-badge.ytp-button[disabled=true]').length > 0) {
        console.log("This video is not streaming.");
        isstreaming = false;
      }
    }
    ForceScrollToTime(false);
  }
  let scrolloffset = 0;
  function _scroll(target) {
    if (scrolloffset === 0) scrolloffset = (PTTChat_Chat[0].clientHeight - target[0].clientHeight) / 2;
    let offset = target[0].offsetTop - scrolloffset;
    if (offset > PTTChat_Chat[0].clientHeight - PTTChat_Chat[0].scrollTop + 15)
      offset = PTTChat_Chat_Main[0].clientHeight - PTTChat_Chat[0].clientHeight + 15;
    else if (offset < 0)
      offset = 0;
    /*console.log("PTTChat_Chat[0].scrollTop" + PTTChat_Chat[0].scrollTop);
    console.log("PTTChat_Chat[0].clientHeight" + PTTChat_Chat[0].clientHeight);
    console.log("PTTChat_Chat_Main[0].clientHeight" + PTTChat_Chat_Main[0].clientHeight);
    console.log("target[0].clientHeight" + target[0].clientHeight);
    console.log("offset" + offset);
    console.log("scrolloffset" + scrolloffset);*/
    if (PTTChat_Chat[0].scrollTop - offset > 15 || PTTChat_Chat[0].scrollTop - offset < -15) {
      scriptscrolltime = Date.now() + 1100;
      if (PTTChat_Chat[0].scrollTop - offset > 1000) { PTTChat_Chat[0].scrollTo({ top: offset + 1000, }); }
      else if (offset - PTTChat_Chat[0].scrollTop > 1000) { PTTChat_Chat[0].scrollTo({ top: offset - 1000, }); }
      if (showalllog) console.log("go to push: " + pushdata.nowpush);
      setTimeout(() => {
        PTTChat_Chat[0].scrollTo({
          top: offset,
          behavior: "smooth"
        });
      }, 10);
    }
  }
  function ForceScrollToTime(forceScroll) {
    forceScroll = (typeof forceScroll !== 'undefined') ? forceScroll : true;
    if (!forceScroll && !AutoScrolling) return;
    if (simulateisstreaming || isstreaming) {
      pushdata.nowpush = pushdata.pushes.length - 1;
    }
    else {
      const playedtime = player.currentTime;
      let nowtime = new Date(streamtime.getTime());
      nowtime.setSeconds(nowtime.getSeconds() + playedtime);
      const prenowpush = pushdata.nowpush;
      /*console.log(nowtime + "-<-" + pushdata.posttime);
      console.log(nowtime.valueOf() + "-<-" + pushdata.posttime.valueOf());
      console.log(nowtime.valueOf() < pushdata.posttime.valueOf());
      console.log(nowtime + "->-" + pushdata.lastpushtime);
      console.log(nowtime.valueOf() + "->-" + pushdata.lastpushtime.valueOf());
      console.log(nowtime.valueOf() > pushdata.lastpushtime.valueOf());*/

      if (nowtime.valueOf() < pushdata.posttime.valueOf()) {
        if (showalllog) console.log("before post:" + nowtime + "<" + pushdata.posttime);
        pushdata.nowpush = 0;
      }
      else if (nowtime.valueOf() > pushdata.lastpushtime.valueOf()) {
        if (showalllog) console.log("after post:" + nowtime + ">" + pushdata.lastpushtime);
        pushdata.nowpush = pushdata.pushcount - 1;
      }
      else {
        let newnewpush = pushdata.nowpush;
        while (pushdata.pushes[newnewpush].date.valueOf() > nowtime.valueOf() && newnewpush > 1) {
          //console.log(pushdata.pushes[newnewpush].date + "->-" + nowtime);
          newnewpush--;
        }
        while (pushdata.pushes[newnewpush].date.valueOf() < nowtime.valueOf() && newnewpush < pushdata.pushes.length - 1) {
          //console.log(pushdata.pushes[newnewpush].date + "-<-" + nowtime);
          newnewpush++;
        }
        pushdata.nowpush = newnewpush;
      }
    }
    if (pushdata.pushes[pushdata.nowpush]) {
      _scroll(pushdata.pushes[pushdata.nowpush].div);
    }
  }
  function PushGenerator(ID, pushtype, pushid, pushmsg, pushtimeH, pushtimem) {
    const ptype = `<h5 class="ptype mr-2 mb-0">` + pushtype + ` </h5>`;
    const pid = `<h5 class="pid mr-2 mb-0 flex-grow-1">` + pushid + `</h5>`;
    const ptime = `<h5 class="ptime mb-0">` + paddingLeft(pushtimeH, +2) + `:` + paddingLeft(pushtimem, +2) + `</h6>`;

    //var pmsg = `<h4 class="f3 mb-0 ml-2 mr-2" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">` + pushmsg + `</h4>`;
    const pmsg = `<h4 class="pmsg mb-0 ml-2 mr-2" style="word-break: break-all;">` + pushmsg + `</h4>`;

    const firstline = `<div class="d-flex flex-row">` + ptype + pid + ptime + `</div>`;
    //var secondline = `<div class="d-flex flex-row">` + pmsg + `</div>`
    const secondline = `<div>` + pmsg + `</div>`

    const mainpush = `<li id="` + ID + `"class="media mb-4"><div class="media-body mw-100">` + firstline + secondline + `</div></li>`;
    return $(mainpush);
  }
  msg["postdata"] = data => {
    if (gotomainchat) {
      gotomainchat = false;
      $(`#nav-item-Chat`)[0].click(); ///
    }
    ParsePostData(data);
    /*console.log(JSON.stringify(data));*/
    if (isstreaming || simulateisstreaming) {
      setTimeout(RegetNewPush, 2500, data.AID, data.board);
    }
  }
  function RegetNewPush(AID, board) {
    if (PTTpostdata.AID === AID && PTTpostdata.board === board) {
      msg.PostMessage("getpost", { AID: AID, board: board, startline: pushdata.lastendline });
    }
  }
  function ParsePostData(data) {
    PTTpostdata = $.extend(true, {}, data);
    if (PTTpostdata.AID === pushdata.AID && PTTpostdata.board === pushdata.board) { }
    else {
      pushdata = {
        AID: PTTpostdata.AID,
        board: PTTpostdata.board,
        posttime: PTTpostdata.posttime,
        lastendline: PTTpostdata.endline,
        lastpushtime: new Date(),
        pushes: [],
        pushcount: 0,
        nowpush: 0,
      };
    }
    console.log(pushdata);
    const pdata = PTTpostdata.pushes;
    let sametime = PTTpostdata.posttime;
    let sametimeIndex = pushdata.pushcount;
    for (let index = 0; index < pdata.length; index++) {
      let newpush = pdata[index];
      newpush.pushid = pushdata.pushcount;
      pushdata.pushcount++;
      if (newpush.date.valueOf() > sametime.valueOf()) {
        //補秒
        const sametimecount = index - sametimeIndex;
        for (let i = sametimeIndex; i < index; i++) {
          const thispart = i - sametimeIndex;
          pushdata.pushes[i].date.setSeconds(thispart * 60 / sametimecount);
        }
        sametime = newpush.date;
        sametimeIndex = index;
      }
      //if (isstream && AutoScrolling){
      const div = PushGenerator(`scroll-targer-` + newpush.pushid, newpush.type, newpush.id, newpush.content, newpush.date.getHours(), newpush.date.getMinutes());
      PTTChat_Chat_Main.append(div);
      newpush.div = div;
      //}
      pushdata.pushes.push(newpush);
    }
    if (pdata.length > 0) pushdata.lastpushtime = pdata[pdata.length - 1].date;
    console.log("pushdata", pushdata);
    UpdateStreamTime();

    /*console.log(PTTpostdata);
    for (let index = 0; index < 90; index++) {
      PTTChat_Chat.append(PushGenerator(`scroll-targer-` + index, "推", "Zoosewu", "太神啦太神啦太神啦太神啦太神啦太神啦太神啦太神啦太神", 0, 0));
    }*/
  };
  function UpdateStreamTime() {
    const result = /(\d\d)\:(\d\d)/.exec(streamtimeinput[0].value);
    streamtime = new Date(pushdata.posttime.getTime());
    streamtime.setHours(+result[1]);
    streamtime.setMinutes(+result[2]);
    if (showalllog) console.log("UpdateStreamTime()");
  }
  /*
  //page push parse
  function GetChatData(ChatUrl, alertfunc, loadpostindex, callback) {
    if (ChatUrl === "") {
      alertfunc(false, "推文讀取失敗:空網址。");
      callback();
      return;
    }
    if (isLoadingPost[loadpostindex] == true) {
      alertfunc(false, "推文讀取中。");
      return;
    }        isLoadingPost[loadpostindex] = true;
    GM_xmlhttpRequest({
      method: "GET",
      url: ChatUrl,
      headers: {
        "User-Agent": "Mozilla/5.0",    // If not specified, navigator.userAgent will be used.
        "Accept": "text/xml"            // If not specified, browser defaults will be used.
      },
      onload: function (response) {
        //console.log(response.responseText);
        if ($(".push", response.responseText).length <= 1) {
          alertfunc(false, "推文讀取失敗:錯誤網址。");
          isLoadingPost[loadpostindex] = false;
          return;
        }
        alertfunc(true, "推文讀取成功。");
        isLoadingPost[loadpostindex] = false;
        GeturlPushData(loadpostindex, response.responseText);
        console.log("推文讀取 2");
      },
      abort: function (response) {
        alertfunc(false, "推文讀取失敗:無法連線。");
        isLoadingPost[loadpostindex] = false;
        console.log(response.responseText);
      }      });    }
  function GeturlPushData(loadpostindex, responseText) {
    urlPushData[loadpostindex] = [];
    //urlPushData = 
    console.log("GeturlPushData");
    ///get post year
    const postMetadata = $(".article-metaline", responseText);
    const postdateText = $(".article-meta-value", postMetadata[2])[0].innerText;
    const postdate = new Date(postdateText);
    const yyyy = postdate.getFullYear();
    //get push
    const pushes = $(".push", responseText);
    console.log(pushes);
    setTimeout(parsePushData, 10, loadpostindex, yyyy, pushes, 0);
  }
  function parsePushData(loadpostindex, yyyy, pushes, Index) {
    let parseFinished = false;
    for (let index = 0; index < Index + 100 && index < pushes.length; index++) {
      const push = pushes[index];
      //get content
      const pcontent = $(".push-content", push)[0].innerText;
      const contentReg = /[^: ].* /;
      const content = contentReg.exec(pcontent);
      //get time
      const ptime = $(".push-ipdatetime", push)[0].innerText;
      const ptimeReg = / ?(\d+)\/?(\d+) ?(\d+):?(\d+)/;
      const ptimeResult = ptimeReg.exec(ptime);
      const MM = ptimeResult[1];
      const dd = ptimeResult[2];
      const hh = ptimeResult[3];
      const mm = ptimeResult[4];
      const date = new Date(yyyy, (MM - 1), dd, hh, mm);
 
      const pushdata = new Object();
      pushdata.type = $(".push-tag", push)[0].innerText;
      pushdata.id = $(".push-userid", push)[0].innerText;
      pushdata.content = content[0];
      pushdata.date = date;
      urlPushData[loadpostindex].push(pushdata);
      if (index == pushes.length - 1) parseFinished = true;
      PTTChat_Chat_Global.append(PushGenerator(`scroll-targer-` + index, pushdata.type, pushdata.id, pushdata.content, pushdata.date.getHours() + ":" + pushdata.date.getMinutes()));
    }
    if (parseFinished) {
      //console.log("parse finished, total " + pushes.length + " messages.");
      //console.log(urlPushData[loadpostindex]);
    }
    else {
      //console.log("parse " + Index + " to " + (Index + 100));
      setTimeout(parsePushData, 10, loadpostindex, yyyy, pushes, Index + 100);
    }
  }
*/
}
//PTT---------------------------------------------------------------------------------------------------------------------
function runPTTScript() {
  //start script
  'use strict'
  let PTT = {
    connect: true,//自動 連線狀態
    login: false,//自動
    controlstate: 0,
    lock: function () {
      PTT.controlstate = 1;
    },
    unlock: function () {
      PTT.controlstate = 0;
      PTT.commands.list = [];
    },
    //0 free,1 lock 手動更新 每次操作都要打開 用完關閉
    pagestate: 0,//自動 ptt的訊息 暫時沒什麼用
    screen: [],//自動 畫面資料
    screenstate: 0,//0 clear, 1 full 自動 畫面是否已更新
    wind: null,//自動
    screenHaveText: function (reg) {
      let result = null;
      //debug用
      if (this.screenstate === 0) {
        const sElement = $("[type='bbsrow']", this.wind.document);
        for (let i = 0; i < sElement.length; i++) {
          const txt = sElement[i].textContent;
          if (result == null) result = reg.exec(txt);
          this.screen.push(txt);
        }
        this.screenstate = 1;
        if (showalllog) console.log("screenHaveText", reg, result);
        return result;
      }
      else {
        for (let i = 0; i < this.screen.length; i++) {
          const txt = this.screen[i];
          result = reg.exec(txt);
          if (result != null) {
            if (showalllog) console.log("screenHaveText", reg, result);
            return result;
          }
        }
        if (showalllog) console.log("screenHaveText", reg, result);
        return null;
      }
    },
    screenclear: function () {
      this.screenstate = 0;
      this.screen = [];
    },
    commands: {
      list: [],
      add: function (reg, input, callback, ...args) {
        const com = { reg, input, callback, args };
        if (showcommand) console.log("Add command ", com);
        this.list.push(com);
      },
      getfirst: function () {
        return this.list[0];
      },
      removefirst: function () {
        this.list.shift();
      }
    },
    autocom: [
      { reg: /您想刪除其他重複登入的連線嗎|您要刪除以上錯誤嘗試的記錄嗎/, input: 'n\n' },
      { reg: /您要刪除以上錯誤嘗試的記錄嗎/, input: 'n\n' },
      { reg: /請按任意鍵繼續/, input: '\n' },
      {
        reg: /系統過載, 請稍後再來\.\.\./, input: '', callback: () => {
          serverfull = true;
          if (PTT.controlstate === 1) {
            PTT.unlock();
            msg.PostMessage("alert", { type: false, msg: "系統過載, 請稍後再來..." });
          }
        }, args: []
      }

    ]
  }
  PTT.wind = window;
  let PTTPost = {
    board: "",
    AID: "",
    title: "",
    posttime: "",
    pushes: [],
    startline: 0,
    endline: 0,
    percent: 0,
  }
  let serverfull = false;
  const insertText = (() => {
    let t = PTT.wind.document.querySelector('#t')
    return str => {
      if (!t) t = PTT.wind.document.querySelector('#t')
      const e = new CustomEvent('paste')
      //debug用
      //console.log("insertText", str);
      e.clipboardData = { getData: () => str }
      t.dispatchEvent(e)
    }
  })()
  function ComLog(cmd) {
    if (showcommand) console.log("execute command:", cmd);
  }

  function chechAutoCommand() {
    let commands = PTT.autocom;
    for (let autoi = 0; autoi < commands.length; autoi++) {
      const cmd = commands[autoi];
      const result = PTT.screenHaveText(cmd.reg);
      if (showcommand) console.log("auto command", cmd, result);
      if (result != null) {
        ComLog(cmd);
        insertText(cmd.input);
        if (typeof cmd.callback !== "undefined") {
          cmd.callback(...cmd.args);
        }
        return true;
      }
    }
    return false;
  }

  function command() {
    const cmd = PTT.commands.getfirst();
    if (typeof cmd !== 'undefined' && PTT.screenHaveText(cmd.reg) != null) {
      PTT.commands.removefirst();
      ComLog(cmd);
      insertText(cmd.input);
      if (typeof cmd.callback == "function") {
        cmd.callback(...cmd.args);
      }
    }
  }
  function OnUpdate() {
    if (showalllog) console.log("OnUpdate start");
    PTT.screenclear();
    if (showalllog) console.log("check autocommand.");
    if (!chechAutoCommand()) {
      if (showalllog) console.log("check command.");
      command();
    }
    if (showPTTscreen) console.log("This is PTT screen", PTT.screen);
    let nextcom = PTT.commands.getfirst();
    if (showcommand && typeof nextcom !== 'undefined') console.log("next command : reg:" + nextcom.reg + "input:" + nextcom.input, nextcom.callback);
    else console.log("next command : none.");
    if (showalllog) console.log("OnUpdate end");
  }
  //hook start
  function hook(obj, key, cb) {
    const fn = obj[key].bind(obj)
    obj[key] = function (...args) {
      fn.apply(this, args)
      cb.apply(this, args)
    }
  }
  hook(unsafeWindow.console, 'log', t => {
    if (typeof t === 'string') {
      if (t.indexOf('page state:') >= 0) {
        const newstate = /->(\d)/.exec(t)[1];
        PTT.pagestate = newstate;
      }
      else if (t === 'view update') {
        serverfull = false;
        OnUpdate();
      }
    }
  });
  //hook end
  function gotoBoard(boardname) {
    const input = boardname + "\n";
    PTT.commands.add(/輸入看板名稱\(按空白鍵自動搜尋\)\:/, input, () => {
      PTTPost.board = boardname;
    });
    insertText("s");
  }
  function gotoPost(postcode) {
    const gotopost = "#" + postcode + "\n\n";
    PTT.commands.add(/文章選讀/, gotopost, () => {
      PTTPost.AID = postcode;
    });
    PTT.commands.add(/.*/, "", () => {
      if (PTT.screenHaveText(/文章選讀/)) {
        msg.PostMessage("alert", { type: false, msg: "文章AID錯誤,文章已消失或是你找錯看板了。" });
      }
      else if (PTT.screenHaveText(/作者/)) {
        if (PTTPost.endline > 1) {
          const gotoline = "1\b" + PTTPost.endline + ".\n";
          insertText(gotoline);
          PTT.commands.add(/目前顯示: 第/, "", _getpush);
        }
        else {
          _getpush();
        }
      }
    });
  }
  function savepush(content, result) {
    const pushdata = {};
    pushdata.type = result[1];
    pushdata.id = result[2];
    pushdata.content = content;
    pushdata.date = new Date(PTTPost.posttime.getFullYear(), result[4] - 1, result[5], result[6], result[7]);
    PTTPost.pushes.push(pushdata);
    //console.log(result);
  }
  function _getpush() {
    const lineresult = PTT.screenHaveText(/目前顯示: 第 (\d+)~(\d+) 行/);
    const startline = lineresult[1];
    const endline = lineresult[2];

    if (PTTPost.posttime === "") {
      let result = PTT.screenHaveText(/時間  (\S{3} \S{3} ...\d{2}:\d{2}:\d{2} \d{4})/);
      PTTPost.posttime = new Date(result[1]);
    }
    for (let i = PTTPost.endline - startline + 1; i < PTT.screen.length; i++) {
      const line = PTT.screen[i];
      const result = /^(→ |推 |噓 )(.+): (.*)(\d\d)\/(\d\d) (\d\d):(\d\d)/.exec(line);
      if (result != null) {
        let content = result[3];
        var reg = /\s+$/g;
        content = content.replace(reg, "");
        savepush(content, result);
      }
    }
    let percentresult = PTT.screenHaveText(/瀏覽 第 .+ 頁 \( *(\d+)%\)/);
    PTTPost.percent = percentresult[1];
    PTTPost.startline = startline;
    PTTPost.endline = endline;
    if (PTT.screenHaveText(/(100%)/) == null) {
      PTT.commands.add(/目前顯示: 第/, "", _getpush);
      insertText(' ');
    }
    else {
      PTT.unlock();
      msg.PostMessage("alert", { type: true, msg: "文章讀取完成。" });
      msg.PostMessage("postdata", PTTPost);
      if (showalllog)
        console.log(PTTPost);
    }
  }
  function GetPostPush(pAID, bname, startline, forceget = false) {
    if ((PTT.connect && PTT.login) || forceget) {
      let searchboard = bname !== PTTPost.board;
      let searchpost = pAID !== PTTPost.AID;
      startline = startline | 1;
      msg.PostMessage("alert", { type: true, msg: "文章讀取中。" });
      if (searchpost) PTTPost = {
        board: "",
        AID: "",
        title: "",
        posttime: "",
        pushes: [],
        startline: 0,
        endline: startline,
        percent: 0,
      }
      PTT.endline = startline;
      if (searchboard) {
        if (showalllog) console.log("新看板 新文章");
        gotoBoard(bname);
        gotoPost(pAID);
      }
      else if (searchpost) {
        if (showalllog) console.log("同看板 新文章");
        gotoPost(pAID);
      }
      else {
        if (showalllog) console.log("同看板 同文章");
        PTTPost.pushes = [];
        if (PTTPost.endline > 22) {
          PTT.commands.add(/目前顯示: 第/, PTTPost.endline + ".\n");
        }
        PTT.commands.add(/目前顯示: 第/, "", _getpush);
        insertText("bf");
      }
    }
    else if (!PTT.connect) {
      msg.PostMessage("alert", { type: false, msg: "PTT已斷線,請重新登入。" });
    }
    else if (!PTT.login) {
      msg.PostMessage("alert", { type: false, msg: "PTT尚未登入,請先登入。" });
    }
  }
  function login(id, pw) {
    if (!PTT.login) {
      const logincheck = () => {
        if (PTT.screenHaveText(/密碼不對或無此帳號。請檢查大小寫及有無輸入錯誤。|請重新輸入/)) {
          msg.PostMessage("alert", { type: false, msg: "登入失敗,帳號或密碼有誤。" });
          PTT.unlock();
        }
        else if (PTT.screenHaveText(/上方為使用者心情點播留言區/)) {
          msg.PostMessage("alert", { type: true, msg: "登入成功。" });
          PTT.login = true;
          PTT.unlock();
          //testcode
          /*(() => {
            PTTLockCheck(GetPostPush, `#1VobIvqM (C_Chat)`);
            insertText("x");
          })();*/
        }
        else if (PTT.screenHaveText(/登入中,請稍候\.\.\./)) {
          PTT.commands.add(/.*/, "", logincheck);
        }
      }

      let result = PTT.screenHaveText(/請輸入代號,或以 guest 參觀,或以 new 註冊/);
      if (result) {
        msg.PostMessage("alert", { type: true, msg: "登入中。" });
        insertText(id + "\n" + pw + "\n");
        PTT.commands.add(/.*/, "", logincheck);
      }
      else {
        PTT.commands.add(/.*/, "", login, id, pw);
      }
    }
    else {
      msg.PostMessage("alert", { type: false, msg: "已經登入,請勿重複登入。" });
      PTT.unlock();
    }
  }
  function PTTLockCheck(callback, ...args) {
    let disbtn = $(`.btn.btn-danger[type=button]`);
    if (disbtn.length > 0) {
      disbtn[0].click();
      PTT.unlock();
      serverfull = false;
      console.log("CLICK");
    }
    if (PTT.controlstate === 1) {
      msg.PostMessage("alert", { type: false, msg: "指令執行中,請稍後再試。" });
    }
    else if (serverfull) {
      msg.PostMessage("alert", { type: false, msg: "系統過載, 請稍後再來..." });
    }
    else {
      PTT.lock();
      callback(...args);
    }
  }
  //end
  msg["login"] = data => { PTTLockCheck(login, data.id, data.pw); };
  msg["getpost"] = data => { PTTLockCheck(GetPostPush, data.AID, data.board, data.startline); };
}





//function
function paddingLeft(str, lenght) {
  str = str + "";
  if (str.length >= lenght)
    return str;
  else
    return paddingLeft("0" + str, lenght);
}
function paddingRight(str, lenght) {
  str = str + "";
  if (str.length >= lenght)
    return str;
  else
    return paddingRight(str + "0", lenght);
}

var dateReviver = function (key, value) {
  if (typeof value === 'string') {
    const a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
    if (a) {
      return new Date(+a[1], +a[2] - 1, +a[3], +a[4] + 8, +a[5], +a[6]);
    }
  }
  return value;
};