// ==UserScript==
// @name AutoPlayNextEpisode
// @version 1.0.2
// @description 自動撥放下一集&紀錄觀看的動畫集數
// @author Jay.Huang
// @match https://v.myself-bbs.com/player/*
// @match https://myself-bbs.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=myself-bbs.com
// @grant none
// @license MIT
// @namespace https://github.com/2jo4u4/MySelfRecorder.git
// ==/UserScript==
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const storeKeyWord = "recorder";
const storeFavorite = "favorite";
const favoriteIconHref = "https://cdn-icons-png.flaticon.com/512/2107/2107845.png";
const favoriteAddIconHref = "https://cdn-icons-png.flaticon.com/512/2001/2001314.png";
const favoriteRemoveIconHref = "https://cdn-icons-png.flaticon.com/512/2001/2001316.png";
const notFoundCoverImg = "https://cdn-icons-png.flaticon.com/512/7214/7214281.png";
const clearLogImg = "https://cdn-icons-png.flaticon.com/512/3602/3602056.png";
const PIPmode = true
var FavoriteBtnStatus;
(function (FavoriteBtnStatus) {
FavoriteBtnStatus[FavoriteBtnStatus["\u672A\u52A0\u5165\u6700\u611B"] = 0] = "\u672A\u52A0\u5165\u6700\u611B";
FavoriteBtnStatus[FavoriteBtnStatus["\u5DF2\u52A0\u5165\u6700\u611B"] = 1] = "\u5DF2\u52A0\u5165\u6700\u611B";
})(FavoriteBtnStatus || (FavoriteBtnStatus = {}));
function dalay() {
return __awaiter(this, arguments, void 0, function* (timeout = 500) {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
});
}
class VideoPlayManager {
constructor() {
// https://v.myself-bbs.com/player/AgADjw0AAmr7uVQ?totalEpisode=11&0=AgADjw0AAmr7uVQ&1=AgADuQ8AAhENCFU
this.url = new URL(window.location.href);
this.totalEpisode = Number(this.url.searchParams.get("totalEpisode") ?? "NaN");
this.currEpisode = Number(this.url.searchParams.get("currEpisode") ?? "NaN");
if(!isNaN(this.totalEpisode)) {
this.from = this.url.searchParams.get('from') ?? null
this.addCrtlBtn();
document.body.onload = () => {
this.getVideoPlay().then(videoEl => {
if (videoEl) {
console.log(videoEl, { PIPmode, requestPictureInPicture: Boolean(videoEl.requestPictureInPicture) })
if(PIPmode && videoEl.requestPictureInPicture) {
videoEl.addEventListener("play", ()=>{
videoEl.requestPictureInPicture().then(()=>{
console.log("auto open pip mode");
})
})
}
videoEl.addEventListener("ended", () => {
this.changeEpisode(true);
});
}
});
}
}
}
changeEpisode(next = true){
const targetNumber = next ? this.currEpisode + 1 : this.currEpisode - 1
const videoUrl = this.url.searchParams.get(targetNumber.toString()) ?? null
if(videoUrl !== null) {
window.location.href = this.getNextURL(videoUrl, targetNumber);
} else {
if(next) {
alert("沒有下一集了,將返回列表。");
if(this.from) {
window.location.href = this.from
}
}
else {
alert("找不到上一集。");
}
}
}
addCrtlBtn(){
const prevBtn = document.createElement("div");
prevBtn.onclick = () => {
this.changeEpisode(false);
}
const nextBtn = document.createElement("div");
nextBtn.onclick = () => {
this.changeEpisode(true);
}
function addStyle(btn, type = "left"){
btn.style.position = "fixed";
btn.style.zIndex = "998";
btn.style.top = "50%";
btn.style[type] = "0px";
btn.style.width = "24px";
btn.style.height = "64px";
btn.style.borderRadius = "12px";
btn.style.border = "1px black solid";
btn.style.backgroundColor = "white";
btn.style.opacity = "0.3";
btn.style.transition = "opacity 0.3s"
btn.onmouseenter = () => {
btn.style.opacity = "1";
}
btn.onmouseleave = () => {
btn.style.opacity = "0.3";
}
}
addStyle(prevBtn, "left");
addStyle(nextBtn, "right");
document.body.append(prevBtn, nextBtn)
}
getVideoPlay() {
return __awaiter(this, arguments, void 0, function* (times = 5) {
let video;
let retry = 0;
while (!Boolean(video) && retry <= times) {
video = document.querySelector("video");
yield dalay(1000);
retry += 1;
}
return video;
});
}
getNextURL(videoUrl, episodeNumber) {
const nextUrl = new URL(videoUrl)
nextUrl.search = this.url.search
nextUrl.searchParams.set("currEpisode", episodeNumber)
return nextUrl.toString();
}
}
class AnimeManager {
constructor() {
this.favoriteList = Tools.getFavorite();
this.createPositionEl();
this.renderFavoriteListUI();
// thread-47934-1-1.html
if (/^\/thread/.test(window.location.pathname)) {
this.page = "episode";
this.animeCode = window.location.pathname.split("-")[1];
this.recordList = Tools.getRecorder();
this.episodeUrls = [];
this.getElement().then(main => {
if (main) {
this.mainEl = main;
this.animeName = this.getAnimeName();
this.anchorEls = Array.from(main.getElementsByClassName("various"));
this.anchorEls.forEach((tagA, index) => {
const url = this.getEpisodeUrlByElement(tagA);
this.enhanceAnchorEl(tagA, index);
if (url) {
this.episodeUrls.push(url);
this.addAutoBtnEachEpisode(tagA, url);
}
});
this.rerenderAnchorElHighight("render");
this.renderEpisodeUI();
this.renderCtrlFavoriteUI();
}
else {
console.warn("找不到動畫集數資訊");
}
});
}
else {
this.page = "overview";
}
document.body.append(this.positionEl);
}
get currAnimeRecord() {
return this.recordList[this.animeCode];
}
renderEpisodeUI() {
const animeCode = this.animeCode;
const container = document.querySelector(".fr.vodlist_index").children[0];
container.style.position = "relative";
// 添加清除按鈕
container.appendChild(UIComponent.cleanWatchLogBtn(() => {
const recorder = Tools.getRecorder();
Tools.setRecorder(Object.assign(Object.assign({}, recorder), { [animeCode]: [] }));
this.rerenderAnchorElHighight("clearAll");
}));
}
// 添加觀看紀錄的高亮提示
rerenderAnchorElHighight(type) {
if (type === "clearAll") {
this.anchorEls.forEach(el => {
el.parentElement.parentElement.parentElement.style.backgroundColor = "unset";
});
}
else if (type === "render" && this.currAnimeRecord && this.currAnimeRecord.length > 1) {
const [recently, ...log] = this.currAnimeRecord;
this.anchorEls[recently].parentElement.parentElement.parentElement.style.backgroundColor = "#ff000080";
log.forEach(episodeIndex => {
this.anchorEls[episodeIndex].parentElement.parentElement.parentElement.style.backgroundColor = "#ff000030";
});
}
}
getEpisodeUrlByElement(el) {
return el.dataset.href || null;
}
addAutoBtnEachEpisode(el, url) {
const span = document.createElement("span");
span.innerText = "自動接續下集";
span.style.cursor = "pointer";
span.style.marginLeft = "4px";
span.onclick = () => {
this.gotoPlayPage(url);
};
el.parentElement.appendChild(span);
}
gotoPlayPage(targetUrl) {
const totalEpisode = `totalEpisode=${this.episodeUrls.length}`;
let keyValue = ""
let currEpisode = 0
this.episodeUrls.forEach((url, index) => {
const episode = index + 1;
if(targetUrl === url) {
currEpisode = episode
}
keyValue += `&${episode}=${url}`;
});
const from = window.location.href;
const url = `${targetUrl}?currEpisode=${currEpisode}&${totalEpisode}${keyValue}&from=${from}`;
window.location.href = url;
}
getElement() {
return __awaiter(this, arguments, void 0, function* (times = 5) {
let main;
let retry = 0;
while (main === undefined && retry <= times) {
main = document.getElementsByClassName("main_list")[0];
yield dalay();
retry += 1;
}
return main;
});
}
enhanceAnchorEl(el, index) {
const animeCode = this.animeCode;
el.onclick = () => {
const recorder = Tools.getRecorder();
const newNumberList = Array.from(new Set(recorder[animeCode] ? [index, ...recorder[animeCode]] : [index]));
Tools.setRecorder(Object.assign(Object.assign({}, recorder), { [animeCode]: newNumberList }));
this.recordList = Tools.getRecorder();
this.rerenderAnchorElHighight("render");
};
}
getAnimeName() {
var _a;
const block = (((_a = document.querySelector("#pt .z")) === null || _a === void 0 ? void 0 : _a.lastElementChild) || null);
if (block) {
return block.innerText.replace(/【(\S|\s|0-9)*/, "");
}
else {
return "";
}
}
/** 建立主畫面定位按鈕元素 */
createPositionEl() {
// 定位主畫面的按鈕
this.positionEl = document.createElement("div");
this.positionEl.id = "positionEl";
this.positionEl.style.position = "fixed";
this.positionEl.style.display = "flex";
this.positionEl.style.flexDirection = "row";
this.positionEl.style.top = "20px";
this.positionEl.style.left = "20px";
}
/** 我的最愛列表 */
renderFavoriteListUI() {
let favoritListFlag = false;
// 父元素
const container = document.createElement("div");
container.id = "container";
container.style.display = "flex";
container.style.flexDirection = "row";
container.style.marginRight = "4px";
// 用於打開最愛列表的按鈕
const favorite_btn = document.createElement("img");
this.ctrlBtnStyle(favorite_btn);
favorite_btn.src = favoriteIconHref;
favorite_btn.title = "打開/關閉最愛列表";
// 被打開的列表
const favorite_list = document.createElement("div");
favorite_list.style.maxHeight = "50vh";
favorite_list.style.display = favoritListFlag ? "flex" : "none";
favorite_list.style.flexDirection = "column";
favorite_list.style.marginTop = "12px";
favorite_list.style.padding = "12px";
favorite_list.style.backdropFilter = "blur(20px)";
favorite_list.style.borderRadius = "12px";
favorite_list.style.overflow = "auto";
favorite_list.style.position = "absolute";
favorite_list.style.width = "max-content";
favorite_list.style.top = "48px";
// 用於顯示開動畫的 Cover 圖定位
const coverImage = document.createElement("div");
coverImage.style.marginTop = "12px";
coverImage.style.position = "relative";
const span = document.createElement("span");
span.innerText = "暫無最愛";
span.style.color = "darkorange";
favorite_btn.onclick = () => {
favoritListFlag = !favoritListFlag;
favorite_list.style.display = favoritListFlag ? "flex" : "none";
span.style.display = this.favoriteList.length === 0 ? "block" : "none";
};
this.favoriteList.forEach((item, index) => {
const card = this.favoriteAnimeItem(coverImage, item, index !== 0 ? 12 : 0);
favorite_list.append(card);
});
favorite_list.append(span);
container.append(favorite_btn, coverImage, favorite_list);
this.positionEl.append(container);
}
favoriteAnimeItem(coverImagePosition, v, marginTop = 0) {
const { animecode, image, name, href } = v;
const card = document.createElement("div");
card.id = animecode;
card.style.marginTop = `${marginTop}px`;
card.style.maxWidth = "300px";
const coverImage = document.createElement("img");
coverImage.src = image;
coverImage.style.position = "absolute";
coverImage.style.top = "0";
coverImage.style.left = "310px";
coverImage.style.border = "6px solid white";
coverImage.style.borderRadius = "12px";
coverImage.style.boxShadow = "4px 6px 8px 6px #60606073";
const link = document.createElement("a");
link.href = href;
link.style.color = "#fff";
link.style.textShadow = "#000 0.1em 0.1em 0.2em";
link.innerText = name;
link.style.display = "flex";
link.style.flexDirection = "column";
link.onmouseenter = () => {
coverImagePosition.append(coverImage);
};
link.onmouseleave = () => {
coverImage.remove();
};
card.append(link);
return card;
}
/** 加入最愛 / 移除最愛 */
renderCtrlFavoriteUI() {
let index = this.favoriteList.findIndex(({ animecode }) => animecode === this.animeCode);
let isFavorite = index !== -1;
const coverPicture = document.querySelector(".info_con .info_img_box img");
const info = {
name: this.animeName,
image: (coverPicture === null || coverPicture === void 0 ? void 0 : coverPicture.src) || notFoundCoverImg,
href: window.location.pathname,
animecode: this.animeCode,
};
const ctrl_btn = document.createElement("img");
this.ctrlBtnStyle(ctrl_btn);
if (isFavorite) {
ctrl_btn.src = favoriteRemoveIconHref;
ctrl_btn.title = "移除最愛";
}
else {
ctrl_btn.src = favoriteAddIconHref;
ctrl_btn.title = "加入最愛";
}
ctrl_btn.addEventListener("click", () => {
if (isFavorite) {
// remove
this.favoriteList.splice(index, 1);
Tools.setFavorite(this.favoriteList);
ctrl_btn.title = "加入最愛";
ctrl_btn.src = favoriteAddIconHref;
}
else {
// add
this.favoriteList = [...this.favoriteList, info];
Tools.setFavorite(this.favoriteList);
ctrl_btn.title = "移除最愛";
ctrl_btn.src = favoriteRemoveIconHref;
}
this.rerenderFavoriteListUI();
isFavorite = !isFavorite;
});
this.positionEl.append(ctrl_btn);
}
rerenderFavoriteListUI() {
const listContainer = this.positionEl.children[0];
const ctrlBtn = this.positionEl.children[1];
listContainer.remove();
ctrlBtn.remove();
this.renderFavoriteListUI();
this.positionEl.append(ctrlBtn);
}
/** 主畫面顯示的按鈕樣式 */
ctrlBtnStyle(el) {
el.style.width = "24px";
el.style.height = "24px";
el.style.padding = "12px";
el.style.borderRadius = "12px";
el.style.backdropFilter = "blur(20px)";
el.style.cursor = "pointer";
el.style.border = "3px solid rgb(130, 130, 130)";
}
}
class Tools {
static setFavorite(data) {
window.localStorage.setItem(storeFavorite, JSON.stringify(data));
}
static getFavorite() {
const str = window.localStorage.getItem(storeFavorite) || "[]";
return JSON.parse(str);
}
static setRecorder(data) {
window.localStorage.setItem(storeKeyWord, JSON.stringify(data));
}
static getRecorder() {
const str = window.localStorage.getItem(storeKeyWord) || "{}";
return JSON.parse(str);
}
}
class NoticeUIChange {
}
class UIComponent {
static cleanWatchLogBtn(clickCB) {
const clearBtn = document.createElement("img");
clearBtn.src = clearLogImg;
clearBtn.style.position = "absolute";
clearBtn.style.top = "6px";
clearBtn.style.right = "6px";
clearBtn.style.cursor = "pointer";
clearBtn.style.width = "24px";
clearBtn.style.height = "24px";
clearBtn.alt = "清除觀看紀錄";
clearBtn.title = "清除觀看紀錄";
clearBtn.onclick = clickCB;
// clearBtn.onclick = function () {
// const recorder = Tools.getRecorder();
// Tools.setRecorder({ ...recorder, [animeCode]: [] });
// mark([]);
// };
return clearBtn;
}
static ctrlFavoriteBtn(initType) {
const btn = document.createElement("img");
let isAlreadyAdd = initType === FavoriteBtnStatus.已加入最愛;
const changeIcon_Remove = () => {
btn.src = favoriteRemoveIconHref;
btn.title = "移除最愛";
};
const changeIcon_Add = () => {
btn.src = favoriteAddIconHref;
btn.title = "加入最愛";
};
btn.style.marginRight = "4px";
btn.style.width = "24px";
btn.style.height = "24px";
btn.style.padding = "12px";
btn.style.borderRadius = "12px";
btn.style.backdropFilter = "blur(20px)";
btn.style.cursor = "pointer";
btn.style.border = "3px solid rgb(130, 130, 130)";
if (isAlreadyAdd) {
changeIcon_Remove();
}
else {
changeIcon_Add();
}
btn.addEventListener("click", () => {
isAlreadyAdd = !isAlreadyAdd;
if (isAlreadyAdd) {
changeIcon_Remove();
}
else {
changeIcon_Add();
}
});
}
}
(function () {
"use strict";
const isVideoPlay = window.location.origin === "https://v.myself-bbs.com";
if (isVideoPlay) {
new VideoPlayManager();
}
else {
new AnimeManager();
}
})();