您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
標記OP時間點 投票 跳過 自動跳過
// ==UserScript== // @name 動畫瘋 OP管理工具 // @version 1.1 // @description 標記OP時間點 投票 跳過 自動跳過 // @namespace thomas2013 // @author thomas2013 // @homepage https://home.gamer.com.tw/homeindex.php?owner=thomas2013 // @match *://ani.gamer.com.tw/animeVideo.php?sn=* // @icon https://i2.bahamut.com.tw/anime/logo.svg // @grant GM.xmlHttpRequest // @license MIT // ==/UserScript== (async function () { const TESTING = false; let serverUrl = '' if (TESTING) { serverUrl = 'http://localhost:3000' } else { serverUrl = 'http://ec2-16-163-84-57.ap-east-1.compute.amazonaws.com:3000' } 'use strict'; const styles = document.createElement('style'); styles.textContent = ` .skTime-input { padding: 1px; border: 3px solid rgb(96, 98, 102); border-radius: 3px; font-size: 17px; width: 34px; transition: all 0.3s ease; outline: none; background-color: rgb(96, 98, 102); color: #2c2828; } .sk-input:hover { border-color: #9e9e9e; } .skTime-input::placeholder { color: #9e9e9e; opacity: 0.7; } .skPass-input { padding: 1px; border: 3px solid rgb(96, 98, 102); border-radius: 3px; font-size: 17px; width: 60px; transition: all 0.3s ease; outline: none; background-color: rgb(96, 98, 102); color: #2c2828; } .skPass-input:hover { border-color: #9e9e9e; } .status-container { position: absolute; top: 60px; right: 20px; width: auto; max-width: 300px; pointer-events: none; } .status-container-top { position: absolute; top: 100px; right: 26%; width: auto; z-index: 999; max-width: 300px; pointer-events: none; } .status-popover { position: relative; background: rgba(33, 33, 33, 0.9); color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); margin-bottom: 10px; transition: all 0.3s ease; display: flex; align-items: center; gap: 10px; pointer-events: auto; opacity: 0; transform: translateX(20px); } .status-popover.show { opacity: 1; transform: translateX(0); } .status-popover.success { background: rgba(46, 125, 50, 0.9); } .status-popover.error { background: rgba(198, 40, 40, 0.9); } .status-popover.warning { background: rgba(255, 152, 0, 0.9); } .status-icon { font-size: 20px; flex-shrink: 0; } .status-message { font-size: 16px; flex-grow: 1; white-space: nowrap; text-overflow: ellipsis; } .status-close { margin-left: 10px; cursor: pointer; opacity: 0.7; transition: opacity 0.2s; flex-shrink: 0; } .status-close:hover { opacity: 1; } .opening-control-button { margin: 10px 0; display: flex; gap: 8px; justify-content: center; } .opening-control-button button { padding: 8px 15px; border-radius: 4px; border: none; background-color:rgb(96, 98, 102); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 14px; cursor: pointer; transition: all 0.2s ease; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .opening-control-button button:hover { background-color: #404244; transform: translateY(-1px); box-shadow: 0 3px 5px rgba(0,0,0,0.15); } .opening-control-button button:active { transform: translateY(1px); box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .opening-report-button { margin: 10px 0; display: flex; gap: 8px; justify-content: center; } .opening-report-button button { padding: 8px 15px; border-radius: 4px; border: none; background-color:rgb(96, 98, 102); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 14px; cursor: pointer; transition: all 0.2s ease; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .opening-report-button button:hover { background-color: #404244; transform: translateY(-1px); box-shadow: 0 3px 5px rgba(0,0,0,0.15); } .opening-report-button button:active { transform: translateY(1px); box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .inter-text { text-align: center; font-size: 10px; font-family: monospace; padding: 10px; background-color: #2e2f31; color: white; border-radius: 4px; margin: 10px 0; } #videoTimer { text-align: center; font-size: 20px; font-family: monospace; padding: 10px; background-color: #2e2f31; color: white; border-radius: 4px; margin: 10px 0; } .inter-text { margin: 0; padding: 0; margin-bottom: 20px; } .parent-container { display: flex; flex-direction: row; flex-wrap: wrap; gap: 10px; align-items: flex-start; } .your-uploaded-time { background:rgb(62, 126, 222); } .rank1-time { position: absolute; top: 0; left: 0; width: 100px; height: 100px; background-color: #3498db; z-index: 1; background:rgb(89, 169, 83); } .sk-container { background:rgb(23, 23, 23); border-radius: 10px; padding: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); max-width: 400px; margin: 10px auto; flex: 0 0 auto; } .sk-time-wrapper { display: flex; flex-direction: column; gap: 6px; } .sk-time-list { display: flex; justify-content: space-between; align-items: center; padding: 12px; border-radius: 6px; font-size: 16px; color: #fff; } .sk-time { font-family: 'Roboto Mono', monospace; font-weight: 500; } .sk-votes { color: #9e9e9e; font-size: 14px; } .sk-vote-controls { align-items: center; display: flex; gap: 4px; } .sk-vote { flex: 1; display: flex; align-items: center; justify-content: center; gap: 4px; padding: 8px 18px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; color: #fff; background: #424242; } .sk-vote--up:hover { background: #2e7d32; } .sk-vote--down:hover { background: #c62828; } .sk-vote-VOTEDP { background: #2e7d32; } .sk-vote-VOTEDN { background: #c62828; } .sk-vote-has-been-chosen { background:rgb(71, 60, 225); } .sk-vote:active { transform: scale(0.98); } .sk-icon { font-size: 12px; } .sk-vote-count { background: rgba(255, 255, 255, 0.1); padding: 2px 6px; border-radius: 4px; font-size: 12px; } .sk-vote--disabled { opacity: 0.5; cursor: not-allowed; } .ani-tab-content__item button.upload_vote { position: absolute; top: 300px; right: 25px; width: 44px; height: 34px; float: none; margin: 0; padding: 0; font-size: 18px; border-radius: 5px; border: 1px solid var(--anime-primary-color); background: var(--anime-primary-color); color: rgba(var(--anime-white-rgb), 1); box-shadow: 0 3px 6px -2px rgba(0, 0, 0, 0.2); z-index: 1; cursor: pointer; outline: none; } .sk-vote-material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 16px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; } .sk-sky-container { position: relative; display: flex; justify-content: center; } .skInfo { position: absolute; top: 8px; right: 85px; width: 25px; height: 25px; float: none; margin: 0; padding: 0px; font-size: 18px; border-radius: 17px; border: 1px solid #707070; background: #9baeb1; color: #f0f0f0f0; box-shadow: 0 3px 6px -2px rgba(0, 0, 0, 0.2); z-index: 1; cursor: pointer; outline: none; } .info-container { position: relative; display: flex; justify-content: center; } .info-trigger { align-items: center; } .sk-introduction { z-index: 1; font-size: 16px; visibility: hidden; position: absolute; right: -70px; top: 140px; transform: translateX(-50%); background-color: rgb(175 175 175 / 90%); padding: 12px; border-radius: 9px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); width: max-content; opacity: 1; translate: revert; transition: opacity 0.3s, visibility 0.3s; } .sk-introduction::after { position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #e5e5e5 transparent transparent transparent; } .sk-introduction div { padding: 3px; } .sk-introduction-title { font-size: 16px; font-weight: bold; } .sk-info-show { visibility: visible; opacity: 1; } .sk-info-hidden { visibility: hidden; opacity: 0; } .sk-sky-button-container { justify-content: center; z-index: 10000; font-size: 16px; position: fixed; right: 20%; top: 4px; align-items: center; transform: translateX(-50%); background-color: rgb(55 57 60); padding: 10px; border-radius: 7px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); width: max-content; translate: revert; display: flex; } .sk-skip-button1 button { padding: 0px 13px; margin: 0px 5px 0px 10px; height: 28px; border: none; border-radius: 3px; background-color: #00b4d8; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; } .sk-skip-button2 { display: flex; align-items: center; } .sk-skip-button2 button { padding: 0px 4px; height: 28px; border: none; border-radius: 3px; background-color: #00b4d8; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; } .sk-sky-button-container button:hover { background-color:rgb(42, 196, 226); } .sk-skip-checkbox { position: relative; } .sk-skip-checkbox::after { content: "自動跳過一次"; position: absolute; background-color: #333; color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; top: 100%; left: 100%; transform: translateX(-50%); white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .sk-skip-button2::after { content: "撤銷跳過"; position: absolute; background-color: #333; color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; top: 100%; left: 100%; transform: translateX(-50%); white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .sk-skip-checkbox:hover::after { opacity: 1; visibility: visible; } `; document.head.appendChild(styles); function skMainTab() { let html = '<div>'; html += '<div class="ani-setting-section is-seperate">'; html += `<h4 class="ani-setting-title ">動畫瘋 OP工具</h4>`; html += '<button id="aniSkRefresh" class="refresh">' html += '<i class="material-icons">refresh</i>' html += '</button>' html += '<div id="skInfo" class="skInfo"></div>' html += '<div id="mainControls" class="opening-control-button">'; html += '<button id="toSkipOpening">跳過85秒</button>'; html += '</div>'; html += '<div id="mainControls" class="opening-report-button">'; html += '<button id="inTime">回報起點</button>'; html += '<button id="outTime">回報終點</button>'; html += '<button id="noTime">沒有OP</button>'; html += '</div>'; html += '<div id="mainControls" class="inter-text">'; html += '<div class="inter-text">(任選)</div>'; html += '</div>'; html += '<div id="subControls" class="opening-control-button">'; html += '<button id="goBackMany"> -1 </button>'; html += '<button id="goBack"> <<< </button>'; html += '<button id="goBackSmall"> < </button>'; html += '<button id="goForwardSmall"> > </button>'; html += '<button id="goForward"> >>> </button>'; html += '<button id="goForwardMany"> +1 </button>'; html += '</div>'; html += '<div>'; html += '<span> </span>'; html += '</div>'; html += '<div>'; html += '<div id="videoTimer"></div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-section is-seperate">'; html += '<h4 class="ani-setting-title ">時間點</h4>'; html += '<div style:"margin-right: auto;" class="ani-setting-item ani-flex">'; html += '<div id="skVoteFrame" class="aniSkList parent-container">'; html += '</div>'; html += '</div>'; html += '<button id="aniSkUpload" class="upload_vote">' html += '<i class="sk-vote-material-icons"><div">投票</div></i>' html += '</button>' html += '</div>'; html += '<div class="ani-setting-section is-seperate">'; html += '<h4 class="ani-setting-title ">設定</h4>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">自動跳過</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">顯示手動按鈕</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">只跳過一次</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">優先使用自己的贊成票</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">優先使用自己的時間點</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">微調時間</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += '<div class="ani-setting-label">入點</div>'; html += '<input id="skInTime" class="skTime-input" type="text" name="ani-textbox" value="0">'; html += '<div class="ani-setting-label">出點</div>'; html += '<input id="skOutTime" class="skTime-input" type="text" name="ani-textbox" value="90">'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">密碼</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += '<div class="ani-setting-label"></div>'; html += '<input id="skPasswd" class="skPass-input" type="text" name="ani-textbox" value="">'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">離線版本(開發中)</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div class="ani-checkbox">'; html += '<label class="ani-checkbox__label">'; html += `<input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked="">`; html += '<div class="ani-checkbox__button"></div>'; html += '</label>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '<div class="ani-setting-item ani-flex">'; html += '<div class="ani-setting-label">'; html += '<span class="ani-setting-label">刪除快取</span>'; html += '</div>'; html += '<div class="ani-setting-value ani-set-flex-right">'; html += '<div>'; html += '<button id="delete-cache" style="font-size: 12px;">刪除</button>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; return html; } function skInfo() { let html = ''; html = ` <div class="sk-sky-container"> <div class="sk-introduction"> <div><a target="_blank" href=${serverUrl}>伺服器狀態</a></div> <div></div> <div class="sk-introduction-title">控制面板:\n</div> <div>回報終點 & 回報終點</div> <div></div> <div>只需選擇其中一個 資料一律以起點儲存\n</div> <div>每個使用者只能回報一個 可以回報新的來修改\n</div> <div>在一段時間後被鎖定\n</div> <div></div> <div>六個控制按鈕分別是 [-1秒 -6幀 -1幀 +1幀 +6幀 +1秒]\n</div> <div></div> <div class="sk-introduction-title">時間點:\n</div> <div>票數最高的會被優先使用\n</div> <div>在一集動畫只能投下"正 / 負"各一張票\n</div> <div></div> <div class="sk-introduction-title">上方手動按鈕:\n</div> <div>跳過OP: 在OP範圍內按下就會跳過OP</div> <div>無標記時會快轉85秒</div> <div> ⯇ : 返回到跳過之前\n</div> <div></div> <div class="sk-introduction-title">設定:\n</div> <div>自動跳過: 在到達OP範圍時跳過OP\n</div> <div></div> <div>顯示手動按鈕: 顯示一組手動跳過OP用的按鈕\n</div> <div>跳過一次: 跳過一次之後不會在觸發跳過\n</div> <div></div> <div>優先贊成票:\n</div> <div>總是使用自己投過贊成的時間點\n</div> <div></div> <div>優先時間點:\n</div> <div>總是使用自己回報的時間點\n</div> <div></div> <div>微調時間: 可以調整跳過OP的判定範圍 推薦設為1與88\n</div> <div>密碼: 在這裡按下ctrl+a全選 ctrl+v貼上密碼 然後enter儲存\n</div> <div>離線版本: 不使用伺服器的資料\n</div> </div> </div> ` return html; } function skInfoTrigger() { let html = '' html = ` <div id="info-trigger" class="info-container"> <span class="info-trigger">?</span> </div> ` return html; } function skSwitchButton() { let html = '' html = ` <div class="ani-tabs__item"> <div id="skipOpening" class="ani-tabs-link ani-tabs-link-opening">OP工具 </div> </div> ` return html; } function skSwitchTab() { let html = '' html = ` <div id="opening-tab-content" class="ani-tab-content__item" style="display: none;"> </div> ` return html; } function skSkyMessage() { let html = '' html = ` <div id="sky-message" class="status-container-top"> </div> ` return html; } function skSkipButton() { let html = '' html = ` <div id="skSkipButContainer" class="sk-sky-button-container"> <div class="sk-skip-checkbox"> <div class="ani-checkbox" style="margin: 0 0 0 5px;"> <label class="ani-checkbox__label"> <input id="skSetting-${adder()}" type="checkbox" name="ani-checkbox" checked=""> <div class="ani-checkbox__button"></div> </label> </div> </div> <div class="sk-skip-button1"> <button id="skSkipButton" class="sk-sky-button__item">跳過OP</button> </div> <div class="sk-skip-button2"> <button id="redoSkipButton" class="skip-button2"> ⯇ </button> </div> </div> ` return html; } function getSN() { const urlObj = document.URL; const sn = urlObj.match(/sn=(\d+)/)[1]; return sn; } function GM_setValue(key, value) { localStorage.setItem(key, value); } function GM_delValue(key) { localStorage.removeItem(key); } function GM_getValue(key) { return localStorage.getItem(key); } // function snFormat(sn, user, vote, time, voted) { // const cache = { // sn: sn, // data: [{ // time: time, // user: user, // vote: vote, // voted: voted // }] // } // return cache; // } // function dataFormat(time, vote, voted) { // const cache = { // time: time, // vote: vote, // voted: voted // } // return cache; // } function GM_loadCache(sn) { try { const cache = {}; cache[sn] = JSON.parse(GM_getValue(sn)); if (cache[sn] === null) { cache[sn] = { sn: sn, data: [] }; GM_setValue([sn], JSON.stringify(cache[sn])); } return cache; } catch (error) { console.log(error); } } function GM_saveCache() { GM_setValue([sn], JSON.stringify(SkCache[sn])) } // async function addCache(content, data) { // content[sn].data.push(data); // return content; // } // async function removeCache(index) { // SkCache[sn].data.splice(index, 1); // return SkCache; // } // async function saveToCache(content) { // GM_setValue([sn], JSON.stringify(content[sn])); // } // async function LocalToCloudData() { // CloudTimes = []; // SkCache[sn].data.forEach((item, index) => { // const voteCache = { // time: item.time, // vote: item.vote, // } // CloudTimes.push(voteCache); // }) // } async function CloudToLocalData() { if (stringBool(skipSetting.offlineMode)) { return; } while (!CloudTimes.length) { await new Promise(resolve => setTimeout(resolve, 100)); } const temp = SkCache[sn].data; SkCache[sn].data = []; CloudTimes.forEach((item, index) => { try { const dataCache = { time: item.time, vote: item.vote, voted: false, uploader: item.uploader } temp.forEach((item2, index2) => { if (item2.time === item.time) { dataCache.voted = item2.voted; } }) SkCache[sn].data.push(dataCache); } catch (error) { return; } }) } function getLocalTime() { const cache = []; SkCache[sn].data.forEach((item, index) => { const voteCache = { time: item.time, vote: item.vote, voted: item.voted, uploader: item.uploader } cache.push(voteCache); }); return cache; } function getOfflineData() { const cache = []; SkCache[sn].offlineData.forEach((item, index) => { const voteCache = { time: item.time, vote: item.vote, voted: item.voted, uploader: item.uploader } cache.push(voteCache); }); return cache; } function stringBool(value) { if (value === "false" || value === false) { return false; } else { return true; } } function adder(reset) { const temp = pageTemp.adderCounter; if (reset === 1) { pageTemp.adderCounter = 0; } else { pageTemp.adderCounter += 1; } return temp; } //SkCache是這個sn的快取 //getLocal可以取得快取內的物件 //LocalTimes是拿來存getLocal拿到的資料 //CloudTimes 伺服器回傳的值 //voteData是投票資料 let SkCache = {}; let VideoTimer; let CloudTimes = []; let LocalTimes = []; let voteData = []; let sn = getSN(); let skipSetting = { autoSkip: false, skipButton: false, skipOnce: false, pickMyVote: false, pickMyTime: false, offlineMode: false, skManualOnce: true, inTime: 0, outTime: 90, password: "" }; let pageTemp = { sn : sn, messageCounter : 0, holdSk : false, runSk : skipSetting.autoSkip, runOnce: false, holdCounter : 0, adderCounter : 0, whenSk: 0 }; let skSettingMap = []; Object.entries(skipSetting).forEach(([key, value],index) => { if (typeof value === 'boolean') { skSettingMap.push([`skSetting-${index}`,key]); } if (GM_getValue(key) != undefined) { skipSetting[key] = GM_getValue(key); } else { GM_setValue(key, value); } }) let userID = ""; try { let cookie = document.cookie.split("; ").filter(cookie => cookie.startsWith("BAHAID")).shift(); userID = cookie ? cookie.split("=").pop() : undefined; } catch (error) { console.error(error); } // html $('.ani-tabs').append(skSwitchButton()); $('.ani-tab-content').append(skSwitchTab()); $("#opening-tab-content").append(skMainTab()); $("#skInfo").append(skInfoTrigger()); $(".top_sky").append(skInfo()); $(".sk-sky-container").append(skSkyMessage()); $(".sk-sky-container").append(skSkipButton()); // 為所有按鈕加上切換功能 const aniTabsLinks = document.querySelectorAll('.ani-tabs-link'); aniTabsLinks.forEach(link => { link.addEventListener('click', () => { let element = document.getElementById('opening-tab-content'); element.style.display = 'none'; element = document.getElementById('skipOpening'); element.classList.add('is-disabled'); element.classList.remove('is-active'); }); }); // 為頁面加上切換功能 const aniTabsLinksOpening = document.getElementById('skipOpening'); aniTabsLinksOpening.addEventListener('click', () => { const otherTabs = Array.from(document.getElementsByClassName("ani-tab-content__item")); otherTabs.forEach(tab => { if (tab.id !== 'opening-tab-content') { tab.style.display = 'none'; } else { tab.style.display = 'block'; } }); let element = document.getElementById('opening-tab-content'); element.style.display = 'block'; const otherButtons = Array.from(document.getElementsByClassName("ani-tabs-link")); otherButtons.forEach(element => { if (element.id !== 'skipOpening') { element.classList.add('is-disabled'); element.classList.remove('is-active'); } else { element.classList.remove('is-disabled'); element.classList.add('is-active'); } }); }); const ControlButton = function() { const controlButtons = ['goBackMany','goBackSmall', 'goBack', 'goForward', 'goForwardSmall', 'goForwardMany']; const controlButtonsSpeed = ['-1','-0.04166', '-0.25', '0.25', '0.04166', '+1']; const toSkipButton = document.getElementById('toSkipOpening'); toSkipButton.addEventListener('click', () => { document.querySelectorAll("video")[0].currentTime += 85; }); controlButtons.forEach((element, index) => { const button = document.getElementById(element); button.addEventListener('click', () => { document.querySelectorAll("video")[0].pause(); document.querySelectorAll("video")[0].currentTime += Number(controlButtonsSpeed[index]); pageTemp.holdSk = false; pageTemp.holdCounter = 0; }); }); } // 主內容 window.onload = async function () { //進度追蹤器 async function updateTimer() { VideoTimer = document.querySelectorAll("video")[0].currentTime; const minute = Math.floor(VideoTimer / 60); const second = Math.floor(VideoTimer % 60); const microSecond = Math.floor((VideoTimer % 1) * 100); if (microSecond < 10) { document.getElementById("videoTimer").textContent = `目前 ${minute}分 ${second}.0${microSecond}秒`; } else { document.getElementById("videoTimer").textContent = `目前 ${minute}分 ${second}.${microSecond}秒`; } sn = getSN(); if (pageTemp.sn !== sn) { await doingRefresh(); pageTemp.sn = sn; } } setInterval(updateTimer, 200) ControlButton(); const refreshButton = document.getElementById('aniSkRefresh'); const uploadButton = document.getElementById('aniSkUpload'); const outTimeButton = document.getElementById('outTime'); const inTimeButton = document.getElementById('inTime'); const noTimeButton = document.getElementById('noTime'); const toDeleteButton = document.getElementById('delete-cache'); function saveSetting(checkbox, settingName) { skipSetting[settingName] = checkbox.checked; GM_setValue(settingName, checkbox.checked); console.log(skipSetting); } function applySettingButton() { try { skSettingMap.forEach((element, index) => { const button = document.getElementById(element[0]); const key = element[1]; button.addEventListener('change', (event) => saveSetting(event.target, key)); $(`#${element[0]}`).prop('checked', stringBool(skipSetting[key])); }) } catch (error) { console.log(error); } try { const microTimeBox = []; microTimeBox.push(document.getElementById('skInTime')) microTimeBox.push(document.getElementById('skOutTime')) microTimeBox.forEach((element, index) => { element.addEventListener('keydown', (event) => { if (event.key === 'Enter') { const Value = element.value; if (index === 0) { if (Value < 0 || Value >90) { element.value = 0 } skipSetting.inTime = Value; GM_setValue('inTime', Value); } else { if (Value < 0 || Value >100) { element.value = 90 } skipSetting.outTime = Value; GM_setValue('outTime', Value); } } }); }); microTimeBox.forEach((element, index) => { element.value = skipSetting[index === 0 ? 'inTime' : 'outTime']; }); } catch (error) { console.log(error); } try { const skPasswordInput = document.getElementById('skPasswd'); skPasswordInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { const Value = skPasswordInput.value; skipSetting.password = Value; GM_setValue('password', Value); } }); skPasswordInput.value = skipSetting.password; } catch (error) { console.log(error); } const skSkipButton = document.getElementById('skSkipButton'); const redoSkipButton = document.getElementById('redoSkipButton'); skSkipButton.addEventListener('click', () => { if (LocalTimes.length > 0) { const startPoint = Number(LocalTimes[0].time) + Number(skipSetting.inTime); const endPoint = Number(LocalTimes[0].time) + Number(skipSetting.outTime); if (VideoTimer > startPoint && VideoTimer < endPoint) { document.querySelectorAll("video")[0].currentTime = endPoint + 0.1; showMessage("跳過", 1, 1500, 1); return; } } else { document.querySelectorAll("video")[0].currentTime += 85; showMessage("前進85秒", 1, 1500, 1); } }); redoSkipButton.addEventListener('click', () => { if (pageTemp.whenSk !== 0) { const time = pageTemp.whenSk; pageTemp.runSk = false; skipSetting.skipOnce = true; document.querySelectorAll("video")[0].currentTime = time; showMessage("回退", 1, 1500, 1); } }); if (stringBool(skipSetting.skipButton)) { const manualButtonContainer = document.getElementById('skSkipButContainer'); manualButtonContainer.classList.add('sk-info-show') } const skSkipOnce = document.getElementById('skSetting-6'); skSkipOnce.addEventListener('change', (event) => { pageTemp.runSk = skipSetting.skManualOnce; pageTemp.runOnce = skipSetting.skManualOnce; }); } async function messageManger (text, status, onTop = 0) { let msgContainer = ''; if (onTop === 1) { msgContainer = 'status-container-top' } else { msgContainer = 'status-container' } let container = document.getElementsByClassName(msgContainer) const tabContent = document.getElementById('opening-tab-content'); if (!tabContent) { return; } if (getComputedStyle(tabContent).position === 'static') { tabContent.style.position = 'relative'; } if (container.length === 0) { container = document.createElement('div'); container.className = msgContainer; tabContent.appendChild(container); } let icon = ''; let type = ''; switch(status) { case 0: icon = '✕'; type = 'error'; break; case 1: icon = '✓'; type = 'success'; break; case 2: icon = '⚠'; type = 'warning'; break; case 3: icon = 'i'; type = 'info'; break; } pageTemp.messageCounter += 1; const counter = pageTemp.messageCounter; const messageHtml = ` <div id="popover-${counter}" class="status-popover ${type}"> <span class="status-icon">${icon}</span> <span class="status-message">${text}</span> <span class="status-close">×</span> </div> `; $(`.${msgContainer}`).append(messageHtml) const element = document.getElementById(`popover-${counter}`); requestAnimationFrame(() => { element.classList.add('show'); }) element.addEventListener('click', () => { element.classList.remove('show'); setTimeout(() => element.remove(), 300); }); return counter; } async function showMessage(text, status, time = 1500, mode) { messageManger(text, status, mode) .then((counter) => { setTimeout(() => { try { const element = document.getElementById(`popover-${counter}`); element.classList.remove('show'); setTimeout(() => element.remove(), 300); } catch (error) { return; } }, time); }) } async function SkRunningTime() { if (LocalTimes.length === 0) { return; } if (!pageTemp.runSk) { return; } if (pageTemp.holdSk) { pageTemp.holdCounter += 1; if (pageTemp.holdCounter > 100) { pageTemp.holdSk = false; } return; } if (VideoTimer > 0) { const startPoint = Number(LocalTimes[0].time) + Number(skipSetting.inTime); const endPoint = Number(LocalTimes[0].time) + Number(skipSetting.outTime); if (VideoTimer > startPoint && VideoTimer < endPoint) { pageTemp.whenSk = VideoTimer; document.querySelectorAll("video")[0].currentTime = endPoint + 0.1; showMessage("跳過", 1, 1500, 1); if (skipSetting.skipOnce || pageTemp.runOnce) { pageTemp.runSk = false; $(`#skSetting-6`).prop('checked', false); } } } } function SkControl(status) { clearInterval(SkRunningTime); if (status === true) { setInterval(SkRunningTime,200); } } function doingRefresh() { pageTemp.runSk = skipSetting.autoSkip; sn = getSN(); SkCache = {}; const content = GM_loadCache(sn); SkCache = content; getTimeData(); SkControl(skipSetting.autoSkip); } async function sendVote() { let votes = []; let count = 0; console.log(voteData); for (const element of voteData) { if (element.voted !== false) { votes.push({time: element.time, vote: element.voted}); count++; } if (count >= 2) { break; }; } const content = { 'user': userID, 'sn': sn, 'votes': votes, 'password': skipSetting.password }; console.log(content); const snJson = JSON.stringify(content); const response = await aniSkVote(snJson); LocalTimes.forEach((element, index) => { const item = voteData[index]; if (element.voted !== false && element.voted !== item.voted) { element.vote += element.voted * -1; } if (item.voted !== false) { element.vote += item.voted; } element.voted = item.voted; }); SkCache[sn].data = LocalTimes; GM_saveCache() newHtml(); } async function getTimeData() { if(stringBool(skipSetting.offlineMode)) { showMessage("離線模式",3); LocalTimes = getOfflineData(); return; } const status = await getAniCloudTime(); if (status.error) { LocalTimes = getLocalTime(); showMessage("取得失敗 使用本地資料", 0, 3000); } else { if (status.found) { CloudToLocalData(); LocalTimes = getLocalTime(); showMessage("更新成功", 1); } else { LocalTimes = []; showMessage("無資料 請標記", 2, 5000); } } for (let i = 0; i > LocalTimes.length; i++) { if (LocalTimes[i].time >= 36000) { LocalTimes.splice(i, 1); i--; } } // if (LocalTimes.length === 0) { // LocalTimes.push({ // time: 59940, // vote: -9999, // voted: false // }); // } // for (const element of LocalTimes) { // if (element.time >= 36000) { // showMessage("找不到本地資料 :(", 0, 4500); // break; // } // } newHtml(); } function newHtml() { let mode = 0; if (stringBool(skipSetting.pickMyTime)) { mode = 1 } if (stringBool(skipSetting.pickMyVote)) { mode = 2 } switch (mode) { case 1: const index1 = LocalTimes.findIndex(item => item.uploader === true); if (index1 !== -1) { const [removed] = LocalTimes.splice(index1, 1); LocalTimes.unshift(removed); } break; case 2: const index2 = LocalTimes.findIndex(item => item.voted === 1); if (index2 !== -1) { const [removed] = LocalTimes.splice(index2, 1); LocalTimes.unshift(removed); } break; default: LocalTimes.sort((a, b) => b.vote - a.vote); } voteData = LocalTimes; SkCache[sn].data = LocalTimes; renderVoteHtml(); updateVoteStatus(); } function renderVoteHtml() { $("#skVoteFrame").empty(); createVoteHtml().forEach((element, index) => { $("#skVoteFrame").append(element) }); createVoteButton(); } function createVoteHtml() { const arr = []; LocalTimes.forEach((element, index) => { const time = element.time; const vote = element.vote; const minute = Math.floor(time / 60); const second = Math.floor(time % 60); const microSecond = Math.floor((time % 1) * 100); const timeString = `${minute}分 ${second}.${microSecond}秒`; const voteString = `(${vote})`; const voteElement = ` <div id="voteContainer-${time}" class="sk-container"> <div id="rank-${time}"></div> <div class="sk-time-wrapper"> <div class="sk-time-list"> <span class="sk-time">${timeString}</span> <span id="skVote-${time}" class="sk-votes">${voteString}</span> </div> <div id="vote-${time}" class="sk-vote-controls"> <button id="skVoteUp-${time}" class="sk-vote sk-vote--up"> <i class="sk-icon">▲</i> </button> <button id="skVoteDown-${time}" class="sk-vote sk-vote--down"> <i class="sk-icon">▼</i> </button> </div> </div> </div> `; arr.push(voteElement); }); return arr; } async function updateVoteStatus() { LocalTimes.forEach((element, index) => { const time = element.time; const vote = element.vote; const item = voteData[index]; let voteString = ""; if (element.voted !== item.voted) { if (item.voted === 1) { voteString = ` (${vote}) +${item.voted}`; } else if (item.voted === -1) { voteString = ` (${vote}) ${item.voted}`; } else { if (element.voted !== false) { if (element.voted === 1) { voteString = ` (${vote}) -1`; } else if (element.voted === -1) { voteString = ` (${vote}) +1`; } } else { voteString = ` (${vote})`; } } } else { voteString = ` (${vote})`; } const voteElement = document.getElementById(`skVote-${time}`); voteElement.textContent = voteString; if (index === 0) { const voteContainer = document.getElementById(`rank-${time}`); if (getComputedStyle(voteContainer).position === 'static') { voteContainer.style.position = 'relative'; } $(`#rank-${time}`).append(`<div></div>`) } if (element.uploader === true) { const voteContainer = document.getElementById(`rank-${time}`); voteContainer.classList.add('your-uploaded-time'); } const voteUp = document.getElementById(`skVoteUp-${time}`); const voteDown = document.getElementById(`skVoteDown-${time}`); voteDown.classList.remove('sk-vote-VOTEDN'); voteDown.classList.remove('sk-vote-has-been-chosen'); voteUp.classList.remove('sk-vote-VOTEDP'); voteUp.classList.remove('sk-vote-has-been-chosen'); if (!item.voted === false) { if (item.vote === 1) { voteUp.classList.add('sk-vote-VOTEDP'); voteDown.classList.remove('sk-vote-VOTEDN'); } else if (item.vote ===-1){ voteUp.classList.remove('sk-vote-VOTEDP'); voteDown.classList.add('sk-vote-VOTEDN'); } } if (element.voted !== false) { if (element.voted === 1) { voteUp.classList.add('sk-vote-has-been-chosen'); voteDown.classList.remove('sk-vote-has-been-chosen'); } else { voteUp.classList.remove('sk-vote-has-been-chosen'); voteDown.classList.add('sk-vote-has-been-chosen'); } } }); } function createVoteButton() { LocalTimes.forEach((item, index) => { const time = item.time; const voteButtonP = document.getElementById(`skVoteUp-${time}`); const voteButtonN = document.getElementById(`skVoteDown-${time}`); voteButtonP.addEventListener('click', () => handleVote(voteButtonP, 1, time, index)); voteButtonN.addEventListener('click', () => handleVote(voteButtonN, -1, time, index)); }) } async function getAniCloudTime() { CloudTimes = []; const content = { 'user': userID, 'sn': sn }; const snJson = JSON.stringify(content); const response = await aniSkGetTime(snJson); if (response === 'error') { return { found : false, error : true }; } if (response.aniFound === 'notfound') { return { found : false, error : false }; } try { response.aniFound.forEach(async aniFound => { CloudTimes.push({ time: aniFound.time, vote: aniFound.vote, uploader: aniFound.uploader }); }) return { found : true, error : false }; } catch (error) { return { found : false, error : true }; } } async function uploadTime(type) { let time = 0; switch (type) { case 'start': time = VideoTimer break; case 'end': time = VideoTimer - 90 break; case 'no': time = 59940; break; default: break; } if (type === 'end' && VideoTimer <=89.99) { return; } if (stringBool(skipSetting.offlineMode)) { SkCache[sn].offlineData = []; SkCache[sn].offlineData.push( { time: VideoTimer, vote: 0, voted: false, uploader: true } ) GM_saveCache(); GM_loadCache(); return; } const content = { time: time, user: userID, sn: sn, password: skipSetting.password }; const data = JSON.stringify(content); const response = await aniSkUploader(data); showMessage(response.message, 1); } function handleVoteCounter(vote, buttonIndex, time) { let holdVote = {voted: null, index: -1, lastVote: []}; voteData.forEach((element, index) => { if (element.voted !== false && element.voted !== vote) { holdVote.voted = element.voted; holdVote.index = index; } holdVote.lastVote.push(element.vote); }) voteData = []; LocalTimes.forEach((element, index) => { voteData.push({ time: element.time, vote: 0, voted: false, uploader: element.uploader }); if (element.time === time && holdVote.lastVote[index] !== vote) { voteData[index].vote = vote; voteData[index].voted = vote; } if (index === holdVote.index && index !== buttonIndex) { voteData[index].vote = holdVote.voted; voteData[index].voted = holdVote.voted; } }); console.log(voteData); } async function handleVote(button, vote, time, buttonIndex) { handleVoteCounter(vote, buttonIndex, time); updateVoteStatus(); } function toggleInfo() { const infoButton = document.getElementsByClassName('sk-introduction'); infoButton[0].classList.toggle('sk-info-show'); } function toggleSkipButton() { const element = document.getElementById('skSkipButContainer'); if (skipSetting.skipButton) { element.classList.add('sk-info-hidden'); element.classList.remove('sk-info-show'); } else { element.classList.remove('sk-info-hidden'); element.classList.add('sk-info-show'); } } async function aniSkVote(content) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "POST", url: `${serverUrl}/api/vote`, data: content, headers: { 'Content-Type': 'application/json' }, onload: function (response) { if (response.status === 200) { resolve(JSON.parse(response.responseText)); showMessage(JSON.parse(response.responseText).message, 1); } else { showMessage(JSON.parse(response.responseText).message, 0); console.error('Error sending data:', response.status, response.statusText); } }, onerror: function (error) { showMessage("網路錯誤",0) console.error('Network Error:', error); reject(error); } }); }); } async function aniSkUploader(content) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "POST", url: `${serverUrl}/api/upload`, data: content, headers: { 'Content-Type': 'application/json' }, onload: function (response) { if (response.status === 200) { resolve(JSON.parse(response.responseText)); } else { showMessage(JSON.parse(response.responseText).message, 0); console.error('Error sending data:', response.status, response.statusText); } }, onerror: function (error) { showMessage("網路錯誤",0) console.error('Network Error:', error); reject(error); } }); }); } async function aniSkGetTime(content) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "POST", url: `${serverUrl}/api/getTime`, data: content, headers: { 'Content-Type': 'application/json' }, onload: function (response) { if (response.status === 200) { resolve(JSON.parse(response.responseText)); } else { showMessage("取得資料失敗", 0) console.error('Error sending data:', response.status, response.statusText); reject(new Error(`HTTP Error ${response.status}: ${response.statusText}`)); } }, onerror: function (error) { showMessage("網路錯誤",0) console.error('Network Error:', error); resolve('error'); } }); }); } getTimeData(); const skButton1 = document.getElementById('skSetting-1'); const infoButton = document.getElementById('info-trigger'); infoButton.addEventListener('click', () => toggleInfo()); skButton1.addEventListener('click', () => toggleSkipButton()); refreshButton.addEventListener('click', () => doingRefresh()); toDeleteButton.addEventListener('click', () => GM_delValue(sn)); uploadButton.addEventListener('click', () => sendVote()); inTimeButton.addEventListener('click', () => uploadTime('start')); outTimeButton.addEventListener('click', () => uploadTime('end')); noTimeButton.addEventListener('click', () => uploadTime('no')); SkCache = GM_loadCache(sn); pageTemp.runSk = skipSetting.autoSkip; setInterval(SkRunningTime,200); applySettingButton(); } })();