// ==UserScript==
// @name 動畫瘋 OP工具
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 可以跳OP的東西
// @author 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-106-75.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: 10000;
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: 212px;
top: 3px;
transform: translateX(-50%);
background-color: rgb(55 57 60);
padding: 9px;
border-radius: 9px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: max-content;
translate: revert;
display: flex;
}
.sk-skip-button1 button {
padding: 5px 12px;
margin: 0 8px;
border: none;
border-radius: 4px;
background-color: #30a9c2;
color: white;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.sk-skip-button2 button {
padding: 6px 4px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.sk-sky-button-container button:hover {
background-color: #0056b3;
}
.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 += '</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 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範圍時跳過他\n</div>
<div>顯示手動按鈕: 顯示一組手動跳過OP用的按鈕\n</div>
<div>跳過一次: 跳過之後回到OP範圍內不會被跳過\n</div>
<div></div>
<div>優先贊成票:\n</div>
<div>總是使用自己投過贊成的時間點\n</div>
<div></div>
<div>優先時間點:\n</div>
<div>總是使用自己回報的時間點\n</div>
<div></div>
<div>微調時間: 可以調整跳過OP的判定範圍\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: 3px 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 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', () => {
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);
}
});
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 (!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;
if (type === 'start') {
time = VideoTimer
} else {
time = VideoTimer - 90
}
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'));
SkCache = GM_loadCache(sn);
pageTemp.runSk = skipSetting.autoSkip;
setInterval(SkRunningTime,200);
applySettingButton();
}
})();